diff --git a/src/poetry/core/constraints/version/version_range.py b/src/poetry/core/constraints/version/version_range.py index 8eda13b98..89e5049d4 100644 --- a/src/poetry/core/constraints/version/version_range.py +++ b/src/poetry/core/constraints/version/version_range.py @@ -12,6 +12,7 @@ VersionRangeConstraint, ) from poetry.core.constraints.version.version_union import VersionUnion +from poetry.core.utils._compat import cached_property if TYPE_CHECKING: @@ -356,14 +357,16 @@ def difference(self, other: VersionConstraint) -> VersionConstraint: def flatten(self) -> list[VersionRangeConstraint]: return [self] + @cached_property def _single_wildcard_range_string(self) -> str: - if not self.is_single_wildcard_range(): + if not self.is_single_wildcard_range: raise ValueError("Not a valid wildcard range") assert self.min is not None assert self.max is not None return f"=={_single_wildcard_range_string(self.min, self.max)}" + @cached_property def is_single_wildcard_range(self) -> bool: # e.g. # - "1.*" equals ">=1.0.dev0, <2" (equivalent to ">=1.0.dev0, <2.0.dev0") @@ -436,7 +439,7 @@ def _compare_max(self, other: VersionRangeConstraint) -> int: def __str__(self) -> str: with suppress(ValueError): - return self._single_wildcard_range_string() + return self._single_wildcard_range_string text = "" diff --git a/src/poetry/core/constraints/version/version_range_constraint.py b/src/poetry/core/constraints/version/version_range_constraint.py index f775510b1..09a44b7af 100644 --- a/src/poetry/core/constraints/version/version_range_constraint.py +++ b/src/poetry/core/constraints/version/version_range_constraint.py @@ -4,6 +4,7 @@ from typing import TYPE_CHECKING from poetry.core.constraints.version.version_constraint import VersionConstraint +from poetry.core.utils._compat import cached_property if TYPE_CHECKING: @@ -33,9 +34,6 @@ def include_max(self) -> bool: @property def allowed_min(self) -> Version | None: - if self.min is None: - return None - # That is a bit inaccurate because # 1) The exclusive ordered comparison >V MUST NOT allow a post-release # of the given version unless V itself is a post release. @@ -47,7 +45,7 @@ def allowed_min(self) -> Version | None: # the callers of allowed_min. return self.min - @property + @cached_property def allowed_max(self) -> Version | None: if self.max is None: return None diff --git a/src/poetry/core/constraints/version/version_union.py b/src/poetry/core/constraints/version/version_union.py index 26900a77f..f65f14eb4 100644 --- a/src/poetry/core/constraints/version/version_union.py +++ b/src/poetry/core/constraints/version/version_union.py @@ -14,6 +14,7 @@ from poetry.core.constraints.version.version_range_constraint import ( VersionRangeConstraint, ) +from poetry.core.utils._compat import cached_property if TYPE_CHECKING: @@ -92,19 +93,17 @@ def is_any(self) -> bool: return False def is_simple(self) -> bool: - return self.excludes_single_version() + return self.excludes_single_version def allows(self, version: Version) -> bool: - if self.excludes_single_version(): + if self.excludes_single_version: # when excluded version is local, special handling is required # to ensure that a constraint (!=2.0+deadbeef) will allow the # provided version (2.0) - from poetry.core.constraints.version.version import Version - from poetry.core.constraints.version.version_range import VersionRange - excluded = VersionRange().difference(self) + excluded = self._excluded_single_version - if isinstance(excluded, Version) and excluded.is_local(): + if excluded.is_local(): return excluded != version return any(constraint.allows(version) for constraint in self._ranges) @@ -252,12 +251,13 @@ def our_next_range(include_current: bool = True) -> bool: def flatten(self) -> list[VersionRangeConstraint]: return self.ranges + @cached_property def _exclude_single_wildcard_range_string(self) -> str: """ Helper method to convert this instance into a wild card range string. """ - if not self.excludes_single_wildcard_range(): + if not self.excludes_single_wildcard_range: raise ValueError("Not a valid wildcard range") idx_order = (0, 1) if self._ranges[0].max else (1, 0) @@ -268,6 +268,7 @@ def _exclude_single_wildcard_range_string(self) -> str: assert two.min is not None return f"!={_single_wildcard_range_string(one.max, two.min)}" + @cached_property def excludes_single_wildcard_range(self) -> bool: if len(self._ranges) != 2: return False @@ -288,11 +289,25 @@ def excludes_single_wildcard_range(self) -> bool: return _is_wildcard_candidate(two.min, one.max, inverted=True) + @cached_property def excludes_single_version(self) -> bool: from poetry.core.constraints.version.version import Version + + return isinstance(self._inverted, Version) + + @cached_property + def _excluded_single_version(self) -> Version: + from poetry.core.constraints.version.version import Version + + excluded = self._inverted + assert isinstance(excluded, Version) + return excluded + + @cached_property + def _inverted(self) -> VersionConstraint: from poetry.core.constraints.version.version_range import VersionRange - return isinstance(VersionRange().difference(self), Version) + return VersionRange().difference(self) def __eq__(self, other: object) -> bool: if not isinstance(other, VersionUnion): @@ -304,12 +319,10 @@ def __hash__(self) -> int: return reduce(op.xor, map(hash, self._ranges)) def __str__(self) -> str: - from poetry.core.constraints.version.version_range import VersionRange - - if self.excludes_single_version(): - return f"!={VersionRange().difference(self)}" + if self.excludes_single_version: + return f"!={self._excluded_single_version}" try: - return self._exclude_single_wildcard_range_string() + return self._exclude_single_wildcard_range_string except ValueError: return " || ".join([str(r) for r in self._ranges]) diff --git a/src/poetry/core/packages/dependency.py b/src/poetry/core/packages/dependency.py index 3f59d5e75..94d12030b 100644 --- a/src/poetry/core/packages/dependency.py +++ b/src/poetry/core/packages/dependency.py @@ -239,8 +239,8 @@ def base_pep_508_name(self) -> str: constraint = self.constraint if isinstance(constraint, VersionUnion): if ( - constraint.excludes_single_version() - or constraint.excludes_single_wildcard_range() + constraint.excludes_single_version + or constraint.excludes_single_wildcard_range ): # This branch is a short-circuit logic for special cases and # avoids having to split and parse constraint again. This has diff --git a/src/poetry/core/utils/_compat.py b/src/poetry/core/utils/_compat.py index 67306eab1..d65eab56d 100644 --- a/src/poetry/core/utils/_compat.py +++ b/src/poetry/core/utils/_compat.py @@ -5,6 +5,14 @@ WINDOWS = sys.platform == "win32" +if sys.version_info < (3, 8): + # no caching for python 3.7 + cached_property = property +else: + import functools + + cached_property = functools.cached_property + if sys.version_info < (3, 11): # compatibility for python <3.11 import tomli as tomllib diff --git a/tests/constraints/version/test_version_range.py b/tests/constraints/version/test_version_range.py index 51508a50b..764e1d13e 100644 --- a/tests/constraints/version/test_version_range.py +++ b/tests/constraints/version/test_version_range.py @@ -672,7 +672,7 @@ def test_is_single_wildcard_range_include_min_include_max( version_range = VersionRange( Version.parse("1.2.dev0"), Version.parse("1.3"), include_min, include_max ) - assert version_range.is_single_wildcard_range() is expected + assert version_range.is_single_wildcard_range is expected @pytest.mark.parametrize( @@ -721,7 +721,7 @@ def test_is_single_wildcard_range( Version.parse(max) if max else None, include_min=True, ) - assert version_range.is_single_wildcard_range() is expected + assert version_range.is_single_wildcard_range is expected @pytest.mark.parametrize( diff --git a/tests/constraints/version/test_version_union.py b/tests/constraints/version/test_version_union.py index 0fd64fe42..c98ae72b4 100644 --- a/tests/constraints/version/test_version_union.py +++ b/tests/constraints/version/test_version_union.py @@ -83,7 +83,7 @@ def test_excludes_single_wildcard_range_basics( ranges: list[VersionRange], expected: bool ) -> None: - assert VersionUnion(*ranges).excludes_single_wildcard_range() is expected + assert VersionUnion(*ranges).excludes_single_wildcard_range is expected @pytest.mark.parametrize( @@ -126,7 +126,7 @@ def test_excludes_single_wildcard_range(max: str, min: str, expected: bool) -> N VersionRange(max=Version.parse(max)), VersionRange(Version.parse(min), include_min=True), ) - assert version_union.excludes_single_wildcard_range() is expected + assert version_union.excludes_single_wildcard_range is expected @pytest.mark.parametrize(