Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion src/poetry/core/constraints/version/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,10 +121,23 @@ def union(self, other: VersionConstraint) -> VersionConstraint:

return VersionUnion.of(self, other)

def difference(self, other: VersionConstraint) -> Version | EmptyConstraint:
def difference(self, other: VersionConstraint) -> VersionConstraint:
if other.allows(self):
return EmptyConstraint()

if isinstance(other, Version) and self.allows(other):
# `self` is a non-local version and `other` a local variant of it,
# e.g. `2.12.1`.difference(`2.12.1+cpu`). The result must not
# allow `other`, which a plain `Version` cannot express, so
# delegate to the equivalent single-point range whose difference
# arithmetic can represent the split.
# See https://github.com/python-poetry/poetry/issues/10965.
from poetry.core.constraints.version.version_range import VersionRange

return VersionRange(
self, self, include_min=True, include_max=True
).difference(other)

return self

def flatten(self) -> list[VersionRangeConstraint]:
Expand Down
19 changes: 19 additions & 0 deletions tests/constraints/version/test_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -518,6 +518,25 @@ def test_difference() -> None:
)


def test_difference_local_version() -> None:
"""The difference of a non-local version and one of its local variants
must not allow the subtracted local variant.

Regression test for https://github.com/python-poetry/poetry/issues/10965
(the solver looped forever because the difference made no progress).
"""
public = Version.parse("2.12.1")
local = Version.parse("2.12.1+cpu")

difference = public.difference(local)

assert not difference.allows(local)
assert difference.allows(public)
# The reverse direction is unchanged: a non-local version constraint
# subsumes its local variants entirely.
assert local.difference(public).is_empty()


@pytest.mark.parametrize(
"version,normalized_version",
[
Expand Down
Loading