Skip to content

Commit 1334da7

Browse files
committed
Fix CI: staticmethod callable on <3.10, coverage gaps
1 parent 439ad91 commit 1334da7

2 files changed

Lines changed: 118 additions & 108 deletions

File tree

src/packaging/specifiers.py

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -81,10 +81,6 @@ def __init__(self, version: Version, kind: int) -> None:
8181
self._kind = kind
8282
self._trimmed_release = _trim_release(version.release)
8383

84-
def __repr__(self) -> str:
85-
label = "AFTER_LOCALS" if self._kind == _AFTER_LOCALS else "AFTER_POSTS"
86-
return f"_ExclusionBound({self.version!r}, {label})"
87-
8884
def _is_family(self, other: Version) -> bool:
8985
"""Is ``other`` a version that this sentinel sorts above?"""
9086
v = self.version
@@ -110,10 +106,9 @@ def __lt__(self, other: object) -> bool:
110106
if self.version != other.version:
111107
return self.version < other.version
112108
return self._kind < other._kind
113-
if isinstance(other, Version):
114-
# self < other iff other is NOT in the family and other > V
115-
return not self._is_family(other) and self.version < other
116-
return NotImplemented
109+
assert isinstance(other, Version)
110+
# self < other iff other is NOT in the family and other > V
111+
return not self._is_family(other) and self.version < other
117112

