Skip to content

Commit

Permalink
bpo-26579: Add object.__getstate__(). (pythonGH-2821)
Browse files Browse the repository at this point in the history
Copying and pickling instances of subclasses of builtin types
bytearray, set, frozenset, collections.OrderedDict, collections.deque,
weakref.WeakSet, and datetime.tzinfo now copies and pickles instance attributes
implemented as slots.
  • Loading branch information
serhiy-storchaka committed Apr 6, 2022
1 parent f82f9ce commit 884eba3
Show file tree
Hide file tree
Showing 25 changed files with 389 additions and 255 deletions.
35 changes: 27 additions & 8 deletions Doc/library/pickle.rst
Original file line number Diff line number Diff line change
Expand Up @@ -509,9 +509,8 @@ The following types can be pickled:

* classes that are defined at the top level of a module

* instances of such classes whose :attr:`~object.__dict__` or the result of
calling :meth:`__getstate__` is picklable (see section :ref:`pickle-inst` for
details).
* instances of such classes whose the result of calling :meth:`__getstate__`
is picklable (see section :ref:`pickle-inst` for details).

Attempts to pickle unpicklable objects will raise the :exc:`PicklingError`
exception; when this happens, an unspecified number of bytes may have already
Expand Down Expand Up @@ -611,11 +610,31 @@ methods:

.. method:: object.__getstate__()

Classes can further influence how their instances are pickled; if the class
defines the method :meth:`__getstate__`, it is called and the returned object
is pickled as the contents for the instance, instead of the contents of the
instance's dictionary. If the :meth:`__getstate__` method is absent, the
instance's :attr:`~object.__dict__` is pickled as usual.
Classes can further influence how their instances are pickled by overriding
the method :meth:`__getstate__`. It is called and the returned object
is pickled as the contents for the instance, instead of a default state.
There are several cases:

* For a class that has no instance :attr:`~object.__dict__` and no
:attr:`~object.__slots__`, the default state is ``None``.

* For a class that has an instance :attr:`~object.__dict__` and no
:attr:`~object.__slots__`, the default state is ``self.__dict__``.

* For a class that has an instance :attr:`~object.__dict__` and
:attr:`~object.__slots__`, the default state is a tuple consisting of two
dictionaries: ``self.__dict__``, and a dictionary mapping slot
names to slot values. Only slots that have a value are
included in the latter.

* For a class that has :attr:`~object.__slots__` and no instance
:attr:`~object.__dict__`, the default state is a tuple whose first item
is ``None`` and whose second item is a dictionary mapping slot names
to slot values described in the previous bullet.

.. versionchanged:: 3.11
Added the default implementation of the ``__getstate__()`` method in the
:class:`object` class.


.. method:: object.__setstate__(state)
Expand Down
9 changes: 9 additions & 0 deletions Doc/whatsnew/3.11.rst
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,15 @@ Other Language Changes
protocols correspondingly.
(Contributed by Serhiy Storchaka in :issue:`12022`.)

* Added :meth:`object.__getstate__` which provides the default
implementation of the ``__getstate__()`` method. :mod:`Copying <copy>`
and :mod:`pickling <pickle>` instances of subclasses of builtin types
:class:`bytearray`, :class:`set`, :class:`frozenset`,
:class:`collections.OrderedDict`, :class:`collections.deque`,
:class:`weakref.WeakSet`, and :class:`datetime.tzinfo` now copies and
pickles instance attributes implemented as :term:`slots <__slots__>`.
(Contributed by Serhiy Storchaka in :issue:`26579`.)


Other CPython Implementation Changes
====================================
Expand Down
5 changes: 5 additions & 0 deletions Include/object.h
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,11 @@ PyAPI_FUNC(void) PyObject_ClearWeakRefs(PyObject *);
*/
PyAPI_FUNC(PyObject *) PyObject_Dir(PyObject *);

/* Pickle support. */
#ifndef Py_LIMITED_API
PyAPI_FUNC(PyObject *) _PyObject_GetState(PyObject *);
#endif


/* Helpers for printing recursive container types */
PyAPI_FUNC(int) Py_ReprEnter(PyObject *);
Expand Down
3 changes: 1 addition & 2 deletions Lib/_weakrefset.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,7 @@ def __contains__(self, item):
return wr in self.data

def __reduce__(self):
return (self.__class__, (list(self),),
getattr(self, '__dict__', None))
return self.__class__, (list(self),), self.__getstate__()

def add(self, item):
if self._pending_removals:
Expand Down
20 changes: 16 additions & 4 deletions Lib/collections/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -271,10 +271,22 @@ def __repr__(self):

def __reduce__(self):
'Return state information for pickling'
inst_dict = vars(self).copy()
for k in vars(OrderedDict()):
inst_dict.pop(k, None)
return self.__class__, (), inst_dict or None, None, iter(self.items())
state = self.__getstate__()
if state:
if isinstance(state, tuple):
state, slots = state
else:
slots = {}
state = state.copy()
slots = slots.copy()
for k in vars(OrderedDict()):
state.pop(k, None)
slots.pop(k, None)
if slots:
state = state, slots
else:
state = state or None
return self.__class__, (), state, None, iter(self.items())

def copy(self):
'od.copy() -> a shallow copy of od'
Expand Down
4 changes: 4 additions & 0 deletions Lib/copyreg.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,10 @@ def _reduce_ex(self, proto):
except AttributeError:
dict = None
else:
if (type(self).__getstate__ is object.__getstate__ and
getattr(self, "__slots__", None)):
raise TypeError("a class that defines __slots__ without "
"defining __getstate__ cannot be pickled")
dict = getstate()
if dict:
return _reconstructor, args, dict
Expand Down
10 changes: 1 addition & 9 deletions Lib/datetime.py
Original file line number Diff line number Diff line change
Expand Up @@ -1169,15 +1169,7 @@ def __reduce__(self):
args = getinitargs()
else:
args = ()
getstate = getattr(self, "__getstate__", None)
if getstate:
state = getstate()
else:
state = getattr(self, "__dict__", None) or None
if state is None:
return (self.__class__, args)
else:
return (self.__class__, args, state)
return (self.__class__, args, self.__getstate__())


class IsoCalendarDate(tuple):
Expand Down
2 changes: 1 addition & 1 deletion Lib/email/headerregistry.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ def __reduce__(self):
self.__class__.__bases__,
str(self),
),
self.__dict__)
self.__getstate__())

@classmethod
def _reconstruct(cls, value):
Expand Down
6 changes: 4 additions & 2 deletions Lib/test/datetimetester.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,8 +139,8 @@ class PicklableFixedOffset(FixedOffset):
def __init__(self, offset=None, name=None, dstoffset=None):
FixedOffset.__init__(self, offset, name, dstoffset)

def __getstate__(self):
return self.__dict__
class PicklableFixedOffsetWithSlots(PicklableFixedOffset):
__slots__ = '_FixedOffset__offset', '_FixedOffset__name', 'spam'

class _TZInfo(tzinfo):
def utcoffset(self, datetime_module):
Expand Down Expand Up @@ -202,6 +202,7 @@ def test_pickling_subclass(self):
offset = timedelta(minutes=-300)
for otype, args in [
(PicklableFixedOffset, (offset, 'cookie')),
(PicklableFixedOffsetWithSlots, (offset, 'cookie')),
(timezone, (offset,)),
(timezone, (offset, "EST"))]:
orig = otype(*args)
Expand All @@ -217,6 +218,7 @@ def test_pickling_subclass(self):
self.assertIs(type(derived), otype)
self.assertEqual(derived.utcoffset(None), offset)
self.assertEqual(derived.tzname(None), oname)
self.assertFalse(hasattr(derived, 'spam'))

def test_issue23600(self):
DSTDIFF = DSTOFFSET = timedelta(hours=1)
Expand Down
4 changes: 3 additions & 1 deletion Lib/test/pickletester.py
Original file line number Diff line number Diff line change
Expand Up @@ -2382,9 +2382,11 @@ def test_reduce_calls_base(self):
def test_bad_getattr(self):
# Issue #3514: crash when there is an infinite loop in __getattr__
x = BadGetattr()
for proto in protocols:
for proto in range(2):
with support.infinite_recursion():
self.assertRaises(RuntimeError, self.dumps, x, proto)
for proto in range(2, pickle.HIGHEST_PROTOCOL + 1):
s = self.dumps(x, proto)

