Skip to content

Commit 8aabbca

Browse files
committed
Fix CI: staticmethod callable on <3.10, coverage gaps
1 parent 282b15e commit 8aabbca

2 files changed

Lines changed: 109 additions & 101 deletions

File tree

src/packaging/specifiers.py

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -70,9 +70,6 @@ def __init__(self, version: Version) -> None:
7070
self.version = version
7171
self._trimmed_release = _trim_release(version.release)
7272

73-
def __repr__(self) -> str:
74-
return f"_PostExcludeBound({self.version!r})"
75-
7673
def _is_post_family(self, other: Version) -> bool:
7774
"""Is ``other`` the same version as self.version, or a post-release of it?
7875
@@ -95,10 +92,9 @@ def __eq__(self, other: object) -> bool:
9592
def __lt__(self, other: object) -> bool:
9693
if isinstance(other, _PostExcludeBound):
9794
return self.version < other.version
98-
if isinstance(other, Version):
99-
# self < other iff other is NOT in the post-family and other > V
100-
return not self._is_post_family(other) and self.version < other
101-
return NotImplemented
95+
assert isinstance(other, Version)
96+
# self < other iff other is NOT in the post-family and other > V
97+
return not self._is_post_family(other) and self.version < other
10298

10399
def __hash__(self) -> int:
104100
return hash(self.version)
@@ -1335,15 +1331,7 @@ def _get_intervals(self) -> list[_SpecifierInterval]:
13351331
list otherwise. ``===`` specs are modeled as full range (no
13361332
constraint).
13371333
"""
1338-
if self._intervals is not None:
1339-
return self._intervals
1340-
if self._is_unsatisfiable is True:
1341-
return []
1342-
13431334
specs = self._specs
1344-
if not specs:
1345-
self._intervals = _FULL_RANGE
1346-
return _FULL_RANGE
13471335

13481336
# Intersect specs' intervals, with early exit on empty intersection.
13491337
result: list[_SpecifierInterval] | None = None

tests/test_specifiers.py

