marker: report which comparisons could not be decided - #47
Merged
Merged
Conversation
Evaluate returns a bare bool, so a caller cannot tell "this marker is false" from "this marker could not be decided". Two things produce the second: `~=` and `===` reaching the generic string-operator table, which has no semantics for them and where packaging raises UndefinedComparison; and an environment variable resolving to "", which EnvironmentFromTarget legitimately does for platform_release and platform_version because a DECLARED target has no kernel to report. EvaluateUndecidable returns both the answer and the comparisons behind it that were undecidable. A consumer that must not discard a dependency edge -- a mirror, an offline bundle, where a dropped edge means a missing package and no fallback -- can then include on a non-empty slice instead of scanning Marker.String() for operators and variable names, which has false positives on quoted literals and a token list to hand-maintain. Variables covers the half this library cannot decide for the caller. A declared "3.13" forces the caller to invent a PythonFullVersion, and an invented value is decidable but arbitrary: `>= "3.13.2"` is false at 3.13.0 and true at 3.13.99. Only the caller knows which fields it fabricated, so it just needs to ask which variables a marker touches. TestInventedPatchLevelIsDecidableButArbitrary pins that distinction. Ordered comparisons on string operands are deliberately NOT undecidable. `<`/`>` returning false and `<=`/`>=` collapsing to equality is faithful to pypa/packaging, whose _operators table is literally the same and where `sys_platform >= "darwin"` evaluates False. Verified against 26.3, and asserted here so a future reader does not "fix" it. Short-circuiting limits reporting to the comparisons actually reached, which is the behaviour a caller wants: an `and` already decided false by its first operand needs no further explanation. Evaluate becomes a wrapper; its behaviour is unchanged and the existing tests pin that. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
jonyoder
added a commit
that referenced
this pull request
Aug 28, 2026
Dates [Unreleased] as 0.9.0 (2026-08-28) and adds its compare link. CHANGELOG only. A minor bump under the 0.x policy because two changes alter observable behaviour: reqtxt now recognizes --all-releases/--only-final/--use-feature (an unrecognized option was assumed boolean, so its argument became a fabricated requirement), and a standalone --hash line is accepted rather than rejected, matching pip. Contents since v0.8.0: tags manylinux floor fix (#43), marker EvaluateUndecidable/Variables (#47), tags IsCompatibleOrNewer/CompileAnyLibc/Archs (#48), reqtxt Source and pip-parity fixes (#49), dependency bumps (#50).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #44.
Evaluatereturns a barebool, so a caller cannot distinguish "this marker is false" from "this marker could not be decided". This adds two methods that recover the difference, and leavesEvaluatebit-for-bit unchanged.What counts as undecidable
Exactly two things, both of which currently answer
falseindistinguishably from a realfalse:~=or===reaching the generic string-operator table, which has no semantics for them. pypa/packaging raisesUndefinedComparisonhere; this package returnsfalseas a documented divergence, because there was no channel to report it on. Note it cannot happen when either operand is version-typed and both sides parse as PEP 440 versions, since the specifier path answers first and answers correctly."".EnvironmentFromTargetlegitimately cannot knowplatform_releaseorplatform_versionfor a declared target and leaves them empty, and comparing""against anything is meaningless. A literal""written by the marker author is not reported.What is deliberately not undecidable
Ordered comparisons on string operands.
<and>returning false, and<=/>=collapsing to equality, is faithful to packaging, whose_operatorstable is literally{"<": lambda: False, "<=": eq, ">=": eq, ">": lambda: False}and wheresys_platform >= "darwin"evaluatesFalse. Verified against 26.3 and asserted inTestEvaluateUndecidable_Decidableso a future reader does not "fix" it.Why
Variablesships alongsideEvaluateUndecidablecannot cover the case where the caller invented a value. A declared interpreter of3.13forces somePythonFullVersion, and the answer then depends on the invention:python_full_version >= "3.13.2"is false at3.13.0and true at3.13.99, with nothing empty and no operator undefined. Only the caller knows which fields it fabricated; it just needs to ask which variables a marker touches.TestInventedPatchLevelIsDecidableButArbitrarypins that distinction explicitly.Without these two methods the only available workaround is a substring scan of
Marker.String()for the operators and variable names, which has false positives on quoted literals (sys_platform == "platform_version"matches) and a token list for every consumer to hand-maintain against this library.Behaviour and scope
Short-circuiting limits reporting to comparisons actually reached, which is what a caller wants: an
andalready decided false by its first operand needs no further explanation.TestEvaluateUndecidable_ShortCircuitLimitsReportingcovers both orderings.Evaluatebecomes a one-line wrapper, and anilcollector makes the reporting path free on that route. The pre-existingmarkertests pin that its behaviour is unchanged.gofmtclean,go vet ./...clean, full module suite green.Found while designing declared-environment filtering for Posit Package Manager's air-gapped PyPI mirror, where a dependency edge dropped because a marker could not be evaluated means a missing package and no fallback.
🤖 Generated with Claude Code