8000 gh-122311: Improve and unify pickle errors · python/cpython@43d94f2 · GitHub
[go: up one dir, main page]

Skip to content

Commit 43d94f2

Browse files
gh-122311: Improve and unify pickle errors
* Raise PicklingError instead of UnicodeEncodeError, ValueError and AttributeError in both implementations. * Chain the original exception to the pickle-specific one as __context__. * Include the error message of ImportError and some AttributeError in the PicklingError error message. * Unify error messages between Python and C implementations. * Refer to documented __reduce__ and __newobj__ callables instead of internal methods (e.g. save_reduce()) or pickle opcodes (e.g. NEWOBJ). * Include more details in error messages (what expected, what got). * Avoid including a potentially long repr of an arbitrary object in error messages.
1 parent 9e551f9 commit 43d94f2

File tree

4 files changed

+249
-223
lines changed

4 files changed

+249
-223
lines changed

Lib/pickle.py

Lines changed: 49 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -322,7 +322,9 @@ def whichmodule(obj, name):
322322
"""Find the module an object belong to."""
323323
dotted_path = name.split('.')
324324
module_name = getattr(obj, '__module__', None)
325-
if module_name is None and '<locals>' not in dotted_path:
325+
if '<locals>' in dotted_path:
326+
raise PicklingError(f"Can't pickle local object {obj!r}")
327+
if module_name is None:
326328
# Protect the iteration by using a list copy of sys.modules against dynamic
327329
# modules that trigger imports of other modules upon calls to getattr.
328330
for module_name, module in sys.modules.copy().items():
@@ -336,22 +338,21 @@ def whichmodule(obj, name):
336338
except AttributeError:
337339
pass
338340
module_name = '__main__'
339-
elif module_name is None:
340-
module_name = '__main__'
341341

342342
try:
343343
__import__(module_name, level=0)
344344
module = sys.modules[module_name]
345+
except (ImportError, ValueError, KeyError) as exc:
346+
raise PicklingError(f"Can't pickle {obj!r}: {exc!s}")
347+
try:
345348
if _getattribute(module, dotted_path) is obj:
346349
return module_name
347-
except (ImportError, KeyError, AttributeError):
348-
raise PicklingError(
349-
"Can't pickle %r: it's not found as %s.%s" %
350-
(obj, module_name, name)) from None
350+
except AttributeError:
351+
raise PicklingError(f"Can't pickle {obj!r}: "
352+
f"it's not found as {module_name}.{name}")
351353

352354
raise PicklingError(
353-
"Can't pickle %r: it's not the same object as %s.%s" %
354-
(obj, module_name, name))
355+
f"Can't pickle {obj!r}: it's not the same object as {module_name}.{name}")
355356

356357
def encode_long(x):
357358
r"""Encode a long to a two's complement little-endian binary string.
@@ -403,6 +404,13 @@ def decode_long(data):
403404
"""
404405
return int.from_bytes(data, byteorder='little', signed=True)
405406

407+
def _T(obj):
408+
cls = type(obj)
409+
module = cls.__module__
410+
if module in (None, 'builtins', '__main__'):
411+
return cls.__qualname__
412+
return f'{module}.{cls.__qualname__}'
413+
406414

407415
_NoValue = object()
408416

@@ -585,8 +593,7 @@ def save(self, obj, save_persistent_id=True):
585593
if reduce is not _NoValue:
586594
rv = reduce()
587595
else:
588-
raise PicklingError("Can't pickle %r object: %r" %
589-
(t.__name__, obj))
596+
raise PicklingError(f"Can't pickle {_T(t)} object")
590597

591598
# Check for string returned by reduce(), meaning "save as global"
592599
if isinstance(rv, str):
@@ -595,13 +602,13 @@ def save(self, obj, save_persistent_id=True):
595602

596603
# Assert that reduce() returned a tuple
597604
if not isinstance(rv, tuple):
598-
raise PicklingError("%s must return string or tuple" % reduce)
605+
raise PicklingError(f'__reduce__ must return a string or tuple, not {_T(rv)}')
599606

600607
# Assert that it returned an appropriately sized tuple
601608
l = len(rv)
602609
if not (2 <= l <= 6):
603-
raise PicklingError("Tuple returned by %s must have "
604-
"two to six elements" % reduce)
610+
raise PicklingError("tuple returned by __reduce__ "
611+
"must contain 2 through 6 elements")
605612

606613
# Save the reduce() output and finally memoize the object
607614
self.save_reduce(obj=obj, *rv)
@@ -626,10 +633,12 @@ def save_reduce(self, func, args, state=None, listitems=None,
626633
dictitems=None, state_setter=None, *, obj=None):
627634
# This API is called by some subclasses
628635

