Skip to content

Commit 65d98d0

Browse files
pierreglaserpitrou
authored andcommitted
bpo-35900: Add a state_setter arg to save_reduce (pythonGH-12588)
Allow reduction methods to return a 6-item tuple where the 6th item specifies a custom state-setting method that's called instead of the regular ``__setstate__`` method.
1 parent 3988986 commit 65d98d0

5 files changed

Lines changed: 114 additions & 15 deletions

File tree

Doc/library/pickle.rst

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -598,7 +598,7 @@ or both.
598598
module; the pickle module searches the module namespace to determine the
599599
object's module. This behaviour is typically useful for singletons.
600600

601-
When a tuple is returned, it must be between two and five items long.
601+
When a tuple is returned, it must be between two and six items long.
602602
Optional items can either be omitted, or ``None`` can be provided as their
603603
value. The semantics of each item are in order:
604604

@@ -629,6 +629,15 @@ or both.
629629
value``. This is primarily used for dictionary subclasses, but may be used
630630
by other classes as long as they implement :meth:`__setitem__`.
631631

632+
* Optionally, a callable with a ``(obj, state)`` signature. This
633+
callable allows the user to programatically control the state-updating
634+
behavior of a specific object, instead of using ``obj``'s static
635+
:meth:`__setstate__` method. If not ``None``, this callable will have
636+
priority over ``obj``'s :meth:`__setstate__`.
637+
638+
.. versionadded:: 3.8
639+
The optional sixth tuple item, ``(obj, state)``, was added.
640+
632641

633642
.. method:: object.__reduce_ex__(protocol)
634643

Lib/pickle.py

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -537,9 +537,9 @@ def save(self, obj, save_persistent_id=True):
537537

538538
# Assert that it returned an appropriately sized tuple
539539
l = len(rv)
540-
if not (2 <= l <= 5):
540+
if not (2 <= l <= 6):
541541
raise PicklingError("Tuple returned by %s must have "
542-
"two to five elements" % reduce)
542+
"two to six elements" % reduce)
543543

544544
# Save the reduce() output and finally memoize the object
545545
self.save_reduce(obj=obj, *rv)
@@ -561,7 +561,7 @@ def save_pers(self, pid):
561561
"persistent IDs in protocol 0 must be ASCII strings")
562562

563563
def save_reduce(self, func, args, state=None, listitems=None,
564-
dictitems=None, obj=None):
564+
dictitems=None, state_setter=None, obj=None):
565565
# This API is called by some subclasses
566566

