Skip to content

Commit 8349403

Browse files
authored
bpo-32873: Treat type variables and special typing forms as immutable by copy and pickle (GH-6216)
This also fixes python/typing#512 This also fixes python/typing#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 # 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
1 parent 0e7144b commit 8349403

File tree

3 files changed

+58
-11
lines changed

3 files changed

+58
-11
lines changed

Lib/test/test_typing.py

+26-6
Original file line numberDiff line numberDiff line change
@@ -1057,20 +1057,20 @@ class C(B[int]):
10571057
self.assertEqual(x.foo, 42)
10581058
self.assertEqual(x.bar, 'abc')
10591059
self.assertEqual(x.__dict__, {'foo': 42, 'bar': 'abc'})
1060-
samples = [Any, Union, Tuple, Callable, ClassVar]
1060+
samples = [Any, Union, Tuple, Callable, ClassVar,
1061+
Union[int, str], ClassVar[List], Tuple[int, ...], Callable[[str], bytes]]
10611062
for s in samples:
10621063
for proto in range(pickle.HIGHEST_PROTOCOL + 1):
10631064
z = pickle.dumps(s, proto)
10641065
x = pickle.loads(z)
10651066
self.assertEqual(s, x)
1066-
more_samples = [List, typing.Iterable, typing.Type]
1067+
more_samples = [List, typing.Iterable, typing.Type, List[int],
1068+
typing.Type[typing.Mapping]]
10671069
for s in more_samples:
10681070
for proto in range(pickle.HIGHEST_PROTOCOL + 1):
10691071
z = pickle.dumps(s, proto)
10701072
x = pickle.loads(z)
1071-
self.assertEqual(repr(s), repr(x)) # TODO: fix this
1072-
# see also comment in test_copy_and_deepcopy
1073-
# the issue is typing/#512
1073+
self.assertEqual(s, x)
10741074

10751075
def test_copy_and_deepcopy(self):
10761076
T = TypeVar('T')
@@ -1082,7 +1082,27 @@ class Node(Generic[T]): ...
10821082
Union['T', int], List['T'], typing.Mapping['T', int]]
10831083
for t in things + [Any]:
10841084
self.assertEqual(t, copy(t))
1085-
self.assertEqual(repr(t), repr(deepcopy(t))) # Use repr() because of TypeVars
1085+
self.assertEqual(t, deepcopy(t))
1086+
1087+
def test_immutability_by_copy_and_pickle(self):
1088+
# Special forms like Union, Any, etc., generic aliases to containers like List,
1089+
# Mapping, etc., and type variabcles are considered immutable by copy and pickle.
1090+
global TP, TPB, TPV # for pickle
1091+
TP = TypeVar('TP')
1092+
TPB = TypeVar('TPB', bound=int)
1093+
TPV = TypeVar('TPV', bytes, str)
1094+
for X in [TP, TPB, TPV, List, typing.Mapping, ClassVar, typing.Iterable,
1095+
Union, Any, Tuple, Callable]:
1096+
self.assertIs(copy(X), X)
1097+
self.assertIs(deepcopy(X), X)
1098+
self.assertIs(pickle.loads(pickle.dumps(X)), X)
1099+
# Check that local type variables are copyable.
1100+
TL = TypeVar('TL')
1101+
TLB = TypeVar('TLB', bound=int)
1102+
TLV = TypeVar('TLV', bytes, str)
1103+
for X in [TL, TLB, TLV]:
1104+
self.assertIs(copy(X), X)
1105+
self.assertIs(deepcopy(X), X)
10861106

10871107
def test_copy_generic_instances(self):
10881108
T = TypeVar('T')

Lib/typing.py

+29-5
Original file line numberDiff line numberDiff line change
@@ -285,8 +285,17 @@ def __init_subclass__(self, *args, **kwds):
285285
if '_root' not in kwds:
286286
raise TypeError("Cannot subclass special typing classes")
287287

288+
class _Immutable:
289+
"""Mixin to indicate that object should not be copied."""
288290

289-
class _SpecialForm(_Final, _root=True):
291+
def __copy__(self):
292+
return self
293+
294+
def __deepcopy__(self, memo):
295+
return self
296+
297+
298+
class _SpecialForm(_Final, _Immutable, _root=True):
290299
"""Internal indicator of special typing constructs.
291300
See _doc instance attribute for specific docs.
292301
"""
@@ -328,8 +337,8 @@ def __hash__(self):
328337
def __repr__(self):
329338
return 'typing.' + self._name
330339

331-
def __copy__(self):
332-
return self # Special forms are immutable.
340+
def __reduce__(self):
341+
return self._name
333342

334343
def __call__(self, *args, **kwds):
335344
raise TypeError(f"Cannot instantiate {self!r}")
@@ -496,7 +505,11 @@ def __repr__(self):
496505
return f'ForwardRef({self.__forward_arg__!r})'
497506

498507

499-
class TypeVar(_Final, _root=True):
508+
def _find_name(mod, name):
509+
return getattr(sys.modules[mod], name)
510+
511+
512+
class TypeVar(_Final, _Immutable, _root=True):
500513
"""Type variable.
501514
502515
Usage::
@@ -536,10 +549,12 @@ def longest(x: A, y: A) -> A:
536549
T.__covariant__ == False
537550
T.__contravariant__ = False
538551
A.__constraints__ == (str, bytes)
552+
553+
Note that only type variables defined in global scope can be pickled.
539554
"""
540555

541556
__slots__ = ('__name__', '__bound__', '__constraints__',
542-
'__covariant__', '__contravariant__')
557+
'__covariant__', '__contravariant__', '_def_mod')
543558

544559
def __init__(self, name, *constraints, bound=None,
545560
covariant=False, contravariant=False):
@@ -558,6 +573,7 @@ def __init__(self, name, *constraints, bound=None,
558573
self.__bound__ = _type_check(bound, "Bound must be a type.")
559574
else:
560575
self.__bound__ = None
576+
self._def_mod = sys._getframe(1).f_globals['__name__'] # for pickling
561577

562578
def __getstate__(self):
563579
return {'name': self.__name__,
@@ -582,6 +598,9 @@ def __repr__(self):
582598
prefix = '~'
583599
return prefix + self.__name__
584600

601+
def __reduce__(self):
602+
return (_find_name, (self._def_mod, self.__name__))
603+
585604

586605
# Special typing constructs Union, Optional, Generic, Callable and Tuple
587606
# use three special attributes for internal bookkeeping of generic types:
@@ -724,6 +743,11 @@ def __subclasscheck__(self, cls):
724743
raise TypeError("Subscripted generics cannot be used with"
725744
" class and instance checks")
726745

746+
def __reduce__(self):
747+
if self._special:
748+
return self._name
749+
return super().__reduce__()
750+
727751

728752
class _VariadicGenericAlias(_GenericAlias, _root=True):
729753
"""Same as _GenericAlias above but for variadic aliases. Currently,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Treat type variables and special typing forms as immutable by copy and
2+
pickle. This fixes several minor issues and inconsistencies, and improves
3+
backwards compatibility with Python 3.6.

0 commit comments

Comments
 (0)