From 6510e3a0038cb8a322f731aacc0467680e70e9d9 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Wed, 24 Jul 2024 08:48:39 +0300 Subject: [PATCH 1/2] gh-122213: Add notes for pickle serialization errors This allows to identify the source of the error. --- Doc/whatsnew/3.14.rst | 7 + Lib/pickle.py | 155 ++++++++--- Lib/test/pickletester.py | 249 ++++++++++++++++++ ...-07-24-08-48-22.gh-issue-122213.o3pdgA.rst | 2 + Modules/_pickle.c | 141 +++++++--- 5 files changed, 486 insertions(+), 68 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2024-07-24-08-48-22.gh-issue-122213.o3pdgA.rst diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index bd8bdcb6732fde..0b30df49ac7608 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -151,6 +151,13 @@ pdb :pdbcmd:`commands` are preserved across hard-coded breakpoints. (Contributed by Tian Gao in :gh:`121450`.) +pickle +------ + +Add notes for pickle serialization errors that allow to identify the source +of the error. +(Contributed by Serhiy Storchaka in :gh:`122213`.) + symtable -------- diff --git a/Lib/pickle.py b/Lib/pickle.py index 115bd893ca1a38..bda6261b56ad9a 100644 --- a/Lib/pickle.py +++ b/Lib/pickle.py @@ -395,6 +395,13 @@ def decode_long(data): """ return int.from_bytes(data, byteorder='little', signed=True) +def _T(obj): + cls = type(obj) + module = cls.__module__ + if module is None or module == 'builtins': + return cls.__qualname__ + return f'{module}.{cls.__qualname__}' + _NoValue = object() @@ -636,13 +643,25 @@ def save_reduce(self, func, args, state=None, listitems=None, raise PicklingError("args[0] from {} args has the wrong class" .format(func_name)) if self.proto >= 4: - save(cls) - save(args) - save(kwargs) + try: + save(cls) + except BaseException as exc: + exc.add_note(f'when serializing {_T(obj)} class') + raise + try: + save(args) + save(kwargs) + except BaseException as exc: + exc.add_note(f'when serializing {_T(obj)} __new__ arguments') + raise write(NEWOBJ_EX) else: func = partial(cls.__new__, cls, *args, **kwargs) - save(func) + try: + save(func) + except BaseException as exc: + exc.add_note(f'when serializing {_T(obj)} reconstructor') + raise save(()) write(REDUCE) elif self.proto >= 2 and func_name == "__newobj__": @@ -680,12 +699,28 @@ def save_reduce(self, func, args, state=None, listitems=None, raise PicklingError( "args[0] from __newobj__ args has the wrong class") args = args[1:] - save(cls) - save(args) + try: + save(cls) + except BaseException as exc: + exc.add_note(f'when serializing {_T(obj)} class') + raise + try: + save(args) + except BaseException as exc: + exc.add_note(f'when serializing {_T(obj)} __new__ arguments') + raise write(NEWOBJ) else: - save(func) - save(args) + try: + save(func) + except BaseException as exc: + exc.add_note(f'when serializing {_T(obj)} reconstructor') + raise + try: + save(args) + except BaseException as exc: + exc.add_note(f'when serializing {_T(obj)} reconstructor arguments') + raise write(REDUCE) if obj is not None: @@ -703,23 +738,35 @@ def save_reduce(self, func, args, state=None, listitems=None, # items and dict items (as (key, value) tuples), or None. if listitems is not None: - self._batch_appends(listitems) + self._batch_appends(listitems, obj) if dictitems is not None: - self._batch_setitems(dictitems) + self._batch_setitems(dictitems, obj) if state is not None: if state_setter is None: - save(state) + try: + save(state) + except BaseException as exc: + exc.add_note(f'when serializing {_T(obj)} state') + raise write(BUILD) else: # If a state_setter is specified, call it instead of load_build # to update obj's with its previous state. # First, push state_setter and its tuple of expected arguments # (obj, state) onto the stack. - save(state_setter) + try: + save(state_setter) + except BaseException as exc: + exc.add_note(f'when serializing {_T(obj)} state setter') + raise save(obj) # simple BINGET opcode as obj is already memoized. - save(state) + try: + save(state) + except BaseException as exc: + exc.add_note(f'when serializing {_T(obj)} state') + raise write(TUPLE2) # Trigger a state_setter(obj, state) function call. write(REDUCE) @@ -899,8 +946,12 @@ def save_tuple(self, obj): save = self.save memo = self.memo if n <= 3 and self.proto >= 2: - for element in obj: - save(element) + for i, element in enumerate(obj): + try: + save(element) + except BaseException as exc: + exc.add_note(f'when serializing {_T(obj)} item {i}') + raise # Subtle. Same as in the big comment below. if id(obj) in memo: get = self.get(memo[id(obj)][0]) @@ -914,8 +965,12 @@ def save_tuple(self, obj): # has more than 3 elements. write = self.write write(MARK) - for element in obj: - save(element) + for i, element in enumerate(obj): + try: + save(element) + except BaseException as exc: + exc.add_note(f'when serializing {_T(obj)} item {i}') + raise if id(obj) in memo: # Subtle. d was not in memo when we entered save_tuple(), so @@ -945,38 +1000,52 @@ def save_list(self, obj): self.write(MARK + LIST) self.memoize(obj) - self._batch_appends(obj) + self._batch_appends(obj, obj) dispatch[list] = save_list _BATCHSIZE = 1000 - def _batch_appends(self, items): + def _batch_appends(self, items, obj): # Helper to batch up APPENDS sequences save = self.save write = self.write if not self.bin: - for x in items: - save(x) + for i, x in enumerate(items): + try: + save(x) + except BaseException as exc: + exc.add_note(f'when serializing {_T(obj)} item {i}') + raise write(APPEND) return it = iter(items) + start = 0 while True: tmp = list(islice(it, self._BATCHSIZE)) n = len(tmp) if n > 1: write(MARK) - for x in tmp: - save(x) + for i, x in enumerate(tmp, start): + try: + save(x) + except BaseException as exc: + exc.add_note(f'when serializing {_T(obj)} item {i}') + raise write(APPENDS) elif n: - save(tmp[0]) + try: + save(tmp[0]) + except BaseException as exc: + exc.add_note(f'when serializing {_T(obj)} item {start}') + raise write(APPEND) # else tmp is empty, and we're done if n < self._BATCHSIZE: return + start += n def save_dict(self, obj): if self.bin: @@ -985,11 +1054,11 @@ def save_dict(self, obj): self.write(MARK + DICT) self.memoize(obj) - self._batch_setitems(obj.items()) + self._batch_setitems(obj.items(), obj) dispatch[dict] = save_dict - def _batch_setitems(self, items): + def _batch_setitems(self, items, obj): # Helper to batch up SETITEMS sequences; proto >= 1 only save = self.save write = self.write @@ -997,7 +1066,11 @@ def _batch_setitems(self, items): if not self.bin: for k, v in items: save(k) - save(v) + try: + save(v) + except BaseException as exc: + exc.add_note(f'when serializing {_T(obj)} item {k!r}') + raise write(SETITEM) return @@ -1009,12 +1082,20 @@ def _batch_setitems(self, items): write(MARK) for k, v in tmp: save(k) - save(v) + try: + save(v) + except BaseException as exc: + exc.add_note(f'when serializing {_T(obj)} item {k!r}') + raise write(SETITEMS) elif n: k, v = tmp[0] save(k) - save(v) + try: + save(v) + except BaseException as exc: + exc.add_note(f'when serializing {_T(obj)} item {k!r}') + raise write(SETITEM) # else tmp is empty, and we're done if n < self._BATCHSIZE: @@ -1037,8 +1118,12 @@ def save_set(self, obj): n = len(batch) if n > 0: write(MARK) - for item in batch: - save(item) + try: + for item in batch: + save(item) + except BaseException as exc: + exc.add_note(f'when serializing {_T(obj)} element') + raise write(ADDITEMS) if n < self._BATCHSIZE: return @@ -1053,8 +1138,12 @@ def save_frozenset(self, obj): return write(MARK) - for item in obj: - save(item) + try: + for item in obj: + save(item) + except BaseException as exc: + exc.add_note(f'when serializing {_T(obj)} element') + raise if id(obj) in self.memo: # If the object is already in the memo, this means it is diff --git a/Lib/test/pickletester.py b/Lib/test/pickletester.py index 9922591ce7114a..084fbe6da8c6c3 100644 --- a/Lib/test/pickletester.py +++ b/Lib/test/pickletester.py @@ -2584,6 +2584,236 @@ def test_setitems_on_non_dicts(self): else: self._check_pickling_with_opcode(obj, pickle.SETITEMS, proto) + def test_unpickleable_reconstructor(self): + obj = REX((UnpickleableCallable(), (), None)) + for proto in protocols: + with self.assertRaises(CustomError) as cm: + self.dumps(obj, proto) + self.assertEqual(cm.exception.__notes__, + ['when serializing test.pickletester.REX reconstructor']) + + def test_unpickleable_reconstructor_args(self): + obj = REX((print, (1, 2, UNPICKLEABLE), None)) + for proto in protocols: + with self.assertRaises(CustomError) as cm: + self.dumps(obj, proto) + self.assertEqual(cm.exception.__notes__, + ['when serializing tuple item 2', + 'when serializing test.pickletester.REX reconstructor arguments']) + + def test_unpickleable_newobj_class(self): + class LocalREX(REX): pass + obj = LocalREX((copyreg.__newobj__, (LocalREX,), None)) + for proto in protocols: + with self.assertRaises((pickle.PicklingError, AttributeError)) as cm: + self.dumps(obj, proto) + if proto >= 2: + self.assertEqual(cm.exception.__notes__, + [f'when serializing {LocalREX.__module__}.{LocalREX.__qualname__} class']) + else: + self.assertEqual(cm.exception.__notes__, + ['when serializing tuple item 0', + f'when serializing {LocalREX.__module__}.{LocalREX.__qualname__} reconstructor arguments']) + + def test_unpickleable_newobj_args(self): + obj = REX((copyreg.__newobj__, (REX, 1, 2, UNPICKLEABLE), None)) + for proto in protocols: + with self.assertRaises(CustomError) as cm: + self.dumps(obj, proto) + if proto >= 2: + self.assertEqual(cm.exception.__notes__, + ['when serializing tuple item 2', + 'when serializing test.pickletester.REX __new__ arguments']) + else: + self.assertEqual(cm.exception.__notes__, + ['when serializing tuple item 3', + 'when serializing test.pickletester.REX reconstructor arguments']) + + def test_unpickleable_newobj_ex_class(self): + class LocalREX(REX): pass + obj = LocalREX((copyreg.__newobj_ex__, (LocalREX, (), {}), None)) + for proto in protocols: + with self.assertRaises((pickle.PicklingError, AttributeError)) as cm: + self.dumps(obj, proto) + if proto >= 4: + self.assertEqual(cm.exception.__notes__, + [f'when serializing {LocalREX.__module__}.{LocalREX.__qualname__} class']) + elif proto >= 2: + self.assertEqual(cm.exception.__notes__, + ['when serializing tuple item 0', + 'when serializing tuple item 1', + 'when serializing functools.partial state', + f'when serializing {LocalREX.__module__}.{LocalREX.__qualname__} reconstructor']) + else: + self.assertEqual(cm.exception.__notes__, + ['when serializing tuple item 0', + f'when serializing {LocalREX.__module__}.{LocalREX.__qualname__} reconstructor arguments']) + + def test_unpickleable_newobj_ex_args(self): + obj = REX((copyreg.__newobj_ex__, (REX, (1, 2, UNPICKLEABLE), {}), None)) + for proto in protocols: + with self.assertRaises(CustomError) as cm: + self.dumps(obj, proto) + if proto >= 4: + self.assertEqual(cm.exception.__notes__, + ['when serializing tuple item 2', + 'when serializing test.pickletester.REX __new__ arguments']) + elif proto >= 2: + self.assertEqual(cm.exception.__notes__, + ['when serializing tuple item 3', + 'when serializing tuple item 1', + 'when serializing functools.partial state', + 'when serializing test.pickletester.REX reconstructor']) + else: + self.assertEqual(cm.exception.__notes__, + ['when serializing tuple item 2', + 'when serializing tuple item 1', + 'when serializing test.pickletester.REX reconstructor arguments']) + + def test_unpickleable_newobj_ex_kwargs(self): + obj = REX((copyreg.__newobj_ex__, (REX, (), {'a': UNPICKLEABLE}), None)) + for proto in protocols: + with self.assertRaises(CustomError) as cm: + self.dumps(obj, proto) + if proto >= 4: + self.assertEqual(cm.exception.__notes__, + ["when serializing dict item 'a'", + 'when serializing test.pickletester.REX __new__ arguments']) + elif proto >= 2: + self.assertEqual(cm.exception.__notes__, + ["when serializing dict item 'a'", + 'when serializing tuple item 2', + 'when serializing functools.partial state', + 'when serializing test.pickletester.REX reconstructor']) + else: + self.assertEqual(cm.exception.__notes__, + ["when serializing dict item 'a'", + 'when serializing tuple item 2', + 'when serializing test.pickletester.REX reconstructor arguments']) + + def test_unpickleable_state(self): + obj = REX_state(UNPICKLEABLE) + for proto in protocols: + with self.assertRaises(CustomError) as cm: + self.dumps(obj, proto) + self.assertEqual(cm.exception.__notes__, + ['when serializing test.pickletester.REX_state state']) + + def test_unpickleable_state_setter(self): + obj = REX((print, (), 'state', None, None, UnpickleableCallable())) + for proto in protocols: + with self.assertRaises(CustomError) as cm: + self.dumps(obj, proto) + self.assertEqual(cm.exception.__notes__, + ['when serializing test.pickletester.REX state setter']) + + def test_unpickleable_state_with_state_setter(self): + obj = REX((print, (), UNPICKLEABLE, None, None, print)) + for proto in protocols: + with self.assertRaises(CustomError) as cm: + self.dumps(obj, proto) + self.assertEqual(cm.exception.__notes__, + ['when serializing test.pickletester.REX state']) + + def test_unpickleable_object_list_items(self): + obj = REX_six([1, 2, UNPICKLEABLE]) + for proto in protocols: + with self.assertRaises(CustomError) as cm: + self.dumps(obj, proto) + self.assertEqual(cm.exception.__notes__, + ['when serializing test.pickletester.REX_six item 2']) + + def test_unpickleable_object_dict_items(self): + obj = REX_seven({'a': UNPICKLEABLE}) + for proto in protocols: + with self.assertRaises(CustomError) as cm: + self.dumps(obj, proto) + self.assertEqual(cm.exception.__notes__, + ["when serializing test.pickletester.REX_seven item 'a'"]) + + def test_unpickleable_list_items(self): + obj = [1, [2, 3, UNPICKLEABLE]] + for proto in protocols: + with self.assertRaises(CustomError) as cm: + self.dumps(obj, proto) + self.assertEqual(cm.exception.__notes__, + ['when serializing list item 2', + 'when serializing list item 1']) + for n in [0, 1, 1000, 1005]: + obj = [*range(n), UNPICKLEABLE] + for proto in protocols: + with self.assertRaises(CustomError) as cm: + self.dumps(obj, proto) + self.assertEqual(cm.exception.__notes__, + [f'when serializing list item {n}']) + + def test_unpickleable_tuple_items(self): + obj = (1, (2, 3, UNPICKLEABLE)) + for proto in protocols: + with self.assertRaises(CustomError) as cm: + self.dumps(obj, proto) + self.assertEqual(cm.exception.__notes__, + ['when serializing tuple item 2', + 'when serializing tuple item 1']) + obj = (*range(10), UNPICKLEABLE) + for proto in protocols: + with self.assertRaises(CustomError) as cm: + self.dumps(obj, proto) + self.assertEqual(cm.exception.__notes__, + ['when serializing tuple item 10']) + + def test_unpickleable_dict_items(self): + obj = {'a': {'b': UNPICKLEABLE}} + for proto in protocols: + with self.assertRaises(CustomError) as cm: + self.dumps(obj, proto) + self.assertEqual(cm.exception.__notes__, + ["when serializing dict item 'b'", + "when serializing dict item 'a'"]) + for n in [0, 1, 1000, 1005]: + obj = dict.fromkeys(range(n)) + obj['a'] = UNPICKLEABLE + for proto in protocols: + with self.subTest(proto=proto, n=n): + with self.assertRaises(CustomError) as cm: + self.dumps(obj, proto) + self.assertEqual(cm.exception.__notes__, + ["when serializing dict item 'a'"]) + + def test_unpickleable_set_items(self): + obj = {UNPICKLEABLE} + for proto in protocols: + with self.subTest(proto=proto): + with self.assertRaises(CustomError) as cm: + self.dumps(obj, proto) + if proto >= 4: + self.assertEqual(cm.exception.__notes__, + ['when serializing set element']) + else: + self.assertEqual(cm.exception.__notes__, + ['when serializing list item 0', + 'when serializing tuple item 0', + 'when serializing set reconstructor arguments']) + + def test_unpickleable_frozenset_items(self): + obj = frozenset({frozenset({UNPICKLEABLE})}) + for proto in protocols: + with self.subTest(proto=proto): + with self.assertRaises(CustomError) as cm: + self.dumps(obj, proto) + if proto >= 4: + self.assertEqual(cm.exception.__notes__, + ['when serializing frozenset element', + 'when serializing frozenset element']) + else: + self.assertEqual(cm.exception.__notes__, + ['when serializing list item 0', + 'when serializing tuple item 0', + 'when serializing frozenset reconstructor arguments', + 'when serializing list item 0', + 'when serializing tuple item 0', + 'when serializing frozenset reconstructor arguments']) + # Exercise framing (proto >= 4) for significant workloads FRAME_SIZE_MIN = 4 @@ -3410,6 +3640,25 @@ def __setstate__(self, state): def __reduce__(self): return type(self), (), self.state +class CustomError(Exception): + pass + +class Unpickleable: + def __reduce__(self): + raise CustomError + +UNPICKLEABLE = Unpickleable() + +class UnpickleableCallable(Unpickleable): + def __call__(self, *args, **kwargs): + pass + +class REX: + def __init__(self, reduce=None): + self.reduce = reduce + def __reduce_ex__(self, proto): + return self.reduce + class REX_None: """ Setting __reduce_ex__ to None should fail """ __reduce_ex__ = None diff --git a/Misc/NEWS.d/next/Library/2024-07-24-08-48-22.gh-issue-122213.o3pdgA.rst b/Misc/NEWS.d/next/Library/2024-07-24-08-48-22.gh-issue-122213.o3pdgA.rst new file mode 100644 index 00000000000000..833a2a676f9298 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2024-07-24-08-48-22.gh-issue-122213.o3pdgA.rst @@ -0,0 +1,2 @@ +Add notes for pickle serialization errors that allow to identify the source +of the error. diff --git a/Modules/_pickle.c b/Modules/_pickle.c index 7eebe922c93ca1..31259230ff106d 100644 --- a/Modules/_pickle.c +++ b/Modules/_pickle.c @@ -15,6 +15,7 @@ #include "pycore_long.h" // _PyLong_AsByteArray() #include "pycore_moduleobject.h" // _PyModule_GetState() #include "pycore_object.h" // _PyNone_Type +#include "pycore_pyerrors.h" // _PyErr_FormatNote #include "pycore_pystate.h" // _PyThreadState_GET() #include "pycore_runtime.h" // _Py_ID() #include "pycore_setobject.h" // _PySet_NextEntry() @@ -2738,8 +2739,10 @@ store_tuple_elements(PickleState *state, PicklerObject *self, PyObject *t, if (element == NULL) return -1; - if (save(state, self, element, 0) < 0) + if (save(state, self, element, 0) < 0) { + _PyErr_FormatNote("when serializing %T item %zd", t, i); return -1; + } } return 0; @@ -2858,11 +2861,12 @@ save_tuple(PickleState *state, PicklerObject *self, PyObject *obj) * Returns 0 on success, <0 on error. */ static int -batch_list(PickleState *state, PicklerObject *self, PyObject *iter) +batch_list(PickleState *state, PicklerObject *self, PyObject *iter, PyObject *origobj) { PyObject *obj = NULL; PyObject *firstitem = NULL; int i, n; + Py_ssize_t total = 0; const char mark_op = MARK; const char append_op = APPEND; @@ -2877,7 +2881,7 @@ batch_list(PickleState *state, PicklerObject *self, PyObject *iter) if (self->proto == 0) { /* APPENDS isn't available; do one at a time. */ - for (;;) { + for (;; total++) { obj = PyIter_Next(iter); if (obj == NULL) { if (PyErr_Occurred()) @@ -2886,8 +2890,10 @@ batch_list(PickleState *state, PicklerObject *self, PyObject *iter) } i = save(state, self, obj, 0); Py_DECREF(obj); - if (i < 0) + if (i < 0) { + _PyErr_FormatNote("when serializing %T item %zd", origobj, total); return -1; + } if (_Pickler_Write(self, &append_op, 1) < 0) return -1; } @@ -2913,8 +2919,10 @@ batch_list(PickleState *state, PicklerObject *self, PyObject *iter) goto error; /* Only one item to write */ - if (save(state, self, firstitem, 0) < 0) + if (save(state, self, firstitem, 0) < 0) { + _PyErr_FormatNote("when serializing %T item %zd", origobj, total); goto error; + } if (_Pickler_Write(self, &append_op, 1) < 0) goto error; Py_CLEAR(firstitem); @@ -2927,16 +2935,22 @@ batch_list(PickleState *state, PicklerObject *self, PyObject *iter) if (_Pickler_Write(self, &mark_op, 1) < 0) goto error; - if (save(state, self, firstitem, 0) < 0) + if (save(state, self, firstitem, 0) < 0) { + _PyErr_FormatNote("when serializing %T item %zd", origobj, total); goto error; + } Py_CLEAR(firstitem); + total++; n = 1; /* Fetch and save up to BATCHSIZE items */ while (obj) { - if (save(state, self, obj, 0) < 0) + if (save(state, self, obj, 0) < 0) { + _PyErr_FormatNote("when serializing %T item %zd", origobj, total); goto error; + } Py_CLEAR(obj); + total++; n += 1; if (n == BATCHSIZE) @@ -2992,8 +3006,10 @@ batch_list_exact(PickleState *state, PicklerObject *self, PyObject *obj) Py_INCREF(item); int err = save(state, self, item, 0); Py_DECREF(item); - if (err < 0) + if (err < 0) { + _PyErr_FormatNote("when serializing %T item 0", obj); return -1; + } if (_Pickler_Write(self, &append_op, 1) < 0) return -1; return 0; @@ -3010,8 +3026,10 @@ batch_list_exact(PickleState *state, PicklerObject *self, PyObject *obj) Py_INCREF(item); int err = save(state, self, item, 0); Py_DECREF(item); - if (err < 0) + if (err < 0) { + _PyErr_FormatNote("when serializing %T item %zd", obj, total); return -1; + } total++; if (++this_batch == BATCHSIZE) break; @@ -3071,7 +3089,7 @@ save_list(PickleState *state, PicklerObject *self, PyObject *obj) Py_DECREF(iter); goto error; } - status = batch_list(state, self, iter); + status = batch_list(state, self, iter, obj); _Py_LeaveRecursiveCall(); Py_DECREF(iter); } @@ -3099,7 +3117,7 @@ save_list(PickleState *state, PicklerObject *self, PyObject *obj) * ugly to bear. */ static int -batch_dict(PickleState *state, PicklerObject *self, PyObject *iter) +batch_dict(PickleState *state, PicklerObject *self, PyObject *iter, PyObject *origobj) { PyObject *obj = NULL; PyObject *firstitem = NULL; @@ -3126,8 +3144,13 @@ batch_dict(PickleState *state, PicklerObject *self, PyObject *iter) return -1; } i = save(state, self, PyTuple_GET_ITEM(obj, 0), 0); - if (i >= 0) + if (i >= 0) { i = save(state, self, PyTuple_GET_ITEM(obj, 1), 0); + if (i < 0) { + _PyErr_FormatNote("when serializing %T item %R", + origobj, PyTuple_GET_ITEM(obj, 0)); + } + } Py_DECREF(obj); if (i < 0) return -1; @@ -3163,8 +3186,11 @@ batch_dict(PickleState *state, PicklerObject *self, PyObject *iter) /* Only one item to write */ if (save(state, self, PyTuple_GET_ITEM(firstitem, 0), 0) < 0) goto error; - if (save(state, self, PyTuple_GET_ITEM(firstitem, 1), 0) < 0) + if (save(state, self, PyTuple_GET_ITEM(firstitem, 1), 0) < 0) { + _PyErr_FormatNote("when serializing %T item %R", + origobj, PyTuple_GET_ITEM(firstitem, 0)); goto error; + } if (_Pickler_Write(self, &setitem_op, 1) < 0) goto error; Py_CLEAR(firstitem); @@ -3179,8 +3205,11 @@ batch_dict(PickleState *state, PicklerObject *self, PyObject *iter) if (save(state, self, PyTuple_GET_ITEM(firstitem, 0), 0) < 0) goto error; - if (save(state, self, PyTuple_GET_ITEM(firstitem, 1), 0) < 0) + if (save(state, self, PyTuple_GET_ITEM(firstitem, 1), 0) < 0) { + _PyErr_FormatNote("when serializing %T item %R", + origobj, PyTuple_GET_ITEM(firstitem, 0)); goto error; + } Py_CLEAR(firstitem); n = 1; @@ -3191,9 +3220,13 @@ batch_dict(PickleState *state, PicklerObject *self, PyObject *iter) "iterator must return 2-tuples"); goto error; } - if (save(state, self, PyTuple_GET_ITEM(obj, 0), 0) < 0 || - save(state, self, PyTuple_GET_ITEM(obj, 1), 0) < 0) + if (save(state, self, PyTuple_GET_ITEM(obj, 0), 0) < 0) goto error; + if (save(state, self, PyTuple_GET_ITEM(obj, 1), 0) < 0) { + _PyErr_FormatNote("when serializing %T item %R", + origobj, PyTuple_GET_ITEM(obj, 0)); + goto error; + } Py_CLEAR(obj); n += 1; @@ -3254,6 +3287,7 @@ batch_dict_exact(PickleState *state, PicklerObject *self, PyObject *obj) goto error; } if (save(state, self, value, 0) < 0) { + _PyErr_FormatNote("when serializing %T item %R", obj, key); goto error; } Py_CLEAR(key); @@ -3275,6 +3309,7 @@ batch_dict_exact(PickleState *state, PicklerObject *self, PyObject *obj) goto error; } if (save(state, self, value, 0) < 0) { + _PyErr_FormatNote("when serializing %T item %R", obj, key); goto error; } Py_CLEAR(key); @@ -3349,7 +3384,7 @@ save_dict(PickleState *state, PicklerObject *self, PyObject *obj) Py_DECREF(iter); goto error; } - status = batch_dict(state, self, iter); + status = batch_dict(state, self, iter, obj); _Py_LeaveRecursiveCall(); Py_DECREF(iter); } @@ -3419,8 +3454,10 @@ save_set(PickleState *state, PicklerObject *self, PyObject *obj) while (_PySet_NextEntryRef(obj, &ppos, &item, &hash)) { err = save(state, self, item, 0); Py_CLEAR(item); - if (err < 0) + if (err < 0) { + _PyErr_FormatNote("when serializing %T element", obj); break; + } if (++i == BATCHSIZE) break; } @@ -3492,6 +3529,7 @@ save_frozenset(PickleState *state, PicklerObject *self, PyObject *obj) break; } if (save(state, self, item, 0) < 0) { + _PyErr_FormatNote("when serializing %T element", obj); Py_DECREF(item); Py_DECREF(iter); return -1; @@ -4055,10 +4093,17 @@ save_reduce(PickleState *st, PicklerObject *self, PyObject *args, } if (self->proto >= 4) { - if (save(st, self, cls, 0) < 0 || - save(st, self, args, 0) < 0 || - save(st, self, kwargs, 0) < 0 || - _Pickler_Write(self, &newobj_ex_op, 1) < 0) { + if (save(st, self, cls, 0) < 0) { + _PyErr_FormatNote("when serializing %T class", obj); + return -1; + } + if (save(st, self, args, 0) < 0 || + save(st, self, kwargs, 0) < 0) + { + _PyErr_FormatNote("when serializing %T __new__ arguments", obj); + return -1; + } + if (_Pickler_Write(self, &newobj_ex_op, 1) < 0) { return -1; } } @@ -4095,14 +4140,18 @@ save_reduce(PickleState *st, PicklerObject *self, PyObject *args, } if (save(st, self, callable, 0) < 0 || - save(st, self, newargs, 0) < 0 || - _Pickler_Write(self, &reduce_op, 1) < 0) { + save(st, self, newargs, 0) < 0) + { + _PyErr_FormatNote("when serializing %T reconstructor", obj); Py_DECREF(newargs); Py_DECREF(callable); return -1; } Py_DECREF(newargs); Py_DECREF(callable); + if (_Pickler_Write(self, &reduce_op, 1) < 0) { + return -1; + } } } else if (use_newobj) { @@ -4166,6 +4215,7 @@ save_reduce(PickleState *st, PicklerObject *self, PyObject *args, /* Save the class and its __new__ arguments. */ if (save(st, self, cls, 0) < 0) { + _PyErr_FormatNote("when serializing %T class", obj); return -1; } @@ -4175,18 +4225,27 @@ save_reduce(PickleState *st, PicklerObject *self, PyObject *args, p = save(st, self, newargtup, 0); Py_DECREF(newargtup); - if (p < 0) + if (p < 0) { + _PyErr_FormatNote("when serializing %T __new__ arguments", obj); return -1; + } /* Add NEWOBJ opcode. */ if (_Pickler_Write(self, &newobj_op, 1) < 0) return -1; } else { /* Not using NEWOBJ. */ - if (save(st, self, callable, 0) < 0 || - save(st, self, argtup, 0) < 0 || - _Pickler_Write(self, &reduce_op, 1) < 0) + if (save(st, self, callable, 0) < 0) { + _PyErr_FormatNote("when serializing %T reconstructor", obj); + return -1; + } + if (save(st, self, argtup, 0) < 0) { + _PyErr_FormatNote("when serializing %T reconstructor arguments", obj); + return -1; + } + if (_Pickler_Write(self, &reduce_op, 1) < 0) { return -1; + } } /* obj can be NULL when save_reduce() is used directly. A NULL obj means @@ -4211,16 +4270,19 @@ save_reduce(PickleState *st, PicklerObject *self, PyObject *args, return -1; } - if (listitems && batch_list(st, self, listitems) < 0) + if (listitems && batch_list(st, self, listitems, obj) < 0) return -1; - if (dictitems && batch_dict(st, self, dictitems) < 0) + if (dictitems && batch_dict(st, self, dictitems, obj) < 0) return -1; if (state) { if (state_setter == NULL) { - if (save(st, self, state, 0) < 0 || - _Pickler_Write(self, &build_op, 1) < 0) + if (save(st, self, state, 0) < 0) { + _PyErr_FormatNote("when serializing %T state", obj); + return -1; + } + if (_Pickler_Write(self, &build_op, 1) < 0) return -1; } else { @@ -4236,9 +4298,18 @@ save_reduce(PickleState *st, PicklerObject *self, PyObject *args, const char tupletwo_op = TUPLE2; const char pop_op = POP; - if (save(st, self, state_setter, 0) < 0 || - save(st, self, obj, 0) < 0 || save(st, self, state, 0) < 0 || - _Pickler_Write(self, &tupletwo_op, 1) < 0 || + if (save(st, self, state_setter, 0) < 0) { + _PyErr_FormatNote("when serializing %T state setter", obj); + return -1; + } + if (save(st, self, obj, 0) < 0) { + return -1; + } + if (save(st, self, state, 0) < 0) { + _PyErr_FormatNote("when serializing %T state", obj); + return -1; + } + if (_Pickler_Write(self, &tupletwo_op, 1) < 0 || _Pickler_Write(self, &reduce_op, 1) < 0 || _Pickler_Write(self, &pop_op, 1) < 0) return -1; From 5b777b6597324dc49c35fe855056836b6c9975e7 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Tue, 6 Aug 2024 17:15:50 +0300 Subject: [PATCH 2/2] Add more notes. --- Lib/pickle.py | 28 +++++----- Lib/test/pickletester.py | 109 ++++++++++++++++++++++++++++++++------- Modules/_pickle.c | 4 ++ 3 files changed, 109 insertions(+), 32 deletions(-) diff --git a/Lib/pickle.py b/Lib/pickle.py index c702e2e1701f76..d1dbc583a19bb1 100644 --- a/Lib/pickle.py +++ b/Lib/pickle.py @@ -600,18 +600,22 @@ def save(self, obj, save_persistent_id=True): self.save_global(obj, rv) return - # Assert that reduce() returned a tuple - if not isinstance(rv, tuple): - raise PicklingError("%s must return string or tuple" % reduce) - - # Assert that it returned an appropriately sized tuple - l = len(rv) - if not (2 <= l <= 6): - raise PicklingError("Tuple returned by %s must have " - "two to six elements" % reduce) - - # Save the reduce() output and finally memoize the object - self.save_reduce(obj=obj, *rv) + try: + # Assert that reduce() returned a tuple + if not isinstance(rv, tuple): + raise PicklingError("%s must return string or tuple" % reduce) + + # Assert that it returned an appropriately sized tuple + l = len(rv) + if not (2 <= l <= 6): + raise PicklingError("Tuple returned by %s must have " + "two to six elements" % reduce) + + # Save the reduce() output and finally memoize the object + self.save_reduce(obj=obj, *rv) + except BaseException as exc: + exc.add_note(f'when serializing {_T(obj)} object') + raise def persistent_id(self, obj): # This exists so a subclass can override it diff --git a/Lib/test/pickletester.py b/Lib/test/pickletester.py index 6b8f73ed6850e8..467180bc4f2f19 100644 --- a/Lib/test/pickletester.py +++ b/Lib/test/pickletester.py @@ -1587,6 +1587,8 @@ def test_bad_reduce_result(self): self.assertIn(str(cm.exception), { f'{obj.__reduce_ex__!r} must return string or tuple', '__reduce__ must return a string or tuple'}) + self.assertEqual(cm.exception.__notes__, [ + 'when serializing test.pickletester.REX object']) obj = REX((print,)) for proto in protocols: @@ -1596,6 +1598,8 @@ def test_bad_reduce_result(self): self.assertIn(str(cm.exception), { f'Tuple returned by {obj.__reduce_ex__!r} must have two to six elements', 'tuple returned by __reduce__ must contain 2 through 6 elements'}) + self.assertEqual(cm.exception.__notes__, [ + 'when serializing test.pickletester.REX object']) obj = REX((print, (), None, None, None, None, None)) for proto in protocols: @@ -1605,6 +1609,8 @@ def test_bad_reduce_result(self): self.assertIn(str(cm.exception), { f'Tuple returned by {obj.__reduce_ex__!r} must have two to six elements', 'tuple returned by __reduce__ must contain 2 through 6 elements'}) + self.assertEqual(cm.exception.__notes__, [ + 'when serializing test.pickletester.REX object']) def test_bad_reconstructor(self): obj = REX((42, ())) @@ -1615,6 +1621,8 @@ def test_bad_reconstructor(self): self.assertIn(str(cm.exception), { 'func from save_reduce() must be callable', 'first item of the tuple returned by __reduce__ must be callable'}) + self.assertEqual(cm.exception.__notes__, [ + 'when serializing test.pickletester.REX object']) def test_unpickleable_reconstructor(self): obj = REX((UnpickleableCallable(), ())) @@ -1623,7 +1631,8 @@ def test_unpickleable_reconstructor(self): with self.assertRaises(CustomError) as cm: self.dumps(obj, proto) self.assertEqual(cm.exception.__notes__, [ - 'when serializing test.pickletester.REX reconstructor']) + 'when serializing test.pickletester.REX reconstructor', + 'when serializing test.pickletester.REX object']) def test_bad_reconstructor_args(self): obj = REX((print, [])) @@ -1634,6 +1643,8 @@ def test_bad_reconstructor_args(self): self.assertIn(str(cm.exception), { 'args from save_reduce() must be a tuple', 'second item of the tuple returned by __reduce__ must be a tuple'}) + self.assertEqual(cm.exception.__notes__, [ + 'when serializing test.pickletester.REX object']) def test_unpickleable_reconstructor_args(self): obj = REX((print, (1, 2, UNPICKLEABLE))) @@ -1643,7 +1654,8 @@ def test_unpickleable_reconstructor_args(self): self.dumps(obj, proto) self.assertEqual(cm.exception.__notes__, [ 'when serializing tuple item 2', - 'when serializing test.pickletester.REX reconstructor arguments']) + 'when serializing test.pickletester.REX reconstructor arguments', + 'when serializing test.pickletester.REX object']) def test_bad_newobj_args(self): obj = REX((copyreg.__newobj__, ())) @@ -1654,6 +1666,8 @@ def test_bad_newobj_args(self): self.assertIn(str(cm.exception), { 'tuple index out of range', '__newobj__ arglist is empty'}) + self.assertEqual(cm.exception.__notes__, [ + 'when serializing test.pickletester.REX object']) obj = REX((copyreg.__newobj__, [REX])) for proto in protocols[2:]: @@ -1663,6 +1677,8 @@ def test_bad_newobj_args(self): self.assertIn(str(cm.exception), { 'args from save_reduce() must be a tuple', 'second item of the tuple returned by __reduce__ must be a tuple'}) + self.assertEqual(cm.exception.__notes__, [ + 'when serializing test.pickletester.REX object']) def test_bad_newobj_class(self): obj = REX((copyreg.__newobj__, (NoNew(),))) @@ -1673,6 +1689,8 @@ def test_bad_newobj_class(self): self.assertIn(str(cm.exception), { 'args[0] from __newobj__ args has no __new__', 'args[0] from __newobj__ args is not a type'}) + self.assertEqual(cm.exception.__notes__, [ + 'when serializing test.pickletester.REX object']) def test_wrong_newobj_class(self): obj = REX((copyreg.__newobj__, (str,))) @@ -1682,6 +1700,8 @@ def test_wrong_newobj_class(self): self.dumps(obj, proto) self.assertEqual(str(cm.exception), 'args[0] from __newobj__ args has the wrong class') + self.assertEqual(cm.exception.__notes__, [ + 'when serializing test.pickletester.REX object']) def test_unpickleable_newobj_class(self): class LocalREX(REX): pass @@ -1692,11 +1712,13 @@ class LocalREX(REX): pass self.dumps(obj, proto) if proto >= 2: self.assertEqual(cm.exception.__notes__, [ - f'when serializing {LocalREX.__module__}.{LocalREX.__qualname__} class']) + f'when serializing {LocalREX.__module__}.{LocalREX.__qualname__} class', + f'when serializing {LocalREX.__module__}.{LocalREX.__qualname__} object']) else: self.assertEqual(cm.exception.__notes__, [ 'when serializing tuple item 0', - f'when serializing {LocalREX.__module__}.{LocalREX.__qualname__} reconstructor arguments']) + f'when serializing {LocalREX.__module__}.{LocalREX.__qualname__} reconstructor arguments', + f'when serializing {LocalREX.__module__}.{LocalREX.__qualname__} object']) def test_unpickleable_newobj_args(self): obj = REX((copyreg.__newobj__, (REX, 1, 2, UNPICKLEABLE))) @@ -1707,11 +1729,13 @@ def test_unpickleable_newobj_args(self): if proto >= 2: self.assertEqual(cm.exception.__notes__, [ 'when serializing tuple item 2', - 'when serializing test.pickletester.REX __new__ arguments']) + 'when serializing test.pickletester.REX __new__ arguments', + 'when serializing test.pickletester.REX object']) else: self.assertEqual(cm.exception.__notes__, [ 'when serializing tuple item 3', - 'when serializing test.pickletester.REX reconstructor arguments']) + 'when serializing test.pickletester.REX reconstructor arguments', + 'when serializing test.pickletester.REX object']) def test_bad_newobj_ex_args(self): obj = REX((copyreg.__newobj_ex__, ())) @@ -1722,6 +1746,8 @@ def test_bad_newobj_ex_args(self): self.assertIn(str(cm.exception), { 'not enough values to unpack (expected 3, got 0)', 'length of the NEWOBJ_EX argument tuple must be exactly 3, not 0'}) + self.assertEqual(cm.exception.__notes__, [ + 'when serializing test.pickletester.REX object']) obj = REX((copyreg.__newobj_ex__, 42)) for proto in protocols[2:]: @@ -1731,6 +1757,8 @@ def test_bad_newobj_ex_args(self): self.assertIn(str(cm.exception), { 'args from save_reduce() must be a tuple', 'second item of the tuple returned by __reduce__ must be a tuple'}) + self.assertEqual(cm.exception.__notes__, [ + 'when serializing test.pickletester.REX object']) obj = REX((copyreg.__newobj_ex__, (REX, 42, {}))) if self.pickler is pickle._Pickler: @@ -1740,6 +1768,8 @@ def test_bad_newobj_ex_args(self): self.dumps(obj, proto) self.assertEqual(str(cm.exception), 'Value after * must be an iterable, not int') + self.assertEqual(cm.exception.__notes__, [ + 'when serializing test.pickletester.REX object']) else: for proto in protocols[2:]: with self.subTest(proto=proto): @@ -1747,6 +1777,8 @@ def test_bad_newobj_ex_args(self): self.dumps(obj, proto) self.assertEqual(str(cm.exception), 'second item from NEWOBJ_EX argument tuple must be a tuple, not int') + self.assertEqual(cm.exception.__notes__, [ + 'when serializing test.pickletester.REX object']) obj = REX((copyreg.__newobj_ex__, (REX, (), []))) if self.pickler is pickle._Pickler: @@ -1756,6 +1788,8 @@ def test_bad_newobj_ex_args(self): self.dumps(obj, proto) self.assertEqual(str(cm.exception), 'functools.partial() argument after ** must be a mapping, not list') + self.assertEqual(cm.exception.__notes__, [ + 'when serializing test.pickletester.REX object']) else: for proto in protocols[2:]: with self.subTest(proto=proto): @@ -1763,6 +1797,8 @@ def test_bad_newobj_ex_args(self): self.dumps(obj, proto) self.assertEqual(str(cm.exception), 'third item from NEWOBJ_EX argument tuple must be a dict, not list') + self.assertEqual(cm.exception.__notes__, [ + 'when serializing test.pickletester.REX object']) def test_bad_newobj_ex__class(self): obj = REX((copyreg.__newobj_ex__, (NoNew(), (), {}))) @@ -1773,6 +1809,8 @@ def test_bad_newobj_ex__class(self): self.assertIn(str(cm.exception), { 'args[0] from __newobj_ex__ args has no __new__', 'first item from NEWOBJ_EX argument tuple must be a class, not NoNew'}) + self.assertEqual(cm.exception.__notes__, [ + 'when serializing test.pickletester.REX object']) def test_wrong_newobj_ex_class(self): if self.pickler is not pickle._Pickler: @@ -1784,6 +1822,8 @@ def test_wrong_newobj_ex_class(self): self.dumps(obj, proto) self.assertEqual(str(cm.exception), 'args[0] from __newobj_ex__ args has the wrong class') + self.assertEqual(cm.exception.__notes__, [ + 'when serializing test.pickletester.REX object']) def test_unpickleable_newobj_ex_class(self): class LocalREX(REX): pass @@ -1794,17 +1834,21 @@ class LocalREX(REX): pass self.dumps(obj, proto) if proto >= 4: self.assertEqual(cm.exception.__notes__, [ - f'when serializing {LocalREX.__module__}.{LocalREX.__qualname__} class']) + f'when serializing {LocalREX.__module__}.{LocalREX.__qualname__} class', + f'when serializing {LocalREX.__module__}.{LocalREX.__qualname__} object']) elif proto >= 2: self.assertEqual(cm.exception.__notes__, [ 'when serializing tuple item 0', 'when serializing tuple item 1', 'when serializing functools.partial state', - f'when serializing {LocalREX.__module__}.{LocalREX.__qualname__} reconstructor']) + 'when serializing functools.partial object', + f'when serializing {LocalREX.__module__}.{LocalREX.__qualname__} reconstructor', + f'when serializing {LocalREX.__module__}.{LocalREX.__qualname__} object']) else: self.assertEqual(cm.exception.__notes__, [ 'when serializing tuple item 0', - f'when serializing {LocalREX.__module__}.{LocalREX.__qualname__} reconstructor arguments']) + f'when serializing {LocalREX.__module__}.{LocalREX.__qualname__} reconstructor arguments', + f'when serializing {LocalREX.__module__}.{LocalREX.__qualname__} object']) def test_unpickleable_newobj_ex_args(self): obj = REX((copyreg.__newobj_ex__, (REX, (1, 2, UNPICKLEABLE), {}))) @@ -1815,18 +1859,22 @@ def test_unpickleable_newobj_ex_args(self): if proto >= 4: self.assertEqual(cm.exception.__notes__, [ 'when serializing tuple item 2', - 'when serializing test.pickletester.REX __new__ arguments']) + 'when serializing test.pickletester.REX __new__ arguments', + 'when serializing test.pickletester.REX object']) elif proto >= 2: self.assertEqual(cm.exception.__notes__, [ 'when serializing tuple item 3', 'when serializing tuple item 1', 'when serializing functools.partial state', - 'when serializing test.pickletester.REX reconstructor']) + 'when serializing functools.partial object', + 'when serializing test.pickletester.REX reconstructor', + 'when serializing test.pickletester.REX object']) else: self.assertEqual(cm.exception.__notes__, [ 'when serializing tuple item 2', 'when serializing tuple item 1', - 'when serializing test.pickletester.REX reconstructor arguments']) + 'when serializing test.pickletester.REX reconstructor arguments', + 'when serializing test.pickletester.REX object']) def test_unpickleable_newobj_ex_kwargs(self): obj = REX((copyreg.__newobj_ex__, (REX, (), {'a': UNPICKLEABLE}))) @@ -1837,18 +1885,22 @@ def test_unpickleable_newobj_ex_kwargs(self): if proto >= 4: self.assertEqual(cm.exception.__notes__, [ "when serializing dict item 'a'", - 'when serializing test.pickletester.REX __new__ arguments']) + 'when serializing test.pickletester.REX __new__ arguments', + 'when serializing test.pickletester.REX object']) elif proto >= 2: self.assertEqual(cm.exception.__notes__, [ "when serializing dict item 'a'", 'when serializing tuple item 2', 'when serializing functools.partial state', - 'when serializing test.pickletester.REX reconstructor']) + 'when serializing functools.partial object', + 'when serializing test.pickletester.REX reconstructor', + 'when serializing test.pickletester.REX object']) else: self.assertEqual(cm.exception.__notes__, [ "when serializing dict item 'a'", 'when serializing tuple item 2', - 'when serializing test.pickletester.REX reconstructor arguments']) + 'when serializing test.pickletester.REX reconstructor arguments', + 'when serializing test.pickletester.REX object']) def test_unpickleable_state(self): obj = REX_state(UNPICKLEABLE) @@ -1857,7 +1909,8 @@ def test_unpickleable_state(self): with self.assertRaises(CustomError) as cm: self.dumps(obj, proto) self.assertEqual(cm.exception.__notes__, [ - 'when serializing test.pickletester.REX_state state']) + 'when serializing test.pickletester.REX_state state', + 'when serializing test.pickletester.REX_state object']) def test_bad_state_setter(self): if self.pickler is pickle._Pickler: @@ -1869,6 +1922,8 @@ def test_bad_state_setter(self): self.dumps(obj, proto) self.assertEqual(str(cm.exception), 'sixth element of the tuple returned by __reduce__ must be a function, not int') + self.assertEqual(cm.exception.__notes__, [ + 'when serializing test.pickletester.REX object']) def test_unpickleable_state_setter(self): obj = REX((print, (), 'state', None, None, UnpickleableCallable())) @@ -1877,7 +1932,8 @@ def test_unpickleable_state_setter(self): with self.assertRaises(CustomError) as cm: self.dumps(obj, proto) self.assertEqual(cm.exception.__notes__, [ - 'when serializing test.pickletester.REX state setter']) + 'when serializing test.pickletester.REX state setter', + 'when serializing test.pickletester.REX object']) def test_unpickleable_state_with_state_setter(self): obj = REX((print, (), UNPICKLEABLE, None, None, print)) @@ -1886,7 +1942,8 @@ def test_unpickleable_state_with_state_setter(self): with self.assertRaises(CustomError) as cm: self.dumps(obj, proto) self.assertEqual(cm.exception.__notes__, [ - 'when serializing test.pickletester.REX state']) + 'when serializing test.pickletester.REX state', + 'when serializing test.pickletester.REX object']) def test_bad_object_list_items(self): # Issue4176: crash when 4th and 5th items of __reduce__() @@ -1899,6 +1956,8 @@ def test_bad_object_list_items(self): self.assertIn(str(cm.exception), { "'int' object is not iterable", 'fourth element of the tuple returned by __reduce__ must be an iterator, not int'}) + self.assertEqual(cm.exception.__notes__, [ + 'when serializing test.pickletester.REX object']) if self.pickler is not pickle._Pickler: # Python implementation is less strict and also accepts iterables. @@ -1910,6 +1969,8 @@ def test_bad_object_list_items(self): self.assertIn(str(cm.exception), { "'int' object is not iterable", 'fourth element of the tuple returned by __reduce__ must be an iterator, not int'}) + self.assertEqual(cm.exception.__notes__, [ + 'when serializing test.pickletester.REX object']) def test_unpickleable_object_list_items(self): obj = REX_six([1, 2, UNPICKLEABLE]) @@ -1918,7 +1979,8 @@ def test_unpickleable_object_list_items(self): with self.assertRaises(CustomError) as cm: self.dumps(obj, proto) self.assertEqual(cm.exception.__notes__, [ - 'when serializing test.pickletester.REX_six item 2']) + 'when serializing test.pickletester.REX_six item 2', + 'when serializing test.pickletester.REX_six object']) def test_bad_object_dict_items(self): # Issue4176: crash when 4th and 5th items of __reduce__() @@ -1931,6 +1993,8 @@ def test_bad_object_dict_items(self): self.assertIn(str(cm.exception), { "'int' object is not iterable", 'fifth element of the tuple returned by __reduce__ must be an iterator, not int'}) + self.assertEqual(cm.exception.__notes__, [ + 'when serializing test.pickletester.REX object']) for proto in protocols: obj = REX((dict, (), None, None, iter([('a',)]))) @@ -1940,6 +2004,8 @@ def test_bad_object_dict_items(self): self.assertIn(str(cm.exception), { 'not enough values to unpack (expected 2, got 1)', 'dict items iterator must return 2-tuples'}) + self.assertEqual(cm.exception.__notes__, [ + 'when serializing test.pickletester.REX object']) if self.pickler is not pickle._Pickler: # Python implementation is less strict and also accepts iterables. @@ -1950,6 +2016,8 @@ def test_bad_object_dict_items(self): self.dumps(obj, proto) self.assertEqual(str(cm.exception), 'dict items iterator must return 2-tuples') + self.assertEqual(cm.exception.__notes__, [ + 'when serializing test.pickletester.REX object']) def test_unpickleable_object_dict_items(self): obj = REX_seven({'a': UNPICKLEABLE}) @@ -1958,7 +2026,8 @@ def test_unpickleable_object_dict_items(self): with self.assertRaises(CustomError) as cm: self.dumps(obj, proto) self.assertEqual(cm.exception.__notes__, [ - "when serializing test.pickletester.REX_seven item 'a'"]) + "when serializing test.pickletester.REX_seven item 'a'", + 'when serializing test.pickletester.REX_seven object']) def test_unpickleable_list_items(self): obj = [1, [2, 3, UNPICKLEABLE]] diff --git a/Modules/_pickle.c b/Modules/_pickle.c index 85b26deda6df7a..a62e8025a34daf 100644 --- a/Modules/_pickle.c +++ b/Modules/_pickle.c @@ -4517,10 +4517,14 @@ save(PickleState *st, PicklerObject *self, PyObject *obj, int pers_save) if (!PyTuple_Check(reduce_value)) { PyErr_SetString(st->PicklingError, "__reduce__ must return a string or tuple"); + _PyErr_FormatNote("when serializing %T object", obj); goto error; } status = save_reduce(st, self, reduce_value, obj); + if (status < 0) { + _PyErr_FormatNote("when serializing %T object", obj); + } if (0) { error: