Skip to content

Commit 8d3c290

Browse files
committed
Issue #14166: Pickler objects now have an optional dispatch_table attribute which allows to set custom per-pickler reduction functions.
Patch by sbt.
1 parent d1c351d commit 8d3c290

File tree

7 files changed

+228
-15
lines changed

7 files changed

+228
-15
lines changed

Doc/library/copyreg.rst

+5-3
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ Such constructors may be factory functions or class instances.
3232
returned by *function* at pickling time. :exc:`TypeError` will be raised if
3333
*object* is a class or *constructor* is not callable.
3434

35-
See the :mod:`pickle` module for more details on the interface expected of
36-
*function* and *constructor*.
37-
35+
See the :mod:`pickle` module for more details on the interface
36+
expected of *function* and *constructor*. Note that the
37+
:attr:`~pickle.Pickler.dispatch_table` attribute of a pickler
38+
object or subclass of :class:`pickle.Pickler` can also be used for
39+
declaring reduction functions.

Doc/library/pickle.rst

+61
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,29 @@ The :mod:`pickle` module exports two classes, :class:`Pickler` and
285285

286286
See :ref:`pickle-persistent` for details and examples of uses.
287287

288+
.. attribute:: dispatch_table
289+
290+
A pickler object's dispatch table is a registry of *reduction
291+
functions* of the kind which can be declared using
292+
:func:`copyreg.pickle`. It is a mapping whose keys are classes
293+
and whose values are reduction functions. A reduction function
294+
takes a single argument of the associated class and should
295+
conform to the same interface as a :meth:`~object.__reduce__`
296+
method.
297+
298+
By default, a pickler object will not have a
299+
:attr:`dispatch_table` attribute, and it will instead use the
300+
global dispatch table managed by the :mod:`copyreg` module.
301+
However, to customize the pickling for a specific pickler object
302+
one can set the :attr:`dispatch_table` attribute to a dict-like
303+
object. Alternatively, if a subclass of :class:`Pickler` has a
304+
:attr:`dispatch_table` attribute then this will be used as the
305+
default dispatch table for instances of that class.
306+
307+
See :ref:`pickle-dispatch` for usage examples.
308+
309+
.. versionadded:: 3.3
310+
288311
.. attribute:: fast
289312

290313
Deprecated. Enable fast mode if set to a true value. The fast mode
@@ -575,6 +598,44 @@ pickle external objects by reference.
575598

576599
.. literalinclude:: ../includes/dbpickle.py
577600

601+
.. _pickle-dispatch:
602+
603+
Dispatch Tables
604+
^^^^^^^^^^^^^^^
605+
606+
If one wants to customize pickling of some classes without disturbing
607+
any other code which depends on pickling, then one can create a
608+
pickler with a private dispatch table.
609+
610+
The global dispatch table managed by the :mod:`copyreg` module is
611+
available as :data:`copyreg.dispatch_table`. Therefore, one may
612+
choose to use a modified copy of :data:`copyreg.dispatch_table` as a
613+
private dispatch table.
614+
615+
For example ::
616+
617+
f = io.BytesIO()
618+
p = pickle.Pickler(f)
619+
p.dispatch_table = copyreg.dispatch_table.copy()
620+
p.dispatch_table[SomeClass] = reduce_SomeClass
621+
622+
creates an instance of :class:`pickle.Pickler` with a private dispatch
623+
table which handles the ``SomeClass`` class specially. Alternatively,
624+
the code ::
625+
626+
class MyPickler(pickle.Pickler):
627+
dispatch_table = copyreg.dispatch_table.copy()
628+
dispatch_table[SomeClass] = reduce_SomeClass
629+
f = io.BytesIO()
630+
p = MyPickler(f)
631+
632+
does the same, but all instances of ``MyPickler`` will by default
633+
share the same dispatch table. The equivalent code using the
634+
:mod:`copyreg` module is ::
635+
636+
copyreg.pickle(SomeClass, reduce_SomeClass)
637+
f = io.BytesIO()
638+
p = pickle.Pickler(f)
578639

579640
.. _pickle-state:
580641

Lib/pickle.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -297,8 +297,8 @@ def save(self, obj, save_persistent_id=True):
297297
f(self, obj) # Call unbound method with explicit self
298298
return
299299

300-
# Check copyreg.dispatch_table
301-
reduce = dispatch_table.get(t)
300+
# Check private dispatch table if any, or else copyreg.dispatch_table
301+
reduce = getattr(self, 'dispatch_table', dispatch_table).get(t)
302302
if reduce:
303303
rv = reduce(obj)
304304
else:

