From 823fbf4e0eb66cbef0eacb7e8dbfb5dc8ea83b40 Mon Sep 17 00:00:00 2001 From: "Eric V. Smith" Date: Sat, 1 May 2021 13:27:30 -0400 Subject: If using a frozen class with slots, add __getstate__ and __setstate__ to set the instance values. (GH-25786) --- Lib/dataclasses.py | 23 +++++++++++++++++++++-- Lib/test/test_dataclasses.py | 13 +++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/Lib/dataclasses.py b/Lib/dataclasses.py index 5e57163..363d0b6 100644 --- a/Lib/dataclasses.py +++ b/Lib/dataclasses.py @@ -1087,14 +1087,28 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen, tuple(f.name for f in std_init_fields)) if slots: - cls = _add_slots(cls) + cls = _add_slots(cls, frozen) abc.update_abstractmethods(cls) return cls -def _add_slots(cls): +# _dataclass_getstate and _dataclass_setstate are needed for pickling frozen +# classes with slots. These could be slighly more performant if we generated +# the code instead of iterating over fields. But that can be a project for +# another day, if performance becomes an issue. +def _dataclass_getstate(self): + return [getattr(self, f.name) for f in fields(self)] + + +def _dataclass_setstate(self, state): + for field, value in zip(fields(self), state): + # use setattr because dataclass may be frozen + object.__setattr__(self, field.name, value) + + +def _add_slots(cls, is_frozen): # Need to create a new class, since we can't set __slots__ # after a class has been created. @@ -1120,6 +1134,11 @@ def _add_slots(cls): if qualname is not None: cls.__qualname__ = qualname + if is_frozen: + # Need this for pickling frozen classes with slots. + cls.__getstate__ = _dataclass_getstate + cls.__setstate__ = _dataclass_setstate + return cls diff --git a/Lib/test/test_dataclasses.py b/Lib/test/test_dataclasses.py index 2fa0ae0..16ee4c7 100644 --- a/Lib/test/test_dataclasses.py +++ b/Lib/test/test_dataclasses.py @@ -2833,6 +2833,19 @@ class TestSlots(unittest.TestCase): self.assertFalse(hasattr(A, "__slots__")) self.assertTrue(hasattr(B, "__slots__")) + # Can't be local to test_frozen_pickle. + @dataclass(frozen=True, slots=True) + class FrozenSlotsClass: + foo: str + bar: int + + def test_frozen_pickle(self): + # bpo-43999 + + assert self.FrozenSlotsClass.__slots__ == ("foo", "bar") + p = pickle.dumps(self.FrozenSlotsClass("a", 1)) + assert pickle.loads(p) == self.FrozenSlotsClass("a", 1) + class TestDescriptors(unittest.TestCase): def test_set_name(self): -- cgit v0.12