Skip to content

Commit 07a6aa3

Browse files
[3.13] gh-125631: Enable setting persistent_id and persistent_load of pickler and unpickler (GH-125752) (GH-126528)
pickle.Pickler.persistent_id and pickle.Unpickler.persistent_load can again be overridden as instance attributes. (cherry picked from commit 223d3dc) Co-authored-by: Serhiy Storchaka <[email protected]>
1 parent 59316a6 commit 07a6aa3

File tree

3 files changed

+146
-2
lines changed

3 files changed

+146
-2
lines changed

Lib/test/test_pickle.py

Lines changed: 80 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,84 @@ def persistent_load(subself, pid):
250250
self.assertEqual(unpickler.load(), 'abc')
251251
self.assertEqual(called, ['abc'])
252252

253+
def test_pickler_instance_attribute(self):
254+
def persistent_id(obj):
255+
called.append(obj)
256+
return obj
257+
258+
for proto in range(pickle.HIGHEST_PROTOCOL + 1):
259+
f = io.BytesIO()
260+
pickler = self.pickler(f, proto)
261+
called = []
262+
old_persistent_id = pickler.persistent_id
263+
pickler.persistent_id = persistent_id
264+
self.assertEqual(pickler.persistent_id, persistent_id)
265+
pickler.dump('abc')
266+
self.assertEqual(called, ['abc'])
267+
self.assertEqual(self.loads(f.getvalue()), 'abc')
268+
del pickler.persistent_id
269+
self.assertEqual(pickler.persistent_id, old_persistent_id)
270+
271+
def test_unpickler_instance_attribute(self):
272+
def persistent_load(pid):
273+
called.append(pid)
274+
return pid
275+
276+
for proto in range(pickle.HIGHEST_PROTOCOL + 1):
277+
unpickler = self.unpickler(io.BytesIO(self.dumps('abc', proto)))
278+
called = []
279+
old_persistent_load = unpickler.persistent_load
280+
unpickler.persistent_load = persistent_load
281+
self.assertEqual(unpickler.persistent_load, persistent_load)
282+
self.assertEqual(unpickler.load(), 'abc')
283+
self.assertEqual(called, ['abc'])
284+
del unpickler.persistent_load
285+
self.assertEqual(unpickler.persistent_load, old_persistent_load)
286+
287+
def test_pickler_super_instance_attribute(self):
288+
class PersPickler(self.pickler):
289+
def persistent_id(subself, obj):
290+
raise AssertionError('should never be called')
291+
def _persistent_id(subself, obj):
292+
called.append(obj)
293+
self.assertIsNone(super().persistent_id(obj))
294+
return obj
295+
296+
for proto in range(pickle.HIGHEST_PROTOCOL + 1):
297+
f = io.BytesIO()
298+
pickler = PersPickler(f, proto)
299+
called = []
300+
old_persistent_id = pickler.persistent_id
301+
pickler.persistent_id = pickler._persistent_id
302+
self.assertEqual(pickler.persistent_id, pickler._persistent_id)
303+
pickler.dump('abc')
304+
self.assertEqual(called, ['abc'])
305+
self.assertEqual(self.loads(f.getvalue()), 'abc')
306+
del pickler.persistent_id
307+
self.assertEqual(pickler.persistent_id, old_persistent_id)
308+
309+
def test_unpickler_super_instance_attribute(self):
310+
class PersUnpickler(self.unpickler):
311+
def persistent_load(subself, pid):
312+
raise AssertionError('should never be called')
313+
def _persistent_load(subself, pid):
314+
called.append(pid)
315+
with self.assertRaises(self.persistent_load_error):
316+
super().persistent_load(pid)
317+
return pid
318+
319+
for proto in range(pickle.HIGHEST_PROTOCOL + 1):
320+
unpickler = PersUnpickler(io.BytesIO(self.dumps('abc', proto)))
321+
called = []
322+
old_persistent_load = unpickler.persistent_load
323+
unpickler.persistent_load = unpickler._persistent_load
324+
self.assertEqual(unpickler.persistent_load, unpickler._persistent_load)
325+
self.assertEqual(unpickler.load(), 'abc')
326+
self.assertEqual(called, ['abc'])
327+
del unpickler.persistent_load
328+
self.assertEqual(unpickler.persistent_load, old_persistent_load)
329+
330+
253331
class PyPicklerUnpicklerObjectTests(AbstractPicklerUnpicklerObjectTests, unittest.TestCase):
254332

