diff --git a/src/packaging/markers.py b/src/packaging/markers.py index a4889cdd..4b3e82d7 100644 --- a/src/packaging/markers.py +++ b/src/packaging/markers.py @@ -28,6 +28,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): @@ -177,16 +182,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("".join([op.serialize(), rhs])) + spec = Specifier("".join([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}.") @@ -234,9 +240,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)) else: assert marker in ["and", "or"] if marker == "or": diff --git a/tests/test_markers.py b/tests/test_markers.py index 5106427d..86406b95 100644 --- a/tests/test_markers.py +++ b/tests/test_markers.py @@ -426,3 +426,22 @@ def test_extras_and_dependency_groups_disallowed(self, variable): 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, environment, expected): + """ + 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