@@ -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+
21402230class 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