Skip to content

Commit 71afa44

Browse files
committed
[3.9] bpo-45520: Backport __getstate__, __setstate__ methods from 3.10 into 3.9 (GH-25786)
1 parent 216c040 commit 71afa44

File tree

3 files changed

+47
-0
lines changed

3 files changed

+47
-0
lines changed

Lib/dataclasses.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -977,6 +977,10 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen):
977977
raise TypeError(f'Cannot overwrite attribute {fn.__name__} '
978978
f'in class {cls.__name__}')
979979

980+
# Need this for pickling frozen classes.
981+
cls.__getstate__ = _dataclass_getstate
982+
cls.__setstate__ = _dataclass_setstate
983+
980984
# Decide if/how we're going to create a hash function.
981985
hash_action = _hash_action[bool(unsafe_hash),
982986
bool(eq),
@@ -995,6 +999,21 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen):
995999
return cls
9961000

9971001

1002+
# _dataclass_getstate and _dataclass_setstate are needed for pickling frozen
1003+
# classes (maybe even with slots).
1004+
# These could be slightly more performant if we generated
1005+
# the code instead of iterating over fields. But that can be a project for
1006+
# another day, if performance becomes an issue.
1007+
def _dataclass_getstate(self):
1008+
return [getattr(self, f.name) for f in fields(self)]
1009+
1010+
1011+
def _dataclass_setstate(self, state):
1012+
for field, value in zip(fields(self), state):
1013+
# use setattr because dataclass may be frozen
1014+
object.__setattr__(self, field.name, value)
1015+
1016+
9981017
def dataclass(cls=None, /, *, init=True, repr=True, eq=True, order=False,
9991018
unsafe_hash=False, frozen=False):
10001019
"""Returns the same class as was passed in, with dunder methods

Lib/test/test_dataclasses.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2775,6 +2775,31 @@ class Derived(Base):
27752775
# We can add a new field to the derived instance.
27762776
d.z = 10
27772777

2778+
# Can't be local to test_frozen_pickle.
2779+
@dataclass(frozen=True)
2780+
class FrozenSlotsClass:
2781+
__slots__ = ('foo', 'bar')
2782+
foo: str
2783+
bar: int
2784+
2785+
@dataclass(frozen=True)
2786+
class FrozenWithoutSlotsClass:
2787+
foo: str
2788+
bar: int
2789+
2790+
def test_frozen_pickle(self):
2791+
# bpo-43999
2792+
# bpo-45520
2793+
2794+
assert self.FrozenSlotsClass.__slots__ == ("foo", "bar")
2795+
for proto in range(pickle.HIGHEST_PROTOCOL + 1):
2796+
with self.subTest(proto=proto):
2797+
p = pickle.dumps(self.FrozenSlotsClass("a", 1), protocol=proto)
2798+
assert pickle.loads(p) == self.FrozenSlotsClass("a", 1)
2799+
2800+
p = pickle.dumps(self.FrozenWithoutSlotsClass("a", 1), protocol=proto)
2801+
assert pickle.loads(p) == self.FrozenWithoutSlotsClass("a", 1)
2802+
27782803
class TestDescriptors(unittest.TestCase):
27792804
def test_set_name(self):
27802805
# See bpo-33141.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fix ``__getstate__`` and ``__setstate__`` methods for frozen
2+
``dataclasses``. It basically backports a part of 3.10's code to support
3+
``pickle`` and ``deepcopy``.

0 commit comments

Comments
 (0)