255333
pickler_class = pickle._Pickler
@@ -373,7 +451,7 @@ class SizeofTests(unittest.TestCase):
373451
check_sizeof = support.check_sizeof
374452

375453
def test_pickler(self):
376-
basesize = support.calcobjsize('6P2n3i2n3i2P')
454+
basesize = support.calcobjsize('7P2n3i2n3i2P')
377455
p = _pickle.Pickler(io.BytesIO())
378456
self.assertEqual(object.__sizeof__(p), basesize)
379457
MT_size = struct.calcsize('3nP0n')
@@ -390,7 +468,7 @@ def test_pickler(self):
390468
0) # Write buffer is cleared after every dump().
391469

392470
def test_unpickler(self):
393-
basesize = support.calcobjsize('2P2nP 2P2n2i5P 2P3n8P2n2i')
471+
basesize = support.calcobjsize('2P2n2P 2P2n2i5P 2P3n8P2n2i')
394472
unpickler = _pickle.Unpickler
395473
P = struct.calcsize('P') # Size of memo table entry.
396474
n = struct.calcsize('n') # Size of mark table entry.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Restore ability to set :attr:`~pickle.Pickler.persistent_id` and
2+
:attr:`~pickle.Unpickler.persistent_load` attributes of instances of the
3+
:class:`!Pickler` and :class:`!Unpickler` classes in the :mod:`pickle`
4+
module.

Modules/_pickle.c

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -612,6 +612,7 @@ typedef struct PicklerObject {
612612
objects to support self-referential objects
613613
pickling. */
614614
PyObject *persistent_id; /* persistent_id() method, can be NULL */
615+
PyObject *persistent_id_attr; /* instance attribute, can be NULL */
615616
PyObject *dispatch_table; /* private dispatch_table, can be NULL */
616617
PyObject *reducer_override; /* hook for invoking user-defined callbacks
617618
instead of save_global when pickling
@@ -654,6 +655,7 @@ typedef struct UnpicklerObject {
654655
size_t memo_len; /* Number of objects in the memo */
655656

656657
PyObject *persistent_load; /* persistent_load() method, can be NULL. */
658+
PyObject *persistent_load_attr; /* instance attribute, can be NULL. */
657659

658660
Py_buffer buffer;
659661
char *input_buffer;
@@ -1107,6 +1109,7 @@ _Pickler_New(PickleState *st)
11071109

11081110
self->memo = memo;
11091111
self->persistent_id = NULL;
1112+
self->persistent_id_attr = NULL;
11101113
self->dispatch_table = NULL;
11111114
self->reducer_override = NULL;
11121115
self->write = NULL;
@@ -1605,6 +1608,7 @@ _Unpickler_New(PyObject *module)
16051608
self->memo_size = MEMO_SIZE;
16061609
self->memo_len = 0;
16071610
self->persistent_load = NULL;
1611+
self->persistent_load_attr = NULL;
16081612
memset(&self->buffer, 0, sizeof(Py_buffer));
16091613
self->input_buffer = NULL;
16101614
self->input_line = NULL;
@@ -5021,6 +5025,33 @@ Pickler_set_memo(PicklerObject *self, PyObject *obj, void *Py_UNUSED(ignored))
50215025
return -1;
50225026
}
50235027

