Skip to content

Commit

Permalink
version: fix local label comparison
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
abn committed Jan 13, 2025
1 parent c5547ec commit 0416da4
Show file tree
Hide file tree
Showing 3 changed files with 38 additions and 7 deletions.
11 changes: 11 additions & 0 deletions src/poetry/core/constraints/version/version_range.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,12 @@ 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_segment_compare_key < other.local_segment_compare_key
):
return False

_this, _other = self.allowed_min, other

assert _this is not None
Expand Down Expand Up @@ -92,6 +98,11 @@ 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_segment_compare_key != _other.local_segment_compare_key
):
return False

if _other > _this:
return False
Expand Down
4 changes: 4 additions & 0 deletions src/poetry/core/version/pep440/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,10 @@ def _make_compare_key(
)
return self.epoch, self.release, _pre, _post, _dev, _local

@property
def local_segment_compare_key(self) -> tuple[int | str, ...]:
return self._compare_key[-1]

@property
def major(self) -> int:
return self.release.major
Expand Down
30 changes: 23 additions & 7 deletions tests/constraints/version/test_version_range.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,29 +111,40 @@ 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 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 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:
one = Version.parse("3.0.0+local.1")
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 not VersionRange(max=one, include_max=True).allows(two)
assert not VersionRange(max=one, include_max=True).allows(three)
Expand All @@ -144,13 +155,18 @@ 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 VersionRange(max=four, include_max=True).allows(one)
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 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(
"base,one,two",
Expand All @@ -162,9 +178,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",
),
],
Expand Down Expand Up @@ -193,7 +209,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",
),
],
Expand Down

0 comments on commit 0416da4

Please sign in to comment.