def test_reduce_bad_iterator(self):
# Issue4176: crash when 4th and 5th items of __reduce__()
Expand Down
20 changes: 14 additions & 6 deletions Lib/test/test_bytes.py
Original file line number Diff line number Diff line change
Expand Up @@ -1940,28 +1940,30 @@ def test_join(self):
def test_pickle(self):
a = self.type2test(b"abcd")
a.x = 10
a.y = self.type2test(b"efgh")
a.z = self.type2test(b"efgh")
for proto in range(pickle.HIGHEST_PROTOCOL + 1):
b = pickle.loads(pickle.dumps(a, proto))
self.assertNotEqual(id(a), id(b))
self.assertEqual(a, b)
self.assertEqual(a.x, b.x)
self.assertEqual(a.y, b.y)
self.assertEqual(a.z, b.z)
self.assertEqual(type(a), type(b))
self.assertEqual(type(a.y), type(b.y))
self.assertEqual(type(a.z), type(b.z))
self.assertFalse(hasattr(b, 'y'))

def test_copy(self):
a = self.type2test(b"abcd")
a.x = 10
a.y = self.type2test(b"efgh")
a.z = self.type2test(b"efgh")
for copy_method in (copy.copy, copy.deepcopy):
b = copy_method(a)
self.assertNotEqual(id(a), id(b))
self.assertEqual(a, b)
self.assertEqual(a.x, b.x)
self.assertEqual(a.y, b.y)
self.assertEqual(a.z, b.z)
self.assertEqual(type(a), type(b))
self.assertEqual(type(a.y), type(b.y))
self.assertEqual(type(a.z), type(b.z))
self.assertFalse(hasattr(b, 'y'))

def test_fromhex(self):
b = self.type2test.fromhex('1a2B30')
Expand Down Expand Up @@ -1994,6 +1996,9 @@ def __init__(me, *args, **kwargs):
class ByteArraySubclass(bytearray):
pass

class ByteArraySubclassWithSlots(bytearray):
__slots__ = ('x', 'y', '__dict__')

class BytesSubclass(bytes):
pass

Expand All @@ -2014,6 +2019,9 @@ def __init__(me, newarg=1, *args, **kwargs):
x = subclass(newarg=4, source=b"abcd")
self.assertEqual(x, b"abcd")

class ByteArraySubclassWithSlotsTest(SubclassTest, unittest.TestCase):
basetype = bytearray
type2test = ByteArraySubclassWithSlots

class BytesSubclassTest(SubclassTest, unittest.TestCase):
basetype = bytes
Expand Down
59 changes: 25 additions & 34 deletions Lib/test/test_deque.py
Original file line number Diff line number Diff line change
Expand Up @@ -781,6 +781,9 @@ def test_runtime_error_on_empty_deque(self):
class Deque(deque):
pass

class DequeWithSlots(deque):
__slots__ = ('x', 'y', '__dict__')

class DequeWithBadIter(deque):
def __iter__(self):
raise TypeError
Expand Down Expand Up @@ -810,40 +813,28 @@ def test_basics(self):
self.assertEqual(len(d), 0)

def test_copy_pickle(self):

d = Deque('abc')

e = d.__copy__()
self.assertEqual(type(d), type(e))
self.assertEqual(list(d), list(e))

e = Deque(d)
self.assertEqual(type(d), type(e))
self.assertEqual(list(d), list(e))

for proto in range(pickle.HIGHEST_PROTOCOL + 1):
s = pickle.dumps(d, proto)
e = pickle.loads(s)
self.assertNotEqual(id(d), id(e))
self.assertEqual(type(d), type(e))
self.assertEqual(list(d), list(e))

d = Deque('abcde', maxlen=4)

e = d.__copy__()
self.assertEqual(type(d), type(e))
self.assertEqual(list(d), list(e))

e = Deque(d)
self.assertEqual(type(d), type(e))
self.assertEqual(list(d), list(e))

