6161import typing
6262from enum import Enum
6363
64+ from typing import Generic , Union , Tuple , Callable , ClassVar
6465from pickle import _Pickler as Pickler
6566from pickle import _getattribute
6667from io import BytesIO
6768from importlib ._bootstrap import _find_spec
6869
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+
6976
7077# cloudpickle is meant for inter process communication: we expect all
7178# communicating processes to run the same Python version hence we favor
@@ -117,7 +124,18 @@ def _whichmodule(obj, name):
117124 - Errors arising during module introspection are ignored, as those errors
118125 are considered unwanted side effects.
119126 """
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+
121139 if module_name is not None :
122140 return module_name
123141 # Protect the iteration by using a copy of sys.modules against dynamic
@@ -140,23 +158,6 @@ def _whichmodule(obj, name):
140158 return None
141159
142160
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-
160161def _is_importable_by_name (obj , name = None ):
161162 """Determine if obj can be pickled as attribute of a file-backed module"""
162163 return _lookup_module_and_qualname (obj , name = name ) is not None
@@ -423,6 +424,18 @@ def _extract_class_dict(cls):
423424 return clsdict
424425
425426
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+
426439class CloudPickler (Pickler ):
427440
428441 dispatch = Pickler .dispatch .copy ()
@@ -611,11 +624,6 @@ def save_dynamic_class(self, obj):
611624 if isinstance (__dict__ , property ):
612625 type_kwargs ['__dict__' ] = __dict__
613626
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-
619627 save = self .save
620628 write = self .write
621629
@@ -715,9 +723,7 @@ def save_function_tuple(self, func):
715723 'doc' : func .__doc__ ,
716724 '_cloudpickle_submodules' : submodules
717725 }
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__' ):
721727 state ['annotations' ] = func .__annotations__
722728 if hasattr (func , '__qualname__' ):
723729 state ['qualname' ] = func .__qualname__
@@ -800,6 +806,14 @@ def save_global(self, obj, name=None, pack=struct.pack):
800806 elif obj in _BUILTIN_TYPE_NAMES :
801807 return self .save_reduce (
802808 _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 )
803817 elif name is not None :
804818 Pickler .save_global (self , obj , name = name )
805819 elif not _is_importable_by_name (obj , name = name ):
@@ -941,6 +955,31 @@ def inject_addons(self):
941955 """Plug in system. Register additional pickling functions if modules already loaded"""
942956 pass
943957
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+
944983
945984# Tornado support
946985
0 commit comments