Skip to content

Commit 0a13436

Browse files
committed
markers: Consistently fallback to Python comparison rules
In the dependency specifiers specification (originally PEP 508), when there is no PEP 440 defined behaviour for evaluating a marker because one or both sides are not valid versions [specifiers], a fallback to Python comparison behaviour is mandated: > The <marker_op> operators that are not in <version_cmp> perform the > same as they do for strings in Python. The <version_cmp> operators use > the version comparison rules of the Version specifier specification > when those are defined (that is when both sides have a valid version > specifier). If there is no defined behaviour of this specification and > the operator exists in Python, then the operator falls back to the > Python behaviour. Otherwise an error should be raised. However, this fallback has been broken for a while. When the right side has PEP 440 defined semantics (being a valid specifier), but the left side is an invalid version, an InvalidVersion error is raised. This patch suppresses said InvalidVersion error and fallbacks as expected.
1 parent 793ee28 commit 0a13436

File tree

3 files changed

+46
-8
lines changed

3 files changed

+46
-8
lines changed

CHANGELOG.rst

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ Changelog
44
*unreleased*
55
~~~~~~~~~~~~
66

7-
No unreleased changes.
7+
* Consistently fallback to Python string comparison behaviour when evaluating
8+
a marker where there is no PEP 440 defined behaviour, notably when the right
9+
side has PEP 440 defined semantics, but the left side is an invalid
10+
version (:issue:`774`)
811

912
24.1 - 2024-06-10
1013
~~~~~~~~~~~~~~~~~

src/packaging/markers.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from ._tokenizer import ParserSyntaxError
1616
from .specifiers import InvalidSpecifier, Specifier
1717
from .utils import canonicalize_name
18+
from .version import InvalidVersion
1819

1920
__all__ = [
2021
"InvalidMarker",
@@ -178,9 +179,18 @@ def _eval_op(lhs: str, op: Op, rhs: str) -> bool:
178179
try:
179180
spec = Specifier("".join([op.serialize(), rhs]))
180181
except InvalidSpecifier:
182+
# As per the specification, if there is no PEP 440 defined
183+
# behaviour because the right side is not a valid version
184+
# specifier, fallback to Python string comparision behaviour.
181185
pass
182186
else:
183-
return spec.contains(lhs, prereleases=True)
187+
try:
188+
return spec.contains(lhs, prereleases=True)
189+
except InvalidVersion:
190+
# Even though there is PEP 440 defined behaviour for the
191+
# right side, fallback as the left left is not a valid
192+
# version.
193+
pass
184194

185195
oper: Operator | None = _operators.get(op.serialize())
186196
if oper is None:

tests/test_markers.py

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
default_environment,
2121
format_full_version,
2222
)
23-
from packaging.version import InvalidVersion
2423

2524
VARIABLES = [
2625
"extra",
@@ -388,12 +387,38 @@ def test_extra_str_normalization(self):
388387
assert str(Marker(rhs)) == f'extra == "{normalized_name}"'
389388

390389
def test_python_full_version_untagged_user_provided(self):
391-
"""A user-provided python_full_version ending with a + fails to parse."""
392-
with pytest.raises(InvalidVersion):
393-
Marker("python_full_version < '3.12'").evaluate(
394-
{"python_full_version": "3.11.1+"}
395-
)
390+
"""A user-provided python_full_version ending with a + uses Python behaviour."""
391+
env = {"python_full_version": "3.11.1+"}
392+
assert Marker("python_full_version < '3.12'").evaluate(env)
396393

397394
def test_python_full_version_untagged(self):
398395
with mock.patch("platform.python_version", return_value="3.11.1+"):
399396
assert Marker("python_full_version < '3.12'").evaluate()
397+
398+
@pytest.mark.parametrize(
399+
("marker_string", "environment", "expected"),
400+
[
401+
("platform_release >= '20.0'", {"platform_release": "21-foobar"}, True),
402+
("platform_release >= '8'", {"platform_release": "6.7.0-gentoo"}, False),
403+
("platform_version == '27'", {"platform_version": "weird string"}, False),
404+
(
405+
"implementation_version == '3.*'",
406+
{"implementation_version": "2_private"},
407+
False,
408+
),
409+
(
410+
"implementation_version == '3.*'",
411+
{"implementation_version": "3.*"},
412+
True,
413+
),
414+
],
415+
)
416+
def test_valid_specifier_invalid_version_fallback_to_python(
417+
self, marker_string: str, environment: dict, expected: bool
418+
):
419+
"""If the right operand is a valid version specifier, but the
420+
left operand is not a valid version, fallback to Python string
421+
comparison behaviour.
422+
"""
423+
args = [] if environment is None else [environment]
424+
assert Marker(marker_string).evaluate(*args) == expected

0 commit comments

Comments
 (0)