for proto in range(pickle.HIGHEST_PROTOCOL + 1):
s = pickle.dumps(d, proto)
e = pickle.loads(s)
self.assertNotEqual(id(d), id(e))
self.assertEqual(type(d), type(e))
self.assertEqual(list(d), list(e))
for cls in Deque, DequeWithSlots:
for d in cls('abc'), cls('abcde', maxlen=4):
d.x = ['x']
d.z = ['z']

e = d.__copy__()
self.assertEqual(type(d), type(e))
self.assertEqual(list(d), list(e))

e = cls(d)
self.assertEqual(type(d), type(e))
self.assertEqual(list(d), list(e))

for proto in range(pickle.HIGHEST_PROTOCOL + 1):
s = pickle.dumps(d, proto)
e = pickle.loads(s)
self.assertNotEqual(id(d), id(e))
self.assertEqual(type(d), type(e))
self.assertEqual(list(d), list(e))
self.assertEqual(e.x, d.x)
self.assertEqual(e.z, d.z)
self.assertFalse(hasattr(e, 'y'))

def test_pickle_recursive(self):
for proto in range(pickle.HIGHEST_PROTOCOL + 1):
Expand Down
1 change: 1 addition & 0 deletions Lib/test/test_descrtut.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ def merge(self, other):
'__ge__',
'__getattribute__',
'__getitem__',
'__getstate__',
'__gt__',
'__hash__',
'__iadd__',
Expand Down
39 changes: 36 additions & 3 deletions Lib/test/test_ordered_dict.py
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,8 @@ def test_copying(self):
# and have a repr/eval round-trip
pairs = [('c', 1), ('b', 2), ('a', 3), ('d', 4), ('e', 5), ('f', 6)]
od = OrderedDict(pairs)
od.x = ['x']
od.z = ['z']
def check(dup):
msg = "\ncopy: %s\nod: %s" % (dup, od)
self.assertIsNot(dup, od, msg)
Expand All @@ -295,13 +297,27 @@ def check(dup):
self.assertEqual(len(dup), len(od))
self.assertEqual(type(dup), type(od))
check(od.copy())
check(copy.copy(od))
check(copy.deepcopy(od))
dup = copy.copy(od)
check(dup)
self.assertIs(dup.x, od.x)
self.assertIs(dup.z, od.z)
self.assertFalse(hasattr(dup, 'y'))
dup = copy.deepcopy(od)
check(dup)
self.assertEqual(dup.x, od.x)
self.assertIsNot(dup.x, od.x)
self.assertEqual(dup.z, od.z)
self.assertIsNot(dup.z, od.z)
self.assertFalse(hasattr(dup, 'y'))
# pickle directly pulls the module, so we have to fake it
with replaced_module('collections', self.module):
for proto in range(pickle.HIGHEST_PROTOCOL + 1):
with self.subTest(proto=proto):
check(pickle.loads(pickle.dumps(od, proto)))
dup = pickle.loads(pickle.dumps(od, proto))
check(dup)
self.assertEqual(dup.x, od.x)
self.assertEqual(dup.z, od.z)
self.assertFalse(hasattr(dup, 'y'))
check(eval(repr(od)))
update_test = OrderedDict()
update_test.update(od)
Expand Down Expand Up @@ -846,6 +862,23 @@ class OrderedDict(c_coll.OrderedDict):
pass


class PurePythonOrderedDictWithSlotsCopyingTests(unittest.TestCase):

module = py_coll
class OrderedDict(py_coll.OrderedDict):
__slots__ = ('x', 'y')
test_copying = OrderedDictTests.test_copying


@unittest.skipUnless(c_coll, 'requires the C version of the collections module')
class CPythonOrderedDictWithSlotsCopyingTests(unittest.TestCase):

module = c_coll
class OrderedDict(c_coll.OrderedDict):
__slots__ = ('x', 'y')
test_copying = OrderedDictTests.test_copying


class PurePythonGeneralMappingTests(mapping_tests.BasicTestMappingProtocol):

@classmethod
Expand Down
Loading

0 comments on commit 884eba3

Please sign in to comment.