118113
def __hash__(self) -> int:
119114
return hash((self.version, self._kind))
@@ -1120,13 +1115,8 @@ def _get_intervals(self) -> list[_SpecifierInterval] | None:
11201115
"""
11211116
if self._intervals is not None:
11221117
return self._intervals
1123-
if self._is_unsatisfiable is True:
1124-
return []
11251118

11261119
specs = self._specs
1127-
if not specs:
1128-
self._intervals = _FULL_RANGE
1129-
return _FULL_RANGE
11301120

11311121
# Intersect specs' intervals, bailing out if we encounter ===
11321122
# (string matching, not version comparison) or if the intersection

tests/test_specifiers.py

Lines changed: 115 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -2137,6 +2137,96 @@ def test_arbitrary_equality_is_intersection_preserving(
21372137
assert versions1 & versions2 == combined_versions
21382138

21392139

2140+
def _version_family(base: str) -> list[str]:
2141+
"""All PEP 440 suffixes and combinations around a base version.
2142+
2143+
Covers dev, alpha/beta/rc (with dev-of-pre and post-of-pre),
2144+
final, post (with dev-of-post), local variants (string, integer,
2145+
multi-segment, distinct labels), and a patch sub-release family.
2146+
"""
2147+
return [
2148+
f"{base}.dev0",
2149+
f"{base}.dev1",
2150+
f"{base}.dev0+local",
2151+
f"{base}a0",
2152+
f"{base}a0.post0.dev0",
2153+
f"{base}a0.post0",
2154+
f"{base}a1.dev1",
2155+
f"{base}a1.dev1+local",
2156+
f"{base}a1",
2157+
f"{base}a1+local",
2158+
f"{base}b1",
2159+
f"{base}b2.post1.dev1",
2160+
f"{base}b2.post1",
2161+
f"{base}rc1.dev1",
2162+
f"{base}rc1",
2163+
f"{base}rc2",
2164+
base,
2165+
f"{base}.0",
2166+
f"{base}.post0.dev0",
2167+
f"{base}.post0",
2168+
f"{base}.post1",
2169+
f"{base}.post1+local",
2170+
f"{base}+local",
2171+
f"{base}+local1",
2172+
f"{base}+local2",
2173+
f"{base}+1",
2174+
f"{base}+1.local",
2175+
f"{base}.1.dev1",
2176+
f"{base}.1a1",
2177+
f"{base}.1",
2178+
f"{base}.1+local",
2179+
f"{base}.1.post1",
2180+
]
2181+
2182+
2183+
_SAMPLE_BASES: list[str] = [
2184+
"0",
2185+
"0.0",
2186+
"1.0",
2187+
"1.1",
2188+
"1.2",
2189+
"2.0",
2190+
"2.1",
2191+
"3.0",
2192+
"1.0.1",
2193+
"1.4.2",
2194+
"2.0.0",
2195+
"3.10.2",
2196+
"3.8",
2197+
"3.9",
2198+
"3.10",
2199+
"3.11",
2200+
"3.12",
2201+
"3.13",
2202+
"3.14",
2203+
"10.0",
2204+
"100.0",
2205+
"1!0.0",
2206+
"1!1.0",
2207+
"1!2.0",
2208+
]
2209+
2210+
2211+
def _build_sample_versions(
2212+
bases: list[str], family_fn: Callable[[str], list[str]]
2213+
) -> list[Version]:
2214+
"""version_family x bases, deduplicated."""
2215+
version_strs: list[str] = []
2216+
for base in bases:
2217+
version_strs.extend(family_fn(base))
2218+
seen: set[str] = set()
2219+
unique: list[str] = []
2220+
for v in version_strs:
2221+
if v not in seen:
2222+
seen.add(v)
2223+
unique.append(v)
2224+
return [Version(v) for v in unique]
2225+
2226+
2227+
_SAMPLE_VERSIONS: list[Version] = _build_sample_versions(_SAMPLE_BASES, _version_family)
2228+
2229+
21402230
class TestIsUnsatisfiable:
21412231
"""Tests for SpecifierSet.is_unsatisfiable() and interval-based filtering.
21422232
@@ -2148,98 +2238,6 @@ class TestIsUnsatisfiable:
21482238
sample of versions (version_family x SAMPLE_BASES).
21492239
"""
21502240

2151-
@staticmethod
2152-
def _version_family(base: str) -> list[str]:
2153-
"""All PEP 440 suffixes and combinations around a base version.
2154-
2155-
Covers dev, alpha/beta/rc (with dev-of-pre and post-of-pre),
2156-
final, post (with dev-of-post), local variants (string, integer,
2157-
multi-segment, distinct labels), and a patch sub-release family.
2158-
"""
2159-
return [
2160-
f"{base}.dev0",
2161-
f"{base}.dev1",
2162-
f"{base}.dev0+local",
2163-
f"{base}a0",
2164-
f"{base}a0.post0.dev0",
2165-
f"{base}a0.post0",
2166-
f"{base}a1.dev1",
2167-
f"{base}a1.dev1+local",
2168-
f"{base}a1",
2169-
f"{base}a1+local",
2170-
f"{base}b1",
2171-
f"{base}b2.post1.dev1",
2172-
f"{base}b2.post1",
2173-
f"{base}rc1.dev1",
2174-
f"{base}rc1",
2175-
f"{base}rc2",
2176-
base,
2177-
f"{base}.0",
2178-
f"{base}.post0.dev0",
2179-
f"{base}.post0",
2180-
f"{base}.post1",
2181-
f"{base}.post1+local",
2182-
f"{base}+local",
2183-
f"{base}+local1",
2184-
f"{base}+local2",
2185-
f"{base}+1",
2186-
f"{base}+1.local",
2187-
f"{base}.1.dev1",
2188-
f"{base}.1a1",
2189-
f"{base}.1",
2190-
f"{base}.1+local",
2191-
f"{base}.1.post1",
2192-
]
2193-
2194-
# Base versions: zero, MAJOR.MINOR, MAJOR.MINOR.PATCH, real-world,
2195-
# large numbers, and epoch versions.
2196-
SAMPLE_BASES: typing.ClassVar[list[str]] = [
2197-
"0",
2198-
"0.0",
2199-
"1.0",
2200-
"1.1",
2201-
"1.2",
2202-
"2.0",
2203-
"2.1",
2204-
"3.0",
2205-
"1.0.1",
2206-
"1.4.2",
2207-
"2.0.0",
2208-
"3.10.2",
2209-
"3.8",
2210-
"3.9",
2211-
"3.10",
2212-
"3.11",
2213-
"3.12",
2214-
"3.13",
2215-
"3.14",
2216-
"10.0",
2217-
"100.0",
2218-
"1!0.0",
2219-
"1!1.0",
2220-
"1!2.0",
2221-
]
2222-
2223-
@staticmethod
2224-
def _build_sample_versions(
2225-
bases: list[str], family_fn: Callable[[str], list[str]]
2226-
) -> list[Version]:
2227-
"""version_family x bases, deduplicated."""
2228-
version_strs: list[str] = []
2229-
for base in bases:
2230-
version_strs.extend(family_fn(base))
2231-
seen: set[str] = set()
2232-
unique: list[str] = []
2233-
for v in version_strs:
2234-
if v not in seen:
2235-
seen.add(v)
2236-
unique.append(v)
2237-
return [Version(v) for v in unique]
2238-
2239-
SAMPLE_VERSIONS: typing.ClassVar[list[Version]] = _build_sample_versions(
2240-
SAMPLE_BASES, _version_family
2241-
)
2242-
22432241
# Specifier sets that must be detected as unsatisfiable.
22442242
UNSATISFIABLE: typing.ClassVar[list[str]] = [
22452243
# Crossed bounds
@@ -2366,7 +2364,7 @@ def test_unsatisfiable(self, spec_str: str) -> None:
23662364
"""Unsatisfiable specs must be detected, and filter must return empty."""
23672365
ss = SpecifierSet(spec_str)
23682366
assert ss.is_unsatisfiable(), f"Expected unsatisfiable: {spec_str!r}"
2369-
result = list(ss.filter(self.SAMPLE_VERSIONS, prereleases=True))
2367+
result = list(ss.filter(_SAMPLE_VERSIONS, prereleases=True))
23702368
assert result == [], (
23712369
f"is_unsatisfiable() but filter matched: "
23722370
f"{[str(v) for v in result]} for {spec_str!r}"
@@ -2384,9 +2382,9 @@ def test_filter_matches_per_spec_filter(self, spec_str: str) -> None:
23842382
if not spec_str:
23852383
return
23862384
ss = SpecifierSet(spec_str)
2387-
interval_result = set(ss.filter(self.SAMPLE_VERSIONS, prereleases=True))
2385+
interval_result = set(ss.filter(_SAMPLE_VERSIONS, prereleases=True))
23882386
manual_result = set()
2389-
for v in self.SAMPLE_VERSIONS:
2387+
for v in _SAMPLE_VERSIONS:
23902388
if all(spec.contains(v, prereleases=True) for spec in ss._specs):
23912389
manual_result.add(v)
23922390
assert interval_result == manual_result, (
@@ -2414,3 +2412,25 @@ def test_and_preserves_unsatisfiable(self) -> None:
24142412
def test_and_satisfiable(self) -> None:
24152413
combined = SpecifierSet(">=1.0") & SpecifierSet("<2.0")
24162414
assert not combined.is_unsatisfiable()
2415+
2416+
def test_and_reuses_interval_cache(self) -> None:
2417+
"""Specifier interval cache is reused when specs are shared via &."""
2418+
s1 = SpecifierSet(">=1.0")
2419+
s2 = SpecifierSet("<2.0")
2420+
# Compute intervals on the original sets first.
2421+
assert not s1.is_unsatisfiable()
2422+
assert not s2.is_unsatisfiable()
2423+
# __and__ reuses the same Specifier objects, so _to_intervals()
2424+
# hits the cache on those Specifier instances.
2425+
combined = s1 & s2
2426+
assert not combined.is_unsatisfiable()
2427+
2428+
def test_interval_bounds_are_hashable(self) -> None:
2429+
"""Interval bounds (including _ExclusionBound sentinels) are hashable."""
2430+
spec = Specifier(">1.0")
2431+
intervals = spec._to_intervals()
2432+
# Bounds are tuples of (version_or_sentinel, inclusive); hashing the
2433+
# interval tuple exercises _ExclusionBound.__hash__.
2434+
for lower, upper in intervals:
2435+
hash(lower)
2436+
hash(upper)

0 commit comments

Comments
 (0)