From 3054f666b38a5e5fd1a7bd9f925d861159d07883 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Tue, 7 Oct 2025 14:03:25 +1100 Subject: [PATCH 1/5] fix(markers): only parse versions on certain keys According to the [environment markers](https://packaging.python.org/en/latest/specifications/dependency-specifiers/#environment-markers), most markers are strings, with only a small subset being use to handle versions. This commit ensures that only those keys which _are_ versions get compared as versions, and all other keys are compared as string literals. Signed-off-by: JP-Ellis --- src/packaging/markers.py | 17 ++++++++++++----- tests/test_markers.py | 21 +++++++++++++++++++++ 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/src/packaging/markers.py b/src/packaging/markers.py index 2ae53358..43576cc3 100644 --- a/src/packaging/markers.py +++ b/src/packaging/markers.py @@ -29,6 +29,11 @@ Operator = Callable[[str, Union[str, AbstractSet[str]]], bool] EvaluateContext = Literal["metadata", "lock_file", "requirement"] MARKERS_ALLOWING_SET = {"extras", "dependency_groups"} +MARKERS_REQUIRING_VERSION = { + "python_version", + "python_full_version", + "implementation_version", +} class InvalidMarker(ValueError): @@ -186,16 +191,17 @@ def _format_marker( } -def _eval_op(lhs: str, op: Op, rhs: str | AbstractSet[str]) -> bool: - if isinstance(rhs, str): +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: - spec = Specifier(f"{op.serialize()}{rhs}") + spec = Specifier(f"{op_str}{rhs}") except InvalidSpecifier: pass else: return spec.contains(lhs, prereleases=True) - oper: Operator | None = _operators.get(op.serialize()) + oper: Operator | None = _operators.get(op_str) if oper is None: raise UndefinedComparison(f"Undefined {op!r} on {lhs!r} and {rhs!r}.") @@ -242,9 +248,10 @@ def _evaluate_markers( lhs_value = lhs.value environment_key = rhs.value rhs_value = environment[environment_key] + 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)) + groups[-1].append(_eval_op(lhs_value, op, rhs_value, key=environment_key)) elif marker == "or": groups.append([]) elif marker == "and": diff --git a/tests/test_markers.py b/tests/test_markers.py index c7aa068a..f9e60dd2 100644 --- a/tests/test_markers.py +++ b/tests/test_markers.py @@ -443,3 +443,24 @@ def test_extras_and_dependency_groups_disallowed(self, variable: str) -> None: with pytest.raises(KeyError): marker.evaluate(context="requirement") + + @pytest.mark.parametrize( + ("marker_string", "environment", "expected"), + [ + ('extra == "v2"', None, False), + ('extra == "v2"', {"extra": ""}, False), + ('extra == "v2"', {"extra": "v2"}, True), + ('extra == "v2"', {"extra": "v2a3"}, False), + ('extra == "v2a3"', {"extra": "v2"}, False), + ('extra == "v2a3"', {"extra": "v2a3"}, True), + ], + ) + def test_version_like_equality( + self, marker_string: str, environment: dict[str, str] | None, expected: bool + ) -> None: + """ + Test for issue #938: Extras are meant to be literal strings, even if + they look like versions, and therefore should not be parsed as version. + """ + marker = Marker(marker_string) + assert marker.evaluate(environment) is expected From 1132aba067922e96730183c2e4ff7eab6287a344 Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Mon, 8 Dec 2025 17:00:38 -0500 Subject: [PATCH 2/5] fix: support platform_release too Signed-off-by: Henry Schreiner --- src/packaging/markers.py | 5 +++-- tests/test_markers.py | 5 ++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/packaging/markers.py b/src/packaging/markers.py index 43576cc3..6a52d71e 100644 --- a/src/packaging/markers.py +++ b/src/packaging/markers.py @@ -30,9 +30,10 @@ EvaluateContext = Literal["metadata", "lock_file", "requirement"] MARKERS_ALLOWING_SET = {"extras", "dependency_groups"} MARKERS_REQUIRING_VERSION = { - "python_version", - "python_full_version", "implementation_version", + "platform_release", + "python_full_version", + "python_version", } diff --git a/tests/test_markers.py b/tests/test_markers.py index f9e60dd2..160de9af 100644 --- a/tests/test_markers.py +++ b/tests/test_markers.py @@ -80,9 +80,12 @@ def test_base_class(self) -> None: class TestOperatorEvaluation: def test_prefers_pep440(self) -> None: - assert Marker('"2.7.9" < "foo"').evaluate(dict(foo="2.7.10")) + assert Marker('"2.7.9" < python_full_version').evaluate(dict(python_full_version="2.7.10")) + assert not Marker('"2.7.9" < python_full_version').evaluate(dict(python_full_version="2.7.8")) def test_falls_back_to_python(self) -> None: + assert Marker('"b" < python_full_version').evaluate(dict(python_full_version="c")) + assert not Marker('"b" < python_full_version').evaluate(dict(python_full_version="a")) assert Marker('"b" > "a"').evaluate(dict(a="a")) def test_fails_when_undefined(self) -> None: From 362d2ec487c32500b7e6a4820f4a6e63c16e919e Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Mon, 8 Dec 2025 17:10:17 -0500 Subject: [PATCH 3/5] fix: follow current spec update Comparisons relying on order are always False. Signed-off-by: Henry Schreiner --- src/packaging/markers.py | 8 ++++---- tests/test_markers.py | 24 ++++++++++++++++++------ 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/src/packaging/markers.py b/src/packaging/markers.py index 6a52d71e..ca3706fe 100644 --- a/src/packaging/markers.py +++ b/src/packaging/markers.py @@ -183,12 +183,12 @@ def _format_marker( _operators: dict[str, Operator] = { "in": lambda lhs, rhs: lhs in rhs, "not in": lambda lhs, rhs: lhs not in rhs, - "<": operator.lt, - "<=": operator.le, + "<": lambda _lhs, _rhs: False, + "<=": operator.eq, "==": operator.eq, "!=": operator.ne, - ">=": operator.ge, - ">": operator.gt, + ">=": operator.eq, + ">": lambda _lhs, _rhs: False, } diff --git a/tests/test_markers.py b/tests/test_markers.py index 160de9af..5a9ca9e6 100644 --- a/tests/test_markers.py +++ b/tests/test_markers.py @@ -80,13 +80,25 @@ def test_base_class(self) -> None: class TestOperatorEvaluation: def test_prefers_pep440(self) -> None: - assert Marker('"2.7.9" < python_full_version').evaluate(dict(python_full_version="2.7.10")) - assert not Marker('"2.7.9" < python_full_version').evaluate(dict(python_full_version="2.7.8")) + assert Marker('"2.7.9" < python_full_version').evaluate( + dict(python_full_version="2.7.10") + ) + assert not Marker('"2.7.9" < python_full_version').evaluate( + dict(python_full_version="2.7.8") + ) - def test_falls_back_to_python(self) -> None: - assert Marker('"b" < python_full_version').evaluate(dict(python_full_version="c")) - assert not Marker('"b" < python_full_version').evaluate(dict(python_full_version="a")) - assert Marker('"b" > "a"').evaluate(dict(a="a")) + def test_new_string_rules(self) -> None: + assert not Marker('"b" < python_full_version').evaluate( + dict(python_full_version="c") + ) + assert not Marker('"b" < python_full_version').evaluate( + dict(python_full_version="a") + ) + assert not Marker('"b" > "a"').evaluate(dict(a="a")) + assert not Marker('"b" < "a"').evaluate(dict(a="a")) + assert not Marker('"b" >= "a"').evaluate(dict(a="a")) + assert not Marker('"b" <= "a"').evaluate(dict(a="a")) + assert Marker('"a" <= "a"').evaluate(dict(a="a")) def test_fails_when_undefined(self) -> None: with pytest.raises(UndefinedComparison): From 54e6b1f46d6e382cc70214d274bc4a5b2c47f87a Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Mon, 29 Dec 2025 17:51:47 -0500 Subject: [PATCH 4/5] Update CHANGELOG with recent changes and fixes --- CHANGELOG.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 1e08562b..bdb32ff9 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -19,6 +19,7 @@ Behavior adaptations: * Adjust arbitrary equality intersection preservation in ``SpecifierSet`` (:pull:`951`) * Return ``False`` instead of raising for ``.contains`` with invalid version (:pull:`932`) * Support arbitrary equality on arbitrary strings for ``Specifier`` and ``SpecifierSet``'s ``filter`` and ``contains`` method. (:pull:`954`) +* Only parse ``Version``s on certain marker keys, return False on unequal ordered comparisons (:pull:`939`) Fixes: From a172dd09063e9bceeca390d4d2bc2491cb675547 Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Mon, 29 Dec 2025 18:43:57 -0500 Subject: [PATCH 5/5] Clarify parsing behavior for Version in CHANGELOG --- CHANGELOG.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index bdb32ff9..78330d29 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -19,7 +19,7 @@ Behavior adaptations: * Adjust arbitrary equality intersection preservation in ``SpecifierSet`` (:pull:`951`) * Return ``False`` instead of raising for ``.contains`` with invalid version (:pull:`932`) * Support arbitrary equality on arbitrary strings for ``Specifier`` and ``SpecifierSet``'s ``filter`` and ``contains`` method. (:pull:`954`) -* Only parse ``Version``s on certain marker keys, return False on unequal ordered comparisons (:pull:`939`) +* Only try to parse as ``Version`` on certain marker keys, return ``False`` on unequal ordered comparisons (:pull:`939`) Fixes: