diff --git a/src/attr/_make.py b/src/attr/_make.py index bc296c648..bd666cc35 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -922,7 +922,7 @@ def slots_getstate(self): """ Automatically created by attrs. """ - return tuple(getattr(self, name) for name in state_attr_names) + return {name: getattr(self, name) for name in state_attr_names} hash_caching_enabled = self._cache_hash @@ -931,8 +931,9 @@ def slots_setstate(self, state): Automatically created by attrs. """ __bound_setattr = _obj_setattr.__get__(self) - for name, value in zip(state_attr_names, state): - __bound_setattr(name, value) + for name in state_attr_names: + if name in state: + __bound_setattr(name, state[name]) # The hash code cache is not included when the object is # serialized, but it still needs to be initialized to None to diff --git a/tests/test_slots.py b/tests/test_slots.py index 6a1776440..e54fe5a1b 100644 --- a/tests/test_slots.py +++ b/tests/test_slots.py @@ -9,6 +9,8 @@ import types import weakref +from unittest import mock + import pytest import attr @@ -743,3 +745,32 @@ def f(self): assert B(11).f == 121 assert B(17).f == 289 + + +@attr.s(slots=True) +class A: + x = attr.ib() + b = attr.ib() + c = attr.ib() + + +@pytest.mark.parametrize("cls", [A]) +def test_slots_unpickle_after_change(cls): + """ + We don't assign properties we don't have anymore if the class has changed. + """ + a = cls(1, 2, 3) + a_pickled = pickle.dumps(a) + a_unpickled = pickle.loads(a_pickled) + assert a_unpickled == a + + @attr.s(slots=True) + class NEW_A: + x = attr.ib() + c = attr.ib() + + with mock.patch(f"{__name__}.A", NEW_A): + new_a = pickle.loads(a_pickled) + assert new_a.x == 1 + assert new_a.c == 3 + assert not hasattr(new_a, "b")