Skip to content

Commit b68980b

Browse files
authored
Fix arbitrary equality intersection preservation in SpecifierSet (#951)
1 parent a1f7056 commit b68980b

File tree

2 files changed

+43
-3
lines changed

2 files changed

+43
-3
lines changed

src/packaging/specifiers.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -322,11 +322,16 @@ def __str__(self) -> str:
322322

323323
@property
324324
def _canonical_spec(self) -> tuple[str, str]:
325+
operator, version = self._spec
326+
if operator == "===":
327+
return operator, version
328+
325329
canonical_version = canonicalize_version(
326-
self._spec[1],
327-
strip_trailing_zero=(self._spec[0] != "~="),
330+
version,
331+
strip_trailing_zero=(operator != "~="),
328332
)
329-
return self._spec[0], canonical_version
333+
334+
return operator, canonical_version
330335

331336
def __hash__(self) -> int:
332337
return hash(self._canonical_spec)

tests/test_specifiers.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1511,3 +1511,38 @@ def test_contains_with_compatible_operator(self):
15111511
combination = SpecifierSet("~=1.18.0") & SpecifierSet("~=1.18")
15121512
assert "1.19.5" not in combination
15131513
assert "1.18.0" in combination
1514+
1515+
@pytest.mark.parametrize(
1516+
("spec1", "spec2", "input_versions"),
1517+
[
1518+
# Test zero padding
1519+
("===1.0", "===1.0.0", ["1.0", "1.0.0"]),
1520+
("===1.0.0", "===1.0", ["1.0", "1.0.0"]),
1521+
("===1.0", "===1.0.0", ["1.0.0", "1.0"]),
1522+
("===1.0.0", "===1.0", ["1.0.0", "1.0"]),
1523+
# Test local versions
1524+
("===1.0", "===1.0+local", ["1.0", "1.0+local"]),
1525+
("===1.0+local", "===1.0", ["1.0", "1.0+local"]),
1526+
("===1.0", "===1.0+local", ["1.0+local", "1.0"]),
1527+
("===1.0+local", "===1.0", ["1.0+local", "1.0"]),
1528+
],
1529+
)
1530+
def test_arbitrary_equality_is_intersection_preserving(
1531+
self, spec1, spec2, input_versions
1532+
):
1533+
"""
1534+
In general we expect for two specifiers s1 and s2, that the two statements
1535+
are equivalent:
1536+
* set((s1, s2).filter(versions))
1537+
* set(s1.filter(versions)) & set(s2.filter(versions)).
1538+
1539+
This is tricky with the arbitrary equality operator (===) since it does
1540+
not follow normal version comparison rules.
1541+
"""
1542+
s1 = Specifier(spec1)
1543+
s2 = Specifier(spec2)
1544+
versions1 = set(s1.filter(input_versions))
1545+
versions2 = set(s2.filter(input_versions))
1546+
combined_versions = set(SpecifierSet(f"{spec1},{spec2}").filter(input_versions))
1547+
1548+
assert versions1 & versions2 == combined_versions

0 commit comments

Comments
 (0)