bpo-32873: Treat type variables and special typing forms as immutable by copy and pickle (GH-6216)
This also fixes python/typingGH-512
This also fixes python/typingGH-511
As was discussed in both issues, some typing forms deserve to be treated
as immutable by copy and pickle modules, so that:
* copy(X) is X
* deepcopy(X) is X
* loads(dumps(X)) is X GH- pickled by reference
This PR adds such behaviour to:
* Type variables
* Special forms like Union, Any, ClassVar
* Unsubscripted generic aliases to containers like List, Mapping, Iterable
This not only resolves inconsistencies mentioned in the issues, but also
improves backwards compatibility with previous versions of Python
(including 3.6).
Note that this requires some dances with __module__ for type variables
(similar to NamedTuple) because the class TypeVar itself is define in typing,
while type variables should get module where they were defined.
https://bugs.python.org/issue32873
(cherry picked from commit 834940375a
)
Co-authored-by: Ivan Levkivskyi <levkivskyi@gmail.com>
This commit is contained in:
parent
05455637f3
commit
d0e04c8244
|
@ -1057,20 +1057,20 @@ class GenericTests(BaseTestCase):
|
||||||
self.assertEqual(x.foo, 42)
|
self.assertEqual(x.foo, 42)
|
||||||
self.assertEqual(x.bar, 'abc')
|
self.assertEqual(x.bar, 'abc')
|
||||||
self.assertEqual(x.__dict__, {'foo': 42, 'bar': 'abc'})
|
self.assertEqual(x.__dict__, {'foo': 42, 'bar': 'abc'})
|
||||||
samples = [Any, Union, Tuple, Callable, ClassVar]
|
samples = [Any, Union, Tuple, Callable, ClassVar,
|
||||||
|
Union[int, str], ClassVar[List], Tuple[int, ...], Callable[[str], bytes]]
|
||||||
for s in samples:
|
for s in samples:
|
||||||
for proto in range(pickle.HIGHEST_PROTOCOL + 1):
|
for proto in range(pickle.HIGHEST_PROTOCOL + 1):
|
||||||
z = pickle.dumps(s, proto)
|
z = pickle.dumps(s, proto)
|
||||||
x = pickle.loads(z)
|
x = pickle.loads(z)
|
||||||
self.assertEqual(s, x)
|
self.assertEqual(s, x)
|
||||||
more_samples = [List, typing.Iterable, typing.Type]
|
more_samples = [List, typing.Iterable, typing.Type, List[int],
|
||||||
|
typing.Type[typing.Mapping]]
|
||||||
for s in more_samples:
|
for s in more_samples:
|
||||||
for proto in range(pickle.HIGHEST_PROTOCOL + 1):
|
for proto in range(pickle.HIGHEST_PROTOCOL + 1):
|
||||||
z = pickle.dumps(s, proto)
|
z = pickle.dumps(s, proto)
|
||||||
x = pickle.loads(z)
|
x = pickle.loads(z)
|
||||||
self.assertEqual(repr(s), repr(x)) # TODO: fix this
|
self.assertEqual(s, x)
|
||||||
# see also comment in test_copy_and_deepcopy
|
|
||||||
# the issue is typing/#512
|
|
||||||
|
|
||||||
def test_copy_and_deepcopy(self):
|
def test_copy_and_deepcopy(self):
|
||||||
T = TypeVar('T')
|
T = TypeVar('T')
|
||||||
|
@ -1082,7 +1082,27 @@ class GenericTests(BaseTestCase):
|
||||||
Union['T', int], List['T'], typing.Mapping['T', int]]
|
Union['T', int], List['T'], typing.Mapping['T', int]]
|
||||||
for t in things + [Any]:
|
for t in things + [Any]:
|
||||||
self.assertEqual(t, copy(t))
|
self.assertEqual(t, copy(t))
|
||||||
self.assertEqual(repr(t), repr(deepcopy(t))) # Use repr() because of TypeVars
|
self.assertEqual(t, deepcopy(t))
|
||||||
|
|
||||||
|
def test_immutability_by_copy_and_pickle(self):
|
||||||
|
# Special forms like Union, Any, etc., generic aliases to containers like List,
|
||||||
|
# Mapping, etc., and type variabcles are considered immutable by copy and pickle.
|
||||||
|
global TP, TPB, TPV # for pickle
|
||||||
|
TP = TypeVar('TP')
|
||||||
|
TPB = TypeVar('TPB', bound=int)
|
||||||
|
TPV = TypeVar('TPV', bytes, str)
|
||||||
|
for X in [TP, TPB, TPV, List, typing.Mapping, ClassVar, typing.Iterable,
|
||||||
|
Union, Any, Tuple, Callable]:
|
||||||
|
self.assertIs(copy(X), X)
|
||||||
|
self.assertIs(deepcopy(X), X)
|
||||||
|
self.assertIs(pickle.loads(pickle.dumps(X)), X)
|
||||||
|
# Check that local type variables are copyable.
|
||||||
|
TL = TypeVar('TL')
|
||||||
|
TLB = TypeVar('TLB', bound=int)
|
||||||
|
TLV = TypeVar('TLV', bytes, str)
|
||||||
|
for X in [TL, TLB, TLV]:
|
||||||
|
self.assertIs(copy(X), X)
|
||||||
|
self.assertIs(deepcopy(X), X)
|
||||||
|
|
||||||
def test_copy_generic_instances(self):
|
def test_copy_generic_instances(self):
|
||||||
T = TypeVar('T')
|
T = TypeVar('T')
|
||||||
|
|
|
@ -285,8 +285,17 @@ class _Final:
|
||||||
if '_root' not in kwds:
|
if '_root' not in kwds:
|
||||||
raise TypeError("Cannot subclass special typing classes")
|
raise TypeError("Cannot subclass special typing classes")
|
||||||
|
|
||||||
|
class _Immutable:
|
||||||
|
"""Mixin to indicate that object should not be copied."""
|
||||||
|
|
||||||
class _SpecialForm(_Final, _root=True):
|
def __copy__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __deepcopy__(self, memo):
|
||||||
|
return self
|
||||||
|
|
||||||
|
|
||||||
|
class _SpecialForm(_Final, _Immutable, _root=True):
|
||||||
"""Internal indicator of special typing constructs.
|
"""Internal indicator of special typing constructs.
|
||||||
See _doc instance attribute for specific docs.
|
See _doc instance attribute for specific docs.
|
||||||
"""
|
"""
|
||||||
|
@ -328,8 +337,8 @@ class _SpecialForm(_Final, _root=True):
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return 'typing.' + self._name
|
return 'typing.' + self._name
|
||||||
|
|
||||||
def __copy__(self):
|
def __reduce__(self):
|
||||||
return self # Special forms are immutable.
|
return self._name
|
||||||
|
|
||||||
def __call__(self, *args, **kwds):
|
def __call__(self, *args, **kwds):
|
||||||
raise TypeError(f"Cannot instantiate {self!r}")
|
raise TypeError(f"Cannot instantiate {self!r}")
|
||||||
|
@ -496,7 +505,11 @@ class ForwardRef(_Final, _root=True):
|
||||||
return f'ForwardRef({self.__forward_arg__!r})'
|
return f'ForwardRef({self.__forward_arg__!r})'
|
||||||
|
|
||||||
|
|
||||||
class TypeVar(_Final, _root=True):
|
def _find_name(mod, name):
|
||||||
|
return getattr(sys.modules[mod], name)
|
||||||
|
|
||||||
|
|
||||||
|
class TypeVar(_Final, _Immutable, _root=True):
|
||||||
"""Type variable.
|
"""Type variable.
|
||||||
|
|
||||||
Usage::
|
Usage::
|
||||||
|
@ -536,10 +549,12 @@ class TypeVar(_Final, _root=True):
|
||||||
T.__covariant__ == False
|
T.__covariant__ == False
|
||||||
T.__contravariant__ = False
|
T.__contravariant__ = False
|
||||||
A.__constraints__ == (str, bytes)
|
A.__constraints__ == (str, bytes)
|
||||||
|
|
||||||
|
Note that only type variables defined in global scope can be pickled.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__slots__ = ('__name__', '__bound__', '__constraints__',
|
__slots__ = ('__name__', '__bound__', '__constraints__',
|
||||||
'__covariant__', '__contravariant__')
|
'__covariant__', '__contravariant__', '_def_mod')
|
||||||
|
|
||||||
def __init__(self, name, *constraints, bound=None,
|
def __init__(self, name, *constraints, bound=None,
|
||||||
covariant=False, contravariant=False):
|
covariant=False, contravariant=False):
|
||||||
|
@ -558,6 +573,7 @@ class TypeVar(_Final, _root=True):
|
||||||
self.__bound__ = _type_check(bound, "Bound must be a type.")
|
self.__bound__ = _type_check(bound, "Bound must be a type.")
|
||||||
else:
|
else:
|
||||||
self.__bound__ = None
|
self.__bound__ = None
|
||||||
|
self._def_mod = sys._getframe(1).f_globals['__name__'] # for pickling
|
||||||
|
|
||||||
def __getstate__(self):
|
def __getstate__(self):
|
||||||
return {'name': self.__name__,
|
return {'name': self.__name__,
|
||||||
|
@ -582,6 +598,9 @@ class TypeVar(_Final, _root=True):
|
||||||
prefix = '~'
|
prefix = '~'
|
||||||
return prefix + self.__name__
|
return prefix + self.__name__
|
||||||
|
|
||||||
|
def __reduce__(self):
|
||||||
|
return (_find_name, (self._def_mod, self.__name__))
|
||||||
|
|
||||||
|
|
||||||
# Special typing constructs Union, Optional, Generic, Callable and Tuple
|
# Special typing constructs Union, Optional, Generic, Callable and Tuple
|
||||||
# use three special attributes for internal bookkeeping of generic types:
|
# use three special attributes for internal bookkeeping of generic types:
|
||||||
|
@ -724,6 +743,11 @@ class _GenericAlias(_Final, _root=True):
|
||||||
raise TypeError("Subscripted generics cannot be used with"
|
raise TypeError("Subscripted generics cannot be used with"
|
||||||
" class and instance checks")
|
" class and instance checks")
|
||||||
|
|
||||||
|
def __reduce__(self):
|
||||||
|
if self._special:
|
||||||
|
return self._name
|
||||||
|
return super().__reduce__()
|
||||||
|
|
||||||
|
|
||||||
class _VariadicGenericAlias(_GenericAlias, _root=True):
|
class _VariadicGenericAlias(_GenericAlias, _root=True):
|
||||||
"""Same as _GenericAlias above but for variadic aliases. Currently,
|
"""Same as _GenericAlias above but for variadic aliases. Currently,
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
Treat type variables and special typing forms as immutable by copy and
|
||||||
|
pickle. This fixes several minor issues and inconsistencies, and improves
|
||||||
|
backwards compatibility with Python 3.6.
|
Loading…
Reference in New Issue