diff --git a/src/poetry/core/constraints/version/__init__.py b/src/poetry/core/constraints/version/__init__.py index ad85d819d..f3b9cac20 100644 --- a/src/poetry/core/constraints/version/__init__.py +++ b/src/poetry/core/constraints/version/__init__.py @@ -2,6 +2,7 @@ from poetry.core.constraints.version.empty_constraint import EmptyConstraint from poetry.core.constraints.version.parser import parse_constraint +from poetry.core.constraints.version.parser import parse_marker_version_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 @@ -21,4 +22,5 @@ "VersionUnion", "constraint_regions", "parse_constraint", + "parse_marker_version_constraint", ] diff --git a/src/poetry/core/constraints/version/parser.py b/src/poetry/core/constraints/version/parser.py index 6e55b984d..a31e5ffbd 100644 --- a/src/poetry/core/constraints/version/parser.py +++ b/src/poetry/core/constraints/version/parser.py @@ -6,10 +6,21 @@ if TYPE_CHECKING: + from poetry.core.constraints.version.version import Version from poetry.core.constraints.version.version_constraint import VersionConstraint def parse_constraint(constraints: str) -> VersionConstraint: + return _parse_constraint(constraints=constraints) + + +def parse_marker_version_constraint(constraints: str) -> VersionConstraint: + return _parse_constraint(constraints=constraints, is_marker_constraint=True) + + +def _parse_constraint( + constraints: str, is_marker_constraint: bool = False +) -> VersionConstraint: if constraints == "*": from poetry.core.constraints.version.version_range import VersionRange @@ -28,9 +39,13 @@ def parse_constraint(constraints: str) -> VersionConstraint: if len(and_constraints) > 1: for constraint in and_constraints: - constraint_objects.append(parse_single_constraint(constraint)) + constraint_objects.append( + parse_single_constraint(constraint, is_marker_constraint) + ) else: - constraint_objects.append(parse_single_constraint(and_constraints[0])) + constraint_objects.append( + parse_single_constraint(and_constraints[0], is_marker_constraint) + ) if len(constraint_objects) == 1: constraint = constraint_objects[0] @@ -49,7 +64,9 @@ def parse_constraint(constraints: str) -> VersionConstraint: return VersionUnion.of(*or_groups) -def parse_single_constraint(constraint: str) -> VersionConstraint: +def parse_single_constraint( + constraint: str, is_marker_constraint: bool = False +) -> 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 @@ -95,26 +112,15 @@ def parse_single_constraint(constraint: str) -> VersionConstraint: 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 + try: + return _make_x_constraint_range( + version=Version.parse(m.group("version")), + invert=op == "!=", + is_marker_constraint=is_marker_constraint, ) - 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 + except ValueError: + raise ValueError(f"Could not parse version constraint: {constraint}") # Basic comparator m = BASIC_CONSTRAINT.match(constraint) @@ -138,10 +144,55 @@ def parse_single_constraint(constraint: str) -> VersionConstraint: return VersionRange(min=version) if op == ">=": return VersionRange(min=version, include_min=True) + + if m.group("wildcard") is not None: + return _make_x_constraint_range( + version=version, + invert=op == "!=", + is_marker_constraint=is_marker_constraint, + ) + 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}") + + +def _make_x_constraint_range( + version: Version, invert: bool = False, is_marker_constraint: bool = False +) -> VersionConstraint: + from poetry.core.constraints.version.version_range import VersionRange + + _is_zero_major = version.major == 0 and version.precision == 1 + + if version.is_postrelease(): + _next = version.next_postrelease() + _is_zero_major = False + elif version.is_stable(): + _next = version.next_stable() + elif version.is_prerelease(): + _next = version.next_prerelease() + _is_zero_major = False + elif version.is_devrelease(): + _next = version.next_devrelease() + _is_zero_major = False + + if _is_zero_major: + result = VersionRange(max=_next.with_precision(3)) + else: + _min = version.with_precision(max(version.precision, 3)) + + if not is_marker_constraint and not _next.is_unstable(): + _min = _min.next_devrelease() + + _max = _next.with_precision(max(version.precision, 3)) + result = VersionRange(_min, _max, include_min=True) + + if invert: + return VersionRange().difference(result) + + return result diff --git a/src/poetry/core/constraints/version/patterns.py b/src/poetry/core/constraints/version/patterns.py index 0dd213cf3..32ff70424 100644 --- a/src/poetry/core/constraints/version/patterns.py +++ b/src/poetry/core/constraints/version/patterns.py @@ -17,12 +17,12 @@ rf"^~=\s*(?P{VERSION_PATTERN})$", re.VERBOSE | re.IGNORECASE ) X_CONSTRAINT = re.compile( - r"^(?P!=|==)?\s*v?(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:\.[xX*])+$" + r"^(?P!=|==)?\s*v?(?P(\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)(\.\*)?$", + rf"^(?P<>|!=|>=?|<=?|==?)?\s*(?P{VERSION_PATTERN}|dev)(?P\.\*)?$", re.VERBOSE | re.IGNORECASE, ) diff --git a/src/poetry/core/constraints/version/version_range.py b/src/poetry/core/constraints/version/version_range.py index cbda6fac2..c63295fd8 100644 --- a/src/poetry/core/constraints/version/version_range.py +++ b/src/poetry/core/constraints/version/version_range.py @@ -21,22 +21,9 @@ def __init__( 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._min = min self._include_min = include_min self._include_max = include_max @@ -48,10 +35,6 @@ def min(self) -> Version | None: 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 @@ -71,27 +54,35 @@ def is_simple(self) -> bool: def allows(self, other: Version) -> bool: if self._min is not None: - if other < self._min: + _this, _other = self.allowed_min, other + + assert _this is not None + + if not _this.is_postrelease() and _other.is_postrelease(): + _other = _other.without_postrelease() + + if not _this.is_local() and _other.is_local(): + _other = other.without_local() + + if _other < _this: return False - if not self._include_min and other == self._min: + if not self._include_min and (_other == self._min or _other == _this): return False - if self.full_max is not None: - _this, _other = self.full_max, other + if self.max is not None: + _this, _other = self.allowed_max, other + + assert _this is not None 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: + if not self._include_max and (_other == self._max or _other == _this): return False return True diff --git a/src/poetry/core/constraints/version/version_range_constraint.py b/src/poetry/core/constraints/version/version_range_constraint.py index a77762778..c62f79d7a 100644 --- a/src/poetry/core/constraints/version/version_range_constraint.py +++ b/src/poetry/core/constraints/version/version_range_constraint.py @@ -21,11 +21,6 @@ def min(self) -> Version | None: def max(self) -> Version | None: raise NotImplementedError() - @property - @abstractmethod - def full_max(self) -> Version | None: - raise NotImplementedError() - @property @abstractmethod def include_min(self) -> bool: @@ -36,44 +31,78 @@ def include_min(self) -> bool: def include_max(self) -> bool: raise NotImplementedError() - def allows_lower(self, other: VersionRangeConstraint) -> bool: + @property + def allowed_min(self) -> Version | None: if self.min is None: - return other.min is not None + return None + + if not self.include_min or self.min.is_unstable(): + return self.min + + if self.min == self.max and (self.include_min or self.include_max): + # this is an equality range + return self.min - if other.min is None: + return self.min + + @property + def allowed_max(self) -> Version | None: + if self.max is None: + return None + + if self.include_max or self.max.is_unstable(): + return self.max + + if self.min == self.max and (self.include_min or self.include_max): + # this is an equality range + return self.max + + return self.max.next_devrelease() + + def allows_lower(self, other: VersionRangeConstraint) -> bool: + _this, _other = self.allowed_min, other.allowed_min + + if _this is None: + return _other is not None + + if _other is None: return False - if self.min < other.min: + if _this < _other: return True - if self.min > other.min: + if _this > _other: 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 + _this, _other = self.allowed_max, other.allowed_max - if other.full_max is None: + if _this is None: + return _other is not None + + if _other is None: return False - if self.full_max < other.full_max: + if _this < _other: return False - if self.full_max > other.full_max: + if _this > _other: 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: + _this, _other = self.allowed_max, other.allowed_min + + if _this is None or _other is None: return False - if self.full_max < other.min: + if _this < _other: return True - if self.full_max > other.min: + if _this > _other: return False return not self.include_max or not other.include_min diff --git a/src/poetry/core/constraints/version/version_union.py b/src/poetry/core/constraints/version/version_union.py index 022aa8b35..de5377470 100644 --- a/src/poetry/core/constraints/version/version_union.py +++ b/src/poetry/core/constraints/version/version_union.py @@ -88,6 +88,18 @@ def is_simple(self) -> bool: return self.excludes_single_version() def allows(self, version: Version) -> bool: + if self.excludes_single_version(): + # when excluded version is local, special handling is required + # to ensure that a constraint (!=2.0+deadbeef) will allow the + # provided version (2.0) + from poetry.core.constraints.version.version import Version + from poetry.core.constraints.version.version_range import VersionRange + + excluded = VersionRange().difference(self) + + if isinstance(excluded, Version) and excluded.is_local(): + return excluded != version + return any([constraint.allows(version) for constraint in self._ranges]) def allows_all(self, other: VersionConstraint) -> bool: @@ -303,26 +315,41 @@ def _excludes_single_wildcard_range_check_is_valid_range( assert one.max is not None assert two.min is not None - max_precision = max(one.max.precision, two.min.precision) + _max = one.max + _min = two.min + + if _max.is_devrelease() and _max.dev is not None and _max.dev.number == 0: + # handle <2.0.0.dev0 || >= 2.1.0 + _max = _max.without_devrelease() + + if _min.is_devrelease(): + assert _min.dev is not None + + if _min.dev.number != 0: + # if both are dev releases, they should both have dev0 + return False + _min = _min.without_devrelease() + + max_precision = max(_max.precision, _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(), + return _min in { + _max.next_major(), + _max.next_minor(), + _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 + from_parts = _max.__class__.from_parts _extras: list[list[int]] = [] _versions: list[Version] = [] - for _version in [one.max, two.min]: + for _version in [_max, _min]: _extra = list(_version.non_semver_parts or []) while len(_extra) < (max_precision - 3): diff --git a/src/poetry/core/packages/utils/utils.py b/src/poetry/core/packages/utils/utils.py index 4b4853ca7..595cbe8e4 100644 --- a/src/poetry/core/packages/utils/utils.py +++ b/src/poetry/core/packages/utils/utils.py @@ -16,7 +16,7 @@ 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.constraints.version import parse_marker_version_constraint from poetry.core.pyproject.toml import PyProjectTOML from poetry.core.version.markers import dnf @@ -316,7 +316,7 @@ def get_python_constraint_from_marker( python_version_markers = markers["python_version"] normalized = normalize_python_version_markers(python_version_markers) - constraint = parse_constraint(normalized) + constraint = parse_marker_version_constraint(normalized) return constraint diff --git a/src/poetry/core/version/markers.py b/src/poetry/core/version/markers.py index cb2d4fcb5..f40d85ab0 100644 --- a/src/poetry/core/version/markers.py +++ b/src/poetry/core/version/markers.py @@ -9,6 +9,7 @@ from typing import Iterable from poetry.core.constraints.version import VersionConstraint +from poetry.core.constraints.version import parse_marker_version_constraint from poetry.core.version.grammars import GRAMMAR_PEP_508_MARKERS from poetry.core.version.parser import Parser @@ -187,9 +188,6 @@ def __init__( from poetry.core.constraints.generic import ( parse_constraint as parse_generic_constraint, ) - from poetry.core.constraints.version import ( - parse_constraint as parse_version_constraint, - ) self._constraint: BaseConstraint | VersionConstraint self._parser: Callable[[str], BaseConstraint | VersionConstraint] @@ -209,7 +207,7 @@ def __init__( self._parser = parse_generic_constraint if name in self._VERSION_LIKE_MARKER_NAME: - self._parser = parse_version_constraint + self._parser = parse_marker_version_constraint if self._operator in {"in", "not in"}: versions = [] diff --git a/src/poetry/core/version/pep440/segments.py b/src/poetry/core/version/pep440/segments.py index 735f523e8..75d1779a7 100644 --- a/src/poetry/core/version/pep440/segments.py +++ b/src/poetry/core/version/pep440/segments.py @@ -5,6 +5,7 @@ from typing import Optional from typing import Tuple from typing import Union +from typing import cast # Release phase IDs according to PEP440 @@ -98,6 +99,54 @@ def next_patch(self) -> Release: extra=tuple(0 for _ in self.extra), ) + def next(self) -> Release: + if self.precision == 1: + return self.next_major() + + if self.precision == 2: + return self.next_minor() + + if self.precision == 3: + return self.next_patch() + + return dataclasses.replace( + self, + major=self.major, + minor=self.minor if self.minor is not None else 0, + patch=self.patch + 1 if self.patch is not None else 1, + extra=(*self.extra[:-1], cast(int, self.extra[-1:]) + 1), + ) + + def with_precision(self, precision: int) -> Release: + if self.precision == precision: + return dataclasses.replace(self) + + if precision == 1: + return Release(major=self.major) + + if precision == 2: + return Release(major=self.major, minor=self.minor or 0) + + if precision == 3: + return Release( + major=self.major, minor=self.minor or 0, patch=self.patch or 0 + ) + + _precision = precision - 3 + + _extra = self.extra + if len(self.extra) > _precision: + _extra = self.extra[:_precision] + elif len(self.extra) < _precision: + _extra = (*self.extra, *[0 for _ in range(_precision - len(self.extra))]) + + return Release( + major=self.major, + minor=self.minor or 0, + patch=self.patch or 0, + extra=_extra, + ) + @dataclasses.dataclass(frozen=True, eq=True, order=True) class ReleaseTag: diff --git a/src/poetry/core/version/pep440/version.py b/src/poetry/core/version/pep440/version.py index eeae009a8..7a146f548 100644 --- a/src/poetry/core/version/pep440/version.py +++ b/src/poetry/core/version/pep440/version.py @@ -229,7 +229,14 @@ def next_patch(self: T) -> T: release = release.next_patch() return self.__class__(epoch=self.epoch, release=release) - def next_prerelease(self: T, next_phase: bool = False) -> PEP440Version: + def next_stable(self: T) -> T: + return self.__class__( + epoch=self.epoch, + release=self.release.next(), + local=self.local, + ) + + def next_prerelease(self: T, next_phase: bool = False) -> T: if self.is_stable(): warnings.warn( "Calling next_prerelease() on a stable release is deprecated for its" @@ -314,4 +321,10 @@ def without_local(self: T) -> T: return self.replace(local=None) def without_postrelease(self: T) -> T: - return self.replace(post=None) + return self.replace(post=None, dev=None) + + def without_devrelease(self: T) -> T: + return self.replace(dev=None) + + def with_precision(self: T, precision: int) -> T: + return self.replace(release=self.release.with_precision(precision)) diff --git a/tests/constraints/version/test_helpers.py b/tests/constraints/version/test_helpers.py index b9a91db26..d44818a24 100644 --- a/tests/constraints/version/test_helpers.py +++ b/tests/constraints/version/test_helpers.py @@ -45,54 +45,54 @@ def test_parse_constraint(input: str, constraint: Version | VersionRange) -> Non ( "v2.*", VersionRange( - Version.from_parts(2, 0, 0), Version.from_parts(3, 0, 0), True + Version.parse("2.0.0.dev0"), Version.from_parts(3, 0, 0), True ), ), ( "2.*.*", VersionRange( - Version.from_parts(2, 0, 0), Version.from_parts(3, 0, 0), True + Version.parse("2.0.0.dev0"), Version.from_parts(3, 0, 0), True ), ), ( "20.*", VersionRange( - Version.from_parts(20, 0, 0), Version.from_parts(21, 0, 0), True + Version.parse("20.0.0.dev0"), Version.from_parts(21, 0, 0), True ), ), ( "20.*.*", VersionRange( - Version.from_parts(20, 0, 0), Version.from_parts(21, 0, 0), True + Version.parse("20.0.0.dev0"), Version.from_parts(21, 0, 0), True ), ), ( "2.0.*", VersionRange( - Version.from_parts(2, 0, 0), Version.from_parts(2, 1, 0), True + Version.parse("2.0.0.dev0"), Version.from_parts(2, 1, 0), True ), ), ( "2.x", VersionRange( - Version.from_parts(2, 0, 0), Version.from_parts(3, 0, 0), True + Version.parse("2.0.0.dev0"), Version.from_parts(3, 0, 0), True ), ), ( "2.x.x", VersionRange( - Version.from_parts(2, 0, 0), Version.from_parts(3, 0, 0), True + Version.parse("2.0.0.dev0"), Version.from_parts(3, 0, 0), True ), ), ( "2.2.X", VersionRange( - Version.from_parts(2, 2, 0), Version.from_parts(2, 3, 0), True + Version.parse("2.2.0.dev0"), Version.from_parts(2, 3, 0), True ), ), - ("0.*", VersionRange(max=Version.from_parts(1, 0, 0))), - ("0.*.*", VersionRange(max=Version.from_parts(1, 0, 0))), - ("0.x", VersionRange(max=Version.from_parts(1, 0, 0))), + ("0.*", VersionRange(max=Version.parse("1.0.0"))), + ("0.*.*", VersionRange(max=Version.parse("1.0.0"))), + ("0.x", VersionRange(max=Version.parse("1.0.0"))), ], ) def test_parse_constraint_wildcard(input: str, constraint: VersionRange) -> None: @@ -334,7 +334,7 @@ def test_parse_constraint_multi_with_epochs(input: str, output: VersionRange) -> def test_parse_constraint_multi_wilcard(input: str) -> None: assert parse_constraint(input) == VersionUnion( VersionRange( - Version.from_parts(2, 7, 0), Version.from_parts(3, 0, 0), True, False + Version.from_parts(2, 7, 0), Version.parse("3.0.0.dev0"), True, False ), VersionRange(Version.from_parts(3, 2, 0), None, True, False), ) @@ -345,19 +345,19 @@ def test_parse_constraint_multi_wilcard(input: str) -> None: [ ( "!=v2.*", - VersionRange(max=Version.parse("2.0")).union( + VersionRange(max=Version.parse("2.0.0.dev0")).union( VersionRange(Version.parse("3.0"), include_min=True) ), ), ( "!=2.*.*", - VersionRange(max=Version.parse("2.0")).union( + VersionRange(max=Version.parse("2.0.0.dev0")).union( VersionRange(Version.parse("3.0"), include_min=True) ), ), ( "!=2.0.*", - VersionRange(max=Version.parse("2.0")).union( + VersionRange(max=Version.parse("2.0.0.dev0")).union( VersionRange(Version.parse("2.1"), include_min=True) ), ), diff --git a/tests/constraints/version/test_parse_constraint.py b/tests/constraints/version/test_parse_constraint.py index c27a7b2cf..58bd2996f 100644 --- a/tests/constraints/version/test_parse_constraint.py +++ b/tests/constraints/version/test_parse_constraint.py @@ -23,7 +23,7 @@ ( "== 3.8.*", VersionRange( - min=Version.from_parts(3, 8), + min=Version.parse("3.8.0.dev0"), max=Version.from_parts(3, 9, 0), include_min=True, ), @@ -31,7 +31,7 @@ ( "== 3.8.x", VersionRange( - min=Version.from_parts(3, 8), + min=Version.parse("3.8.0.dev0"), max=Version.from_parts(3, 9, 0), include_min=True, ), @@ -246,6 +246,15 @@ include_min=True, ), ), + ( + "2.0.post1.*", + VersionRange( + min=Version.parse("2.0.post1.dev0"), + max=Version.parse("2.0.post2"), + include_min=True, + include_max=False, + ), + ), ], ) @pytest.mark.parametrize(("with_whitespace_padding",), [(True,), (False,)]) diff --git a/tests/constraints/version/test_version_range.py b/tests/constraints/version/test_version_range.py index fcac2477d..9eb582d2f 100644 --- a/tests/constraints/version/test_version_range.py +++ b/tests/constraints/version/test_version_range.py @@ -5,6 +5,7 @@ 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 parse_constraint @pytest.fixture() @@ -80,7 +81,7 @@ def v300b1() -> Version: @pytest.mark.parametrize( "base,other", [ - pytest.param(Version.parse("3.0.0"), Version.parse("3.0.0-1"), id="post"), + pytest.param(Version.parse("3.0.0-1"), Version.parse("3.0.0-1"), id="post"), pytest.param( Version.parse("3.0.0"), Version.parse("3.0.0+local.1"), id="local" ), @@ -111,7 +112,7 @@ def test_allows_post_releases_with_post_and_local_min() -> None: three = Version.parse("3.0.0-1+local.1") four = Version.parse("3.0.0+local.2") - assert VersionRange(min=one, include_min=True).allows(two) + assert not VersionRange(min=one, include_min=True).allows(two) assert VersionRange(min=one, include_min=True).allows(three) assert VersionRange(min=one, include_min=True).allows(four) @@ -124,8 +125,8 @@ def test_allows_post_releases_with_post_and_local_min() -> None: assert not VersionRange(min=three, include_min=True).allows(four) assert not VersionRange(min=four, include_min=True).allows(one) - assert VersionRange(min=four, include_min=True).allows(two) - assert VersionRange(min=four, include_min=True).allows(three) + assert not VersionRange(min=four, include_min=True).allows(two) + assert not VersionRange(min=four, include_min=True).allows(three) def test_allows_post_releases_with_post_and_local_max() -> None: @@ -134,8 +135,8 @@ def test_allows_post_releases_with_post_and_local_max() -> None: three = Version.parse("3.0.0-1+local.1") four = Version.parse("3.0.0+local.2") - assert VersionRange(max=one, include_max=True).allows(two) - assert VersionRange(max=one, include_max=True).allows(three) + assert not VersionRange(max=one, include_max=True).allows(two) + assert not VersionRange(max=one, include_max=True).allows(three) assert not VersionRange(max=one, include_max=True).allows(four) assert VersionRange(max=two, include_max=True).allows(one) @@ -147,8 +148,8 @@ def test_allows_post_releases_with_post_and_local_max() -> None: assert VersionRange(max=three, include_max=True).allows(four) assert VersionRange(max=four, include_max=True).allows(one) - assert VersionRange(max=four, include_max=True).allows(two) - assert VersionRange(max=four, include_max=True).allows(three) + assert not VersionRange(max=four, include_max=True).allows(two) + assert not VersionRange(max=four, include_max=True).allows(three) @pytest.mark.parametrize( @@ -345,7 +346,7 @@ def test_allows_any( # pre-release min does not allow lesser than itself range = VersionRange(Version.parse("1.9b1"), include_min=True) assert not range.allows_any( - VersionRange(Version.parse("1.8.0"), Version.parse("1.9.0"), include_min=True) + VersionRange(Version.parse("1.8.0"), Version.parse("1.9.0b0"), include_min=True) ) @@ -446,15 +447,207 @@ def test_union( assert result == VersionRange(v003, v200) -def test_include_max_prerelease(v200: Version, v300: Version, v300b1: Version) -> None: - result = VersionRange(v200, v300) - - assert not result.allows(v300b1) - assert not result.allows_any(VersionRange(v300b1)) - assert not result.allows_all(VersionRange(v200, v300b1)) - - result = VersionRange(v200, v300, always_include_max_prerelease=True) - - assert result.allows(v300b1) - assert result.allows_any(VersionRange(v300b1)) - assert result.allows_all(VersionRange(v200, v300b1)) +@pytest.mark.parametrize( + ("version", "spec", "expected"), + [ + (v, s, True) + for v, s in [ + # Test the equality operation + ("2.0", "==2"), + ("2.0", "==2.0"), + ("2.0", "==2.0.0"), + ("2.0+deadbeef", "==2"), + ("2.0+deadbeef", "==2.0"), + ("2.0+deadbeef", "==2.0.0"), + ("2.0+deadbeef", "==2+deadbeef"), + ("2.0+deadbeef", "==2.0+deadbeef"), + ("2.0+deadbeef", "==2.0.0+deadbeef"), + ("2.0+deadbeef.0", "==2.0.0+deadbeef.00"), + # Test the equality operation with a prefix + ("2.dev1", "==2.*"), + ("2a1", "==2.*"), + ("2a1.post1", "==2.*"), + ("2b1", "==2.*"), + ("2b1.dev1", "==2.*"), + ("2c1", "==2.*"), + ("2c1.post1.dev1", "==2.*"), + ("2rc1", "==2.*"), + ("2", "==2.*"), + ("2.0", "==2.*"), + ("2.0.0", "==2.*"), + ("2.0.post1", "==2.0.post1.*"), + ("2.0.post1.dev1", "==2.0.post1.*"), + ("2.1+local.version", "==2.1.*"), + # Test the in-equality operation + ("2.1", "!=2"), + ("2.1", "!=2.0"), + ("2.0.1", "!=2"), + ("2.0.1", "!=2.0"), + ("2.0.1", "!=2.0.0"), + ("2.0", "!=2.0+deadbeef"), + # Test the in-equality operation with a prefix + ("2.0", "!=3.*"), + ("2.1", "!=2.0.*"), + # Test the greater than equal operation + ("2.0", ">=2"), + ("2.0", ">=2.0"), + ("2.0", ">=2.0.0"), + ("2.0.post1", ">=2"), + ("2.0.post1.dev1", ">=2"), + ("3", ">=2"), + # Test the less than equal operation + ("2.0", "<=2"), + ("2.0", "<=2.0"), + ("2.0", "<=2.0.0"), + ("2.0.dev1", "<=2"), + ("2.0a1", "<=2"), + ("2.0a1.dev1", "<=2"), + ("2.0b1", "<=2"), + ("2.0b1.post1", "<=2"), + ("2.0c1", "<=2"), + ("2.0c1.post1.dev1", "<=2"), + ("2.0rc1", "<=2"), + ("1", "<=2"), + # Test the greater than operation + ("3", ">2"), + ("2.1", ">2.0"), + ("2.0.1", ">2"), + ("2.1.post1", ">2"), + ("2.1+local.version", ">2"), + # Test the less than operation + ("1", "<2"), + ("2.0", "<2.1"), + ("2.0.dev0", "<2.1"), + # Test the compatibility operation + ("1", "~=1.0"), + ("1.0.1", "~=1.0"), + ("1.1", "~=1.0"), + ("1.9999999", "~=1.0"), + ("1.1", "~=1.0a1"), + # Test that epochs are handled sanely + ("2!1.0", "~=2!1.0"), + ("2!1.0", "==2!1.*"), + ("2!1.0", "==2!1.0"), + ("2!1.0", "!=1.0"), + ("1.0", "!=2!1.0"), + ("1.0", "<=2!0.1"), + ("2!1.0", ">=2.0"), + ("1.0", "<2!0.1"), + ("2!1.0", ">2.0"), + # Test some normalization rules + ("2.0.5", ">2.0dev"), + ] + ] + + [ + (v, s, False) + for v, s in [ + # Test the equality operation + ("2.1", "==2"), + ("2.1", "==2.0"), + ("2.1", "==2.0.0"), + ("2.0", "==2.0+deadbeef"), + # Test the equality operation with a prefix + ("2.0", "==3.*"), + ("2.1", "==2.0.*"), + # Test the in-equality operation + ("2.0", "!=2"), + ("2.0", "!=2.0"), + ("2.0", "!=2.0.0"), + ("2.0+deadbeef", "!=2"), + ("2.0+deadbeef", "!=2.0"), + ("2.0+deadbeef", "!=2.0.0"), + ("2.0+deadbeef", "!=2+deadbeef"), + ("2.0+deadbeef", "!=2.0+deadbeef"), + ("2.0+deadbeef", "!=2.0.0+deadbeef"), + ("2.0+deadbeef.0", "!=2.0.0+deadbeef.00"), + # Test the in-equality operation with a prefix + ("2.dev1", "!=2.*"), + ("2a1", "!=2.*"), + ("2a1.post1", "!=2.*"), + ("2b1", "!=2.*"), + ("2b1.dev1", "!=2.*"), + ("2c1", "!=2.*"), + ("2c1.post1.dev1", "!=2.*"), + ("2rc1", "!=2.*"), + ("2", "!=2.*"), + ("2.0", "!=2.*"), + ("2.0.0", "!=2.*"), + ("2.0.post1", "!=2.0.post1.*"), + ("2.0.post1.dev1", "!=2.0.post1.*"), + # Test the greater than equal operation + ("2.0.dev1", ">=2"), + ("2.0a1", ">=2"), + ("2.0a1.dev1", ">=2"), + ("2.0b1", ">=2"), + ("2.0b1.post1", ">=2"), + ("2.0c1", ">=2"), + ("2.0c1.post1.dev1", ">=2"), + ("2.0rc1", ">=2"), + ("1", ">=2"), + # Test the less than equal operation + ("2.0.post1", "<=2"), + ("2.0.post1.dev1", "<=2"), + ("3", "<=2"), + # Test the greater than operation + ("1", ">2"), + ("2.0.dev1", ">2"), + ("2.0a1", ">2"), + ("2.0a1.post1", ">2"), + ("2.0b1", ">2"), + ("2.0b1.dev1", ">2"), + ("2.0c1", ">2"), + ("2.0c1.post1.dev1", ">2"), + ("2.0rc1", ">2"), + ("2.0", ">2"), + ("2.0.post1", ">2"), + ("2.0.post1.dev1", ">2"), + ("2.0+local.version", ">2"), + # Test the less than operation + ("2.0.dev1", "<2"), + ("2.0a1", "<2"), + ("2.0a1.post1", "<2"), + ("2.0b1", "<2"), + ("2.0b2.dev1", "<2"), + ("2.0c1", "<2"), + ("2.0c1.post1.dev1", "<2"), + ("2.0rc1", "<2"), + ("2.0", "<2"), + ("2.post1", "<2"), + ("2.post1.dev1", "<2"), + ("3", "<2"), + # Test the compatibility operation + ("2.0", "~=1.0"), + ("1.1.0", "~=1.0.0"), + ("1.1.post1", "~=1.0.0"), + # Test that epochs are handled sanely + ("1.0", "~=2!1.0"), + ("2!1.0", "~=1.0"), + ("2!1.0", "==1.0"), + ("1.0", "==2!1.0"), + ("2!1.0", "==1.*"), + ("1.0", "==2!1.*"), + ("2!1.0", "!=2!1.0"), + ] + ], +) +def test_specifiers(version: str, spec: str, expected: bool) -> None: + constraint = parse_constraint(spec) + + v = Version.parse(version) + + if expected: + # Test that the plain string form works + # assert version in spec + assert constraint.allows(v) + + # Test that the version instance form works + # assert version in spec + assert constraint.allows(v) + else: + # Test that the plain string form works + # assert version not in spec + assert not constraint.allows(v) + + # Test that the version instance form works + # assert version not in spec + assert not constraint.allows(v) diff --git a/tests/packages/test_main.py b/tests/packages/test_main.py index 6f9c31557..6728e3cff 100644 --- a/tests/packages/test_main.py +++ b/tests/packages/test_main.py @@ -37,7 +37,7 @@ def test_dependency_from_pep_508_with_constraint() -> None: dep = Dependency.create_from_pep_508(name) assert dep.name == "requests" - assert str(dep.constraint) == ">=2.12.0,<2.17.0 || >=2.18.0,<3.0" + assert str(dep.constraint) == ">=2.12.0,<2.17.0.dev0 || >=2.18.0,<3.0" def test_dependency_from_pep_508_with_extras() -> None: diff --git a/tests/packages/utils/test_utils.py b/tests/packages/utils/test_utils.py index a9949752e..8f008674e 100644 --- a/tests/packages/utils/test_utils.py +++ b/tests/packages/utils/test_utils.py @@ -6,6 +6,7 @@ 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.constraints.version import parse_marker_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 @@ -230,7 +231,7 @@ def test_create_nested_marker_version_constraint( ) def test_get_python_constraint_from_marker(marker: str, constraint: str) -> None: marker_parsed = parse_marker(marker) - constraint_parsed = parse_version_constraint(constraint) + constraint_parsed = parse_marker_version_constraint(constraint) assert get_python_constraint_from_marker(marker_parsed) == constraint_parsed