From 8b1f2cc87e2268f4d63c2344fac8698548459a05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Randy=20D=C3=B6ring?= <30527984+radoering@users.noreply.github.com> Date: Sun, 19 May 2024 13:45:25 +0200 Subject: [PATCH] markers: add new method to reduce a marker by a Python constraint That is useful for removing redundant information from markers. For example, if the project's Python constraint is `>=3.9`, a dependency's marker like `python_version >= 3.7` carries the same information as an `AnyMarker` for this project. (The dependency is required for all supported Python versions.) --- src/poetry/core/version/markers.py | 66 ++++++++++++++++++++++++ tests/version/test_markers.py | 83 ++++++++++++++++++++++++++++++ 2 files changed, 149 insertions(+) diff --git a/src/poetry/core/version/markers.py b/src/poetry/core/version/markers.py index bb2f23006..1740d7c1e 100644 --- a/src/poetry/core/version/markers.py +++ b/src/poetry/core/version/markers.py @@ -20,6 +20,7 @@ from poetry.core.constraints.generic import MultiConstraint from poetry.core.constraints.generic import UnionConstraint from poetry.core.constraints.version import VersionConstraint +from poetry.core.constraints.version import VersionUnion from poetry.core.constraints.version.exceptions import ParseConstraintError from poetry.core.version.grammars import GRAMMAR_PEP_508_MARKERS from poetry.core.version.parser import Parser @@ -107,6 +108,12 @@ def exclude(self, marker_name: str) -> BaseMarker: def only(self, *marker_names: str) -> BaseMarker: raise NotImplementedError + @abstractmethod + def reduce_by_python_constraint( + self, python_constraint: VersionConstraint + ) -> BaseMarker: + raise NotImplementedError + @abstractmethod def invert(self) -> BaseMarker: raise NotImplementedError @@ -145,6 +152,11 @@ def exclude(self, marker_name: str) -> BaseMarker: def only(self, *marker_names: str) -> BaseMarker: return self + def reduce_by_python_constraint( + self, python_constraint: VersionConstraint + ) -> BaseMarker: + return self + def invert(self) -> EmptyMarker: return EmptyMarker() @@ -186,6 +198,11 @@ def exclude(self, marker_name: str) -> EmptyMarker: def only(self, *marker_names: str) -> BaseMarker: return self + def reduce_by_python_constraint( + self, python_constraint: VersionConstraint + ) -> BaseMarker: + return self + def invert(self) -> AnyMarker: return AnyMarker() @@ -283,6 +300,11 @@ def only(self, *marker_names: str) -> BaseMarker: return self + def reduce_by_python_constraint( + self, python_constraint: VersionConstraint + ) -> BaseMarker: + return self + def intersect(self, other: BaseMarker) -> BaseMarker: if isinstance(other, SingleMarkerLike): merged = _merge_single_markers(self, other, MultiMarker) @@ -395,6 +417,18 @@ def value(self) -> str: def _key(self) -> tuple[object, ...]: return self._name, self._operator, self._value + def reduce_by_python_constraint( + self, python_constraint: VersionConstraint + ) -> BaseMarker: + if self.name in PYTHON_VERSION_MARKERS: + assert isinstance(self._constraint, VersionConstraint) + if self._constraint.allows_all(python_constraint): + return AnyMarker() + elif not self._constraint.allows_any(python_constraint): + return EmptyMarker() + + return self + def invert(self) -> BaseMarker: if self._operator in ("===", "=="): operator = "!=" @@ -670,6 +704,13 @@ def exclude(self, marker_name: str) -> BaseMarker: def only(self, *marker_names: str) -> BaseMarker: return self.of(*(m.only(*marker_names) for m in self._markers)) + def reduce_by_python_constraint( + self, python_constraint: VersionConstraint + ) -> BaseMarker: + return self.of( + *(m.reduce_by_python_constraint(python_constraint) for m in self._markers) + ) + def invert(self) -> BaseMarker: markers = [marker.invert() for marker in self._markers] @@ -839,6 +880,31 @@ def exclude(self, marker_name: str) -> BaseMarker: def only(self, *marker_names: str) -> BaseMarker: return self.of(*(m.only(*marker_names) for m in self._markers)) + def reduce_by_python_constraint( + self, python_constraint: VersionConstraint + ) -> BaseMarker: + from poetry.core.packages.utils.utils import get_python_constraint_from_marker + + markers: Iterable[BaseMarker] = self._markers + if isinstance(python_constraint, VersionUnion): + python_only_markers = [] + other_markers = [] + for m in self._markers: + if m == m.only(*PYTHON_VERSION_MARKERS): + python_only_markers.append(m) + else: + other_markers.append(m) + if get_python_constraint_from_marker( + self.of(*python_only_markers) + ).allows_all(python_constraint): + if not other_markers: + return AnyMarker() + markers = other_markers + + return self.of( + *(m.reduce_by_python_constraint(python_constraint) for m in markers) + ) + def invert(self) -> BaseMarker: markers = [marker.invert() for marker in self._markers] return MultiMarker(*markers) diff --git a/tests/version/test_markers.py b/tests/version/test_markers.py index 56a711ed6..7e9164a9e 100644 --- a/tests/version/test_markers.py +++ b/tests/version/test_markers.py @@ -8,6 +8,7 @@ from poetry.core.constraints.generic import UnionConstraint from poetry.core.constraints.generic import parse_constraint as parse_generic_constraint +from poetry.core.constraints.version import parse_constraint as parse_version_constraint from poetry.core.version.markers import AnyMarker from poetry.core.version.markers import AtomicMarkerUnion from poetry.core.version.markers import EmptyMarker @@ -1175,6 +1176,88 @@ def test_only(marker: str, only: list[str], expected: str) -> None: assert str(m.only(*only)) == expected +@pytest.mark.parametrize( + ("marker", "constraint", "expected"), + [ + ("", "~3.8", ""), + ("", "~3.8", ""), + ('sys_platform == "linux"', "~3.8", 'sys_platform == "linux"'), + ('python_version >= "3.8"', "~3.8", ""), + ('python_version > "3.8"', "~3.8", 'python_version > "3.8"'), + ('python_version >= "3.9"', "~3.8", ""), + ('python_full_version >= "3.8.0"', "~3.8", ""), + ('python_full_version >= "3.8.1"', "~3.8", 'python_full_version >= "3.8.1"'), + ('python_full_version < "3.8.0"', "~3.8", ""), + ('python_version >= "3.8" and python_version < "3.9"', "~3.8", ""), + ('python_version >= "3.7" and python_version < "4.0"', "~3.8", ""), + ( + 'python_full_version >= "3.8.1" and python_version < "3.9"', + "~3.8", + 'python_full_version >= "3.8.1"', + ), + ( + 'python_version >= "3.8" and python_full_version < "3.8.2"', + "~3.8", + 'python_full_version < "3.8.2"', + ), + ( + 'python_version >= "3.8" and sys_platform == "linux" and python_version < "3.9"', + "~3.8", + 'sys_platform == "linux"', + ), + ('python_version < "3.8" or python_version >= "3.9"', "~3.9", ""), + ( + 'python_version < "3.8" or python_version >= "3.9"', + ">=3.7", + 'python_version < "3.8" or python_version >= "3.9"', + ), + ('python_version < "3.8" or python_version >= "3.9"', "~3.7", ""), + ( + 'python_version < "3.8" or python_version >= "3.9"', + "<=3.10", + 'python_version < "3.8" or python_version >= "3.9"', + ), + ( + ( + 'python_version < "3.8"' + ' or python_version >= "3.9" and sys_platform == "linux"' + ), + "~3.9", + 'sys_platform == "linux"', + ), + ('python_version < "3.8" or python_version >= "3.9"', "~3.7 || ~3.9", ""), + ( + 'python_version < "3.8" or python_version >= "3.9"', + "~3.6 || ~3.8", + 'python_version < "3.8"', + ), + ( + ( + 'python_version < "3.8" or sys_platform == "linux"' + ' or python_version >= "3.9"' + ), + "~3.7 || ~3.9", + 'sys_platform == "linux"', + ), + ( + ( + 'python_version < "3.8" or sys_platform == "linux"' + ' or python_version >= "3.9" or sys_platform == "win32"' + ), + "~3.7 || ~3.9", + 'sys_platform == "linux" or sys_platform == "win32"', + ), + ], +) +def test_reduce_by_python_constraint( + marker: str, constraint: str, expected: str +) -> None: + m = parse_marker(marker) + c = parse_version_constraint(constraint) + + assert str(m.reduce_by_python_constraint(c)) == expected + + def test_union_of_a_single_marker_is_the_single_marker() -> None: union = MarkerUnion.of(SingleMarker("python_version", ">= 2.7"))