1515import functools
1616import itertools
1717import re
18+ import sys
1819import typing
19- from typing import Any , Callable , Final , Iterable , Iterator , Sequence , TypeVar , Union
20+ from typing import (
21+ TYPE_CHECKING ,
22+ Any ,
23+ Callable ,
24+ Final ,
25+ Iterable ,
26+ Iterator ,
27+ Sequence ,
28+ TypeVar ,
29+ Union ,
30+ )
2031
2132from .utils import canonicalize_version
2233from .version import InvalidVersion , Version
2334
35+ if sys .version_info >= (3 , 10 ):
36+ from typing import TypeGuard # pragma: no cover
37+ elif TYPE_CHECKING :
38+ from typing_extensions import TypeGuard
39+
2440__all__ = [
2541 "BaseSpecifier" ,
2642 "InvalidSpecifier" ,
@@ -33,6 +49,19 @@ def __dir__() -> list[str]:
3349 return __all__
3450
3551
52+ def _validate_spec (spec : object , / ) -> TypeGuard [tuple [str , str ]]:
53+ return (
54+ isinstance (spec , tuple )
55+ and len (spec ) == 2
56+ and isinstance (spec [0 ], str )
57+ and isinstance (spec [1 ], str )
58+ )
59+
60+
61+ def _validate_pre (pre : object , / ) -> TypeGuard [bool | None ]:
62+ return pre is None or isinstance (pre , bool )
63+
64+
3665T = TypeVar ("T" )
3766UnparsedVersion = Union [Version , str ]
3867UnparsedVersionVar = TypeVar ("UnparsedVersionVar" , bound = UnparsedVersion )
@@ -722,6 +751,46 @@ def prereleases(self) -> bool | None:
722751 def prereleases (self , value : bool | None ) -> None :
723752 self ._prereleases = value
724753
754+ def __getstate__ (self ) -> tuple [tuple [str , str ], bool | None ]:
755+ # Return state as a 2-item tuple for compactness:
756+ # ((operator, version), prereleases)
757+ # Cache members are excluded and will be recomputed on demand.
758+ return (self ._spec , self ._prereleases )
759+
760+ def __setstate__ (self , state : object ) -> None :
761+ # Always discard cached values - they will be recomputed on demand.
762+ self ._spec_version = None
763+ self ._wildcard_split = None
764+ self ._ranges = None
765+
766+ if isinstance (state , tuple ):
767+ if len (state ) == 2 :
768+ # New format (26.2+): ((operator, version), prereleases)
769+ spec , prereleases = state
770+ if _validate_spec (spec ) and _validate_pre (prereleases ):
771+ self ._spec = spec
772+ self ._prereleases = prereleases
773+ return
774+ if len (state ) == 2 and isinstance (state [1 ], dict ):
775+ # Format (packaging 26.0-26.1): (None, {slot: value}).
776+ _ , slot_dict = state
777+ spec = slot_dict .get ("_spec" )
778+ prereleases = slot_dict .get ("_prereleases" , "invalid" )
779+ if _validate_spec (spec ) and _validate_pre (prereleases ):
780+ self ._spec = spec
781+ self ._prereleases = prereleases
782+ return
783+ if isinstance (state , dict ):
784+ # Old format (packaging <= 25.x, no __slots__): state is a plain dict.
785+ spec = state .get ("_spec" )
786+ prereleases = state .get ("_prereleases" , "invalid" )
787+ if _validate_spec (spec ) and _validate_pre (prereleases ):
788+ self ._spec = spec
789+ self ._prereleases = prereleases
790+ return
791+
792+ raise TypeError (f"Cannot restore Specifier from { state !r} " )
793+
725794 @property
726795 def operator (self ) -> str :
727796 """The operator of this specifier.
@@ -1347,6 +1416,69 @@ def prereleases(self, value: bool | None) -> None:
13471416 self ._prereleases = value
13481417 self ._is_unsatisfiable = None
13491418
1419+ def __getstate__ (self ) -> tuple [tuple [Specifier , ...], bool | None ]:
1420+ # Return state as a 2-item tuple for compactness:
1421+ # (specs, prereleases)
1422+ # Cache members are excluded and will be recomputed on demand.
1423+ return (self ._specs , self ._prereleases )
1424+
1425+ def __setstate__ (self , state : object ) -> None :
1426+ # Always discard cached values - they will be recomputed on demand.
1427+ self ._resolved_ops = None
1428+ self ._is_unsatisfiable = None
1429+
1430+ if isinstance (state , tuple ):
1431+ if len (state ) == 2 :
1432+ # New format (26.2+): (specs, prereleases)
1433+ specs , prereleases = state
1434+ if (
1435+ isinstance (specs , tuple )
1436+ and all (isinstance (s , Specifier ) for s in specs )
1437+ and _validate_pre (prereleases )
1438+ ):
1439+ self ._specs = specs
1440+ self ._prereleases = prereleases
1441+ self ._canonicalized = len (specs ) <= 1
1442+ self ._has_arbitrary = any ("===" in str (s ) for s in specs )
1443+ return
1444+ if len (state ) == 2 and isinstance (state [1 ], dict ):
1445+ # Format (packaging 26.0-26.1): (None, {slot: value}).
1446+ _ , slot_dict = state
1447+ specs = slot_dict .get ("_specs" , ())
1448+ prereleases = slot_dict .get ("_prereleases" )
1449+ # Convert frozenset to tuple (26.0 stored as frozenset)
1450+ if isinstance (specs , frozenset ):
1451+ specs = tuple (sorted (specs , key = str ))
1452+ if (
1453+ isinstance (specs , tuple )
1454+ and all (isinstance (s , Specifier ) for s in specs )
1455+ and _validate_pre (prereleases )
1456+ ):
1457+ self ._specs = specs
1458+ self ._prereleases = prereleases
1459+ self ._canonicalized = len (self ._specs ) <= 1
1460+ self ._has_arbitrary = any ("===" in str (s ) for s in self ._specs )
1461+ return
1462+ if isinstance (state , dict ):
1463+ # Old format (packaging <= 25.x, no __slots__): state is a plain dict.
1464+ specs = state .get ("_specs" , ())
1465+ prereleases = state .get ("_prereleases" )
1466+ # Convert frozenset to tuple (26.0 stored as frozenset)
1467+ if isinstance (specs , frozenset ):
1468+ specs = tuple (sorted (specs , key = str ))
1469+ if (
1470+ isinstance (specs , tuple )
1471+ and all (isinstance (s , Specifier ) for s in specs )
1472+ and _validate_pre (prereleases )
1473+ ):
1474+ self ._specs = specs
1475+ self ._prereleases = prereleases
1476+ self ._canonicalized = len (self ._specs ) <= 1
1477+ self ._has_arbitrary = any ("===" in str (s ) for s in self ._specs )
1478+ return
1479+
1480+ raise TypeError (f"Cannot restore SpecifierSet from { state !r} " )
1481+
13501482 def __repr__ (self ) -> str :
13511483 """A representation of the specifier set that shows all internal state.
13521484
0 commit comments