Lines changed: 106 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -2131,6 +2131,91 @@ def test_arbitrary_equality_is_intersection_preserving(
21312131
assert versions1 & versions2 == combined_versions
21322132

21332133

2134+
def _version_family(base: str) -> list[str]:
2135+
"""All PEP 440 suffixes and combinations around a base version."""
2136+
return [
2137+
f"{base}.dev0",
2138+
f"{base}.dev1",
2139+
f"{base}.dev0+local",
2140+
f"{base}a0",
2141+
f"{base}a0.post0.dev0",
2142+
f"{base}a0.post0",
2143+
f"{base}a1.dev1",
2144+
f"{base}a1.dev1+local",
2145+
f"{base}a1",
2146+
f"{base}a1+local",
2147+
f"{base}b1",
2148+
f"{base}b2.post1.dev1",
2149+
f"{base}b2.post1",
2150+
f"{base}rc1.dev1",
2151+
f"{base}rc1",
2152+
f"{base}rc2",
2153+
base,
2154+
f"{base}.0",
2155+
f"{base}.post0.dev0",
2156+
f"{base}.post0",
2157+
f"{base}.post1",
2158+
f"{base}.post1+local",
2159+
f"{base}+local",
2160+
f"{base}+local1",
2161+
f"{base}+local2",
2162+
f"{base}+1",
2163+
f"{base}+1.local",
2164+
f"{base}.1.dev1",
2165+
f"{base}.1a1",
2166+
f"{base}.1",
2167+
f"{base}.1+local",
2168+
f"{base}.1.post1",
2169+
]
2170+
2171+
2172+
_SAMPLE_BASES: list[str] = [
2173+
"0",
2174+
"0.0",
2175+
"1.0",
2176+
"1.1",
2177+
"1.2",
2178+
"2.0",
2179+
"2.1",
2180+
"3.0",
2181+
"1.0.1",
2182+
"1.4.2",
2183+
"2.0.0",
2184+
"3.10.2",
2185+
"3.8",
2186+
"3.9",
2187+
"3.10",
2188+
"3.11",
2189+
"3.12",
2190+
"3.13",
2191+
"3.14",
2192+
"10.0",
2193+
"100.0",
2194+
"1!0.0",
2195+
"1!1.0",
2196+
"1!2.0",
2197+
]
2198+
2199+
2200+
def _build_sample_versions(
2201+
bases: list[str], family_fn: Callable[[str], list[str]]
2202+
) -> list[Version]:
2203+
"""version_family x bases, deduplicated."""
2204+
version_strs: list[str] = []
2205+
for base in bases:
2206+
version_strs.extend(family_fn(base))
2207+
seen: set[str] = set()
2208+
unique: list[str] = []
2209+
for v in version_strs:
2210+
if v not in seen:
2211+
seen.add(v)
2212+
unique.append(v)
2213+
return [Version(v) for v in unique]
2214+
2215+
2216+
_SAMPLE_VERSIONS: list[Version] = _build_sample_versions(_SAMPLE_BASES, _version_family)
2217+
2218+
21342219
class TestIsUnsatisfiable:
21352220
"""Tests for SpecifierSet.is_unsatisfiable().
21362221
@@ -2139,91 +2224,6 @@ class TestIsUnsatisfiable:
21392224
- SATISFIABLE: not falsely reported as unsatisfiable.
21402225
"""
21412226

2142-
@staticmethod
2143-
def _version_family(base: str) -> list[str]:
2144-
"""All PEP 440 suffixes and combinations around a base version."""
2145-
return [
2146-
f"{base}.dev0",
2147-
f"{base}.dev1",
2148-
f"{base}.dev0+local",
2149-
f"{base}a0",
2150-
f"{base}a0.post0.dev0",
2151-
f"{base}a0.post0",
2152-
f"{base}a1.dev1",
2153-
f"{base}a1.dev1+local",
2154-
f"{base}a1",
2155-
f"{base}a1+local",
2156-
f"{base}b1",
2157-
f"{base}b2.post1.dev1",
2158-
f"{base}b2.post1",
2159-
f"{base}rc1.dev1",
2160-
f"{base}rc1",
2161-
f"{base}rc2",
2162-
base,
2163-
f"{base}.0",
2164-
f"{base}.post0.dev0",
2165-
f"{base}.post0",
2166-
f"{base}.post1",
2167-
f"{base}.post1+local",
2168-
f"{base}+local",
2169-
f"{base}+local1",
2170-
f"{base}+local2",
2171-
f"{base}+1",
2172-
f"{base}+1.local",
2173-
f"{base}.1.dev1",
2174-
f"{base}.1a1",
2175-
f"{base}.1",
2176-
f"{base}.1+local",
2177-
f"{base}.1.post1",
2178-
]
2179-
2180-
SAMPLE_BASES: typing.ClassVar[list[str]] = [
2181-
"0",
2182-
"0.0",
2183-
"1.0",
2184-
"1.1",
2185-
"1.2",
2186-
"2.0",
2187-
"2.1",
2188-
"3.0",
2189-
"1.0.1",
2190-
"1.4.2",
2191-
"2.0.0",
2192-
"3.10.2",
2193-
"3.8",
2194-
"3.9",
2195-
"3.10",
2196-
"3.11",
2197-
"3.12",
2198-
"3.13",
2199-
"3.14",
2200-
"10.0",
2201-
"100.0",
2202-
"1!0.0",
2203-
"1!1.0",
2204-
"1!2.0",
2205-
]
2206-
2207-
@staticmethod
2208-
def _build_sample_versions(
2209-
bases: list[str], family_fn: Callable[[str], list[str]]
2210-
) -> list[Version]:
2211-
"""version_family x bases, deduplicated."""
2212-
version_strs: list[str] = []
2213-
for base in bases:
2214-
version_strs.extend(family_fn(base))
2215-
seen: set[str] = set()
2216-
unique: list[str] = []
2217-
for v in version_strs:
2218-
if v not in seen:
2219-
seen.add(v)
2220-
unique.append(v)
2221-
return [Version(v) for v in unique]
2222-
2223-
SAMPLE_VERSIONS: typing.ClassVar[list[Version]] = _build_sample_versions(
2224-
SAMPLE_BASES, _version_family
2225-
)
2226-
22272227
UNSATISFIABLE: typing.ClassVar[list[str]] = [
22282228
# Crossed bounds
22292229
">=2.0,<1.0",
@@ -2344,7 +2344,7 @@ def test_unsatisfiable(self, spec_str: str) -> None:
23442344
"""Unsatisfiable specs must be detected, and filter must return empty."""
23452345
ss = SpecifierSet(spec_str)
23462346
assert ss.is_unsatisfiable(), f"Expected unsatisfiable: {spec_str!r}"
2347-
result = list(ss.filter(self.SAMPLE_VERSIONS, prereleases=True))
2347+
result = list(ss.filter(_SAMPLE_VERSIONS, prereleases=True))
23482348
assert result == [], (
23492349
f"is_unsatisfiable() but filter matched: "
23502350
f"{[str(v) for v in result]} for {spec_str!r}"
@@ -2375,3 +2375,23 @@ def test_and_preserves_unsatisfiable(self) -> None:
23752375
def test_and_satisfiable(self) -> None:
23762376
combined = SpecifierSet(">=1.0") & SpecifierSet("<2.0")
23772377
assert not combined.is_unsatisfiable()
2378+
2379+
def test_and_reuses_interval_cache(self) -> None:
2380+
"""Specifier interval cache is reused when specs are shared via &."""
2381+
s1 = SpecifierSet(">=1.0")
2382+
s2 = SpecifierSet("<2.0")
2383+
# Compute intervals on the original sets first.
2384+
assert not s1.is_unsatisfiable()
2385+
assert not s2.is_unsatisfiable()
2386+
# __and__ reuses the same Specifier objects, so _to_intervals()
2387+
# hits the cache on those Specifier instances.
2388+
combined = s1 & s2
2389+
assert not combined.is_unsatisfiable()
2390+
2391+
def test_interval_bounds_are_hashable(self) -> None:
2392+
"""Interval bounds (including _PostExcludeBound sentinels) are hashable."""
2393+
spec = Specifier(">1.0")
2394+
intervals = spec._to_intervals()
2395+
for lower, upper in intervals:
2396+
hash(lower)
2397+
hash(upper)

0 commit comments

Comments
 (0)