@@ -817,6 +817,147 @@ def test_specifier_hash_for_compatible_operator(self) -> None:
817817 assert hash (Specifier ("~=1.18.0" )) != hash (Specifier ("~=1.18" ))
818818
819819
820+ class TestSpecifierInternal :
821+ """Tests for internal Specifier._spec_version cache behavior.
822+
823+ Specifier._spec_version is a one-element cache that stores the parsed Version
824+ corresponding to Specifier.version after the first time it is needed for
825+ comparison, these tests validate that the cache is set and never changed.
826+ """
827+
828+ @pytest .mark .parametrize (
829+ ("specifier" , "test_versions" ),
830+ [
831+ (">=1.0" , ["0.9" , "1.0" , "1.1" , "2.0" ]),
832+ ("<=1.0" , ["0.9" , "1.0" , "1.1" , "2.0" ]),
833+ (">1.0" , ["0.9" , "1.0" , "1.1" , "2.0" ]),
834+ ("<1.0" , ["0.9" , "1.0" , "1.1" , "2.0" ]),
835+ ("==1.0" , ["0.9" , "1.0" , "1.1" , "2.0" ]),
836+ ("!=1.0" , ["0.9" , "1.0" , "1.1" , "2.0" ]),
837+ ("~=1.0" , ["0.9" , "1.0" , "1.1" , "2.0" ]),
838+ (">=1.0a1" , ["0.9" , "1.0a1" , "1.0" , "1.1" ]),
839+ (">=1.0.post1" , ["0.9" , "1.0" , "1.0.post1" , "1.1" ]),
840+ (">=1.0.dev1" , ["0.9" , "1.0.dev1" , "1.0" , "1.1" ]),
841+ ("==1.0+local" , ["1.0" , "1.0+local" , "1.0+other" , "1.1" ]),
842+ (">=1!1.0" , ["0!2.0" , "1!0.9" , "1!1.0" , "1!1.1" ]),
843+ ],
844+ )
845+ def test_spec_version_cache_consistency (
846+ self , specifier : str , test_versions : list [str ]
847+ ) -> None :
848+ """Cache is set on first contains and remains unchanged."""
849+ spec = Specifier (specifier , prereleases = True )
850+ assert spec ._spec_version is None
851+
852+ _ = test_versions [0 ] in spec
853+ assert spec ._spec_version == (spec .version , Version (spec .version ))
854+ initial_cache = spec ._spec_version
855+
856+ for v in test_versions [1 :]:
857+ _ = v in spec
858+ assert spec ._spec_version is initial_cache
859+
860+ _ = hash (spec )
861+ assert spec ._spec_version is initial_cache
862+
863+ _ = spec .prereleases
864+ assert spec ._spec_version is initial_cache
865+
866+ _ = spec == Specifier (specifier )
867+ assert spec ._spec_version is initial_cache
868+
869+ @pytest .mark .parametrize (
870+ ("specifier" , "test_versions" ),
871+ [
872+ (
873+ "==1.0.*" ,
874+ ["0.9" , "1.0" , "1.0.1" , "1.0a1" , "1.0.dev1" , "1.0.post1" , "1.0+local" ],
875+ ),
876+ (
877+ "!=1.0.*" ,
878+ ["0.9" , "1.0" , "1.0.1" , "1.0a1" , "1.0.dev1" , "1.0.post1" , "1.0+local" ],
879+ ),
880+ ],
881+ )
882+ def test_spec_version_cache_with_wildcards (
883+ self , specifier : str , test_versions : list [str ]
884+ ) -> None :
885+ """Wildcard specifiers use prefix matching, cache stays None."""
886+ spec = Specifier (specifier , prereleases = True )
887+
888+ for v in test_versions :
889+ _ = v in spec
890+ _ = spec .prereleases
891+ _ = hash (spec )
892+
893+ assert spec ._spec_version is None
894+
895+ @pytest .mark .parametrize (
896+ "specifier" ,
897+ [
898+ "===1.0" ,
899+ "===1.0.0+local" ,
900+ "===1.0.dev1" ,
901+ ],
902+ )
903+ def test_spec_version_cache_with_arbitrary_equality (self , specifier : str ) -> None :
904+ spec = Specifier (specifier )
905+
906+ _ = "1.0" in spec
907+ _ = spec .prereleases
908+ _ = hash (spec )
909+
910+ assert spec ._spec_version == (spec .version , Version (spec .version ))
911+
912+ @pytest .mark .parametrize (
913+ ("specifier" , "versions" ),
914+ [
915+ (
916+ "~=1.4.2" ,
917+ [
918+ # Matching versions
919+ "1.4.2" ,
920+ "1.4.3.dev1" ,
921+ "1.4.3a1" ,
922+ "1.4.3" ,
923+ "1.4.3.post1" ,
924+ "1.4.3+local" ,
925+ # Not matching versions
926+ "1.4.1" ,
927+ "1.4.1.post1" ,
928+ "1.5.0.dev0" ,
929+ "1.5.0a1" ,
930+ "1.5.0" ,
931+ "2.0" ,
932+ "2.0+local" ,
933+ ],
934+ ),
935+ ],
936+ )
937+ def test_spec_version_cache_compatible_operator (
938+ self ,
939+ specifier : str ,
940+ versions : list [str ],
941+ ) -> None :
942+ """~= caches the original spec version, not the prefix used for ==."""
943+ spec = Specifier (specifier , prereleases = True )
944+ assert spec ._spec_version is None
945+
946+ assert versions [0 ] in spec
947+ assert spec ._spec_version == (spec .version , Version (spec .version ))
948+ initial_cache = spec ._spec_version
949+
950+ for v in versions [1 :]:
951+ _ = v in spec
952+ assert spec ._spec_version is initial_cache
953+
954+ _ = hash (spec )
955+ assert spec ._spec_version is initial_cache
956+
957+ _ = spec .prereleases
958+ assert spec ._spec_version is initial_cache
959+
960+
820961class TestSpecifierSet :
821962 @pytest .mark .parametrize ("version" , VERSIONS )
822963 def test_empty_specifier (self , version : str ) -> None :
0 commit comments