Lib/test/pickletester.py

+99
Original file line numberDiff line numberDiff line change
@@ -1605,6 +1605,105 @@ def test_unpickling_buffering_readline(self):
16051605
self.assertEqual(unpickler.load(), data)
16061606

16071607

1608+
# Tests for dispatch_table attribute
1609+
1610+
REDUCE_A = 'reduce_A'
1611+
1612+
class AAA(object):
1613+
def __reduce__(self):
1614+
return str, (REDUCE_A,)
1615+
1616+
class BBB(object):
1617+
pass
1618+
1619+
class AbstractDispatchTableTests(unittest.TestCase):
1620+
1621+
def test_default_dispatch_table(self):
1622+
# No dispatch_table attribute by default
1623+
f = io.BytesIO()
1624+
p = self.pickler_class(f, 0)
1625+
with self.assertRaises(AttributeError):
1626+
p.dispatch_table
1627+
self.assertFalse(hasattr(p, 'dispatch_table'))
1628+
1629+
def test_class_dispatch_table(self):
1630+
# A dispatch_table attribute can be specified class-wide
1631+
dt = self.get_dispatch_table()
1632+
1633+
class MyPickler(self.pickler_class):
1634+
dispatch_table = dt
1635+
1636+
def dumps(obj, protocol=None):
1637+
f = io.BytesIO()
1638+
p = MyPickler(f, protocol)
1639+
self.assertEqual(p.dispatch_table, dt)
1640+
p.dump(obj)
1641+
return f.getvalue()
1642+
1643+
self._test_dispatch_table(dumps, dt)
1644+
1645+
def test_instance_dispatch_table(self):
1646+
# A dispatch_table attribute can also be specified instance-wide
1647+
dt = self.get_dispatch_table()
1648+
1649+
def dumps(obj, protocol=None):
1650+
f = io.BytesIO()
1651+
p = self.pickler_class(f, protocol)
1652+
p.dispatch_table = dt
1653+
self.assertEqual(p.dispatch_table, dt)
1654+
p.dump(obj)
1655+
return f.getvalue()
1656+
1657+
self._test_dispatch_table(dumps, dt)
1658+
1659+
def _test_dispatch_table(self, dumps, dispatch_table):
1660+
def custom_load_dump(obj):
1661+
return pickle.loads(dumps(obj, 0))
1662+
1663+
def default_load_dump(obj):
1664+
return pickle.loads(pickle.dumps(obj, 0))
1665+
1666+
# pickling complex numbers using protocol 0 relies on copyreg
1667+
# so check pickling a complex number still works
1668+
z = 1 + 2j
1669+
self.assertEqual(custom_load_dump(z), z)
1670+
self.assertEqual(default_load_dump(z), z)
1671+
1672+
# modify pickling of complex
1673+
REDUCE_1 = 'reduce_1'
1674+
def reduce_1(obj):
1675+
return str, (REDUCE_1,)
1676+
dispatch_table[complex] = reduce_1
1677+
self.assertEqual(custom_load_dump(z), REDUCE_1)
1678+
self.assertEqual(default_load_dump(z), z)
1679+
1680+
# check picklability of AAA and BBB
1681+
a = AAA()
1682+
b = BBB()
1683+
self.assertEqual(custom_load_dump(a), REDUCE_A)
1684+
self.assertIsInstance(custom_load_dump(b), BBB)
1685+
self.assertEqual(default_load_dump(a), REDUCE_A)
1686+
self.assertIsInstance(default_load_dump(b), BBB)
1687+
1688+
# modify pickling of BBB
1689+
dispatch_table[BBB] = reduce_1
1690+
self.assertEqual(custom_load_dump(a), REDUCE_A)
1691+
self.assertEqual(custom_load_dump(b), REDUCE_1)
1692+
self.assertEqual(default_load_dump(a), REDUCE_A)
1693+
self.assertIsInstance(default_load_dump(b), BBB)
1694+
1695+
# revert pickling of BBB and modify pickling of AAA
1696+
REDUCE_2 = 'reduce_2'
1697+
def reduce_2(obj):
1698+
return str, (REDUCE_2,)
1699+
dispatch_table[AAA] = reduce_2
1700+
del dispatch_table[BBB]
1701+
self.assertEqual(custom_load_dump(a), REDUCE_2)
1702+
self.assertIsInstance(custom_load_dump(b), BBB)
1703+
self.assertEqual(default_load_dump(a), REDUCE_A)
1704+
self.assertIsInstance(default_load_dump(b), BBB)
1705+
1706+
16081707
if __name__ == "__main__":
16091708
# Print some stuff that can be used to rewrite DATA{0,1,2}
16101709
from pickletools import dis