567567
if not isinstance(args, tuple):
@@ -655,8 +655,25 @@ def save_reduce(self, func, args, state=None, listitems=None,
655655
self._batch_setitems(dictitems)
656656

657657
if state is not None:
658-
save(state)
659-
write(BUILD)
658+
if state_setter is None:
659+
save(state)
660+
write(BUILD)
661+
else:
662+
# If a state_setter is specified, call it instead of load_build
663+
# to update obj's with its previous state.
664+
# First, push state_setter and its tuple of expected arguments
665+
# (obj, state) onto the stack.
666+
save(state_setter)
667+
save(obj) # simple BINGET opcode as obj is already memoized.
668+
save(state)
669+
write(TUPLE2)
670+
# Trigger a state_setter(obj, state) function call.
671+
write(REDUCE)
672+
# The purpose of state_setter is to carry-out an
673+
# inplace modification of obj. We do not care about what the
674+
# method might return, so its output is eventually removed from
675+
# the stack.
676+
write(POP)
660677

661678
# Methods below this point are dispatched through the dispatch table
662679

Lib/test/pickletester.py

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2992,7 +2992,26 @@ def __reduce__(self):
29922992
return str, (REDUCE_A,)
29932993

29942994
class BBB(object):
2995-
pass
2995+
def __init__(self):
2996+
# Add an instance attribute to enable state-saving routines at pickling
2997+
# time.
2998+
self.a = "some attribute"
2999+
3000+
def __setstate__(self, state):
3001+
self.a = "BBB.__setstate__"
3002+
3003+
3004+
def setstate_bbb(obj, state):
3005+
"""Custom state setter for BBB objects
3006+
3007+
Such callable may be created by other persons than the ones who created the
3008+
BBB class. If passed as the state_setter item of a custom reducer, this
3009+
allows for custom state setting behavior of BBB objects. One can think of
3010+
it as the analogous of list_setitems or dict_setitems but for foreign
3011+
classes/functions.
3012+
"""
3013+
obj.a = "custom state_setter"
3014+
29963015

29973016
class AbstractDispatchTableTests(unittest.TestCase):
29983017

@@ -3081,6 +3100,25 @@ def reduce_2(obj):
30813100
self.assertEqual(default_load_dump(a), REDUCE_A)
30823101
self.assertIsInstance(default_load_dump(b), BBB)
30833102

3103+
# End-to-end testing of save_reduce with the state_setter keyword
3104+
# argument. This is a dispatch_table test as the primary goal of
3105+
# state_setter is to tweak objects reduction behavior.
3106+
# In particular, state_setter is useful when the default __setstate__
3107+
# behavior is not flexible enough.
3108+
3109+
# No custom reducer for b has been registered for now, so
3110+
# BBB.__setstate__ should be used at unpickling time
3111+
self.assertEqual(default_load_dump(b).a, "BBB.__setstate__")
3112+
3113+
def reduce_bbb(obj):
3114+
return BBB, (), obj.__dict__, None, None, setstate_bbb
3115+
3116+
dispatch_table[BBB] = reduce_bbb
3117+
3118+
# The custom reducer reduce_bbb includes a state setter, that should
3119+
# have priority over BBB.__setstate__
3120+
self.assertEqual(custom_load_dump(b).a, "custom state_setter")
3121+
30843122

30853123
if __name__ == "__main__":
30863124
# Print some stuff that can be used to rewrite DATA{0,1,2}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Allow reduction methods to return a 6-item tuple where the 6th item specifies a
2+
custom state-setting method that's called instead of the regular
3+
``__setstate__`` method.

Modules/_pickle.c

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3662,6 +3662,7 @@ save_reduce(PicklerObject *self, PyObject *args, PyObject *obj)
36623662
PyObject *state = NULL;
36633663
PyObject *listitems = Py_None;
36643664
PyObject *dictitems = Py_None;
3665+
PyObject *state_setter = Py_None;
36653666
PickleState *st = _Pickle_GetGlobalState();
36663667
Py_ssize_t size;
36673668
int use_newobj = 0, use_newobj_ex = 0;
@@ -3672,14 +3673,15 @@ save_reduce(PicklerObject *self, PyObject *args, PyObject *obj)
36723673
const char newobj_ex_op = NEWOBJ_EX;
36733674

36743675
size = PyTuple_Size(args);
3675-
if (size < 2 || size > 5) {
3676+
if (size < 2 || size > 6) {
36763677
PyErr_SetString(st->PicklingError, "tuple returned by "
3677-
"__reduce__ must contain 2 through 5 elements");
3678+
"__reduce__ must contain 2 through 6 elements");
36783679
return -1;
36793680
}
36803681

3681-
if (!PyArg_UnpackTuple(args, "save_reduce", 2, 5,
3682-
&callable, &argtup, &state, &listitems, &dictitems))
3682+
if (!PyArg_UnpackTuple(args, "save_reduce", 2, 6,
3683+
&callable, &argtup, &state, &listitems, &dictitems,
3684+
&state_setter))
36833685
return -1;
36843686

36853687
if (!PyCallable_Check(callable)) {
@@ -3714,6 +3716,15 @@ save_reduce(PicklerObject *self, PyObject *args, PyObject *obj)
37143716
return -1;
37153717
}
37163718

3719+
if (state_setter == Py_None)
3720+
state_setter = NULL;
3721+
else if (!PyCallable_Check(state_setter)) {
3722+
PyErr_Format(st->PicklingError, "sixth element of the tuple "
3723+
"returned by __reduce__ must be a function, not %s",
3724+
Py_TYPE(state_setter)->tp_name);
3725+
return -1;
3726+
}
3727+
37173728
if (self->proto >= 2) {
37183729
PyObject *name;
37193730
_Py_IDENTIFIER(__name__);
@@ -3933,11 +3944,32 @@ save_reduce(PicklerObject *self, PyObject *args, PyObject *obj)
39333944
return -1;
39343945

39353946
if (state) {
3936-
if (save(self, state, 0) < 0 ||
3937-
_Pickler_Write(self, &build_op, 1) < 0)
3938-
return -1;
3939-
}
3947+
if (state_setter == NULL) {
3948+
if (save(self, state, 0) < 0 ||
3949+
_Pickler_Write(self, &build_op, 1) < 0)
3950+
return -1;
3951+
}
3952+
else {
3953+
3954+
/* If a state_setter is specified, call it instead of load_build to
3955+
* update obj's with its previous state.
3956+
* The first 4 save/write instructions push state_setter and its
3957+
* tuple of expected arguments (obj, state) onto the stack. The
3958+
* REDUCE opcode triggers the state_setter(obj, state) function
3959+
* call. Finally, because state-updating routines only do in-place
3960+
* modification, the whole operation has to be stack-transparent.
3961+
* Thus, we finally pop the call's output from the stack.*/
39403962

3963+
const char tupletwo_op = TUPLE2;
3964+
const char pop_op = POP;
3965+
if (save(self, state_setter, 0) < 0 ||
3966+
save(self, obj, 0) < 0 || save(self, state, 0) < 0 ||
3967+
_Pickler_Write(self, &tupletwo_op, 1) < 0 ||
3968+
_Pickler_Write(self, &reduce_op, 1) < 0 ||
3969+
_Pickler_Write(self, &pop_op, 1) < 0)
3970+
return -1;
3971+
}
3972+
}
39413973
return 0;
39423974
}
39433975

0 commit comments

Comments
 (0)