From 64446f9cebebf3922a8a0d228a21438774e3e312 Mon Sep 17 00:00:00 2001 From: Arun Babu Neelicattu Date: Thu, 26 May 2022 17:53:51 +0200 Subject: [PATCH 1/2] version: fix local label comparison Prior to this change, local label comparison was inconsistent with PEP 440. This change ensures that the local version label is checked for equivalence using a strict string equality comparison. Resolves: python-poetry/poetry#4729 --- src/poetry/core/semver/version.py | 2 -- src/poetry/core/semver/version_range.py | 9 ++++++ tests/semver/test_version.py | 3 +- tests/semver/test_version_range.py | 42 +++++++++++++++++-------- 4 files changed, 40 insertions(+), 16 deletions(-) diff --git a/src/poetry/core/semver/version.py b/src/poetry/core/semver/version.py index 63d67fddc..89826a0d5 100644 --- a/src/poetry/core/semver/version.py +++ b/src/poetry/core/semver/version.py @@ -86,8 +86,6 @@ def allows(self, version: Version | None) -> bool: # 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() - elif _this.is_local() and not _other.is_local(): - _this = _this.without_local() # allow weak equality to allow `3.0.0-1` for `3.0.0` if not _this.is_postrelease() and _other.is_postrelease(): diff --git a/src/poetry/core/semver/version_range.py b/src/poetry/core/semver/version_range.py index 6e2539a2f..97422a5fd 100644 --- a/src/poetry/core/semver/version_range.py +++ b/src/poetry/core/semver/version_range.py @@ -69,6 +69,11 @@ def is_simple(self) -> bool: def allows(self, other: Version) -> bool: if self._min is not None: + if self._min.is_local() and ( + not other.is_local() or self._min.local != other.local + ): + return False + if other < self._min: return False @@ -81,6 +86,10 @@ def allows(self, other: Version) -> bool: 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() + elif _this.is_local() and ( + not _other.is_local() or _this.local != _other.local + ): + return False if not _this.is_postrelease() and _other.is_postrelease(): # allow weak equality to allow `3.0.0-1` for `<=3.0.0` diff --git a/tests/semver/test_version.py b/tests/semver/test_version.py index 90d3c503c..ab90cf993 100644 --- a/tests/semver/test_version.py +++ b/tests/semver/test_version.py @@ -127,7 +127,8 @@ def test_allows_with_local() -> None: assert not v.allows(Version.parse("1.3.3")) assert not v.allows(Version.parse("1.2.3-dev")) assert not v.allows(Version.parse("1.2.3+build.2")) - assert v.allows(Version.parse("1.2.3-1")) + assert not v.allows(Version.parse("1.2.3-1")) + assert v.allows(Version.parse("1.2.3-1+build.1")) assert v.allows(Version.parse("1.2.3-1+build.1")) diff --git a/tests/semver/test_version_range.py b/tests/semver/test_version_range.py index 9cd414530..cb75d2800 100644 --- a/tests/semver/test_version_range.py +++ b/tests/semver/test_version_range.py @@ -110,22 +110,32 @@ def test_allows_post_releases_with_post_and_local_min() -> None: two = Version.parse("3.0.0-1") three = Version.parse("3.0.0-1+local.1") four = Version.parse("3.0.0+local.2") + five = Version.parse("4.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) + assert not VersionRange(min=one, include_min=True).allows(four) + assert not VersionRange(min=one, include_min=True).allows(five) assert not VersionRange(min=two, include_min=True).allows(one) assert VersionRange(min=two, include_min=True).allows(three) assert not VersionRange(min=two, include_min=True).allows(four) + assert VersionRange(min=two, include_min=True).allows(five) assert not VersionRange(min=three, include_min=True).allows(one) assert not VersionRange(min=three, include_min=True).allows(two) assert not VersionRange(min=three, include_min=True).allows(four) + assert not VersionRange(min=three, include_min=True).allows(five) 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) + assert VersionRange(min=four, include_min=True).allows(five) + + assert not VersionRange(min=five, include_max=True).allows(one) + assert not VersionRange(min=five, include_max=True).allows(two) + assert not VersionRange(max=five, include_max=True).allows(three) + assert not VersionRange(min=five, include_max=True).allows(four) def test_allows_post_releases_with_post_and_local_max() -> None: @@ -133,8 +143,9 @@ def test_allows_post_releases_with_post_and_local_max() -> None: two = Version.parse("3.0.0-1") three = Version.parse("3.0.0-1+local.1") four = Version.parse("3.0.0+local.2") + five = Version.parse("4.0.0+local.2") - assert VersionRange(max=one, include_max=True).allows(two) + assert not 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(four) @@ -143,12 +154,17 @@ def test_allows_post_releases_with_post_and_local_max() -> None: assert VersionRange(max=two, include_max=True).allows(four) assert VersionRange(max=three, include_max=True).allows(one) - assert VersionRange(max=three, include_max=True).allows(two) - assert VersionRange(max=three, include_max=True).allows(four) + assert not VersionRange(max=three, include_max=True).allows(two) + assert not VersionRange(max=three, include_max=True).allows(four) + + assert not VersionRange(max=four, include_max=True).allows(one) + assert not VersionRange(max=four, include_max=True).allows(two) + assert not VersionRange(max=four, include_max=True).allows(three) - 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=five, include_max=True).allows(one) + assert not VersionRange(max=five, include_max=True).allows(two) + assert not VersionRange(max=five, include_max=True).allows(three) + assert VersionRange(max=five, include_max=True).allows(four) @pytest.mark.parametrize( @@ -161,9 +177,9 @@ def test_allows_post_releases_with_post_and_local_max() -> None: id="post", ), pytest.param( - Version.parse("3.0.0"), Version.parse("3.0.0+local.1"), - Version.parse("3.0.0+local.2"), + Version.parse("3.0.0-1+local.1"), + Version.parse("3.0.0-2+local.1"), id="local", ), ], @@ -192,7 +208,7 @@ def test_allows_post_releases_explicit_with_max( pytest.param( Version.parse("3.0.0"), Version.parse("3.0.0+local.1"), - Version.parse("3.0.0+local.2"), + Version.parse("3.0.0-1+local.1"), id="local", ), ], From c4bb9bd2caa9b7c039ae711ced80a3ce74771eaa Mon Sep 17 00:00:00 2001 From: Arun Babu Neelicattu Date: Thu, 26 May 2022 18:26:19 +0200 Subject: [PATCH 2/2] versions: preserve any local label when bumping --- src/poetry/core/version/pep440/version.py | 12 +++++-- tests/semver/test_parse_constraint.py | 39 +++++++++++++++++++++++ 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/src/poetry/core/version/pep440/version.py b/src/poetry/core/version/pep440/version.py index c1252a6f4..ab71c9e66 100644 --- a/src/poetry/core/version/pep440/version.py +++ b/src/poetry/core/version/pep440/version.py @@ -205,7 +205,7 @@ def next_major(self: T) -> T: release = self.release if self.is_stable() or Release(self.release.major, 0, 0) < self.release: release = self.release.next_major() - return self.__class__(epoch=self.epoch, release=release) + return self.__class__(epoch=self.epoch, release=release, local=self.local) def next_minor(self: T) -> T: release = self.release @@ -214,12 +214,13 @@ def next_minor(self: T) -> T: or Release(self.release.major, self.release.minor, 0) < self.release ): release = self.release.next_minor() - return self.__class__(epoch=self.epoch, release=release) + return self.__class__(epoch=self.epoch, release=release, local=self.local) def next_patch(self: T) -> T: return self.__class__( epoch=self.epoch, release=self.release.next_patch() if self.is_stable() else self.release, + local=self.local, ) def next_prerelease(self: T, next_phase: bool = False) -> PEP440Version: @@ -228,7 +229,9 @@ def next_prerelease(self: T, next_phase: bool = False) -> PEP440Version: pre = self.pre.next_phase() if next_phase else self.pre.next() else: pre = ReleaseTag(RELEASE_PHASE_ID_ALPHA) - return self.__class__(epoch=self.epoch, release=self.release, pre=pre) + return self.__class__( + epoch=self.epoch, release=self.release, pre=pre, local=self.local + ) def next_postrelease(self: T) -> T: if self.is_postrelease(): @@ -242,6 +245,7 @@ def next_postrelease(self: T) -> T: pre=self.pre, dev=self.dev, post=post, + local=self.local, ) def next_devrelease(self: T) -> T: @@ -256,6 +260,7 @@ def next_devrelease(self: T) -> T: pre=self.pre, post=self.post, dev=dev, + local=self.local, ) def first_prerelease(self: T) -> T: @@ -263,6 +268,7 @@ def first_prerelease(self: T) -> T: epoch=self.epoch, release=self.release, pre=ReleaseTag(RELEASE_PHASE_ID_ALPHA), + local=self.local, ) def replace(self: T, **kwargs: Any) -> T: diff --git a/tests/semver/test_parse_constraint.py b/tests/semver/test_parse_constraint.py index ae6f2d3a1..9e8bbaf2d 100644 --- a/tests/semver/test_parse_constraint.py +++ b/tests/semver/test_parse_constraint.py @@ -246,6 +246,45 @@ include_min=True, ), ), + ( + "^1.10+cu113", + VersionRange( + min=Version.from_parts(1, 10, local="cu113"), + max=Version.from_parts(2, 0, local="cu113"), + include_min=True, + ), + ), + ( + "~1.10.0+cu113", + VersionRange( + min=Version.from_parts(1, 10, 0, local="cu113"), + max=Version.from_parts(1, 11, 0, local="cu113"), + include_min=True, + ), + ), + ( + ">=1.10.0+cu113", + VersionRange( + min=Version.from_parts(1, 10, 0, local="cu113"), + include_min=True, + ), + ), + ( + ">=1.10.0+cu113,<2.0", + VersionRange( + min=Version.from_parts(1, 10, 0, local="cu113"), + max=Version.from_parts(2, 0), + include_min=True, + ), + ), + ( + ">=1.10.0+cu113,<2.0+cu113", + VersionRange( + min=Version.from_parts(1, 10, 0, local="cu113"), + max=Version.from_parts(2, 0, local="cu113"), + include_min=True, + ), + ), ], ) @pytest.mark.parametrize(("with_whitespace_padding",), [(True,), (False,)])