Skip to content

Commit 223d3dc

Browse files
gh-125631: Enable setting persistent_id and persistent_load of pickler and unpickler (GH-125752)
pickle.Pickler.persistent_id and pickle.Unpickler.persistent_load can again be overridden as instance attributes.
1 parent 2a6b6b3 commit 223d3dc

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
@@ -613,6 +613,7 @@ typedef struct PicklerObject {
613613
objects to support self-referential objects
614614
pickling. */
615615
PyObject *persistent_id; /* persistent_id() method, can be NULL */
616+
PyObject *persistent_id_attr; /* instance attribute, can be NULL */
616617
PyObject *dispatch_table; /* private dispatch_table, can be NULL */
617618
PyObject *reducer_override; /* hook for invoking user-defined callbacks
618619
instead of save_global when pickling
@@ -655,6 +656,7 @@ typedef struct UnpicklerObject {
655656
size_t memo_len; /* Number of objects in the memo */
656657

657658
PyObject *persistent_load; /* persistent_load() method, can be NULL. */
659+
PyObject *persistent_load_attr; /* instance attribute, can be NULL. */
658660

659661
Py_buffer buffer;
660662
char *input_buffer;
@@ -1108,6 +1110,7 @@ _Pickler_New(PickleState *st)
11081110

11091111
self->memo = memo;
11101112
self->persistent_id = NULL;
1113+
self->persistent_id_attr = NULL;
11111114
self->dispatch_table = NULL;
11121115
self->reducer_override = NULL;
11131116
self->write = NULL;
@@ -1606,6 +1609,7 @@ _Unpickler_New(PyObject *module)
16061609
self->memo_size = MEMO_SIZE;
16071610
self->memo_len = 0;
16081611
self->persistent_load = NULL;
1612+
self->persistent_load_attr = NULL;
16091613
memset(&self->buffer, 0, sizeof(Py_buffer));
16101614
self->input_buffer = NULL;
16111615
self->input_line = NULL;
@@ -5092,6 +5096,33 @@ Pickler_set_memo(PicklerObject *self, PyObject *obj, void *Py_UNUSED(ignored))
50925096
return -1;
50935097
}
50945098

5099+
static PyObject *
5100+
Pickler_getattr(PyObject *self, PyObject *name)
5101+
{
5102+
if (PyUnicode_Check(name)
5103+
&& PyUnicode_EqualToUTF8(name, "persistent_id")
5104+
&& ((PicklerObject *)self)->persistent_id_attr)
5105+
{
5106+
return Py_NewRef(((PicklerObject *)self)->persistent_id_attr);
5107+
}
5108+
5109+
return PyObject_GenericGetAttr(self, name);
5110+
}
5111+
5112+
static int
5113+
Pickler_setattr(PyObject *self, PyObject *name, PyObject *value)
5114+
{
5115+
if (PyUnicode_Check(name)
5116+
&& PyUnicode_EqualToUTF8(name, "persistent_id"))
5117+
{
5118+
Py_XINCREF(value);
5119+
Py_XSETREF(((PicklerObject *)self)->persistent_id_attr, value);
5120+
return 0;
5121+
}
5122+
5123+
return PyObject_GenericSetAttr(self, name, value);
5124+
}
5125+
50955126
static PyMemberDef Pickler_members[] = {
50965127
{"bin", Py_T_INT, offsetof(PicklerObject, bin)},
50975128
{"fast", Py_T_INT, offsetof(PicklerObject, fast)},
@@ -5107,6 +5138,8 @@ static PyGetSetDef Pickler_getsets[] = {
51075138

51085139
static PyType_Slot pickler_type_slots[] = {
51095140
{Py_tp_dealloc, Pickler_dealloc},
5141+
{Py_tp_getattro, Pickler_getattr},
5142+
{Py_tp_setattro, Pickler_setattr},
51105143
{Py_tp_methods, Pickler_methods},
51115144
{Py_tp_members, Pickler_members},
51125145
{Py_tp_getset, Pickler_getsets},
@@ -7566,6 +7599,33 @@ Unpickler_set_memo(UnpicklerObject *self, PyObject *obj, void *Py_UNUSED(ignored
75667599
return -1;
75677600
}
75687601

7602+
static PyObject *
7603+
Unpickler_getattr(PyObject *self, PyObject *name)
7604+
{
7605+
if (PyUnicode_Check(name)
7606+
&& PyUnicode_EqualToUTF8(name, "persistent_load")
7607+
&& ((UnpicklerObject *)self)->persistent_load_attr)
7608+
{
7609+
return Py_NewRef(((UnpicklerObject *)self)->persistent_load_attr);
7610+
}
7611+
7612+
return PyObject_GenericGetAttr(self, name);
7613+
}
7614+
7615+
static int
7616+
Unpickler_setattr(PyObject *self, PyObject *name, PyObject *value)
7617+
{
7618+
if (PyUnicode_Check(name)
7619+
&& PyUnicode_EqualToUTF8(name, "persistent_load"))
7620+
{
7621+
Py_XINCREF(value);
7622+
Py_XSETREF(((UnpicklerObject *)self)->persistent_load_attr, value);
7623+
return 0;
7624+
}
7625+
7626+
return PyObject_GenericSetAttr(self, name, value);
7627+
}
7628+
75697629
static PyGetSetDef Unpickler_getsets[] = {
75707630
{"memo", (getter)Unpickler_get_memo, (setter)Unpickler_set_memo},
75717631
{NULL}
@@ -7574,6 +7634,8 @@ static PyGetSetDef Unpickler_getsets[] = {
75747634
static PyType_Slot unpickler_type_slots[] = {
75757635
{Py_tp_dealloc, Unpickler_dealloc},
75767636
{Py_tp_doc, (char *)_pickle_Unpickler___init____doc__},
7637+
{Py_tp_getattro, Unpickler_getattr},
7638+
{Py_tp_setattro, Unpickler_setattr},
75777639
{Py_tp_traverse, Unpickler_traverse},
75787640
{Py_tp_clear, Unpickler_clear},
75797641
{Py_tp_methods, Unpickler_methods},

0 commit comments

Comments
 (0)