629-
if not isinstance(args, tuple):
630-
raise PicklingError("args from save_reduce() must be a tuple")
631636
if not callable(func):
632-
raise PicklingError("func from save_reduce() must be callable")
637+
raise PicklingError(f"first item of the tuple returned by __reduce__ "
638+
f"must be callable, not {_T(func)}")
639+
if not isinstance(args, tuple):
640+
raise PicklingError(f"second item of the tuple returned by __reduce__ "
641+
f"must be a tuple, not {_T(args)}")
633642

634643
save = self.save
635644
write = self.write
@@ -638,11 +647,10 @@ def save_reduce(self, func, args, state=None, listitems=None,
638647
if self.proto >= 2 and func_name == "__newobj_ex__":
639648
cls, args, kwargs = args
640649
if not hasattr(cls, "__new__"):
641-
raise PicklingError("args[0] from {} args has no __new__"
642-
.format(func_name))
650+
raise PicklingError("first argument to __newobj_ex__() has no __new__")
643651
if obj is not None and cls is not obj.__class__:
644-
raise PicklingError("args[0] from {} args has the wrong class"
645-
.format(func_name))
652+
raise PicklingError(f"first argument to __newobj_ex__() "
653+
f"must be {obj.__class__!r}, not {cls!r}")
646654
if self.proto >= 4:
647655
save(cls)
648656
save(args)
@@ -682,11 +690,10 @@ def save_reduce(self, func, args, state=None, listitems=None,
682690
# Python 2.2).
683691
cls = args[0]
684692
if not hasattr(cls, "__new__"):
685-
raise PicklingError(
686-
"args[0] from __newobj__ args has no __new__")
693+
raise PicklingError("first argument to __newobj__() has no __new__")
687694
if obj is not None and cls is not obj.__class__:
688-
raise PicklingError(
689-
"args[0] from __newobj__ args has the wrong class")
695+
raise PicklingError(f"first argument to __newobj__() "
696+
f"must be {obj.__class__!r}, not {cls!r}")
690697
args = args[1:]
691698
save(cls)
692699
save(args)
@@ -1128,8 +1135,7 @@ def save_global(self, obj, name=None):
11281135
def _save_toplevel_by_name(self, module_name, name):
11291136
if self.proto >= 3:
11301137
# Non-ASCII identifiers are supported only with protocols >= 3.
1131-
self.write(GLOBAL + bytes(module_name, "utf-8") + b'\n' +
1132-
bytes(name, "utf-8") + b'\n')
1138+
encoding = "utf-8"
11331139
else:
11341140
if self.fix_imports:
11351141
r_name_mapping = _compat_pickle.REVERSE_NAME_MAPPING
@@ -1138,13 +1144,19 @@ def _save_toplevel_by_name(self, module_name, name):
11381144
module_name, name = r_name_mapping[(module_name, name)]
11391145
elif module_name in r_import_mapping:
11401146
module_name = r_import_mapping[module_name]
1141-
try:
1142-
self.write(GLOBAL + bytes(module_name, "ascii") + b'\n' +
1143-
bytes(name, "ascii") + b'\n')
1144-
except UnicodeEncodeError:
1145-
raise PicklingError(
1146-
"can't pickle global identifier '%s.%s' using "
1147-
"pickle protocol %i" % (module_name, name, self.proto)) from None
1147+
encoding = "ascii"
1148+
try:
1149+
self.write(GLOBAL + bytes(module_name, encoding) + b'\n')
1150+
except UnicodeEncodeError:
1151+
raise PicklingError(
1152+
f"can't pickle module identifier {module_name!r} using "
1153+
f"pickle protocol {self.proto}")
1154+
try:
1155+
self.write(bytes(name, encoding) + b'\n')
1156+
except UnicodeEncodeError:
1157+
raise PicklingError(
1158+
f"can't pickle global identifier {name!r} using "
1159+
f"pickle protocol {self.proto}")
11481160

11491161
def save_type(self, obj):
11501162
if obj is type(None):
@@ -1605,17 +1617,13 @@ def find_class(self, module, name):
16051617
elif module in _compat_pickle.IMPORT_MAPPING:
16061618
module = _compat_pickle.IMPORT_MAPPING[module]
16071619
__import__(module, level=0)
1608-
if self.proto >= 4:
1609-
module = sys.modules[module]
1620+
if self.proto >= 4 and '.' in name:
16101621
dotted_path = name.split('.')
1611-
if '<locals>' in dotted_path:
1612-
raise AttributeError(
1613-
f"Can't get local attribute {name!r} on {module!r}")
16141622
try:
1615-
return _getattribute(module, dotted_path)
1623+
return _getattribute(sys.modules[module], dotted_path)
16161624
except AttributeError:
16171625
raise AttributeError(
1618-
f"Can't get attribute {name!r} on {module!r}") from None
1626+
f"Can't resolve path {name!r} on module {module!r}")
16191627
else:
16201628
return getattr(sys.modules[module], name)
16211629

0 commit comments

Comments
 (0)
0