Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
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 <[email protected]>
  • Loading branch information
JP-Ellis authored and henryiii committed Jan 6, 2026
commit 3054f666b38a5e5fd1a7bd9f925d861159d07883
17 changes: 12 additions & 5 deletions src/packaging/markers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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}.")

Expand Down Expand Up @@ -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":
Expand Down
21 changes: 21 additions & 0 deletions tests/test_markers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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