5028+
static PyObject *
5029+
Pickler_getattr(PyObject *self, PyObject *name)
5030+
{
5031+
if (PyUnicode_Check(name)
5032+
&& PyUnicode_EqualToUTF8(name, "persistent_id")
5033+
&& ((PicklerObject *)self)->persistent_id_attr)
5034+
{
5035+
return Py_NewRef(((PicklerObject *)self)->persistent_id_attr);
5036+
}
5037+
5038+
return PyObject_GenericGetAttr(self, name);
5039+
}
5040+
5041+
static int
5042+
Pickler_setattr(PyObject *self, PyObject *name, PyObject *value)
5043+
{
5044+
if (PyUnicode_Check(name)
5045+
&& PyUnicode_EqualToUTF8(name, "persistent_id"))
5046+
{
5047+
Py_XINCREF(value);
5048+
Py_XSETREF(((PicklerObject *)self)->persistent_id_attr, value);
5049+
return 0;
5050+
}
5051+
5052+
return PyObject_GenericSetAttr(self, name, value);
5053+
}
5054+
50245055
static PyMemberDef Pickler_members[] = {
50255056
{"bin", Py_T_INT, offsetof(PicklerObject, bin)},
50265057
{"fast", Py_T_INT, offsetof(PicklerObject, fast)},
@@ -5036,6 +5067,8 @@ static PyGetSetDef Pickler_getsets[] = {
50365067

50375068
static PyType_Slot pickler_type_slots[] = {
50385069
{Py_tp_dealloc, Pickler_dealloc},
5070+
{Py_tp_getattro, Pickler_getattr},
5071+
{Py_tp_setattro, Pickler_setattr},
50395072
{Py_tp_methods, Pickler_methods},
50405073
{Py_tp_members, Pickler_members},
50415074
{Py_tp_getset, Pickler_getsets},
@@ -7476,6 +7509,33 @@ Unpickler_set_memo(UnpicklerObject *self, PyObject *obj, void *Py_UNUSED(ignored
74767509
return -1;
74777510
}
74787511

7512+
static PyObject *
7513+
Unpickler_getattr(PyObject *self, PyObject *name)
7514+
{
7515+
if (PyUnicode_Check(name)
7516+
&& PyUnicode_EqualToUTF8(name, "persistent_load")
7517+
&& ((UnpicklerObject *)self)->persistent_load_attr)
7518+
{
7519+
return Py_NewRef(((UnpicklerObject *)self)->persistent_load_attr);
7520+
}
7521+
7522+
return PyObject_GenericGetAttr(self, name);
7523+
}
7524+
7525+
static int
7526+
Unpickler_setattr(PyObject *self, PyObject *name, PyObject *value)
7527+
{
7528+
if (PyUnicode_Check(name)
7529+
&& PyUnicode_EqualToUTF8(name, "persistent_load"))
7530+
{
7531+
Py_XINCREF(value);
7532+
Py_XSETREF(((UnpicklerObject *)self)->persistent_load_attr, value);
7533+
return 0;
7534+
}
7535+
7536+
return PyObject_GenericSetAttr(self, name, value);
7537+
}
7538+
74797539
static PyGetSetDef Unpickler_getsets[] = {
74807540
{"memo", (getter)Unpickler_get_memo, (setter)Unpickler_set_memo},
74817541
{NULL}
@@ -7484,6 +7544,8 @@ static PyGetSetDef Unpickler_getsets[] = {
74847544
static PyType_Slot unpickler_type_slots[] = {
74857545
{Py_tp_dealloc, Unpickler_dealloc},
74867546
{Py_tp_doc, (char *)_pickle_Unpickler___init____doc__},
7547+
{Py_tp_getattro, Unpickler_getattr},
7548+
{Py_tp_setattro, Unpickler_setattr},
74877549
{Py_tp_traverse, Unpickler_traverse},
74887550
{Py_tp_clear, Unpickler_clear},
74897551
{Py_tp_methods, Unpickler_methods},

0 commit comments

Comments
 (0)