Lib/test/test_pickle.py

+27-1
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import pickle
22
import io
3+
import collections
34

45
from test import support
56

67
from test.pickletester import AbstractPickleTests
78
from test.pickletester import AbstractPickleModuleTests
89
from test.pickletester import AbstractPersistentPicklerTests
910
from test.pickletester import AbstractPicklerUnpicklerObjectTests
11+
from test.pickletester import AbstractDispatchTableTests
1012
from test.pickletester import BigmemPickleTests
1113

1214
try:
@@ -80,6 +82,18 @@ class PyPicklerUnpicklerObjectTests(AbstractPicklerUnpicklerObjectTests):
8082
unpickler_class = pickle._Unpickler
8183

8284

85+
class PyDispatchTableTests(AbstractDispatchTableTests):
86+
pickler_class = pickle._Pickler
87+
def get_dispatch_table(self):
88+
return pickle.dispatch_table.copy()
89+
90+
91+
class PyChainDispatchTableTests(AbstractDispatchTableTests):
92+
pickler_class = pickle._Pickler
93+
def get_dispatch_table(self):
94+
return collections.ChainMap({}, pickle.dispatch_table)
95+
96+
8397
if has_c_implementation:
8498
class CPicklerTests(PyPicklerTests):
8599
pickler = _pickle.Pickler
@@ -101,14 +115,26 @@ class CPicklerUnpicklerObjectTests(AbstractPicklerUnpicklerObjectTests):
101115
pickler_class = _pickle.Pickler
102116
unpickler_class = _pickle.Unpickler
103117

118+
class CDispatchTableTests(AbstractDispatchTableTests):
119+
pickler_class = pickle.Pickler
120+
def get_dispatch_table(self):
121+
return pickle.dispatch_table.copy()
122+
123+
class CChainDispatchTableTests(AbstractDispatchTableTests):
124+
pickler_class = pickle.Pickler
125+
def get_dispatch_table(self):
126+
return collections.ChainMap({}, pickle.dispatch_table)
127+
104128

105129
def test_main():
106-
tests = [PickleTests, PyPicklerTests, PyPersPicklerTests]
130+
tests = [PickleTests, PyPicklerTests, PyPersPicklerTests,
131+
PyDispatchTableTests, PyChainDispatchTableTests]
107132
if has_c_implementation:
108133
tests.extend([CPicklerTests, CPersPicklerTests,
109134
CDumpPickle_LoadPickle, DumpPickle_CLoadPickle,
110135
PyPicklerUnpicklerObjectTests,
111136
CPicklerUnpicklerObjectTests,
137+
CDispatchTableTests, CChainDispatchTableTests,
112138
InMemoryPickleTests])
113139
support.run_unittest(*tests)
114140
support.run_doctest(pickle)

Misc/NEWS

+4
Original file line numberDiff line numberDiff line change
@@ -511,6 +511,10 @@ Core and Builtins
511511
Library
512512
-------
513513

514+
- Issue #14166: Pickler objects now have an optional ``dispatch_table``
515+
attribute which allows to set custom per-pickler reduction functions.
516+
Patch by sbt.
517+
514518
- Issue #14177: marshal.loads() now raises TypeError when given an unicode
515519
string. Patch by Guilherme Gonçalves.
516520

Modules/_pickle.c

