diff --git a/src/attr/_make.py b/src/attr/_make.py index e5b3760c1..1c633d901 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -946,7 +946,17 @@ def _create_slots_class(self): # We only add the names of attributes that aren't inherited. # Setting __slots__ to inherited attributes wastes memory. slot_names = [name for name in names if name not in base_names] + original_slots = self._cls_dict.get("__slots__", ()) + if isinstance(original_slots, str): + original_slots = (original_slots,) + + for slot in original_slots: + if slot not in slot_names and slot not in base_names: + slot_names.append(slot) + + for slot in original_slots: + cd.pop(slot, 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..953916cb4 100644 --- a/tests/test_slots.py +++ b/tests/test_slots.py @@ -282,6 +282,122 @@ class C2(C1): assert {"x": 1, "y": 2, "z": "test"} == attr.asdict(c2) +def test_slots_declared_on_attrs_class_are_preserved(): + """User-defined slots are preserved on attrs classes.""" + + @attrs.define + class A: + __slots__ = ("another_private", "private") + public: str = "foo" + + a = A() + a.private = 1 + a.another_private = 2 + + assert "private" in A.__slots__ + assert "another_private" in A.__slots__ + assert a.private == 1 + assert a.another_private == 2 + + +def test_string_slot_declared_on_attrs_class_is_preserved(): + """String-based user-defined slots are preserved.""" + + @attrs.define + class A: + __slots__ = "private" # noqa: PLC0205 + public: str = "foo" + + a = A() + a.private = 1 + + assert "private" in A.__slots__ + assert a.private == 1 + + +def test_user_defined_weakref_slot_is_preserved(): + """A user-defined __weakref__ slot is preserved.""" + + @attrs.define + class A: + __slots__ = ("__weakref__", "private") + public: str = "foo" + + a = A() + a.private = 1 + + assert "__weakref__" in A.__slots__ + assert "private" in A.__slots__ + assert a.private == 1 + + +def test_user_defined_slots_are_preserved_with_inherited_slots(): + """User-defined slots are preserved alongside inherited slots.""" + + class Parent: + __slots__ = ("parent_slot",) + + @attrs.define + class Child(Parent): + __slots__ = ("another_private", "private") + public: str = "foo" + + child = Child() + child.parent_slot = 1 + child.private = 2 + child.another_private = 3 + + assert "private" in Child.__slots__ + assert "another_private" in Child.__slots__ + assert "parent_slot" not in Child.__slots__ + assert child.private == 2 + assert child.another_private == 3 + assert child.parent_slot == 1 + + +def test_user_defined_slots_already_exist_in_attrs_base_class(): + """User-defined slots do not duplicate slots inherited from attrs.""" + + @attrs.define + class Parent: + parent_slot: int = 0 + + @attrs.define + class Child(Parent): + __slots__ = ("parent_slot", "private") + public: str = "foo" + + child = Child() + child.parent_slot = 1 + child.private = 2 + + assert "parent_slot" not in Child.__slots__ + assert "private" in Child.__slots__ + assert child.parent_slot == 1 + assert child.private == 2 + + +def test_user_defined_slots_already_exist_in_base_class(): + """User-defined slots already present in a base are not duplicated.""" + + class Parent: + __slots__ = ("parent_slot",) + + @attrs.define + class Child(Parent): + __slots__ = ("parent_slot", "private") + public: str = "foo" + + child = Child() + child.parent_slot = 1 + child.private = 2 + + assert "parent_slot" not in Child.__slots__ + assert "private" in Child.__slots__ + assert child.parent_slot == 1 + assert child.private == 2 + + def test_inheritance_from_slots_with_attribute_override(): """ Inheriting from a slotted class doesn't re-create existing slots