diff --git a/src/packaging/markers.py b/src/packaging/markers.py index a4889cdd..898d0dde 100644 --- a/src/packaging/markers.py +++ b/src/packaging/markers.py @@ -8,9 +8,10 @@ import os import platform import sys +from functools import partial from typing import AbstractSet, Any, Callable, Literal, TypedDict, Union, cast -from ._parser import MarkerAtom, MarkerList, Op, Value, Variable +from ._parser import MarkerAtom, MarkerItem, MarkerList, Op, Value, Variable from ._parser import parse_marker as _parse_marker from ._tokenizer import ParserSyntaxError from .specifiers import InvalidSpecifier, Specifier @@ -213,36 +214,41 @@ def _normalize( return lhs, rhs +def _evaluate_marker_item( + marker: MarkerItem, environment: dict[str, str | AbstractSet[str]] +) -> bool: + lhs, op, rhs = marker + if isinstance(lhs, Variable): + environment_key = lhs.value + lhs_value = environment[environment_key] + rhs_value: str | AbstractSet[str] = rhs.value + else: + lhs_value = lhs.value + environment_key = rhs.value + rhs_value = environment[environment_key] + assert isinstance(lhs_value, str), "lhs value must be a string" + lhs_value, rhs_value = _normalize(lhs_value, rhs_value, key=environment_key) + return _eval_op(lhs_value, op, rhs_value) + + def _evaluate_markers( markers: MarkerList, environment: dict[str, str | AbstractSet[str]] ) -> bool: - groups: list[list[bool]] = [[]] + groups: list[list[Callable[[], bool]]] = [[]] for marker in markers: assert isinstance(marker, (list, tuple, str)) if isinstance(marker, list): - groups[-1].append(_evaluate_markers(marker, environment)) + groups[-1].append(partial(_evaluate_markers, marker, environment)) elif isinstance(marker, tuple): - lhs, op, rhs = marker - - if isinstance(lhs, Variable): - environment_key = lhs.value - lhs_value = environment[environment_key] - rhs_value = rhs.value - else: - 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(partial(_evaluate_marker_item, marker, environment)) else: assert marker in ["and", "or"] if marker == "or": groups.append([]) - return any(all(item) for item in groups) + return any(all(item() for item in group) for group in groups) def format_full_version(info: sys._version_info) -> str: diff --git a/tests/test_markers.py b/tests/test_markers.py index 5106427d..35624d1f 100644 --- a/tests/test_markers.py +++ b/tests/test_markers.py @@ -92,6 +92,16 @@ def test_allows_prerelease(self): {"python_full_version": "3.11.0a5"} ) + def test_boolean_shortcut_and(self): + assert not Marker("sys_platform == 'foo' and platform_release >= '6'").evaluate( + {"platform_release": "foo-bar-baz"} + ) + + def test_boolean_shortcut_or(self): + assert Marker("python_version >= '2.6' or platform_release >= '6'").evaluate( + {"platform_release": "foo-bar-baz"} + ) + FakeVersionInfo = collections.namedtuple( "FakeVersionInfo", ["major", "minor", "micro", "releaselevel", "serial"] @@ -172,6 +182,9 @@ def test_parses_valid(self, marker_string): "python_version >= 1.0 and (python_version)", '(python_version == "2.7" and os_name == "linux"', '(python_version == "2.7") with random text', + 'python_version *= "2.7"', + "python_version ~= 2.7", + 'python_version >= "2.7" or os_name *= "linux"', ], ) def test_parses_invalid(self, marker_string):