Skip to content
Merged
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
11 changes: 10 additions & 1 deletion src/poetry/core/constraints/version/version_range.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,16 @@ def intersect(self, other: VersionConstraint) -> VersionConstraint:
return other

# `>=1.2.3+local` intersects `1.2.3` to return `>=1.2.3+local,<1.2.4`.
if self.min is not None and self.min.is_local() and other.allows(self.min):
# Skip when ``other`` is itself a local version: for an exclusive
# lower bound like ``>1.2.3+local`` intersected with the excluded
# point ``1.2.3+local``, broadening would wrongly return a
# non-empty range instead of the correct empty constraint.
if (
self.min is not None
and self.min.is_local()
and not other.is_local()
and other.allows(self.min)
):
upper = other.stable.next_patch()
return _range_or_empty(
min=self.min,
Expand Down
77 changes: 77 additions & 0 deletions tests/constraints/version/test_version_range.py
Original file line number Diff line number Diff line change
Expand Up @@ -735,6 +735,83 @@ def test_difference_returns_empty_constraint_not_empty_range() -> None:
assert isinstance(result, EmptyConstraint)


def test_intersect_with_local_version_other_does_not_broaden_exclusive_min() -> None:
"""Regression test: ``>0.21.0+cpu,<0.22.0 ∩ ==0.21.0+cpu`` must be
empty. Previously returned ``>0.21.0+cpu,<0.21.1`` because the
``>=X+local ∩ public_X`` broadening fired for local ``other`` too.
"""
excluded_point = Version.parse("0.21.0+cpu")
upper = Version.parse("0.22.0")

exclusive = VersionRange(
excluded_point, upper, include_min=False, include_max=False
Comment thread
radoering marked this conversation as resolved.
)
assert isinstance(exclusive.intersect(excluded_point), EmptyConstraint)
assert isinstance(excluded_point.intersect(exclusive), EmptyConstraint)

# Inclusive-lower case still returns the literally-equal point.
inclusive = VersionRange(excluded_point, upper, include_min=True, include_max=False)
assert inclusive.intersect(excluded_point) == excluded_point
assert excluded_point.intersect(inclusive) == excluded_point

# Original motivating case (``>=X+local ∩ public X``) still broadens.
public = Version.parse("0.21.0")
range_ge_local = VersionRange(
excluded_point, Version.parse("1.0"), include_min=True, include_max=False
)
broadened_range = VersionRange(
excluded_point,
public.next_patch(),
include_min=True,
include_max=False,
)
assert range_ge_local.intersect(public) == broadened_range
assert public.intersect(range_ge_local) == broadened_range


@pytest.mark.parametrize(
("min_local", "other_local"),
[
# other lex-orders after min: in range → return other.
("a", "b"),
("cpu", "cu124"),
("cpu", "cpu1"),
# other lex-orders before min: out of range → empty.
("b", "a"),
("cu124", "cpu"),
# literal equal: handled by self.allows(other) on line 204 for
# inclusive; falls through to the special case for exclusive.
("cpu", "cpu"),
],
)
@pytest.mark.parametrize("include_min", [True, False])
def test_intersect_with_two_local_versions(
min_local: str, other_local: str, include_min: bool
) -> None:
"""Cross-local intersection: ``self.min`` and ``other`` both carry
local segments. ``==X+other`` matches only the literal point
``X+other``, so the result is just the point if it falls in the
range and empty otherwise — never a broadened range."""
self_min = Version.parse(f"1.2.3+{min_local}")
other = Version.parse(f"1.2.3+{other_local}")
upper = Version.parse("2.0")

rng = VersionRange(self_min, upper, include_min=include_min)
expected = other if rng.allows(other) else EmptyConstraint()
assert rng.intersect(other) == expected
assert other.intersect(rng) == expected


def test_intersect_punctured_range_with_excluded_point_is_empty() -> None:
"""A punctured ``VersionUnion`` (``>=A,!=V,<B``) intersected with
the excluded point ``V`` must be empty.
"""
punctured = parse_constraint(">=0.21.0,!=0.21.0+cpu,<0.22.0")
excluded_point = parse_constraint("==0.21.0+cpu")
assert punctured.intersect(excluded_point).is_empty()
assert excluded_point.intersect(punctured).is_empty()


def test_parsed_strict_max_excludes_dev_releases_of_stable() -> None:
"""PEP 440: ``<V`` for stable V MUST NOT allow pre-/dev-releases of V.
The parser canonicalizes to ``<V.dev0`` so ``allows`` reports correctly."""
Expand Down
Loading