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