From bf59e63b112384a740b9d1f19e4d57466b854522 Mon Sep 17 00:00:00 2001 From: Contributor Date: Mon, 23 Mar 2026 22:36:08 +0800 Subject: [PATCH 1/3] Fix marker version comparison when variable is on the RHS Fixes #934. PEP 508 allows marker expressions with the variable on either side, e.g.: python_version >= '3.9' '3.9' <= python_version # equivalent Previously _evaluate_markers passed the operands to _eval_op in the original (possibly reversed) order without adjusting for the direction. For version-comparison operators this caused _eval_op to construct a Specifier from the environment value (e.g. '3.13.7') and then check containment of the literal pattern ('3.13.*'), which failed because the wildcard is not a valid version string. Fix: thread an invert flag through _eval_op. When the marker variable was on the RHS and the operator is a directional comparison (not 'in' / 'not in'), swap the operands back and flip the operator so the Specifier is always built from the literal and the environment value is the thing being checked. Membership operators (in / not in) already work correctly in either direction and are left untouched. Add regression tests covering ==, <=, > with version markers and == / != with non-version markers when the variable is on the RHS. --- src/packaging/markers.py | 46 +++++++++++++++++++++++++++++--- tests/test_markers.py | 57 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 99 insertions(+), 4 deletions(-) diff --git a/src/packaging/markers.py b/src/packaging/markers.py index 65d7f330b..91349ea47 100644 --- a/src/packaging/markers.py +++ b/src/packaging/markers.py @@ -201,7 +201,7 @@ def _format_marker( return marker -_operators: dict[str, Operator] = { +_OPERATORS: dict[str, Operator] = { "in": lambda lhs, rhs: lhs in rhs, "not in": lambda lhs, rhs: lhs not in rhs, "<": lambda _lhs, _rhs: False, @@ -212,8 +212,38 @@ def _format_marker( ">": lambda _lhs, _rhs: False, } +# Operator inversion map: lhs op rhs ⟺ rhs (inv op) lhs +_OP_INVERSION: dict[str, str] = { + "<": ">", + ">": "<", + "<=": ">=", + ">=": "<=", + "==": "==", + "!=": "!=", + "in": "in", + "not in": "not in", +} + + +def _eval_op( + lhs: str, op: Op, rhs: str | AbstractSet[str], *, key: str, invert: bool = False +) -> bool: + """Evaluate a marker comparison. + + When *invert* is ``True``, the caller passed operands in reversed order + (because the marker variable was on the RHS). For version markers we + swap them back and flip the operator so ``_eval_op`` always sees the + canonical ``env-value op spec-pattern`` order. For set-based markers + (extras / dependency_groups with ``in`` / ``not in``) the original + ``literal-in-set`` order is already correct, so no swap is needed. + """ + # Only swap for directional comparison operators. Membership operators + # (in / not in) perform a substring or set-membership check where the + # original order (literal lhs, env rhs) is already correct. + if invert and op.value not in ("in", "not in") and key in MARKERS_REQUIRING_VERSION: + lhs, rhs = cast("str", rhs), cast("str | AbstractSet[str]", lhs) + op = Op(_OP_INVERSION[op.value]) -def _eval_op(lhs: str, op: Op, rhs: str | AbstractSet[str], *, key: str) -> bool: op_str = op.serialize() if key in MARKERS_REQUIRING_VERSION: try: @@ -223,7 +253,7 @@ def _eval_op(lhs: str, op: Op, rhs: str | AbstractSet[str], *, key: str) -> bool else: return spec.contains(lhs, prereleases=True) - oper: Operator | None = _operators.get(op_str) + oper: Operator | None = _OPERATORS.get(op_str) if oper is None: raise UndefinedComparison(f"Undefined {op!r} on {lhs!r} and {rhs!r}.") @@ -273,7 +303,15 @@ def _evaluate_markers( assert isinstance(lhs_value, str), "lhs must be a string" lhs_value, rhs_value = _normalize(lhs_value, rhs_value, key=environment_key) - groups[-1].append(_eval_op(lhs_value, op, rhs_value, key=environment_key)) + + # When the marker variable is on the RHS, tell _eval_op so it can + # swap operands back for version comparison markers. For set-based + # markers (in / not in with extras / dependency_groups) the + # original order already works, so _eval_op skips the swap. + var_on_rhs = isinstance(rhs, Variable) + groups[-1].append( + _eval_op(lhs_value, op, rhs_value, key=environment_key, invert=var_on_rhs) + ) elif marker == "or": groups.append([]) elif marker == "and": diff --git a/tests/test_markers.py b/tests/test_markers.py index eda41e409..7b811cee2 100644 --- a/tests/test_markers.py +++ b/tests/test_markers.py @@ -486,6 +486,63 @@ def test_version_like_equality( marker = Marker(marker_string) assert marker.evaluate(environment) is expected + @pytest.mark.parametrize( + ("marker_string", "environment", "expected"), + [ + # Issue #934: marker variable on RHS should work like LHS + ( + "'3.13.*' == python_full_version", + {"python_full_version": "3.13.7"}, + True, + ), + ( + "'3.13.*' == python_full_version", + {"python_full_version": "3.14.0"}, + False, + ), + ( + "'3.9' <= python_version", + {"python_version": "3.13"}, + True, + ), + ( + "'3.14' > python_version", + {"python_version": "3.13"}, + True, + ), + ( + "'3.14' > python_version", + {"python_version": "3.15"}, + False, + ), + # Non-version markers with variable on RHS + ( + "'posix' == os_name", + {"os_name": "posix"}, + True, + ), + ( + "'nt' != os_name", + {"os_name": "posix"}, + True, + ), + ], + ) + def test_marker_variable_on_rhs( + self, marker_string: str, environment: dict[str, str], expected: bool + ) -> None: + """ + Test for issue #934: Marker version comparison fails when the marker + variable is on the RHS of a term. + + The spec allows marker variables on either side, e.g.:: + + python_version >= '3.9' + '3.9' <= python_version + """ + marker = Marker(marker_string) + assert marker.evaluate(environment) is expected + def test_and_operator_evaluates_true() -> None: env = {"python_version": "3.8", "os_name": "posix"} From 9968f7781bb38d7d210d3c62238e56496e7c6831 Mon Sep 17 00:00:00 2001 From: r266-tech Date: Tue, 7 Apr 2026 16:32:17 +0800 Subject: [PATCH 2/3] fix: wrap long line in _evaluate_markers to satisfy ruff E501 --- src/packaging/markers.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/packaging/markers.py b/src/packaging/markers.py index 91349ea47..cc2f92a0d 100644 --- a/src/packaging/markers.py +++ b/src/packaging/markers.py @@ -310,7 +310,11 @@ def _evaluate_markers( # original order already works, so _eval_op skips the swap. var_on_rhs = isinstance(rhs, Variable) groups[-1].append( - _eval_op(lhs_value, op, rhs_value, key=environment_key, invert=var_on_rhs) + _eval_op( + lhs_value, op, rhs_value, + key=environment_key, + invert=var_on_rhs, + ) ) elif marker == "or": groups.append([]) From 5d547c3b448b03f2d81111d1c7d9c29f1b60254d Mon Sep 17 00:00:00 2001 From: r266-tech Date: Thu, 9 Apr 2026 17:16:09 +0800 Subject: [PATCH 3/3] style: apply ruff format to markers.py --- src/packaging/markers.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/packaging/markers.py b/src/packaging/markers.py index cc2f92a0d..254df0c75 100644 --- a/src/packaging/markers.py +++ b/src/packaging/markers.py @@ -311,7 +311,9 @@ def _evaluate_markers( var_on_rhs = isinstance(rhs, Variable) groups[-1].append( _eval_op( - lhs_value, op, rhs_value, + lhs_value, + op, + rhs_value, key=environment_key, invert=var_on_rhs, )