diff --git a/changelog.d/1642.change.md b/changelog.d/1642.change.md new file mode 100644 index 000000000..6b36dd0b6 --- /dev/null +++ b/changelog.d/1642.change.md @@ -0,0 +1 @@ +Fixed `@attrs.define` dropping slots declared on the slotted class itself when the attribute set is rewritten. diff --git a/src/attr/_make.py b/src/attr/_make.py index e5b3760c1..85036e0a0 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -947,6 +947,21 @@ def _create_slots_class(self): # Setting __slots__ to inherited attributes wastes memory. slot_names = [name for name in names if name not in base_names] + # Preserve slots declared on the class itself: they would otherwise + # be lost when we rewrite `__slots__` below. + own_slots = tuple(self._cls.__dict__.get("__slots__", ())) + own_new_slots = [ + name + for name in own_slots + if name not in slot_names and name not in existing_slots + ] + slot_names = own_new_slots + slot_names + # Remove stale descriptors for the class's own slots from the new + # class dict -- the type machinery recreates them from `__slots__` + # and keeping both raises ValueError on class creation. + for name in own_new_slots: + cd.pop(name, None) + # There are slots for attributes from current class # that are defined in parent classes. # As their descriptors may be overridden by a child class, diff --git a/tests/test_slots.py b/tests/test_slots.py index a74c32b03..12bb49cf6 100644 --- a/tests/test_slots.py +++ b/tests/test_slots.py @@ -282,6 +282,24 @@ class C2(C1): assert {"x": 1, "y": 2, "z": "test"} == attr.asdict(c2) +def test_own_slots_preserved(): + """ + Slots declared on the slotted attrs class itself are preserved. + """ + + @attrs.define + class A: + __slots__ = ("private",) + + public: str = "foo" + + a = A() + a.private = 1 + assert 1 == a.private + assert "foo" == a.public + assert "private" in A.__slots__ + + def test_inheritance_from_slots_with_attribute_override(): """ Inheriting from a slotted class doesn't re-create existing slots