+30-9
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,7 @@ typedef struct PicklerObject {
319319
objects to support self-referential objects
320320
pickling. */
321321
PyObject *pers_func; /* persistent_id() method, can be NULL */
322+
PyObject *dispatch_table; /* private dispatch_table, can be NULL */
322323
PyObject *arg;
323324

324325
PyObject *write; /* write() method of the output stream. */
@@ -764,6 +765,7 @@ _Pickler_New(void)
764765
return NULL;
765766

766767
self->pers_func = NULL;
768+
self->dispatch_table = NULL;
767769
self->arg = NULL;
768770
self->write = NULL;
769771
self->proto = 0;
@@ -3176,17 +3178,24 @@ save(PicklerObject *self, PyObject *obj, int pers_save)
31763178
/* XXX: This part needs some unit tests. */
31773179

31783180
/* Get a reduction callable, and call it. This may come from
3179-
* copyreg.dispatch_table, the object's __reduce_ex__ method,
3180-
* or the object's __reduce__ method.
3181+
* self.dispatch_table, copyreg.dispatch_table, the object's
3182+
* __reduce_ex__ method, or the object's __reduce__ method.
31813183
*/
3182-
reduce_func = PyDict_GetItem(dispatch_table, (PyObject *)type);
3184+
if (self->dispatch_table == NULL) {
3185+
reduce_func = PyDict_GetItem(dispatch_table, (PyObject *)type);
3186+
/* PyDict_GetItem() unlike PyObject_GetItem() and
3187+
PyObject_GetAttr() returns a borrowed ref */
3188+
Py_XINCREF(reduce_func);
3189+
} else {
3190+
reduce_func = PyObject_GetItem(self->dispatch_table, (PyObject *)type);
3191+
if (reduce_func == NULL) {
3192+
if (PyErr_ExceptionMatches(PyExc_KeyError))
3193+
PyErr_Clear();
3194+
else
3195+
goto error;
3196+
}
3197+
}
31833198
if (reduce_func != NULL) {
3184-
/* Here, the reference count of the reduce_func object returned by
3185-
PyDict_GetItem needs to be increased to be consistent with the one
3186-
returned by PyObject_GetAttr. This is allow us to blindly DECREF
3187-
reduce_func at the end of the save() routine.
3188-
*/
3189-
Py_INCREF(reduce_func);
31903199
Py_INCREF(obj);
31913200
reduce_value = _Pickler_FastCall(self, reduce_func, obj);
31923201
}
@@ -3359,6 +3368,7 @@ Pickler_dealloc(PicklerObject *self)
33593368
Py_XDECREF(self->output_buffer);
33603369
Py_XDECREF(self->write);
33613370
Py_XDECREF(self->pers_func);
3371+
Py_XDECREF(self->dispatch_table);
33623372
Py_XDECREF(self->arg);
33633373
Py_XDECREF(self->fast_memo);
33643374

@@ -3372,6 +3382,7 @@ Pickler_traverse(PicklerObject *self, visitproc visit, void *arg)
33723382
{
33733383
Py_VISIT(self->write);
33743384
Py_VISIT(self->pers_func);
3385+
Py_VISIT(self->dispatch_table);
33753386
Py_VISIT(self->arg);
33763387
Py_VISIT(self->fast_memo);
33773388
return 0;
@@ -3383,6 +3394,7 @@ Pickler_clear(PicklerObject *self)
33833394
Py_CLEAR(self->output_buffer);
33843395
Py_CLEAR(self->write);
33853396
Py_CLEAR(self->pers_func);
3397+
Py_CLEAR(self->dispatch_table);
33863398
Py_CLEAR(self->arg);
33873399
Py_CLEAR(self->fast_memo);
33883400

@@ -3427,6 +3439,7 @@ Pickler_init(PicklerObject *self, PyObject *args, PyObject *kwds)
34273439
PyObject *proto_obj = NULL;
34283440
PyObject *fix_imports = Py_True;
34293441
_Py_IDENTIFIER(persistent_id);
3442+
_Py_IDENTIFIER(dispatch_table);
34303443

34313444
if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|OO:Pickler",
34323445
kwlist, &file, &proto_obj, &fix_imports))
@@ -3468,6 +3481,13 @@ Pickler_init(PicklerObject *self, PyObject *args, PyObject *kwds)
34683481
if (self->pers_func == NULL)
34693482
return -1;
34703483
}
3484+
self->dispatch_table = NULL;
3485+
if (_PyObject_HasAttrId((PyObject *)self, &PyId_dispatch_table)) {
3486+
self->dispatch_table = _PyObject_GetAttrId((PyObject *)self,
3487+
&PyId_dispatch_table);
3488+
if (self->dispatch_table == NULL)
3489+
return -1;
3490+
}
34713491
return 0;
34723492
}
34733493

@@ -3749,6 +3769,7 @@ Pickler_set_persid(PicklerObject *self, PyObject *value)
37493769
static PyMemberDef Pickler_members[] = {
37503770
{"bin", T_INT, offsetof(PicklerObject, bin)},
37513771
{"fast", T_INT, offsetof(PicklerObject, fast)},
3772+
{"dispatch_table", T_OBJECT_EX, offsetof(PicklerObject, dispatch_table)},
37523773
{NULL}
37533774
};
37543775

0 commit comments

Comments
 (0)