61
61
import typing
62
62
from enum import Enum
63
63
64
+ from typing import Generic , Union , Tuple , Callable , ClassVar
64
65
from pickle import _Pickler as Pickler
65
66
from pickle import _getattribute
66
67
from io import BytesIO
67
68
from importlib ._bootstrap import _find_spec
68
69
70
+ try : # pragma: no branch
71
+ import typing_extensions as _typing_extensions
72
+ from typing_extensions import Literal , Final
73
+ except ImportError :
74
+ _typing_extensions = Literal = Final = None
75
+
69
76
70
77
# cloudpickle is meant for inter process communication: we expect all
71
78
# communicating processes to run the same Python version hence we favor
@@ -117,7 +124,18 @@ def _whichmodule(obj, name):
117
124
- Errors arising during module introspection are ignored, as those errors
118
125
are considered unwanted side effects.
119
126
"""
120
- module_name = _get_module_attr (obj )
127
+ if sys .version_info [:2 ] < (3 , 7 ) and isinstance (obj , typing .TypeVar ): # pragma: no branch # noqa
128
+ # Workaround bug in old Python versions: prior to Python 3.7,
129
+ # T.__module__ would always be set to "typing" even when the TypeVar T
130
+ # would be defined in a different module.
131
+ #
132
+ # For such older Python versions, we ignore the __module__ attribute of
133
+ # TypeVar instances and instead exhaustively lookup those instances in
134
+ # all currently imported modules.
135
+ module_name = None
136
+ else :
137
+ module_name = getattr (obj , '__module__' , None )
138
+
121
139
if module_name is not None :
122
140
return module_name
123
141
# Protect the iteration by using a copy of sys.modules against dynamic
@@ -140,23 +158,6 @@ def _whichmodule(obj, name):
140
158
return None
141
159
142
160
143
- if sys .version_info [:2 ] < (3 , 7 ): # pragma: no branch
144
- # Workaround bug in old Python versions: prior to Python 3.7, T.__module__
145
- # would always be set to "typing" even when the TypeVar T would be defined
146
- # in a different module.
147
- #
148
- # For such older Python versions, we ignore the __module__ attribute of
149
- # TypeVar instances and instead exhaustively lookup those instances in all
150
- # currently imported modules via the _whichmodule function.
151
- def _get_module_attr (obj ):
152
- if isinstance (obj , typing .TypeVar ):
153
- return None
154
- return getattr (obj , '__module__' , None )
155
- else :
156
- def _get_module_attr (obj ):
157
- return getattr (obj , '__module__' , None )
158
-
159
-
160
161
def _is_importable_by_name (obj , name = None ):
161
162
"""Determine if obj can be pickled as attribute of a file-backed module"""
162
163
return _lookup_module_and_qualname (obj , name = name ) is not None
@@ -423,6 +424,18 @@ def _extract_class_dict(cls):
423
424
return clsdict
424
425
425
426
427
+ if sys .version_info [:2 ] < (3 , 7 ): # pragma: no branch
428
+ def _is_parametrized_type_hint (obj ):
429
+ # This is very cheap but might generate false positives.
430
+ origin = getattr (obj , '__origin__' , None ) # typing Constructs
431
+ values = getattr (obj , '__values__' , None ) # typing_extensions.Literal
432
+ type_ = getattr (obj , '__type__' , None ) # typing_extensions.Final
433
+ return origin is not None or values is not None or type_ is not None
434
+
435
+ def _create_parametrized_type_hint (origin , args ):
436
+ return origin [args ]
437
+
438
+
426
439
class CloudPickler (Pickler ):
427
440
428
441
dispatch = Pickler .dispatch .copy ()
@@ -611,11 +624,6 @@ def save_dynamic_class(self, obj):
611
624
if isinstance (__dict__ , property ):
612
625
type_kwargs ['__dict__' ] = __dict__
613
626
614
- if sys .version_info < (3 , 7 ):
615
- # Although annotations were added in Python 3.4, It is not possible
616
- # to properly pickle them until Python 3.7. (See #193)
617
- clsdict .pop ('__annotations__' , None )
618
-
619
627
save = self .save
620
628
write = self .write
621
629
@@ -715,9 +723,7 @@ def save_function_tuple(self, func):
715
723
'doc' : func .__doc__ ,
716
724
'_cloudpickle_submodules' : submodules
717
725
}
718
- if hasattr (func , '__annotations__' ) and sys .version_info >= (3 , 7 ):
719
- # Although annotations were added in Python3.4, It is not possible
720
- # to properly pickle them until Python3.7. (See #193)
726
+ if hasattr (func , '__annotations__' ):
721
727
state ['annotations' ] = func .__annotations__
722
728
if hasattr (func , '__qualname__' ):
723
729
state ['qualname' ] = func .__qualname__
@@ -800,6 +806,14 @@ def save_global(self, obj, name=None, pack=struct.pack):
800
806
elif obj in _BUILTIN_TYPE_NAMES :
801
807
return self .save_reduce (
802
808
_builtin_type , (_BUILTIN_TYPE_NAMES [obj ],), obj = obj )
809
+
810
+ if sys .version_info [:2 ] < (3 , 7 ) and _is_parametrized_type_hint (obj ): # noqa # pragma: no branch
811
+ # Parametrized typing constructs in Python < 3.7 are not compatible
812
+ # with type checks and ``isinstance`` semantics. For this reason,
813
+ # it is easier to detect them using a duck-typing-based check
814
+ # (``_is_parametrized_type_hint``) than to populate the Pickler's
815
+ # dispatch with type-specific savers.
816
+ self ._save_parametrized_type_hint (obj )
803
817
elif name is not None :
804
818
Pickler .save_global (self , obj , name = name )
805
819
elif not _is_importable_by_name (obj , name = name ):
@@ -941,6 +955,31 @@ def inject_addons(self):
941
955
"""Plug in system. Register additional pickling functions if modules already loaded"""
942
956
pass
943
957
958
+ if sys .version_info < (3 , 7 ): # pragma: no branch
959
+ def _save_parametrized_type_hint (self , obj ):
960
+ # The distorted type check sematic for typing construct becomes:
961
+ # ``type(obj) is type(TypeHint)``, which means "obj is a
962
+ # parametrized TypeHint"
963
+ if type (obj ) is type (Literal ): # pragma: no branch
964
+ initargs = (Literal , obj .__values__ )
965
+ elif type (obj ) is type (Final ): # pragma: no branch
966
+ initargs = (Final , obj .__type__ )
967
+ elif type (obj ) is type (ClassVar ):
968
+ initargs = (ClassVar , obj .__type__ )
969
+ elif type (obj ) in [type (Union ), type (Tuple ), type (Generic )]:
970
+ initargs = (obj .__origin__ , obj .__args__ )
971
+ elif type (obj ) is type (Callable ):
972
+ args = obj .__args__
973
+ if args [0 ] is Ellipsis :
974
+ initargs = (obj .__origin__ , args )
975
+ else :
976
+ initargs = (obj .__origin__ , (list (args [:- 1 ]), args [- 1 ]))
977
+ else : # pragma: no cover
978
+ raise pickle .PicklingError (
979
+ "Cloudpickle Error: Unknown type {}" .format (type (obj ))
980
+ )
981
+ self .save_reduce (_create_parametrized_type_hint , initargs , obj = obj )
982
+
944
983
945
984
# Tornado support
946
985
0 commit comments