diff --git a/tests/packages/constraints/__init__.py b/src/poetry/core/constraints/__init__.py similarity index 100% rename from tests/packages/constraints/__init__.py rename to src/poetry/core/constraints/__init__.py diff --git a/src/poetry/core/constraints/generic/__init__.py b/src/poetry/core/constraints/generic/__init__.py new file mode 100644 index 000000000..ccd7f5a6b --- /dev/null +++ b/src/poetry/core/constraints/generic/__init__.py @@ -0,0 +1,20 @@ +from __future__ import annotations + +from poetry.core.constraints.generic.any_constraint import AnyConstraint +from poetry.core.constraints.generic.base_constraint import BaseConstraint +from poetry.core.constraints.generic.constraint import Constraint +from poetry.core.constraints.generic.empty_constraint import EmptyConstraint +from poetry.core.constraints.generic.multi_constraint import MultiConstraint +from poetry.core.constraints.generic.parser import parse_constraint +from poetry.core.constraints.generic.union_constraint import UnionConstraint + + +__all__ = [ + "AnyConstraint", + "BaseConstraint", + "Constraint", + "EmptyConstraint", + "MultiConstraint", + "UnionConstraint", + "parse_constraint", +] diff --git a/src/poetry/core/packages/constraints/any_constraint.py b/src/poetry/core/constraints/generic/any_constraint.py similarity index 86% rename from src/poetry/core/packages/constraints/any_constraint.py rename to src/poetry/core/constraints/generic/any_constraint.py index 7bbea514f..db230869f 100644 --- a/src/poetry/core/packages/constraints/any_constraint.py +++ b/src/poetry/core/constraints/generic/any_constraint.py @@ -1,7 +1,7 @@ from __future__ import annotations -from poetry.core.packages.constraints.base_constraint import BaseConstraint -from poetry.core.packages.constraints.empty_constraint import EmptyConstraint +from poetry.core.constraints.generic.base_constraint import BaseConstraint +from poetry.core.constraints.generic.empty_constraint import EmptyConstraint class AnyConstraint(BaseConstraint): diff --git a/src/poetry/core/packages/constraints/base_constraint.py b/src/poetry/core/constraints/generic/base_constraint.py similarity index 100% rename from src/poetry/core/packages/constraints/base_constraint.py rename to src/poetry/core/constraints/generic/base_constraint.py diff --git a/src/poetry/core/packages/constraints/constraint.py b/src/poetry/core/constraints/generic/constraint.py similarity index 90% rename from src/poetry/core/packages/constraints/constraint.py rename to src/poetry/core/constraints/generic/constraint.py index 013449fdc..3180efb1a 100644 --- a/src/poetry/core/packages/constraints/constraint.py +++ b/src/poetry/core/constraints/generic/constraint.py @@ -2,9 +2,9 @@ import operator -from poetry.core.packages.constraints import AnyConstraint -from poetry.core.packages.constraints.base_constraint import BaseConstraint -from poetry.core.packages.constraints.empty_constraint import EmptyConstraint +from poetry.core.constraints.generic.any_constraint import AnyConstraint +from poetry.core.constraints.generic.base_constraint import BaseConstraint +from poetry.core.constraints.generic.empty_constraint import EmptyConstraint class Constraint(BaseConstraint): @@ -78,7 +78,7 @@ def difference(self, other: BaseConstraint) -> Constraint | EmptyConstraint: return self def intersect(self, other: BaseConstraint) -> BaseConstraint: - from poetry.core.packages.constraints.multi_constraint import MultiConstraint + from poetry.core.constraints.generic.multi_constraint import MultiConstraint if isinstance(other, Constraint): if other == self: @@ -99,9 +99,7 @@ def intersect(self, other: BaseConstraint) -> BaseConstraint: def union(self, other: BaseConstraint) -> BaseConstraint: if isinstance(other, Constraint): - from poetry.core.packages.constraints.union_constraint import ( - UnionConstraint, - ) + from poetry.core.constraints.generic.union_constraint import UnionConstraint if other == self: return self diff --git a/src/poetry/core/packages/constraints/empty_constraint.py b/src/poetry/core/constraints/generic/empty_constraint.py similarity index 92% rename from src/poetry/core/packages/constraints/empty_constraint.py rename to src/poetry/core/constraints/generic/empty_constraint.py index 147c43707..83d0d148d 100644 --- a/src/poetry/core/packages/constraints/empty_constraint.py +++ b/src/poetry/core/constraints/generic/empty_constraint.py @@ -1,6 +1,6 @@ from __future__ import annotations -from poetry.core.packages.constraints.base_constraint import BaseConstraint +from poetry.core.constraints.generic.base_constraint import BaseConstraint class EmptyConstraint(BaseConstraint): diff --git a/src/poetry/core/packages/constraints/multi_constraint.py b/src/poetry/core/constraints/generic/multi_constraint.py similarity index 95% rename from src/poetry/core/packages/constraints/multi_constraint.py rename to src/poetry/core/constraints/generic/multi_constraint.py index b62c5876e..0a1f05f8e 100644 --- a/src/poetry/core/packages/constraints/multi_constraint.py +++ b/src/poetry/core/constraints/generic/multi_constraint.py @@ -1,7 +1,7 @@ from __future__ import annotations -from poetry.core.packages.constraints.base_constraint import BaseConstraint -from poetry.core.packages.constraints.constraint import Constraint +from poetry.core.constraints.generic.base_constraint import BaseConstraint +from poetry.core.constraints.generic.constraint import Constraint class MultiConstraint(BaseConstraint): diff --git a/src/poetry/core/constraints/generic/parser.py b/src/poetry/core/constraints/generic/parser.py new file mode 100644 index 000000000..7ef2b1a29 --- /dev/null +++ b/src/poetry/core/constraints/generic/parser.py @@ -0,0 +1,64 @@ +from __future__ import annotations + +import re + +from typing import TYPE_CHECKING + +from poetry.core.constraints.generic.any_constraint import AnyConstraint +from poetry.core.constraints.generic.constraint import Constraint +from poetry.core.constraints.generic.union_constraint import UnionConstraint + + +if TYPE_CHECKING: + from poetry.core.constraints.generic.base_constraint import BaseConstraint + + +BASIC_CONSTRAINT = re.compile(r"^(!?==?)?\s*([^\s]+?)\s*$") + + +def parse_constraint(constraints: str) -> BaseConstraint: + if constraints == "*": + return AnyConstraint() + + or_constraints = re.split(r"\s*\|\|?\s*", constraints.strip()) + or_groups = [] + for constraints in or_constraints: + and_constraints = re.split( + r"(?< ,]) *(? 1: + for constraint in and_constraints: + constraint_objects.append(parse_single_constraint(constraint)) + else: + constraint_objects.append(parse_single_constraint(and_constraints[0])) + + if len(constraint_objects) == 1: + constraint = constraint_objects[0] + else: + constraint = constraint_objects[0] + for next_constraint in constraint_objects[1:]: + constraint = constraint.intersect(next_constraint) + + or_groups.append(constraint) + + if len(or_groups) == 1: + return or_groups[0] + else: + return UnionConstraint(*or_groups) + + +def parse_single_constraint(constraint: str) -> Constraint: + # Basic comparator + m = BASIC_CONSTRAINT.match(constraint) + if m: + op = m.group(1) + if op is None: + op = "==" + + version = m.group(2).strip() + + return Constraint(version, op) + + raise ValueError(f"Could not parse version constraint: {constraint}") diff --git a/src/poetry/core/packages/constraints/union_constraint.py b/src/poetry/core/constraints/generic/union_constraint.py similarity index 93% rename from src/poetry/core/packages/constraints/union_constraint.py rename to src/poetry/core/constraints/generic/union_constraint.py index dba84af70..8db1bd87c 100644 --- a/src/poetry/core/packages/constraints/union_constraint.py +++ b/src/poetry/core/constraints/generic/union_constraint.py @@ -1,9 +1,9 @@ from __future__ import annotations -from poetry.core.packages.constraints.base_constraint import BaseConstraint -from poetry.core.packages.constraints.constraint import Constraint -from poetry.core.packages.constraints.empty_constraint import EmptyConstraint -from poetry.core.packages.constraints.multi_constraint import MultiConstraint +from poetry.core.constraints.generic.base_constraint import BaseConstraint +from poetry.core.constraints.generic.constraint import Constraint +from poetry.core.constraints.generic.empty_constraint import EmptyConstraint +from poetry.core.constraints.generic.multi_constraint import MultiConstraint class UnionConstraint(BaseConstraint): diff --git a/src/poetry/core/constraints/version/__init__.py b/src/poetry/core/constraints/version/__init__.py new file mode 100644 index 000000000..ad85d819d --- /dev/null +++ b/src/poetry/core/constraints/version/__init__.py @@ -0,0 +1,24 @@ +from __future__ import annotations + +from poetry.core.constraints.version.empty_constraint import EmptyConstraint +from poetry.core.constraints.version.parser import parse_constraint +from poetry.core.constraints.version.util import constraint_regions +from poetry.core.constraints.version.version import Version +from poetry.core.constraints.version.version_constraint import VersionConstraint +from poetry.core.constraints.version.version_range import VersionRange +from poetry.core.constraints.version.version_range_constraint import ( + VersionRangeConstraint, +) +from poetry.core.constraints.version.version_union import VersionUnion + + +__all__ = [ + "EmptyConstraint", + "Version", + "VersionConstraint", + "VersionRange", + "VersionRangeConstraint", + "VersionUnion", + "constraint_regions", + "parse_constraint", +] diff --git a/src/poetry/core/constraints/version/empty_constraint.py b/src/poetry/core/constraints/version/empty_constraint.py new file mode 100644 index 000000000..5fd1733cb --- /dev/null +++ b/src/poetry/core/constraints/version/empty_constraint.py @@ -0,0 +1,53 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from poetry.core.constraints.version.version_constraint import VersionConstraint + + +if TYPE_CHECKING: + from poetry.core.constraints.version.version import Version + from poetry.core.constraints.version.version_range_constraint import ( + VersionRangeConstraint, + ) + + +class EmptyConstraint(VersionConstraint): + def is_empty(self) -> bool: + return True + + def is_any(self) -> bool: + return False + + def is_simple(self) -> bool: + return True + + def allows(self, version: Version) -> bool: + return False + + def allows_all(self, other: VersionConstraint) -> bool: + return other.is_empty() + + def allows_any(self, other: VersionConstraint) -> bool: + return False + + def intersect(self, other: VersionConstraint) -> EmptyConstraint: + return self + + def union(self, other: VersionConstraint) -> VersionConstraint: + return other + + def difference(self, other: VersionConstraint) -> EmptyConstraint: + return self + + def flatten(self) -> list[VersionRangeConstraint]: + return [] + + def __str__(self) -> str: + return "" + + def __eq__(self, other: object) -> bool: + if not isinstance(other, VersionConstraint): + return False + + return other.is_empty() diff --git a/src/poetry/core/constraints/version/exceptions.py b/src/poetry/core/constraints/version/exceptions.py new file mode 100644 index 000000000..d06e56f7c --- /dev/null +++ b/src/poetry/core/constraints/version/exceptions.py @@ -0,0 +1,5 @@ +from __future__ import annotations + + +class ParseConstraintError(ValueError): + pass diff --git a/src/poetry/core/constraints/version/parser.py b/src/poetry/core/constraints/version/parser.py new file mode 100644 index 000000000..6e55b984d --- /dev/null +++ b/src/poetry/core/constraints/version/parser.py @@ -0,0 +1,147 @@ +from __future__ import annotations + +import re + +from typing import TYPE_CHECKING + + +if TYPE_CHECKING: + from poetry.core.constraints.version.version_constraint import VersionConstraint + + +def parse_constraint(constraints: str) -> VersionConstraint: + if constraints == "*": + from poetry.core.constraints.version.version_range import VersionRange + + return VersionRange() + + or_constraints = re.split(r"\s*\|\|?\s*", constraints.strip()) + or_groups = [] + for constraints in or_constraints: + # allow trailing commas for robustness (even though it may not be + # standard-compliant it seems to occur in some packages) + constraints = constraints.rstrip(",").rstrip() + and_constraints = re.split( + "(?< ,]) *(? 1: + for constraint in and_constraints: + constraint_objects.append(parse_single_constraint(constraint)) + else: + constraint_objects.append(parse_single_constraint(and_constraints[0])) + + if len(constraint_objects) == 1: + constraint = constraint_objects[0] + else: + constraint = constraint_objects[0] + for next_constraint in constraint_objects[1:]: + constraint = constraint.intersect(next_constraint) + + or_groups.append(constraint) + + if len(or_groups) == 1: + return or_groups[0] + else: + from poetry.core.constraints.version.version_union import VersionUnion + + return VersionUnion.of(*or_groups) + + +def parse_single_constraint(constraint: str) -> VersionConstraint: + from poetry.core.constraints.version.patterns import BASIC_CONSTRAINT + from poetry.core.constraints.version.patterns import CARET_CONSTRAINT + from poetry.core.constraints.version.patterns import TILDE_CONSTRAINT + from poetry.core.constraints.version.patterns import TILDE_PEP440_CONSTRAINT + from poetry.core.constraints.version.patterns import X_CONSTRAINT + from poetry.core.constraints.version.version import Version + from poetry.core.constraints.version.version_range import VersionRange + from poetry.core.constraints.version.version_union import VersionUnion + + m = re.match(r"(?i)^v?[xX*](\.[xX*])*$", constraint) + if m: + return VersionRange() + + # Tilde range + m = TILDE_CONSTRAINT.match(constraint) + if m: + version = Version.parse(m.group("version")) + high = version.stable.next_minor() + if version.release.precision == 1: + high = version.stable.next_major() + + return VersionRange(version, high, include_min=True) + + # PEP 440 Tilde range (~=) + m = TILDE_PEP440_CONSTRAINT.match(constraint) + if m: + version = Version.parse(m.group("version")) + if version.release.precision == 2: + high = version.stable.next_major() + else: + high = version.stable.next_minor() + + return VersionRange(version, high, include_min=True) + + # Caret range + m = CARET_CONSTRAINT.match(constraint) + if m: + version = Version.parse(m.group("version")) + + return VersionRange(version, version.next_breaking(), include_min=True) + + # X Range + m = X_CONSTRAINT.match(constraint) + if m: + op = m.group("op") + major = int(m.group(2)) + minor = m.group(3) + + if minor is not None: + version = Version.from_parts(major, int(minor), 0) + result: VersionConstraint = VersionRange( + version, version.next_minor(), include_min=True + ) + else: + if major == 0: + result = VersionRange(max=Version.from_parts(1, 0, 0)) + else: + version = Version.from_parts(major, 0, 0) + + result = VersionRange(version, version.next_major(), include_min=True) + + if op == "!=": + result = VersionRange().difference(result) + + return result + + # Basic comparator + m = BASIC_CONSTRAINT.match(constraint) + if m: + op = m.group("op") + version_string = m.group("version") + + if version_string == "dev": + version_string = "0.0-dev" + + try: + version = Version.parse(version_string) + except ValueError: + raise ValueError(f"Could not parse version constraint: {constraint}") + + if op == "<": + return VersionRange(max=version) + if op == "<=": + return VersionRange(max=version, include_max=True) + if op == ">": + return VersionRange(min=version) + if op == ">=": + return VersionRange(min=version, include_min=True) + if op == "!=": + return VersionUnion(VersionRange(max=version), VersionRange(min=version)) + return version + + from poetry.core.constraints.version.exceptions import ParseConstraintError + + raise ParseConstraintError(f"Could not parse version constraint: {constraint}") diff --git a/src/poetry/core/constraints/version/patterns.py b/src/poetry/core/constraints/version/patterns.py new file mode 100644 index 000000000..0dd213cf3 --- /dev/null +++ b/src/poetry/core/constraints/version/patterns.py @@ -0,0 +1,28 @@ +from __future__ import annotations + +import re + +from packaging.version import VERSION_PATTERN + + +COMPLETE_VERSION = re.compile(VERSION_PATTERN, re.VERBOSE | re.IGNORECASE) + +CARET_CONSTRAINT = re.compile( + rf"^\^(?P{VERSION_PATTERN})$", re.VERBOSE | re.IGNORECASE +) +TILDE_CONSTRAINT = re.compile( + rf"^~(?!=)\s*(?P{VERSION_PATTERN})$", re.VERBOSE | re.IGNORECASE +) +TILDE_PEP440_CONSTRAINT = re.compile( + rf"^~=\s*(?P{VERSION_PATTERN})$", re.VERBOSE | re.IGNORECASE +) +X_CONSTRAINT = re.compile( + r"^(?P!=|==)?\s*v?(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:\.[xX*])+$" +) + +# note that we also allow technically incorrect version patterns with astrix (eg: 3.5.*) +# as this is supported by pip and appears in metadata within python packages +BASIC_CONSTRAINT = re.compile( + rf"^(?P<>|!=|>=?|<=?|==?)?\s*(?P{VERSION_PATTERN}|dev)(\.\*)?$", + re.VERBOSE | re.IGNORECASE, +) diff --git a/src/poetry/core/constraints/version/util.py b/src/poetry/core/constraints/version/util.py new file mode 100644 index 000000000..d81d11cd1 --- /dev/null +++ b/src/poetry/core/constraints/version/util.py @@ -0,0 +1,58 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from poetry.core.constraints.version.version_range import VersionRange + + +if TYPE_CHECKING: + from poetry.core.constraints.version.version_constraint import VersionConstraint + + +def constraint_regions(constraints: list[VersionConstraint]) -> list[VersionRange]: + """ + Transform a list of VersionConstraints into a list of VersionRanges that mark out + the distinct regions of version-space. + + eg input >=3.6 and >=2.7,<3.0.0 || >=3.4.0 + output <2.7, >=2.7,<3.0.0, >=3.0.0,<3.4.0, >=3.4.0,<3.6, >=3.6. + """ + flattened = [] + for constraint in constraints: + flattened += constraint.flatten() + + mins = { + (constraint.min, not constraint.include_min) + for constraint in flattened + if constraint.min is not None + } + maxs = { + (constraint.max, constraint.include_max) + for constraint in flattened + if constraint.max is not None + } + + edges = sorted(mins | maxs) + if not edges: + return [VersionRange(None, None)] + + start = edges[0] + regions = [ + VersionRange(None, start[0], include_max=start[1]), + ] + + for low, high in zip(edges, edges[1:]): + version_range = VersionRange( + low[0], + high[0], + include_min=not low[1], + include_max=high[1], + ) + regions.append(version_range) + + end = edges[-1] + regions.append( + VersionRange(end[0], None, include_min=not end[1]), + ) + + return regions diff --git a/src/poetry/core/constraints/version/version.py b/src/poetry/core/constraints/version/version.py new file mode 100644 index 000000000..6f84ed6b4 --- /dev/null +++ b/src/poetry/core/constraints/version/version.py @@ -0,0 +1,184 @@ +from __future__ import annotations + +import dataclasses + +from typing import TYPE_CHECKING + +from poetry.core.constraints.version.empty_constraint import EmptyConstraint +from poetry.core.constraints.version.version_range_constraint import ( + VersionRangeConstraint, +) +from poetry.core.constraints.version.version_union import VersionUnion +from poetry.core.version.pep440 import Release +from poetry.core.version.pep440.version import PEP440Version + + +if TYPE_CHECKING: + from poetry.core.constraints.version.version_constraint import VersionConstraint + from poetry.core.version.pep440 import LocalSegmentType + from poetry.core.version.pep440 import ReleaseTag + + +@dataclasses.dataclass(frozen=True) +class Version(PEP440Version, VersionRangeConstraint): + """ + A version constraint representing a single version. + """ + + @property + def precision(self) -> int: + return self.release.precision + + @property + def stable(self) -> Version: + if self.is_stable(): + return self + + post = self.post if self.pre is None else None + return Version(release=self.release, post=post, epoch=self.epoch) + + def next_breaking(self) -> Version: + if self.major > 0 or self.minor is None: + return self.stable.next_major() + + if self.minor > 0 or self.patch is None: + return self.stable.next_minor() + + return self.stable.next_patch() + + @property + def min(self) -> Version: + return self + + @property + def max(self) -> Version: + return self + + @property + def full_max(self) -> Version: + return self + + @property + def include_min(self) -> bool: + return True + + @property + def include_max(self) -> bool: + return True + + def is_any(self) -> bool: + return False + + def is_empty(self) -> bool: + return False + + def is_simple(self) -> bool: + return True + + def allows(self, version: Version | None) -> bool: + if version is None: + return False + + _this, _other = self, version + + # allow weak equality to allow `3.0.0+local.1` for `3.0.0` + if not _this.is_local() and _other.is_local(): + _other = _other.without_local() + + return _this == _other + + def allows_all(self, other: VersionConstraint) -> bool: + return other.is_empty() or ( + self.allows(other) if isinstance(other, self.__class__) else other == self + ) + + def allows_any(self, other: VersionConstraint) -> bool: + if isinstance(other, Version): + return self.allows(other) + + return other.allows(self) + + def intersect(self, other: VersionConstraint) -> Version | EmptyConstraint: + if other.allows(self): + return self + + if isinstance(other, Version) and self.allows(other): + return other + + return EmptyConstraint() + + def union(self, other: VersionConstraint) -> VersionConstraint: + from poetry.core.constraints.version.version_range import VersionRange + + if other.allows(self): + return other + + if isinstance(other, VersionRangeConstraint): + if self.allows(other.min): + return VersionRange( + other.min, + other.max, + include_min=True, + include_max=other.include_max, + ) + + if self.allows(other.max): + return VersionRange( + other.min, + other.max, + include_min=other.include_min, + include_max=True, + ) + + return VersionUnion.of(self, other) + + def difference(self, other: VersionConstraint) -> Version | EmptyConstraint: + if other.allows(self): + return EmptyConstraint() + + return self + + def flatten(self) -> list[VersionRangeConstraint]: + return [self] + + def __str__(self) -> str: + return self.text + + def __repr__(self) -> str: + return f"" + + def __eq__(self, other: object) -> bool: + from poetry.core.constraints.version.version_range import VersionRange + + if isinstance(other, VersionRange): + return ( + self == other.min + and self == other.max + and (other.include_min or other.include_max) + ) + return super().__eq__(other) + + @classmethod + def from_parts( + cls, + major: int, + minor: int | None = None, + patch: int | None = None, + extra: int | tuple[int, ...] = (), + pre: ReleaseTag | None = None, + post: ReleaseTag | None = None, + dev: ReleaseTag | None = None, + local: LocalSegmentType = None, + *, + epoch: int = 0, + ) -> Version: + if isinstance(extra, int): + extra = (extra,) + return cls( + release=Release(major=major, minor=minor, patch=patch, extra=extra), + pre=pre, + post=post, + dev=dev, + local=local, + epoch=epoch, + ) diff --git a/src/poetry/core/constraints/version/version_constraint.py b/src/poetry/core/constraints/version/version_constraint.py new file mode 100644 index 000000000..089a9b094 --- /dev/null +++ b/src/poetry/core/constraints/version/version_constraint.py @@ -0,0 +1,53 @@ +from __future__ import annotations + +from abc import abstractmethod +from typing import TYPE_CHECKING + + +if TYPE_CHECKING: + from poetry.core.constraints.version.version import Version + from poetry.core.constraints.version.version_range_constraint import ( + VersionRangeConstraint, + ) + + +class VersionConstraint: + @abstractmethod + def is_empty(self) -> bool: + raise NotImplementedError() + + @abstractmethod + def is_any(self) -> bool: + raise NotImplementedError() + + @abstractmethod + def is_simple(self) -> bool: + raise NotImplementedError() + + @abstractmethod + def allows(self, version: Version) -> bool: + raise NotImplementedError() + + @abstractmethod + def allows_all(self, other: VersionConstraint) -> bool: + raise NotImplementedError() + + @abstractmethod + def allows_any(self, other: VersionConstraint) -> bool: + raise NotImplementedError() + + @abstractmethod + def intersect(self, other: VersionConstraint) -> VersionConstraint: + raise NotImplementedError() + + @abstractmethod + def union(self, other: VersionConstraint) -> VersionConstraint: + raise NotImplementedError() + + @abstractmethod + def difference(self, other: VersionConstraint) -> VersionConstraint: + raise NotImplementedError() + + @abstractmethod + def flatten(self) -> list[VersionRangeConstraint]: + raise NotImplementedError() diff --git a/src/poetry/core/constraints/version/version_range.py b/src/poetry/core/constraints/version/version_range.py new file mode 100644 index 000000000..cbda6fac2 --- /dev/null +++ b/src/poetry/core/constraints/version/version_range.py @@ -0,0 +1,429 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from poetry.core.constraints.version.empty_constraint import EmptyConstraint +from poetry.core.constraints.version.version_range_constraint import ( + VersionRangeConstraint, +) +from poetry.core.constraints.version.version_union import VersionUnion + + +if TYPE_CHECKING: + from poetry.core.constraints.version.version import Version + from poetry.core.constraints.version.version_constraint import VersionConstraint + + +class VersionRange(VersionRangeConstraint): + def __init__( + self, + min: Version | None = None, + max: Version | None = None, + include_min: bool = False, + include_max: bool = False, + always_include_max_prerelease: bool = False, + ) -> None: + full_max = max + if ( + not always_include_max_prerelease + and not include_max + and full_max is not None + and full_max.is_stable() + and not full_max.is_postrelease() + and (min is None or min.is_stable() or min.release != full_max.release) + ): + full_max = full_max.first_prerelease() + + self._min = min + self._max = max + self._full_max = full_max + self._include_min = include_min + self._include_max = include_max + + @property + def min(self) -> Version | None: + return self._min + + @property + def max(self) -> Version | None: + return self._max + + @property + def full_max(self) -> Version | None: + return self._full_max + + @property + def include_min(self) -> bool: + return self._include_min + + @property + def include_max(self) -> bool: + return self._include_max + + def is_empty(self) -> bool: + return False + + def is_any(self) -> bool: + return self._min is None and self._max is None + + def is_simple(self) -> bool: + return self._min is None or self._max is None + + def allows(self, other: Version) -> bool: + if self._min is not None: + if other < self._min: + return False + + if not self._include_min and other == self._min: + return False + + if self.full_max is not None: + _this, _other = self.full_max, other + + if not _this.is_local() and _other.is_local(): + # allow weak equality to allow `3.0.0+local.1` for `<=3.0.0` + _other = _other.without_local() + + if not _this.is_postrelease() and _other.is_postrelease(): + # allow weak equality to allow `3.0.0-1` for `<=3.0.0` + _other = _other.without_postrelease() + + if _other > _this: + return False + + if not self._include_max and _other == _this: + return False + + return True + + def allows_all(self, other: VersionConstraint) -> bool: + from poetry.core.constraints.version.version import Version + + if other.is_empty(): + return True + + if isinstance(other, Version): + return self.allows(other) + + if isinstance(other, VersionUnion): + return all([self.allows_all(constraint) for constraint in other.ranges]) + + if isinstance(other, VersionRangeConstraint): + return not other.allows_lower(self) and not other.allows_higher(self) + + raise ValueError(f"Unknown VersionConstraint type {other}.") + + def allows_any(self, other: VersionConstraint) -> bool: + from poetry.core.constraints.version.version import Version + + if other.is_empty(): + return False + + if isinstance(other, Version): + return self.allows(other) + + if isinstance(other, VersionUnion): + return any([self.allows_any(constraint) for constraint in other.ranges]) + + if isinstance(other, VersionRangeConstraint): + return not other.is_strictly_lower(self) and not other.is_strictly_higher( + self + ) + + raise ValueError(f"Unknown VersionConstraint type {other}.") + + def intersect(self, other: VersionConstraint) -> VersionConstraint: + from poetry.core.constraints.version.version import Version + + if other.is_empty(): + return other + + if isinstance(other, VersionUnion): + return other.intersect(self) + + # A range and a Version just yields the version if it's in the range. + if isinstance(other, Version): + if self.allows(other): + return other + + return EmptyConstraint() + + if not isinstance(other, VersionRangeConstraint): + raise ValueError(f"Unknown VersionConstraint type {other}.") + + if self.allows_lower(other): + if self.is_strictly_lower(other): + return EmptyConstraint() + + intersect_min = other.min + intersect_include_min = other.include_min + else: + if other.is_strictly_lower(self): + return EmptyConstraint() + + intersect_min = self._min + intersect_include_min = self._include_min + + if self.allows_higher(other): + intersect_max = other.max + intersect_include_max = other.include_max + else: + intersect_max = self._max + intersect_include_max = self._include_max + + if intersect_min is None and intersect_max is None: + return VersionRange() + + # If the range is just a single version. + if intersect_min == intersect_max: + # Because we already verified that the lower range isn't strictly + # lower, there must be some overlap. + assert intersect_include_min and intersect_include_max + assert intersect_min is not None + + return intersect_min + + # If we got here, there is an actual range. + return VersionRange( + intersect_min, intersect_max, intersect_include_min, intersect_include_max + ) + + def union(self, other: VersionConstraint) -> VersionConstraint: + from poetry.core.constraints.version.version import Version + + if isinstance(other, Version): + if self.allows(other): + return self + + if other == self.min: + return VersionRange( + self.min, self.max, include_min=True, include_max=self.include_max + ) + + if other == self.max: + return VersionRange( + self.min, self.max, include_min=self.include_min, include_max=True + ) + + return VersionUnion.of(self, other) + + if isinstance(other, VersionRangeConstraint): + # If the two ranges don't overlap, we won't be able to create a single + # VersionRange for both of them. + edges_touch = ( + self.max == other.min and (self.include_max or other.include_min) + ) or (self.min == other.max and (self.include_min or other.include_max)) + + if not edges_touch and not self.allows_any(other): + return VersionUnion.of(self, other) + + if self.allows_lower(other): + union_min = self.min + union_include_min = self.include_min + else: + union_min = other.min + union_include_min = other.include_min + + if self.allows_higher(other): + union_max = self.max + union_include_max = self.include_max + else: + union_max = other.max + union_include_max = other.include_max + + return VersionRange( + union_min, + union_max, + include_min=union_include_min, + include_max=union_include_max, + ) + + return VersionUnion.of(self, other) + + def difference(self, other: VersionConstraint) -> VersionConstraint: + from poetry.core.constraints.version.version import Version + + if other.is_empty(): + return self + + if isinstance(other, Version): + if not self.allows(other): + return self + + if other == self.min: + if not self.include_min: + return self + + return VersionRange(self.min, self.max, False, self.include_max) + + if other == self.max: + if not self.include_max: + return self + + return VersionRange(self.min, self.max, self.include_min, False) + + return VersionUnion.of( + VersionRange(self.min, other, self.include_min, False), + VersionRange(other, self.max, False, self.include_max), + ) + elif isinstance(other, VersionRangeConstraint): + if not self.allows_any(other): + return self + + before: VersionConstraint | None + if not self.allows_lower(other): + before = None + elif self.min == other.min: + before = self.min + else: + before = VersionRange( + self.min, other.min, self.include_min, not other.include_min + ) + + after: VersionConstraint | None + if not self.allows_higher(other): + after = None + elif self.max == other.max: + after = self.max + else: + after = VersionRange( + other.max, self.max, not other.include_max, self.include_max + ) + + if before is None and after is None: + return EmptyConstraint() + + if before is None: + assert after is not None + return after + + if after is None: + return before + + return VersionUnion.of(before, after) + elif isinstance(other, VersionUnion): + ranges: list[VersionRangeConstraint] = [] + current: VersionRangeConstraint = self + + for range in other.ranges: + # Skip any ranges that are strictly lower than [current]. + if range.is_strictly_lower(current): + continue + + # If we reach a range strictly higher than [current], no more ranges + # will be relevant so we can bail early. + if range.is_strictly_higher(current): + break + + difference = current.difference(range) + if difference.is_empty(): + return EmptyConstraint() + elif isinstance(difference, VersionUnion): + # If [range] split [current] in half, we only need to continue + # checking future ranges against the latter half. + ranges.append(difference.ranges[0]) + current = difference.ranges[-1] + else: + assert isinstance(difference, VersionRangeConstraint) + current = difference + + if not ranges: + return current + + return VersionUnion.of(*(ranges + [current])) + + raise ValueError(f"Unknown VersionConstraint type {other}.") + + def flatten(self) -> list[VersionRangeConstraint]: + return [self] + + def __eq__(self, other: object) -> bool: + if not isinstance(other, VersionRangeConstraint): + return False + + return ( + self._min == other.min + and self._max == other.max + and self._include_min == other.include_min + and self._include_max == other.include_max + ) + + def __lt__(self, other: VersionRangeConstraint) -> bool: + return self._cmp(other) < 0 + + def __le__(self, other: VersionRangeConstraint) -> bool: + return self._cmp(other) <= 0 + + def __gt__(self, other: VersionRangeConstraint) -> bool: + return self._cmp(other) > 0 + + def __ge__(self, other: VersionRangeConstraint) -> bool: + return self._cmp(other) >= 0 + + def _cmp(self, other: VersionRangeConstraint) -> int: + if self.min is None: + if other.min is None: + return self._compare_max(other) + + return -1 + elif other.min is None: + return 1 + + if self.min > other.min: + return 1 + elif self.min < other.min: + return -1 + + if self.include_min != other.include_min: + return -1 if self.include_min else 1 + + return self._compare_max(other) + + def _compare_max(self, other: VersionRangeConstraint) -> int: + if self.max is None: + if other.max is None: + return 0 + + return 1 + elif other.max is None: + return -1 + + if self.max > other.max: + return 1 + elif self.max < other.max: + return -1 + + if self.include_max != other.include_max: + return 1 if self.include_max else -1 + + return 0 + + def __str__(self) -> str: + text = "" + + if self.min is not None: + text += ">=" if self.include_min else ">" + text += self.min.text + + if self.max is not None: + if self.min is not None: + text += "," + + op = "<=" if self.include_max else "<" + text += f"{op}{self.max.text}" + + if self.min is None and self.max is None: + return "*" + + return text + + def __repr__(self) -> str: + return f"" + + def __hash__(self) -> int: + return ( + hash(self.min) + ^ hash(self.max) + ^ hash(self.include_min) + ^ hash(self.include_max) + ) diff --git a/src/poetry/core/constraints/version/version_range_constraint.py b/src/poetry/core/constraints/version/version_range_constraint.py new file mode 100644 index 000000000..a77762778 --- /dev/null +++ b/src/poetry/core/constraints/version/version_range_constraint.py @@ -0,0 +1,93 @@ +from __future__ import annotations + +from abc import abstractmethod +from typing import TYPE_CHECKING + +from poetry.core.constraints.version.version_constraint import VersionConstraint + + +if TYPE_CHECKING: + from poetry.core.constraints.version.version import Version + + +class VersionRangeConstraint(VersionConstraint): + @property + @abstractmethod + def min(self) -> Version | None: + raise NotImplementedError() + + @property + @abstractmethod + def max(self) -> Version | None: + raise NotImplementedError() + + @property + @abstractmethod + def full_max(self) -> Version | None: + raise NotImplementedError() + + @property + @abstractmethod + def include_min(self) -> bool: + raise NotImplementedError() + + @property + @abstractmethod + def include_max(self) -> bool: + raise NotImplementedError() + + def allows_lower(self, other: VersionRangeConstraint) -> bool: + if self.min is None: + return other.min is not None + + if other.min is None: + return False + + if self.min < other.min: + return True + + if self.min > other.min: + return False + + return self.include_min and not other.include_min + + def allows_higher(self, other: VersionRangeConstraint) -> bool: + if self.full_max is None: + return other.max is not None + + if other.full_max is None: + return False + + if self.full_max < other.full_max: + return False + + if self.full_max > other.full_max: + return True + + return self.include_max and not other.include_max + + def is_strictly_lower(self, other: VersionRangeConstraint) -> bool: + if self.full_max is None or other.min is None: + return False + + if self.full_max < other.min: + return True + + if self.full_max > other.min: + return False + + return not self.include_max or not other.include_min + + def is_strictly_higher(self, other: VersionRangeConstraint) -> bool: + return other.is_strictly_lower(self) + + def is_adjacent_to(self, other: VersionRangeConstraint) -> bool: + if self.max != other.min: + return False + + return ( + self.include_max + and not other.include_min + or not self.include_max + and other.include_min + ) diff --git a/src/poetry/core/constraints/version/version_union.py b/src/poetry/core/constraints/version/version_union.py new file mode 100644 index 000000000..022aa8b35 --- /dev/null +++ b/src/poetry/core/constraints/version/version_union.py @@ -0,0 +1,425 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from poetry.core.constraints.version.empty_constraint import EmptyConstraint +from poetry.core.constraints.version.version_constraint import VersionConstraint +from poetry.core.constraints.version.version_range_constraint import ( + VersionRangeConstraint, +) + + +if TYPE_CHECKING: + from poetry.core.constraints.version.version import Version + + +class VersionUnion(VersionConstraint): + """ + A version constraint representing a union of multiple disjoint version + ranges. + + An instance of this will only be created if the version can't be represented + as a non-compound value. + """ + + def __init__(self, *ranges: VersionRangeConstraint) -> None: + self._ranges = list(ranges) + + @property + def ranges(self) -> list[VersionRangeConstraint]: + return self._ranges + + @classmethod + def of(cls, *ranges: VersionConstraint) -> VersionConstraint: + from poetry.core.constraints.version.version_range import VersionRange + + flattened: list[VersionRangeConstraint] = [] + for constraint in ranges: + if constraint.is_empty(): + continue + + if isinstance(constraint, VersionUnion): + flattened += constraint.ranges + continue + + assert isinstance(constraint, VersionRangeConstraint) + flattened.append(constraint) + + if not flattened: + return EmptyConstraint() + + if any([constraint.is_any() for constraint in flattened]): + return VersionRange() + + # Only allow Versions and VersionRanges here so we can more easily reason + # about everything in flattened. _EmptyVersions and VersionUnions are + # filtered out above. + for constraint in flattened: + if not isinstance(constraint, VersionRangeConstraint): + raise ValueError(f"Unknown VersionConstraint type {constraint}.") + + flattened.sort() + + merged: list[VersionRangeConstraint] = [] + for constraint in flattened: + # Merge this constraint with the previous one, but only if they touch. + if not merged or ( + not merged[-1].allows_any(constraint) + and not merged[-1].is_adjacent_to(constraint) + ): + merged.append(constraint) + else: + new_constraint = merged[-1].union(constraint) + assert isinstance(new_constraint, VersionRangeConstraint) + merged[-1] = new_constraint + + if len(merged) == 1: + return merged[0] + + return VersionUnion(*merged) + + def is_empty(self) -> bool: + return False + + def is_any(self) -> bool: + return False + + def is_simple(self) -> bool: + return self.excludes_single_version() + + def allows(self, version: Version) -> bool: + return any([constraint.allows(version) for constraint in self._ranges]) + + def allows_all(self, other: VersionConstraint) -> bool: + our_ranges = iter(self._ranges) + their_ranges = iter(other.flatten()) + + our_current_range = next(our_ranges, None) + their_current_range = next(their_ranges, None) + + while our_current_range and their_current_range: + if our_current_range.allows_all(their_current_range): + their_current_range = next(their_ranges, None) + else: + our_current_range = next(our_ranges, None) + + return their_current_range is None + + def allows_any(self, other: VersionConstraint) -> bool: + our_ranges = iter(self._ranges) + their_ranges = iter(other.flatten()) + + our_current_range = next(our_ranges, None) + their_current_range = next(their_ranges, None) + + while our_current_range and their_current_range: + if our_current_range.allows_any(their_current_range): + return True + + if their_current_range.allows_higher(our_current_range): + our_current_range = next(our_ranges, None) + else: + their_current_range = next(their_ranges, None) + + return False + + def intersect(self, other: VersionConstraint) -> VersionConstraint: + our_ranges = iter(self._ranges) + their_ranges = iter(other.flatten()) + new_ranges = [] + + our_current_range = next(our_ranges, None) + their_current_range = next(their_ranges, None) + + while our_current_range and their_current_range: + intersection = our_current_range.intersect(their_current_range) + + if not intersection.is_empty(): + new_ranges.append(intersection) + + if their_current_range.allows_higher(our_current_range): + our_current_range = next(our_ranges, None) + else: + their_current_range = next(their_ranges, None) + + return VersionUnion.of(*new_ranges) + + def union(self, other: VersionConstraint) -> VersionConstraint: + return VersionUnion.of(self, other) + + def difference(self, other: VersionConstraint) -> VersionConstraint: + our_ranges = iter(self._ranges) + their_ranges = iter(other.flatten()) + new_ranges: list[VersionConstraint] = [] + + state = { + "current": next(our_ranges, None), + "their_range": next(their_ranges, None), + } + + def their_next_range() -> bool: + state["their_range"] = next(their_ranges, None) + if state["their_range"]: + return True + + assert state["current"] is not None + new_ranges.append(state["current"]) + our_current = next(our_ranges, None) + while our_current: + new_ranges.append(our_current) + our_current = next(our_ranges, None) + + return False + + def our_next_range(include_current: bool = True) -> bool: + if include_current: + assert state["current"] is not None + new_ranges.append(state["current"]) + + our_current = next(our_ranges, None) + if not our_current: + return False + + state["current"] = our_current + + return True + + while True: + if state["their_range"] is None: + break + + assert state["current"] is not None + if state["their_range"].is_strictly_lower(state["current"]): + if not their_next_range(): + break + + continue + + if state["their_range"].is_strictly_higher(state["current"]): + if not our_next_range(): + break + + continue + + difference = state["current"].difference(state["their_range"]) + if isinstance(difference, VersionUnion): + assert len(difference.ranges) == 2 + new_ranges.append(difference.ranges[0]) + state["current"] = difference.ranges[-1] + + if not their_next_range(): + break + elif difference.is_empty(): + if not our_next_range(False): + break + else: + assert isinstance(difference, VersionRangeConstraint) + state["current"] = difference + + if state["current"].allows_higher(state["their_range"]): + if not their_next_range(): + break + else: + if not our_next_range(): + break + + if not new_ranges: + return EmptyConstraint() + + if len(new_ranges) == 1: + return new_ranges[0] + + return VersionUnion.of(*new_ranges) + + def flatten(self) -> list[VersionRangeConstraint]: + return self.ranges + + 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(): + raise ValueError("Not a valid wildcard range") + + # we assume here that since it is a single exclusion range + # that it is one of "< 2.0.0 || >= 2.1.0" or ">= 2.1.0 || < 2.0.0" + # and the one with the max is the first part + idx_order = (0, 1) if self._ranges[0].max else (1, 0) + one = self._ranges[idx_order[0]].max + assert one is not None + two = self._ranges[idx_order[1]].min + assert two is not None + + # versions can have both semver and non semver parts + parts_one = [ + one.major, + one.minor or 0, + one.patch or 0, + *list(one.non_semver_parts or []), + ] + parts_two = [ + two.major, + two.minor or 0, + two.patch or 0, + *list(two.non_semver_parts or []), + ] + + # we assume here that a wildcard range implies that the part following the + # first part that is different in the second range is the wildcard, this means + # that multiple wildcards are not supported right now. + parts = [] + + for idx, part in enumerate(parts_one): + parts.append(str(part)) + if parts_two[idx] != part: + # since this part is different the next one is the wildcard + # for example, "< 2.0.0 || >= 2.1.0" gets us a wildcard range + # 2.0.* + parts.append("*") + break + else: + # we should not ever get here, however it is likely that poorly + # constructed metadata exists + raise ValueError("Not a valid wildcard range") + + return f"!={'.'.join(parts)}" + + @staticmethod + def _excludes_single_wildcard_range_check_is_valid_range( + one: VersionRangeConstraint, two: VersionRangeConstraint + ) -> bool: + """ + Helper method to determine if two versions define a single wildcard range. + + In cases where !=2.0.* was parsed by us, the union is of the range + <2.0.0 || >=2.1.0. In user defined ranges, precision might be different. + For example, a union <2.0 || >= 2.1.0 is still !=2.0.*. In order to + handle these cases we make sure that if precisions do not match, extra + checks are performed to validate that the constraint is a valid single + wildcard range. + """ + + assert one.max is not None + assert two.min is not None + + max_precision = max(one.max.precision, two.min.precision) + + if max_precision <= 3: + # In cases where both versions have a precision less than 3, + # we can make use of the next major/minor/patch versions. + return two.min in { + one.max.next_major(), + one.max.next_minor(), + one.max.next_patch(), + } + else: + # When there are non-semver parts in one of the versions, we need to + # ensure we use zero padded version and in addition to next major/minor/ + # patch versions, also check each next release for the extra parts. + from_parts = one.max.__class__.from_parts + + _extras: list[list[int]] = [] + _versions: list[Version] = [] + + for _version in [one.max, two.min]: + _extra = list(_version.non_semver_parts or []) + + while len(_extra) < (max_precision - 3): + # pad zeros for extra parts to ensure precisions are equal + _extra.append(0) + + # create a new release with unspecified parts padded with zeros + _padded_version: Version = from_parts( + major=_version.major, + minor=_version.minor or 0, + patch=_version.patch or 0, + extra=tuple(_extra), + ) + + _extras.append(_extra) + _versions.append(_padded_version) + + _extra_one = _extras[0] + _padded_version_one = _versions[0] + _padded_version_two = _versions[1] + + _check_versions = { + _padded_version_one.next_major(), + _padded_version_one.next_minor(), + _padded_version_one.next_patch(), + } + + # for each non-semver (extra) part, bump a version + for idx in range(len(_extra_one)): + _extra = [ + *_extra_one[: idx - 1], + (_extra_one[idx] + 1), + *_extra_one[idx + 1 :], + ] + _check_versions.add( + from_parts( + _padded_version_one.major, + _padded_version_one.minor, + _padded_version_one.patch, + tuple(_extra), + ) + ) + + return _padded_version_two in _check_versions + + def excludes_single_wildcard_range(self) -> bool: + from poetry.core.constraints.version.version_range import VersionRange + + if len(self._ranges) != 2: + return False + + idx_order = (0, 1) if self._ranges[0].max else (1, 0) + one = self._ranges[idx_order[0]] + two = self._ranges[idx_order[1]] + + is_range_exclusion = ( + one.max and not one.include_max and two.min and two.include_min + ) + + if not is_range_exclusion: + return False + + if not self._excludes_single_wildcard_range_check_is_valid_range(one, two): + return False + + return isinstance(VersionRange().difference(self), VersionRange) + + def excludes_single_version(self) -> bool: + from poetry.core.constraints.version.version import Version + from poetry.core.constraints.version.version_range import VersionRange + + return isinstance(VersionRange().difference(self), Version) + + def __eq__(self, other: object) -> bool: + if not isinstance(other, VersionUnion): + return False + + return self._ranges == other.ranges + + def __hash__(self) -> int: + h = hash(self._ranges[0]) + + for range in self._ranges[1:]: + h ^= hash(range) + + return h + + def __str__(self) -> str: + from poetry.core.constraints.version.version_range import VersionRange + + if self.excludes_single_version(): + return f"!={VersionRange().difference(self)}" + + try: + return self._exclude_single_wildcard_range_string() + except ValueError: + return " || ".join([str(r) for r in self._ranges]) + + def __repr__(self) -> str: + return f"" diff --git a/src/poetry/core/factory.py b/src/poetry/core/factory.py index 0698e466f..2e8295e01 100644 --- a/src/poetry/core/factory.py +++ b/src/poetry/core/factory.py @@ -227,9 +227,12 @@ def create_dependency( groups: list[str] | None = None, root_dir: Path | None = None, ) -> Dependency: - from poetry.core.packages.constraints import ( + 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.packages.dependency import Dependency from poetry.core.packages.dependency_group import MAIN_GROUP from poetry.core.packages.directory_dependency import DirectoryDependency @@ -237,7 +240,6 @@ def create_dependency( from poetry.core.packages.url_dependency import URLDependency from poetry.core.packages.utils.utils import create_nested_marker from poetry.core.packages.vcs_dependency import VCSDependency - from poetry.core.semver.helpers import parse_constraint from poetry.core.version.markers import AnyMarker from poetry.core.version.markers import parse_marker @@ -345,7 +347,7 @@ def create_dependency( marker = marker.intersect( parse_marker( create_nested_marker( - "python_version", parse_constraint(python_versions) + "python_version", parse_version_constraint(python_versions) ) ) ) diff --git a/src/poetry/core/masonry/builders/wheel.py b/src/poetry/core/masonry/builders/wheel.py index 705f7f1ed..17625a0ba 100644 --- a/src/poetry/core/masonry/builders/wheel.py +++ b/src/poetry/core/masonry/builders/wheel.py @@ -21,12 +21,12 @@ from packaging.tags import sys_tags from poetry.core import __version__ +from poetry.core.constraints.version import parse_constraint from poetry.core.masonry.builders.builder import Builder from poetry.core.masonry.builders.sdist import SdistBuilder from poetry.core.masonry.utils.helpers import escape_name from poetry.core.masonry.utils.helpers import normalize_file_permissions from poetry.core.masonry.utils.package_include import PackageInclude -from poetry.core.semver.helpers import parse_constraint if TYPE_CHECKING: diff --git a/src/poetry/core/packages/constraints/__init__.py b/src/poetry/core/packages/constraints/__init__.py index d61a2cb79..585f2219d 100644 --- a/src/poetry/core/packages/constraints/__init__.py +++ b/src/poetry/core/packages/constraints/__init__.py @@ -1,64 +1,23 @@ from __future__ import annotations -import re - -from poetry.core.packages.constraints.any_constraint import AnyConstraint -from poetry.core.packages.constraints.base_constraint import BaseConstraint -from poetry.core.packages.constraints.constraint import Constraint -from poetry.core.packages.constraints.empty_constraint import EmptyConstraint -from poetry.core.packages.constraints.multi_constraint import MultiConstraint -from poetry.core.packages.constraints.union_constraint import UnionConstraint - - -BASIC_CONSTRAINT = re.compile(r"^(!?==?)?\s*([^\s]+?)\s*$") - - -def parse_constraint(constraints: str) -> BaseConstraint: - if constraints == "*": - return AnyConstraint() - - or_constraints = re.split(r"\s*\|\|?\s*", constraints.strip()) - or_groups = [] - for constraints in or_constraints: - and_constraints = re.split( - r"(?< ,]) *(? 1: - for constraint in and_constraints: - constraint_objects.append(parse_single_constraint(constraint)) - else: - constraint_objects.append(parse_single_constraint(and_constraints[0])) - - if len(constraint_objects) == 1: - constraint = constraint_objects[0] - else: - constraint = constraint_objects[0] - for next_constraint in constraint_objects[1:]: - constraint = constraint.intersect(next_constraint) - - or_groups.append(constraint) - - if len(or_groups) == 1: - return or_groups[0] - else: - return UnionConstraint(*or_groups) - - -def parse_single_constraint(constraint: str) -> Constraint: - # Basic comparator - m = BASIC_CONSTRAINT.match(constraint) - if m: - op = m.group(1) - if op is None: - op = "==" - - version = m.group(2).strip() - - return Constraint(version, op) - - raise ValueError(f"Could not parse version constraint: {constraint}") +import warnings + +from poetry.core.constraints.generic import AnyConstraint +from poetry.core.constraints.generic import BaseConstraint +from poetry.core.constraints.generic import Constraint +from poetry.core.constraints.generic import EmptyConstraint +from poetry.core.constraints.generic import MultiConstraint +from poetry.core.constraints.generic import UnionConstraint +from poetry.core.constraints.generic import parse_constraint +from poetry.core.constraints.generic.parser import parse_single_constraint + + +warnings.warn( + "poetry.core.packages.constraints is deprecated." + " Use poetry.core.constraints.generic instead.", + DeprecationWarning, + stacklevel=2, +) __all__ = [ diff --git a/src/poetry/core/packages/dependency.py b/src/poetry/core/packages/dependency.py index 6f97f2ad0..e75edf319 100644 --- a/src/poetry/core/packages/dependency.py +++ b/src/poetry/core/packages/dependency.py @@ -10,25 +10,23 @@ from typing import Iterable from typing import TypeVar -from poetry.core.packages.constraints import ( - parse_constraint as parse_generic_constraint, -) +from poetry.core.constraints.generic import parse_constraint as parse_generic_constraint +from poetry.core.constraints.version import VersionRangeConstraint +from poetry.core.constraints.version import parse_constraint from poetry.core.packages.dependency_group import MAIN_GROUP from poetry.core.packages.specification import PackageSpecification from poetry.core.packages.utils.utils import contains_group_without_marker from poetry.core.packages.utils.utils import create_nested_marker from poetry.core.packages.utils.utils import normalize_python_version_markers -from poetry.core.semver.helpers import parse_constraint -from poetry.core.semver.version_range_constraint import VersionRangeConstraint from poetry.core.version.markers import parse_marker if TYPE_CHECKING: from packaging.utils import NormalizedName + from poetry.core.constraints.version import VersionConstraint from poetry.core.packages.directory_dependency import DirectoryDependency from poetry.core.packages.file_dependency import FileDependency - from poetry.core.semver.version_constraint import VersionConstraint from poetry.core.version.markers import BaseMarker T = TypeVar("T", bound="Dependency") @@ -167,8 +165,8 @@ def marker(self) -> BaseMarker: @marker.setter def marker(self, marker: str | BaseMarker) -> None: + from poetry.core.constraints.version import parse_constraint from poetry.core.packages.utils.utils import convert_markers - from poetry.core.semver.helpers import parse_constraint from poetry.core.version.markers import BaseMarker from poetry.core.version.markers import parse_marker @@ -230,8 +228,8 @@ def in_extras(self) -> list[str]: @property def base_pep_508_name(self) -> str: - from poetry.core.semver.version import Version - from poetry.core.semver.version_union import VersionUnion + from poetry.core.constraints.version import Version + from poetry.core.constraints.version import VersionUnion requirement = self.pretty_name diff --git a/src/poetry/core/packages/package.py b/src/poetry/core/packages/package.py index 8c51f6fc6..11f7cc93e 100644 --- a/src/poetry/core/packages/package.py +++ b/src/poetry/core/packages/package.py @@ -11,20 +11,20 @@ from typing import Iterator from typing import TypeVar +from poetry.core.constraints.version import parse_constraint from poetry.core.packages.dependency_group import MAIN_GROUP from poetry.core.packages.specification import PackageSpecification from poetry.core.packages.utils.utils import create_nested_marker -from poetry.core.semver.helpers import parse_constraint from poetry.core.version.markers import parse_marker if TYPE_CHECKING: from packaging.utils import NormalizedName + from poetry.core.constraints.version import Version + from poetry.core.constraints.version import VersionConstraint from poetry.core.packages.dependency import Dependency from poetry.core.packages.dependency_group import DependencyGroup - from poetry.core.semver.version import Version - from poetry.core.semver.version_constraint import VersionConstraint from poetry.core.spdx.license import License from poetry.core.version.markers import BaseMarker @@ -211,7 +211,7 @@ def all_requires( def _set_version( self, version: str | Version, pretty_version: str | None = None ) -> None: - from poetry.core.semver.version import Version + from poetry.core.constraints.version import Version if not isinstance(version, Version): self._version = Version.parse(version) @@ -290,7 +290,7 @@ def license(self, value: str | License | None) -> None: @property def all_classifiers(self) -> list[str]: - from poetry.core.semver.version import Version + from poetry.core.constraints.version import Version classifiers = copy.copy(self.classifiers) diff --git a/src/poetry/core/packages/project_package.py b/src/poetry/core/packages/project_package.py index 1d6f5505f..3af4eb16e 100644 --- a/src/poetry/core/packages/project_package.py +++ b/src/poetry/core/packages/project_package.py @@ -3,13 +3,13 @@ from typing import TYPE_CHECKING from typing import Any -from poetry.core.semver.helpers import parse_constraint +from poetry.core.constraints.version import parse_constraint from poetry.core.version.markers import parse_marker if TYPE_CHECKING: from poetry.core.packages.dependency import Dependency - from poetry.core.semver.version import Version + from poetry.core.constraints.version import Version from poetry.core.packages.package import Package from poetry.core.packages.utils.utils import create_nested_marker diff --git a/src/poetry/core/packages/utils/utils.py b/src/poetry/core/packages/utils/utils.py index 3e9bcab97..4b4853ca7 100644 --- a/src/poetry/core/packages/utils/utils.py +++ b/src/poetry/core/packages/utils/utils.py @@ -14,16 +14,16 @@ from urllib.parse import urlsplit from urllib.request import url2pathname +from poetry.core.constraints.version import Version +from poetry.core.constraints.version import VersionRange +from poetry.core.constraints.version import parse_constraint from poetry.core.pyproject.toml import PyProjectTOML -from poetry.core.semver.helpers import parse_constraint -from poetry.core.semver.version import Version -from poetry.core.semver.version_range import VersionRange from poetry.core.version.markers import dnf if TYPE_CHECKING: - from poetry.core.packages.constraints import BaseConstraint - from poetry.core.semver.version_constraint import VersionConstraint + from poetry.core.constraints.generic import BaseConstraint + from poetry.core.constraints.version import VersionConstraint from poetry.core.version.markers import BaseMarker # Even though we've `from __future__ import annotations`, mypy doesn't seem to like @@ -203,10 +203,10 @@ def create_nested_marker( name: str, constraint: BaseConstraint | VersionConstraint, ) -> str: - from poetry.core.packages.constraints.constraint import Constraint - from poetry.core.packages.constraints.multi_constraint import MultiConstraint - from poetry.core.packages.constraints.union_constraint import UnionConstraint - from poetry.core.semver.version_union import VersionUnion + from poetry.core.constraints.generic import Constraint + from poetry.core.constraints.generic import MultiConstraint + from poetry.core.constraints.generic import UnionConstraint + from poetry.core.constraints.version import VersionUnion if constraint.is_any(): return "" @@ -297,8 +297,8 @@ def create_nested_marker( def get_python_constraint_from_marker( marker: BaseMarker, ) -> VersionConstraint: - from poetry.core.semver.empty_constraint import EmptyConstraint - from poetry.core.semver.version_range import VersionRange + from poetry.core.constraints.version import EmptyConstraint + from poetry.core.constraints.version import VersionRange python_marker = marker.only("python_version", "python_full_version") if python_marker.is_any(): diff --git a/src/poetry/core/semver/__init__.py b/src/poetry/core/semver/__init__.py index e69de29bb..be3955935 100644 --- a/src/poetry/core/semver/__init__.py +++ b/src/poetry/core/semver/__init__.py @@ -0,0 +1,10 @@ +from __future__ import annotations + +import warnings + + +warnings.warn( + "poetry.core.semver is deprecated. Use poetry.core.constraints.version instead.", + DeprecationWarning, + stacklevel=2, +) diff --git a/src/poetry/core/semver/empty_constraint.py b/src/poetry/core/semver/empty_constraint.py index 5cd7ea73b..8559866dc 100644 --- a/src/poetry/core/semver/empty_constraint.py +++ b/src/poetry/core/semver/empty_constraint.py @@ -1,51 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from poetry.core.constraints.version import EmptyConstraint -from poetry.core.semver.version_constraint import VersionConstraint - -if TYPE_CHECKING: - from poetry.core.semver.version import Version - from poetry.core.semver.version_range_constraint import VersionRangeConstraint - - -class EmptyConstraint(VersionConstraint): - def is_empty(self) -> bool: - return True - - def is_any(self) -> bool: - return False - - def is_simple(self) -> bool: - return True - - def allows(self, version: Version) -> bool: - return False - - def allows_all(self, other: VersionConstraint) -> bool: - return other.is_empty() - - def allows_any(self, other: VersionConstraint) -> bool: - return False - - def intersect(self, other: VersionConstraint) -> EmptyConstraint: - return self - - def union(self, other: VersionConstraint) -> VersionConstraint: - return other - - def difference(self, other: VersionConstraint) -> EmptyConstraint: - return self - - def flatten(self) -> list[VersionRangeConstraint]: - return [] - - def __str__(self) -> str: - return "" - - def __eq__(self, other: object) -> bool: - if not isinstance(other, VersionConstraint): - return False - - return other.is_empty() +__all__ = ["EmptyConstraint"] diff --git a/src/poetry/core/semver/exceptions.py b/src/poetry/core/semver/exceptions.py index d06e56f7c..7f7bfaef8 100644 --- a/src/poetry/core/semver/exceptions.py +++ b/src/poetry/core/semver/exceptions.py @@ -1,5 +1,6 @@ from __future__ import annotations +from poetry.core.constraints.version.exceptions import ParseConstraintError -class ParseConstraintError(ValueError): - pass + +__all__ = ["ParseConstraintError"] diff --git a/src/poetry/core/semver/helpers.py b/src/poetry/core/semver/helpers.py index 0ce090d89..065d366f3 100644 --- a/src/poetry/core/semver/helpers.py +++ b/src/poetry/core/semver/helpers.py @@ -1,147 +1,7 @@ from __future__ import annotations -import re +from poetry.core.constraints.version.parser import parse_constraint +from poetry.core.constraints.version.parser import parse_single_constraint -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from poetry.core.semver.version_constraint import VersionConstraint - - -def parse_constraint(constraints: str) -> VersionConstraint: - if constraints == "*": - from poetry.core.semver.version_range import VersionRange - - return VersionRange() - - or_constraints = re.split(r"\s*\|\|?\s*", constraints.strip()) - or_groups = [] - for constraints in or_constraints: - # allow trailing commas for robustness (even though it may not be - # standard-compliant it seems to occur in some packages) - constraints = constraints.rstrip(",").rstrip() - and_constraints = re.split( - "(?< ,]) *(? 1: - for constraint in and_constraints: - constraint_objects.append(parse_single_constraint(constraint)) - else: - constraint_objects.append(parse_single_constraint(and_constraints[0])) - - if len(constraint_objects) == 1: - constraint = constraint_objects[0] - else: - constraint = constraint_objects[0] - for next_constraint in constraint_objects[1:]: - constraint = constraint.intersect(next_constraint) - - or_groups.append(constraint) - - if len(or_groups) == 1: - return or_groups[0] - else: - from poetry.core.semver.version_union import VersionUnion - - return VersionUnion.of(*or_groups) - - -def parse_single_constraint(constraint: str) -> VersionConstraint: - from poetry.core.semver.patterns import BASIC_CONSTRAINT - from poetry.core.semver.patterns import CARET_CONSTRAINT - from poetry.core.semver.patterns import TILDE_CONSTRAINT - from poetry.core.semver.patterns import TILDE_PEP440_CONSTRAINT - from poetry.core.semver.patterns import X_CONSTRAINT - from poetry.core.semver.version import Version - from poetry.core.semver.version_range import VersionRange - from poetry.core.semver.version_union import VersionUnion - - m = re.match(r"(?i)^v?[xX*](\.[xX*])*$", constraint) - if m: - return VersionRange() - - # Tilde range - m = TILDE_CONSTRAINT.match(constraint) - if m: - version = Version.parse(m.group("version")) - high = version.stable.next_minor() - if version.release.precision == 1: - high = version.stable.next_major() - - return VersionRange(version, high, include_min=True) - - # PEP 440 Tilde range (~=) - m = TILDE_PEP440_CONSTRAINT.match(constraint) - if m: - version = Version.parse(m.group("version")) - if version.release.precision == 2: - high = version.stable.next_major() - else: - high = version.stable.next_minor() - - return VersionRange(version, high, include_min=True) - - # Caret range - m = CARET_CONSTRAINT.match(constraint) - if m: - version = Version.parse(m.group("version")) - - return VersionRange(version, version.next_breaking(), include_min=True) - - # X Range - m = X_CONSTRAINT.match(constraint) - if m: - op = m.group("op") - major = int(m.group(2)) - minor = m.group(3) - - if minor is not None: - version = Version.from_parts(major, int(minor), 0) - result: VersionConstraint = VersionRange( - version, version.next_minor(), include_min=True - ) - else: - if major == 0: - result = VersionRange(max=Version.from_parts(1, 0, 0)) - else: - version = Version.from_parts(major, 0, 0) - - result = VersionRange(version, version.next_major(), include_min=True) - - if op == "!=": - result = VersionRange().difference(result) - - return result - - # Basic comparator - m = BASIC_CONSTRAINT.match(constraint) - if m: - op = m.group("op") - version_string = m.group("version") - - if version_string == "dev": - version_string = "0.0-dev" - - try: - version = Version.parse(version_string) - except ValueError: - raise ValueError(f"Could not parse version constraint: {constraint}") - - if op == "<": - return VersionRange(max=version) - if op == "<=": - return VersionRange(max=version, include_max=True) - if op == ">": - return VersionRange(min=version) - if op == ">=": - return VersionRange(min=version, include_min=True) - if op == "!=": - return VersionUnion(VersionRange(max=version), VersionRange(min=version)) - return version - - from poetry.core.semver.exceptions import ParseConstraintError - - raise ParseConstraintError(f"Could not parse version constraint: {constraint}") +__all__ = ["parse_constraint", "parse_single_constraint"] diff --git a/src/poetry/core/semver/patterns.py b/src/poetry/core/semver/patterns.py index 0dd213cf3..b2d865859 100644 --- a/src/poetry/core/semver/patterns.py +++ b/src/poetry/core/semver/patterns.py @@ -1,28 +1,18 @@ from __future__ import annotations -import re +from poetry.core.constraints.version.patterns import BASIC_CONSTRAINT +from poetry.core.constraints.version.patterns import CARET_CONSTRAINT +from poetry.core.constraints.version.patterns import COMPLETE_VERSION +from poetry.core.constraints.version.patterns import TILDE_CONSTRAINT +from poetry.core.constraints.version.patterns import TILDE_PEP440_CONSTRAINT +from poetry.core.constraints.version.patterns import X_CONSTRAINT -from packaging.version import VERSION_PATTERN - -COMPLETE_VERSION = re.compile(VERSION_PATTERN, re.VERBOSE | re.IGNORECASE) - -CARET_CONSTRAINT = re.compile( - rf"^\^(?P{VERSION_PATTERN})$", re.VERBOSE | re.IGNORECASE -) -TILDE_CONSTRAINT = re.compile( - rf"^~(?!=)\s*(?P{VERSION_PATTERN})$", re.VERBOSE | re.IGNORECASE -) -TILDE_PEP440_CONSTRAINT = re.compile( - rf"^~=\s*(?P{VERSION_PATTERN})$", re.VERBOSE | re.IGNORECASE -) -X_CONSTRAINT = re.compile( - r"^(?P!=|==)?\s*v?(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:\.[xX*])+$" -) - -# note that we also allow technically incorrect version patterns with astrix (eg: 3.5.*) -# as this is supported by pip and appears in metadata within python packages -BASIC_CONSTRAINT = re.compile( - rf"^(?P<>|!=|>=?|<=?|==?)?\s*(?P{VERSION_PATTERN}|dev)(\.\*)?$", - re.VERBOSE | re.IGNORECASE, -) +__all__ = [ + "COMPLETE_VERSION", + "CARET_CONSTRAINT", + "TILDE_CONSTRAINT", + "TILDE_PEP440_CONSTRAINT", + "X_CONSTRAINT", + "BASIC_CONSTRAINT", +] diff --git a/src/poetry/core/semver/util.py b/src/poetry/core/semver/util.py index b70e85df4..eabcc06b0 100644 --- a/src/poetry/core/semver/util.py +++ b/src/poetry/core/semver/util.py @@ -1,58 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from poetry.core.constraints.version import constraint_regions -from poetry.core.semver.version_range import VersionRange - -if TYPE_CHECKING: - from poetry.core.semver.version_constraint import VersionConstraint - - -def constraint_regions(constraints: list[VersionConstraint]) -> list[VersionRange]: - """ - Transform a list of VersionConstraints into a list of VersionRanges that mark out - the distinct regions of version-space. - - eg input >=3.6 and >=2.7,<3.0.0 || >=3.4.0 - output <2.7, >=2.7,<3.0.0, >=3.0.0,<3.4.0, >=3.4.0,<3.6, >=3.6. - """ - flattened = [] - for constraint in constraints: - flattened += constraint.flatten() - - mins = { - (constraint.min, not constraint.include_min) - for constraint in flattened - if constraint.min is not None - } - maxs = { - (constraint.max, constraint.include_max) - for constraint in flattened - if constraint.max is not None - } - - edges = sorted(mins | maxs) - if not edges: - return [VersionRange(None, None)] - - start = edges[0] - regions = [ - VersionRange(None, start[0], include_max=start[1]), - ] - - for low, high in zip(edges, edges[1:]): - version_range = VersionRange( - low[0], - high[0], - include_min=not low[1], - include_max=high[1], - ) - regions.append(version_range) - - end = edges[-1] - regions.append( - VersionRange(end[0], None, include_min=not end[1]), - ) - - return regions +__all__ = ["constraint_regions"] diff --git a/src/poetry/core/semver/version.py b/src/poetry/core/semver/version.py index 66a1bbe5a..d4ec91afb 100644 --- a/src/poetry/core/semver/version.py +++ b/src/poetry/core/semver/version.py @@ -1,182 +1,6 @@ from __future__ import annotations -import dataclasses +from poetry.core.constraints.version import Version -from typing import TYPE_CHECKING -from poetry.core.semver.empty_constraint import EmptyConstraint -from poetry.core.semver.version_range_constraint import VersionRangeConstraint -from poetry.core.semver.version_union import VersionUnion -from poetry.core.version.pep440 import Release -from poetry.core.version.pep440.version import PEP440Version - - -if TYPE_CHECKING: - from poetry.core.semver.version_constraint import VersionConstraint - from poetry.core.version.pep440 import LocalSegmentType - from poetry.core.version.pep440 import ReleaseTag - - -@dataclasses.dataclass(frozen=True) -class Version(PEP440Version, VersionRangeConstraint): - """ - A version constraint representing a single version. - """ - - @property - def precision(self) -> int: - return self.release.precision - - @property - def stable(self) -> Version: - if self.is_stable(): - return self - - post = self.post if self.pre is None else None - return Version(release=self.release, post=post, epoch=self.epoch) - - def next_breaking(self) -> Version: - if self.major > 0 or self.minor is None: - return self.stable.next_major() - - if self.minor > 0 or self.patch is None: - return self.stable.next_minor() - - return self.stable.next_patch() - - @property - def min(self) -> Version: - return self - - @property - def max(self) -> Version: - return self - - @property - def full_max(self) -> Version: - return self - - @property - def include_min(self) -> bool: - return True - - @property - def include_max(self) -> bool: - return True - - def is_any(self) -> bool: - return False - - def is_empty(self) -> bool: - return False - - def is_simple(self) -> bool: - return True - - def allows(self, version: Version | None) -> bool: - if version is None: - return False - - _this, _other = self, version - - # allow weak equality to allow `3.0.0+local.1` for `3.0.0` - if not _this.is_local() and _other.is_local(): - _other = _other.without_local() - - return _this == _other - - def allows_all(self, other: VersionConstraint) -> bool: - return other.is_empty() or ( - self.allows(other) if isinstance(other, self.__class__) else other == self - ) - - def allows_any(self, other: VersionConstraint) -> bool: - if isinstance(other, Version): - return self.allows(other) - - return other.allows(self) - - def intersect(self, other: VersionConstraint) -> Version | EmptyConstraint: - if other.allows(self): - return self - - if isinstance(other, Version) and self.allows(other): - return other - - return EmptyConstraint() - - def union(self, other: VersionConstraint) -> VersionConstraint: - from poetry.core.semver.version_range import VersionRange - - if other.allows(self): - return other - - if isinstance(other, VersionRangeConstraint): - if self.allows(other.min): - return VersionRange( - other.min, - other.max, - include_min=True, - include_max=other.include_max, - ) - - if self.allows(other.max): - return VersionRange( - other.min, - other.max, - include_min=other.include_min, - include_max=True, - ) - - return VersionUnion.of(self, other) - - def difference(self, other: VersionConstraint) -> Version | EmptyConstraint: - if other.allows(self): - return EmptyConstraint() - - return self - - def flatten(self) -> list[VersionRangeConstraint]: - return [self] - - def __str__(self) -> str: - return self.text - - def __repr__(self) -> str: - return f"" - - def __eq__(self, other: object) -> bool: - from poetry.core.semver.version_range import VersionRange - - if isinstance(other, VersionRange): - return ( - self == other.min - and self == other.max - and (other.include_min or other.include_max) - ) - return super().__eq__(other) - - @classmethod - def from_parts( - cls, - major: int, - minor: int | None = None, - patch: int | None = None, - extra: int | tuple[int, ...] = (), - pre: ReleaseTag | None = None, - post: ReleaseTag | None = None, - dev: ReleaseTag | None = None, - local: LocalSegmentType = None, - *, - epoch: int = 0, - ) -> Version: - if isinstance(extra, int): - extra = (extra,) - return cls( - release=Release(major=major, minor=minor, patch=patch, extra=extra), - pre=pre, - post=post, - dev=dev, - local=local, - epoch=epoch, - ) +__all__ = ["Version"] diff --git a/src/poetry/core/semver/version_constraint.py b/src/poetry/core/semver/version_constraint.py index 8a35a33d0..8cc5d9478 100644 --- a/src/poetry/core/semver/version_constraint.py +++ b/src/poetry/core/semver/version_constraint.py @@ -1,51 +1,6 @@ from __future__ import annotations -from abc import abstractmethod -from typing import TYPE_CHECKING +from poetry.core.constraints.version import VersionConstraint -if TYPE_CHECKING: - from poetry.core.semver.version import Version - from poetry.core.semver.version_range_constraint import VersionRangeConstraint - - -class VersionConstraint: - @abstractmethod - def is_empty(self) -> bool: - raise NotImplementedError() - - @abstractmethod - def is_any(self) -> bool: - raise NotImplementedError() - - @abstractmethod - def is_simple(self) -> bool: - raise NotImplementedError() - - @abstractmethod - def allows(self, version: Version) -> bool: - raise NotImplementedError() - - @abstractmethod - def allows_all(self, other: VersionConstraint) -> bool: - raise NotImplementedError() - - @abstractmethod - def allows_any(self, other: VersionConstraint) -> bool: - raise NotImplementedError() - - @abstractmethod - def intersect(self, other: VersionConstraint) -> VersionConstraint: - raise NotImplementedError() - - @abstractmethod - def union(self, other: VersionConstraint) -> VersionConstraint: - raise NotImplementedError() - - @abstractmethod - def difference(self, other: VersionConstraint) -> VersionConstraint: - raise NotImplementedError() - - @abstractmethod - def flatten(self) -> list[VersionRangeConstraint]: - raise NotImplementedError() +__all__ = ["VersionConstraint"] diff --git a/src/poetry/core/semver/version_range.py b/src/poetry/core/semver/version_range.py index 3a6aff195..76117c885 100644 --- a/src/poetry/core/semver/version_range.py +++ b/src/poetry/core/semver/version_range.py @@ -1,427 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from poetry.core.constraints.version import VersionRange -from poetry.core.semver.empty_constraint import EmptyConstraint -from poetry.core.semver.version_range_constraint import VersionRangeConstraint -from poetry.core.semver.version_union import VersionUnion - -if TYPE_CHECKING: - from poetry.core.semver.version import Version - from poetry.core.semver.version_constraint import VersionConstraint - - -class VersionRange(VersionRangeConstraint): - def __init__( - self, - min: Version | None = None, - max: Version | None = None, - include_min: bool = False, - include_max: bool = False, - always_include_max_prerelease: bool = False, - ) -> None: - full_max = max - if ( - not always_include_max_prerelease - and not include_max - and full_max is not None - and full_max.is_stable() - and not full_max.is_postrelease() - and (min is None or min.is_stable() or min.release != full_max.release) - ): - full_max = full_max.first_prerelease() - - self._min = min - self._max = max - self._full_max = full_max - self._include_min = include_min - self._include_max = include_max - - @property - def min(self) -> Version | None: - return self._min - - @property - def max(self) -> Version | None: - return self._max - - @property - def full_max(self) -> Version | None: - return self._full_max - - @property - def include_min(self) -> bool: - return self._include_min - - @property - def include_max(self) -> bool: - return self._include_max - - def is_empty(self) -> bool: - return False - - def is_any(self) -> bool: - return self._min is None and self._max is None - - def is_simple(self) -> bool: - return self._min is None or self._max is None - - def allows(self, other: Version) -> bool: - if self._min is not None: - if other < self._min: - return False - - if not self._include_min and other == self._min: - return False - - if self.full_max is not None: - _this, _other = self.full_max, other - - if not _this.is_local() and _other.is_local(): - # allow weak equality to allow `3.0.0+local.1` for `<=3.0.0` - _other = _other.without_local() - - if not _this.is_postrelease() and _other.is_postrelease(): - # allow weak equality to allow `3.0.0-1` for `<=3.0.0` - _other = _other.without_postrelease() - - if _other > _this: - return False - - if not self._include_max and _other == _this: - return False - - return True - - def allows_all(self, other: VersionConstraint) -> bool: - from poetry.core.semver.version import Version - - if other.is_empty(): - return True - - if isinstance(other, Version): - return self.allows(other) - - if isinstance(other, VersionUnion): - return all([self.allows_all(constraint) for constraint in other.ranges]) - - if isinstance(other, VersionRangeConstraint): - return not other.allows_lower(self) and not other.allows_higher(self) - - raise ValueError(f"Unknown VersionConstraint type {other}.") - - def allows_any(self, other: VersionConstraint) -> bool: - from poetry.core.semver.version import Version - - if other.is_empty(): - return False - - if isinstance(other, Version): - return self.allows(other) - - if isinstance(other, VersionUnion): - return any([self.allows_any(constraint) for constraint in other.ranges]) - - if isinstance(other, VersionRangeConstraint): - return not other.is_strictly_lower(self) and not other.is_strictly_higher( - self - ) - - raise ValueError(f"Unknown VersionConstraint type {other}.") - - def intersect(self, other: VersionConstraint) -> VersionConstraint: - from poetry.core.semver.version import Version - - if other.is_empty(): - return other - - if isinstance(other, VersionUnion): - return other.intersect(self) - - # A range and a Version just yields the version if it's in the range. - if isinstance(other, Version): - if self.allows(other): - return other - - return EmptyConstraint() - - if not isinstance(other, VersionRangeConstraint): - raise ValueError(f"Unknown VersionConstraint type {other}.") - - if self.allows_lower(other): - if self.is_strictly_lower(other): - return EmptyConstraint() - - intersect_min = other.min - intersect_include_min = other.include_min - else: - if other.is_strictly_lower(self): - return EmptyConstraint() - - intersect_min = self._min - intersect_include_min = self._include_min - - if self.allows_higher(other): - intersect_max = other.max - intersect_include_max = other.include_max - else: - intersect_max = self._max - intersect_include_max = self._include_max - - if intersect_min is None and intersect_max is None: - return VersionRange() - - # If the range is just a single version. - if intersect_min == intersect_max: - # Because we already verified that the lower range isn't strictly - # lower, there must be some overlap. - assert intersect_include_min and intersect_include_max - assert intersect_min is not None - - return intersect_min - - # If we got here, there is an actual range. - return VersionRange( - intersect_min, intersect_max, intersect_include_min, intersect_include_max - ) - - def union(self, other: VersionConstraint) -> VersionConstraint: - from poetry.core.semver.version import Version - - if isinstance(other, Version): - if self.allows(other): - return self - - if other == self.min: - return VersionRange( - self.min, self.max, include_min=True, include_max=self.include_max - ) - - if other == self.max: - return VersionRange( - self.min, self.max, include_min=self.include_min, include_max=True - ) - - return VersionUnion.of(self, other) - - if isinstance(other, VersionRangeConstraint): - # If the two ranges don't overlap, we won't be able to create a single - # VersionRange for both of them. - edges_touch = ( - self.max == other.min and (self.include_max or other.include_min) - ) or (self.min == other.max and (self.include_min or other.include_max)) - - if not edges_touch and not self.allows_any(other): - return VersionUnion.of(self, other) - - if self.allows_lower(other): - union_min = self.min - union_include_min = self.include_min - else: - union_min = other.min - union_include_min = other.include_min - - if self.allows_higher(other): - union_max = self.max - union_include_max = self.include_max - else: - union_max = other.max - union_include_max = other.include_max - - return VersionRange( - union_min, - union_max, - include_min=union_include_min, - include_max=union_include_max, - ) - - return VersionUnion.of(self, other) - - def difference(self, other: VersionConstraint) -> VersionConstraint: - from poetry.core.semver.version import Version - - if other.is_empty(): - return self - - if isinstance(other, Version): - if not self.allows(other): - return self - - if other == self.min: - if not self.include_min: - return self - - return VersionRange(self.min, self.max, False, self.include_max) - - if other == self.max: - if not self.include_max: - return self - - return VersionRange(self.min, self.max, self.include_min, False) - - return VersionUnion.of( - VersionRange(self.min, other, self.include_min, False), - VersionRange(other, self.max, False, self.include_max), - ) - elif isinstance(other, VersionRangeConstraint): - if not self.allows_any(other): - return self - - before: VersionConstraint | None - if not self.allows_lower(other): - before = None - elif self.min == other.min: - before = self.min - else: - before = VersionRange( - self.min, other.min, self.include_min, not other.include_min - ) - - after: VersionConstraint | None - if not self.allows_higher(other): - after = None - elif self.max == other.max: - after = self.max - else: - after = VersionRange( - other.max, self.max, not other.include_max, self.include_max - ) - - if before is None and after is None: - return EmptyConstraint() - - if before is None: - assert after is not None - return after - - if after is None: - return before - - return VersionUnion.of(before, after) - elif isinstance(other, VersionUnion): - ranges: list[VersionRangeConstraint] = [] - current: VersionRangeConstraint = self - - for range in other.ranges: - # Skip any ranges that are strictly lower than [current]. - if range.is_strictly_lower(current): - continue - - # If we reach a range strictly higher than [current], no more ranges - # will be relevant so we can bail early. - if range.is_strictly_higher(current): - break - - difference = current.difference(range) - if difference.is_empty(): - return EmptyConstraint() - elif isinstance(difference, VersionUnion): - # If [range] split [current] in half, we only need to continue - # checking future ranges against the latter half. - ranges.append(difference.ranges[0]) - current = difference.ranges[-1] - else: - assert isinstance(difference, VersionRangeConstraint) - current = difference - - if not ranges: - return current - - return VersionUnion.of(*(ranges + [current])) - - raise ValueError(f"Unknown VersionConstraint type {other}.") - - def flatten(self) -> list[VersionRangeConstraint]: - return [self] - - def __eq__(self, other: object) -> bool: - if not isinstance(other, VersionRangeConstraint): - return False - - return ( - self._min == other.min - and self._max == other.max - and self._include_min == other.include_min - and self._include_max == other.include_max - ) - - def __lt__(self, other: VersionRangeConstraint) -> bool: - return self._cmp(other) < 0 - - def __le__(self, other: VersionRangeConstraint) -> bool: - return self._cmp(other) <= 0 - - def __gt__(self, other: VersionRangeConstraint) -> bool: - return self._cmp(other) > 0 - - def __ge__(self, other: VersionRangeConstraint) -> bool: - return self._cmp(other) >= 0 - - def _cmp(self, other: VersionRangeConstraint) -> int: - if self.min is None: - if other.min is None: - return self._compare_max(other) - - return -1 - elif other.min is None: - return 1 - - if self.min > other.min: - return 1 - elif self.min < other.min: - return -1 - - if self.include_min != other.include_min: - return -1 if self.include_min else 1 - - return self._compare_max(other) - - def _compare_max(self, other: VersionRangeConstraint) -> int: - if self.max is None: - if other.max is None: - return 0 - - return 1 - elif other.max is None: - return -1 - - if self.max > other.max: - return 1 - elif self.max < other.max: - return -1 - - if self.include_max != other.include_max: - return 1 if self.include_max else -1 - - return 0 - - def __str__(self) -> str: - text = "" - - if self.min is not None: - text += ">=" if self.include_min else ">" - text += self.min.text - - if self.max is not None: - if self.min is not None: - text += "," - - op = "<=" if self.include_max else "<" - text += f"{op}{self.max.text}" - - if self.min is None and self.max is None: - return "*" - - return text - - def __repr__(self) -> str: - return f"" - - def __hash__(self) -> int: - return ( - hash(self.min) - ^ hash(self.max) - ^ hash(self.include_min) - ^ hash(self.include_max) - ) +__all__ = ["VersionRange"] diff --git a/src/poetry/core/semver/version_range_constraint.py b/src/poetry/core/semver/version_range_constraint.py index 03a431c69..7af781cd5 100644 --- a/src/poetry/core/semver/version_range_constraint.py +++ b/src/poetry/core/semver/version_range_constraint.py @@ -1,93 +1,6 @@ from __future__ import annotations -from abc import abstractmethod -from typing import TYPE_CHECKING +from poetry.core.constraints.version import VersionRangeConstraint -from poetry.core.semver.version_constraint import VersionConstraint - -if TYPE_CHECKING: - from poetry.core.semver.version import Version - - -class VersionRangeConstraint(VersionConstraint): - @property - @abstractmethod - def min(self) -> Version | None: - raise NotImplementedError() - - @property - @abstractmethod - def max(self) -> Version | None: - raise NotImplementedError() - - @property - @abstractmethod - def full_max(self) -> Version | None: - raise NotImplementedError() - - @property - @abstractmethod - def include_min(self) -> bool: - raise NotImplementedError() - - @property - @abstractmethod - def include_max(self) -> bool: - raise NotImplementedError() - - def allows_lower(self, other: VersionRangeConstraint) -> bool: - if self.min is None: - return other.min is not None - - if other.min is None: - return False - - if self.min < other.min: - return True - - if self.min > other.min: - return False - - return self.include_min and not other.include_min - - def allows_higher(self, other: VersionRangeConstraint) -> bool: - if self.full_max is None: - return other.max is not None - - if other.full_max is None: - return False - - if self.full_max < other.full_max: - return False - - if self.full_max > other.full_max: - return True - - return self.include_max and not other.include_max - - def is_strictly_lower(self, other: VersionRangeConstraint) -> bool: - if self.full_max is None or other.min is None: - return False - - if self.full_max < other.min: - return True - - if self.full_max > other.min: - return False - - return not self.include_max or not other.include_min - - def is_strictly_higher(self, other: VersionRangeConstraint) -> bool: - return other.is_strictly_lower(self) - - def is_adjacent_to(self, other: VersionRangeConstraint) -> bool: - if self.max != other.min: - return False - - return ( - self.include_max - and not other.include_min - or not self.include_max - and other.include_min - ) +__all__ = ["VersionRangeConstraint"] diff --git a/src/poetry/core/semver/version_union.py b/src/poetry/core/semver/version_union.py index 26edb7426..567c17cff 100644 --- a/src/poetry/core/semver/version_union.py +++ b/src/poetry/core/semver/version_union.py @@ -1,423 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from poetry.core.constraints.version import VersionUnion -from poetry.core.semver.empty_constraint import EmptyConstraint -from poetry.core.semver.version_constraint import VersionConstraint -from poetry.core.semver.version_range_constraint import VersionRangeConstraint - -if TYPE_CHECKING: - from poetry.core.semver.version import Version - - -class VersionUnion(VersionConstraint): - """ - A version constraint representing a union of multiple disjoint version - ranges. - - An instance of this will only be created if the version can't be represented - as a non-compound value. - """ - - def __init__(self, *ranges: VersionRangeConstraint) -> None: - self._ranges = list(ranges) - - @property - def ranges(self) -> list[VersionRangeConstraint]: - return self._ranges - - @classmethod - def of(cls, *ranges: VersionConstraint) -> VersionConstraint: - from poetry.core.semver.version_range import VersionRange - - flattened: list[VersionRangeConstraint] = [] - for constraint in ranges: - if constraint.is_empty(): - continue - - if isinstance(constraint, VersionUnion): - flattened += constraint.ranges - continue - - assert isinstance(constraint, VersionRangeConstraint) - flattened.append(constraint) - - if not flattened: - return EmptyConstraint() - - if any([constraint.is_any() for constraint in flattened]): - return VersionRange() - - # Only allow Versions and VersionRanges here so we can more easily reason - # about everything in flattened. _EmptyVersions and VersionUnions are - # filtered out above. - for constraint in flattened: - if not isinstance(constraint, VersionRangeConstraint): - raise ValueError(f"Unknown VersionConstraint type {constraint}.") - - flattened.sort() - - merged: list[VersionRangeConstraint] = [] - for constraint in flattened: - # Merge this constraint with the previous one, but only if they touch. - if not merged or ( - not merged[-1].allows_any(constraint) - and not merged[-1].is_adjacent_to(constraint) - ): - merged.append(constraint) - else: - new_constraint = merged[-1].union(constraint) - assert isinstance(new_constraint, VersionRangeConstraint) - merged[-1] = new_constraint - - if len(merged) == 1: - return merged[0] - - return VersionUnion(*merged) - - def is_empty(self) -> bool: - return False - - def is_any(self) -> bool: - return False - - def is_simple(self) -> bool: - return self.excludes_single_version() - - def allows(self, version: Version) -> bool: - return any([constraint.allows(version) for constraint in self._ranges]) - - def allows_all(self, other: VersionConstraint) -> bool: - our_ranges = iter(self._ranges) - their_ranges = iter(other.flatten()) - - our_current_range = next(our_ranges, None) - their_current_range = next(their_ranges, None) - - while our_current_range and their_current_range: - if our_current_range.allows_all(their_current_range): - their_current_range = next(their_ranges, None) - else: - our_current_range = next(our_ranges, None) - - return their_current_range is None - - def allows_any(self, other: VersionConstraint) -> bool: - our_ranges = iter(self._ranges) - their_ranges = iter(other.flatten()) - - our_current_range = next(our_ranges, None) - their_current_range = next(their_ranges, None) - - while our_current_range and their_current_range: - if our_current_range.allows_any(their_current_range): - return True - - if their_current_range.allows_higher(our_current_range): - our_current_range = next(our_ranges, None) - else: - their_current_range = next(their_ranges, None) - - return False - - def intersect(self, other: VersionConstraint) -> VersionConstraint: - our_ranges = iter(self._ranges) - their_ranges = iter(other.flatten()) - new_ranges = [] - - our_current_range = next(our_ranges, None) - their_current_range = next(their_ranges, None) - - while our_current_range and their_current_range: - intersection = our_current_range.intersect(their_current_range) - - if not intersection.is_empty(): - new_ranges.append(intersection) - - if their_current_range.allows_higher(our_current_range): - our_current_range = next(our_ranges, None) - else: - their_current_range = next(their_ranges, None) - - return VersionUnion.of(*new_ranges) - - def union(self, other: VersionConstraint) -> VersionConstraint: - return VersionUnion.of(self, other) - - def difference(self, other: VersionConstraint) -> VersionConstraint: - our_ranges = iter(self._ranges) - their_ranges = iter(other.flatten()) - new_ranges: list[VersionConstraint] = [] - - state = { - "current": next(our_ranges, None), - "their_range": next(their_ranges, None), - } - - def their_next_range() -> bool: - state["their_range"] = next(their_ranges, None) - if state["their_range"]: - return True - - assert state["current"] is not None - new_ranges.append(state["current"]) - our_current = next(our_ranges, None) - while our_current: - new_ranges.append(our_current) - our_current = next(our_ranges, None) - - return False - - def our_next_range(include_current: bool = True) -> bool: - if include_current: - assert state["current"] is not None - new_ranges.append(state["current"]) - - our_current = next(our_ranges, None) - if not our_current: - return False - - state["current"] = our_current - - return True - - while True: - if state["their_range"] is None: - break - - assert state["current"] is not None - if state["their_range"].is_strictly_lower(state["current"]): - if not their_next_range(): - break - - continue - - if state["their_range"].is_strictly_higher(state["current"]): - if not our_next_range(): - break - - continue - - difference = state["current"].difference(state["their_range"]) - if isinstance(difference, VersionUnion): - assert len(difference.ranges) == 2 - new_ranges.append(difference.ranges[0]) - state["current"] = difference.ranges[-1] - - if not their_next_range(): - break - elif difference.is_empty(): - if not our_next_range(False): - break - else: - assert isinstance(difference, VersionRangeConstraint) - state["current"] = difference - - if state["current"].allows_higher(state["their_range"]): - if not their_next_range(): - break - else: - if not our_next_range(): - break - - if not new_ranges: - return EmptyConstraint() - - if len(new_ranges) == 1: - return new_ranges[0] - - return VersionUnion.of(*new_ranges) - - def flatten(self) -> list[VersionRangeConstraint]: - return self.ranges - - 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(): - raise ValueError("Not a valid wildcard range") - - # we assume here that since it is a single exclusion range - # that it is one of "< 2.0.0 || >= 2.1.0" or ">= 2.1.0 || < 2.0.0" - # and the one with the max is the first part - idx_order = (0, 1) if self._ranges[0].max else (1, 0) - one = self._ranges[idx_order[0]].max - assert one is not None - two = self._ranges[idx_order[1]].min - assert two is not None - - # versions can have both semver and non semver parts - parts_one = [ - one.major, - one.minor or 0, - one.patch or 0, - *list(one.non_semver_parts or []), - ] - parts_two = [ - two.major, - two.minor or 0, - two.patch or 0, - *list(two.non_semver_parts or []), - ] - - # we assume here that a wildcard range implies that the part following the - # first part that is different in the second range is the wildcard, this means - # that multiple wildcards are not supported right now. - parts = [] - - for idx, part in enumerate(parts_one): - parts.append(str(part)) - if parts_two[idx] != part: - # since this part is different the next one is the wildcard - # for example, "< 2.0.0 || >= 2.1.0" gets us a wildcard range - # 2.0.* - parts.append("*") - break - else: - # we should not ever get here, however it is likely that poorly - # constructed metadata exists - raise ValueError("Not a valid wildcard range") - - return f"!={'.'.join(parts)}" - - @staticmethod - def _excludes_single_wildcard_range_check_is_valid_range( - one: VersionRangeConstraint, two: VersionRangeConstraint - ) -> bool: - """ - Helper method to determine if two versions define a single wildcard range. - - In cases where !=2.0.* was parsed by us, the union is of the range - <2.0.0 || >=2.1.0. In user defined ranges, precision might be different. - For example, a union <2.0 || >= 2.1.0 is still !=2.0.*. In order to - handle these cases we make sure that if precisions do not match, extra - checks are performed to validate that the constraint is a valid single - wildcard range. - """ - - assert one.max is not None - assert two.min is not None - - max_precision = max(one.max.precision, two.min.precision) - - if max_precision <= 3: - # In cases where both versions have a precision less than 3, - # we can make use of the next major/minor/patch versions. - return two.min in { - one.max.next_major(), - one.max.next_minor(), - one.max.next_patch(), - } - else: - # When there are non-semver parts in one of the versions, we need to - # ensure we use zero padded version and in addition to next major/minor/ - # patch versions, also check each next release for the extra parts. - from_parts = one.max.__class__.from_parts - - _extras: list[list[int]] = [] - _versions: list[Version] = [] - - for _version in [one.max, two.min]: - _extra = list(_version.non_semver_parts or []) - - while len(_extra) < (max_precision - 3): - # pad zeros for extra parts to ensure precisions are equal - _extra.append(0) - - # create a new release with unspecified parts padded with zeros - _padded_version: Version = from_parts( - major=_version.major, - minor=_version.minor or 0, - patch=_version.patch or 0, - extra=tuple(_extra), - ) - - _extras.append(_extra) - _versions.append(_padded_version) - - _extra_one = _extras[0] - _padded_version_one = _versions[0] - _padded_version_two = _versions[1] - - _check_versions = { - _padded_version_one.next_major(), - _padded_version_one.next_minor(), - _padded_version_one.next_patch(), - } - - # for each non-semver (extra) part, bump a version - for idx in range(len(_extra_one)): - _extra = [ - *_extra_one[: idx - 1], - (_extra_one[idx] + 1), - *_extra_one[idx + 1 :], - ] - _check_versions.add( - from_parts( - _padded_version_one.major, - _padded_version_one.minor, - _padded_version_one.patch, - tuple(_extra), - ) - ) - - return _padded_version_two in _check_versions - - def excludes_single_wildcard_range(self) -> bool: - from poetry.core.semver.version_range import VersionRange - - if len(self._ranges) != 2: - return False - - idx_order = (0, 1) if self._ranges[0].max else (1, 0) - one = self._ranges[idx_order[0]] - two = self._ranges[idx_order[1]] - - is_range_exclusion = ( - one.max and not one.include_max and two.min and two.include_min - ) - - if not is_range_exclusion: - return False - - if not self._excludes_single_wildcard_range_check_is_valid_range(one, two): - return False - - return isinstance(VersionRange().difference(self), VersionRange) - - def excludes_single_version(self) -> bool: - from poetry.core.semver.version import Version - from poetry.core.semver.version_range import VersionRange - - return isinstance(VersionRange().difference(self), Version) - - def __eq__(self, other: object) -> bool: - if not isinstance(other, VersionUnion): - return False - - return self._ranges == other.ranges - - def __hash__(self) -> int: - h = hash(self._ranges[0]) - - for range in self._ranges[1:]: - h ^= hash(range) - - return h - - def __str__(self) -> str: - from poetry.core.semver.version_range import VersionRange - - if self.excludes_single_version(): - return f"!={VersionRange().difference(self)}" - - try: - return self._exclude_single_wildcard_range_string() - except ValueError: - return " || ".join([str(r) for r in self._ranges]) - - def __repr__(self) -> str: - return f"" +__all__ = ["VersionUnion"] diff --git a/src/poetry/core/version/helpers.py b/src/poetry/core/version/helpers.py index 435d32aff..eafac8fd9 100644 --- a/src/poetry/core/version/helpers.py +++ b/src/poetry/core/version/helpers.py @@ -2,13 +2,13 @@ from typing import TYPE_CHECKING -from poetry.core.semver.helpers import parse_constraint -from poetry.core.semver.version import Version -from poetry.core.semver.version_union import VersionUnion +from poetry.core.constraints.version import Version +from poetry.core.constraints.version import VersionUnion +from poetry.core.constraints.version import parse_constraint if TYPE_CHECKING: - from poetry.core.semver.version_constraint import VersionConstraint + from poetry.core.constraints.version import VersionConstraint PYTHON_VERSION = [ "2.7.*", diff --git a/src/poetry/core/version/markers.py b/src/poetry/core/version/markers.py index abf3cc28b..cb2d4fcb5 100644 --- a/src/poetry/core/version/markers.py +++ b/src/poetry/core/version/markers.py @@ -8,7 +8,7 @@ from typing import Callable from typing import Iterable -from poetry.core.semver.version_constraint import VersionConstraint +from poetry.core.constraints.version import VersionConstraint from poetry.core.version.grammars import GRAMMAR_PEP_508_MARKERS from poetry.core.version.parser import Parser @@ -16,7 +16,7 @@ if TYPE_CHECKING: from lark import Tree - from poetry.core.packages.constraints import BaseConstraint + from poetry.core.constraints.generic import BaseConstraint class InvalidMarker(ValueError): @@ -184,10 +184,12 @@ class SingleMarker(BaseMarker): def __init__( self, name: str, constraint: str | BaseConstraint | VersionConstraint ) -> None: - from poetry.core.packages.constraints import ( + from poetry.core.constraints.generic import ( parse_constraint as parse_generic_constraint, ) - from poetry.core.semver.helpers import parse_constraint + from poetry.core.constraints.version import ( + parse_constraint as parse_version_constraint, + ) self._constraint: BaseConstraint | VersionConstraint self._parser: Callable[[str], BaseConstraint | VersionConstraint] @@ -207,7 +209,7 @@ def __init__( self._parser = parse_generic_constraint if name in self._VERSION_LIKE_MARKER_NAME: - self._parser = parse_constraint + self._parser = parse_version_constraint if self._operator in {"in", "not in"}: versions = [] @@ -321,9 +323,7 @@ def invert(self) -> BaseMarker: # This one is more tricky to handle # since it's technically a multi marker # so the inverse will be a union of inverse - from poetry.core.semver.version_range_constraint import ( - VersionRangeConstraint, - ) + from poetry.core.constraints.version import VersionRangeConstraint if not isinstance(self._constraint, VersionRangeConstraint): # The constraint must be a version range, otherwise diff --git a/src/poetry/core/version/requirements.py b/src/poetry/core/version/requirements.py index b20de2ffd..cadc28f7e 100644 --- a/src/poetry/core/version/requirements.py +++ b/src/poetry/core/version/requirements.py @@ -2,8 +2,8 @@ import urllib.parse as urlparse -from poetry.core.semver.exceptions import ParseConstraintError -from poetry.core.semver.helpers import parse_constraint +from poetry.core.constraints.version import parse_constraint +from poetry.core.constraints.version.exceptions import ParseConstraintError from poetry.core.version.grammars import GRAMMAR_PEP_508_CONSTRAINTS from poetry.core.version.markers import _compact_markers from poetry.core.version.parser import Parser diff --git a/tests/semver/__init__.py b/tests/constraints/__init__.py similarity index 100% rename from tests/semver/__init__.py rename to tests/constraints/__init__.py diff --git a/tests/constraints/generic/__init__.py b/tests/constraints/generic/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/packages/constraints/test_constraint.py b/tests/constraints/generic/test_constraint.py similarity index 92% rename from tests/packages/constraints/test_constraint.py rename to tests/constraints/generic/test_constraint.py index 27d3c089e..c96ef4d98 100644 --- a/tests/packages/constraints/test_constraint.py +++ b/tests/constraints/generic/test_constraint.py @@ -4,15 +4,15 @@ import pytest -from poetry.core.packages.constraints import AnyConstraint -from poetry.core.packages.constraints.constraint import Constraint -from poetry.core.packages.constraints.empty_constraint import EmptyConstraint -from poetry.core.packages.constraints.multi_constraint import MultiConstraint -from poetry.core.packages.constraints.union_constraint import UnionConstraint +from poetry.core.constraints.generic import AnyConstraint +from poetry.core.constraints.generic import Constraint +from poetry.core.constraints.generic import EmptyConstraint +from poetry.core.constraints.generic import MultiConstraint +from poetry.core.constraints.generic import UnionConstraint if TYPE_CHECKING: - from poetry.core.packages.constraints import BaseConstraint + from poetry.core.constraints.generic import BaseConstraint def test_allows() -> None: diff --git a/tests/packages/constraints/test_main.py b/tests/constraints/generic/test_main.py similarity index 80% rename from tests/packages/constraints/test_main.py rename to tests/constraints/generic/test_main.py index e95c04cad..2707fd92f 100644 --- a/tests/packages/constraints/test_main.py +++ b/tests/constraints/generic/test_main.py @@ -2,11 +2,11 @@ import pytest -from poetry.core.packages.constraints import parse_constraint -from poetry.core.packages.constraints.any_constraint import AnyConstraint -from poetry.core.packages.constraints.constraint import Constraint -from poetry.core.packages.constraints.multi_constraint import MultiConstraint -from poetry.core.packages.constraints.union_constraint import UnionConstraint +from poetry.core.constraints.generic import AnyConstraint +from poetry.core.constraints.generic import Constraint +from poetry.core.constraints.generic import MultiConstraint +from poetry.core.constraints.generic import UnionConstraint +from poetry.core.constraints.generic import parse_constraint @pytest.mark.parametrize( diff --git a/tests/packages/constraints/test_multi_constraint.py b/tests/constraints/generic/test_multi_constraint.py similarity index 89% rename from tests/packages/constraints/test_multi_constraint.py rename to tests/constraints/generic/test_multi_constraint.py index 5d8cb36b6..583305d86 100644 --- a/tests/packages/constraints/test_multi_constraint.py +++ b/tests/constraints/generic/test_multi_constraint.py @@ -1,7 +1,7 @@ from __future__ import annotations -from poetry.core.packages.constraints.constraint import Constraint -from poetry.core.packages.constraints.multi_constraint import MultiConstraint +from poetry.core.constraints.generic import Constraint +from poetry.core.constraints.generic import MultiConstraint def test_allows() -> None: diff --git a/tests/packages/constraints/test_union_constraint.py b/tests/constraints/generic/test_union_constraint.py similarity index 87% rename from tests/packages/constraints/test_union_constraint.py rename to tests/constraints/generic/test_union_constraint.py index 6545dae5b..59d3a9d32 100644 --- a/tests/packages/constraints/test_union_constraint.py +++ b/tests/constraints/generic/test_union_constraint.py @@ -1,7 +1,7 @@ from __future__ import annotations -from poetry.core.packages.constraints.constraint import Constraint -from poetry.core.packages.constraints.union_constraint import UnionConstraint +from poetry.core.constraints.generic import Constraint +from poetry.core.constraints.generic import UnionConstraint def test_allows() -> None: diff --git a/tests/constraints/version/__init__.py b/tests/constraints/version/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/semver/test_helpers.py b/tests/constraints/version/test_helpers.py similarity index 98% rename from tests/semver/test_helpers.py rename to tests/constraints/version/test_helpers.py index 41d0cab30..a0f79627e 100644 --- a/tests/semver/test_helpers.py +++ b/tests/constraints/version/test_helpers.py @@ -2,10 +2,10 @@ import pytest -from poetry.core.semver.helpers import parse_constraint -from poetry.core.semver.version import Version -from poetry.core.semver.version_range import VersionRange -from poetry.core.semver.version_union import VersionUnion +from poetry.core.constraints.version import Version +from poetry.core.constraints.version import VersionRange +from poetry.core.constraints.version import VersionUnion +from poetry.core.constraints.version import parse_constraint from poetry.core.version.pep440 import ReleaseTag diff --git a/tests/semver/test_parse_constraint.py b/tests/constraints/version/test_parse_constraint.py similarity index 96% rename from tests/semver/test_parse_constraint.py rename to tests/constraints/version/test_parse_constraint.py index ae6f2d3a1..c27a7b2cf 100644 --- a/tests/semver/test_parse_constraint.py +++ b/tests/constraints/version/test_parse_constraint.py @@ -2,10 +2,10 @@ import pytest -from poetry.core.semver.helpers import parse_constraint -from poetry.core.semver.version import Version -from poetry.core.semver.version_range import VersionRange -from poetry.core.semver.version_union import VersionUnion +from poetry.core.constraints.version import Version +from poetry.core.constraints.version import VersionRange +from poetry.core.constraints.version import VersionUnion +from poetry.core.constraints.version import parse_constraint from poetry.core.version.pep440 import ReleaseTag diff --git a/tests/semver/test_utils.py b/tests/constraints/version/test_utils.py similarity index 89% rename from tests/semver/test_utils.py rename to tests/constraints/version/test_utils.py index 413cac69a..f562c8798 100644 --- a/tests/semver/test_utils.py +++ b/tests/constraints/version/test_utils.py @@ -4,14 +4,14 @@ import pytest -from poetry.core.semver.empty_constraint import EmptyConstraint -from poetry.core.semver.util import constraint_regions -from poetry.core.semver.version import Version -from poetry.core.semver.version_range import VersionRange +from poetry.core.constraints.version import EmptyConstraint +from poetry.core.constraints.version import Version +from poetry.core.constraints.version import VersionRange +from poetry.core.constraints.version import constraint_regions if TYPE_CHECKING: - from poetry.core.semver.version_constraint import VersionConstraint + from poetry.core.constraints.version import VersionConstraint PY27 = Version.parse("2.7") diff --git a/tests/semver/test_version.py b/tests/constraints/version/test_version.py similarity index 98% rename from tests/semver/test_version.py rename to tests/constraints/version/test_version.py index a1c333552..c8469a400 100644 --- a/tests/semver/test_version.py +++ b/tests/constraints/version/test_version.py @@ -4,15 +4,15 @@ import pytest -from poetry.core.semver.empty_constraint import EmptyConstraint -from poetry.core.semver.version import Version -from poetry.core.semver.version_range import VersionRange +from poetry.core.constraints.version import EmptyConstraint +from poetry.core.constraints.version import Version +from poetry.core.constraints.version import VersionRange from poetry.core.version.exceptions import InvalidVersion from poetry.core.version.pep440 import ReleaseTag if TYPE_CHECKING: - from poetry.core.semver.version_constraint import VersionConstraint + from poetry.core.constraints.version import VersionConstraint @pytest.mark.parametrize( diff --git a/tests/semver/test_version_range.py b/tests/constraints/version/test_version_range.py similarity index 98% rename from tests/semver/test_version_range.py rename to tests/constraints/version/test_version_range.py index 9cd414530..fcac2477d 100644 --- a/tests/semver/test_version_range.py +++ b/tests/constraints/version/test_version_range.py @@ -2,9 +2,9 @@ import pytest -from poetry.core.semver.empty_constraint import EmptyConstraint -from poetry.core.semver.version import Version -from poetry.core.semver.version_range import VersionRange +from poetry.core.constraints.version import EmptyConstraint +from poetry.core.constraints.version import Version +from poetry.core.constraints.version import VersionRange @pytest.fixture() diff --git a/tests/packages/test_dependency.py b/tests/packages/test_dependency.py index bdcb3aef6..a2aef3840 100644 --- a/tests/packages/test_dependency.py +++ b/tests/packages/test_dependency.py @@ -2,8 +2,8 @@ import pytest +from poetry.core.constraints.version.exceptions import ParseConstraintError from poetry.core.packages.dependency import Dependency -from poetry.core.semver.exceptions import ParseConstraintError from poetry.core.version.markers import parse_marker diff --git a/tests/packages/test_main.py b/tests/packages/test_main.py index 0c43597c2..6f9c31557 100644 --- a/tests/packages/test_main.py +++ b/tests/packages/test_main.py @@ -2,10 +2,10 @@ from typing import cast +from poetry.core.constraints.version import Version from poetry.core.packages.dependency import Dependency from poetry.core.packages.url_dependency import URLDependency from poetry.core.packages.vcs_dependency import VCSDependency -from poetry.core.semver.version import Version def test_dependency_from_pep_508() -> None: diff --git a/tests/packages/test_package.py b/tests/packages/test_package.py index 610870a57..77787ee64 100644 --- a/tests/packages/test_package.py +++ b/tests/packages/test_package.py @@ -7,6 +7,7 @@ import pytest +from poetry.core.constraints.version import Version from poetry.core.factory import Factory from poetry.core.packages.dependency import Dependency from poetry.core.packages.dependency_group import DependencyGroup @@ -16,7 +17,6 @@ from poetry.core.packages.project_package import ProjectPackage from poetry.core.packages.url_dependency import URLDependency from poetry.core.packages.vcs_dependency import VCSDependency -from poetry.core.semver.version import Version @pytest.fixture() diff --git a/tests/packages/utils/test_utils.py b/tests/packages/utils/test_utils.py index 408835399..a9949752e 100644 --- a/tests/packages/utils/test_utils.py +++ b/tests/packages/utils/test_utils.py @@ -4,12 +4,12 @@ import pytest -from poetry.core.packages.constraints import parse_constraint +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.packages.utils.utils import convert_markers from poetry.core.packages.utils.utils import create_nested_marker from poetry.core.packages.utils.utils import get_python_constraint_from_marker from poetry.core.packages.utils.utils import is_python_project -from poetry.core.semver.helpers import parse_constraint as parse_version_constraint from poetry.core.version.markers import parse_marker @@ -100,7 +100,8 @@ def test_convert_markers( ) def test_create_nested_marker_base_constraint(constraint: str, expected: str) -> None: assert ( - create_nested_marker("sys_platform", parse_constraint(constraint)) == expected + create_nested_marker("sys_platform", parse_generic_constraint(constraint)) + == expected ) diff --git a/tests/test_factory.py b/tests/test_factory.py index 3849d4d4d..6415473a2 100644 --- a/tests/test_factory.py +++ b/tests/test_factory.py @@ -7,10 +7,10 @@ import pytest +from poetry.core.constraints.version import parse_constraint from poetry.core.factory import Factory from poetry.core.packages.url_dependency import URLDependency from poetry.core.packages.vcs_dependency import VCSDependency -from poetry.core.semver.helpers import parse_constraint from poetry.core.toml import TOMLFile from poetry.core.version.markers import SingleMarker diff --git a/tests/version/test_requirements.py b/tests/version/test_requirements.py index 06ff17ff6..5c0e289e1 100644 --- a/tests/version/test_requirements.py +++ b/tests/version/test_requirements.py @@ -6,7 +6,7 @@ import pytest -from poetry.core.semver.helpers import parse_constraint +from poetry.core.constraints.version import parse_constraint from poetry.core.version.requirements import InvalidRequirement from poetry.core.version.requirements import Requirement