From 1e0115eefe4140e305bed4a5dafd9330627ea2ea Mon Sep 17 00:00:00 2001 From: Emad Date: Sat, 17 Oct 2026 23:36:14 +0300 Subject: [PATCH 1/3] Preserve user-defined slots on attrs classes --- src/attr/_make.py | 11 ++++++++++ tests/test_slots.py | 51 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+) diff --git a/src/attr/_make.py b/src/attr/_make.py index e5b3760c1..c12c9b8c1 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -946,7 +946,18 @@ 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: + if slot in slot_names: + 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..85dabc35c 100644 --- a/tests/test_slots.py +++ b/tests/test_slots.py @@ -282,6 +282,57 @@ 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__ = ("private",) + public: str = "foo" + + a = A() + a.private = 1 + + assert "private" in A.__slots__ + assert a.private == 1 + + +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_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__ = ("private",) + public: str = "foo" + + child = Child() + child.parent_slot = 1 + child.private = 2 + + assert "private" in Child.__slots__ + assert "parent_slot" not in Child.__slots__ + assert child.private == 2 + assert child.parent_slot == 1 + + def test_inheritance_from_slots_with_attribute_override(): """ Inheriting from a slotted class doesn't re-create existing slots From a626b9f294495ba05f4dde414a14805703c61ab6 Mon Sep 17 00:00:00 2001 From: Emad Date: Sun, 18 Oct 2026 03:22:50 +0300 Subject: [PATCH 2/3] test: expand coverage for user-defined slots --- tests/test_slots.py | 47 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 45 insertions(+), 2 deletions(-) diff --git a/tests/test_slots.py b/tests/test_slots.py index 85dabc35c..5c4a3326b 100644 --- a/tests/test_slots.py +++ b/tests/test_slots.py @@ -287,14 +287,17 @@ def test_slots_declared_on_attrs_class_are_preserved(): @attrs.define class A: - __slots__ = ("private",) + __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(): @@ -312,6 +315,22 @@ class A: 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.""" @@ -320,17 +339,41 @@ class Parent: @attrs.define class Child(Parent): - __slots__ = ("private",) + __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_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(): From 2f306322efe6d8f3b5f7ba6f106ad689d55ae50d Mon Sep 17 00:00:00 2001 From: Emad Date: Sun, 18 Oct 2026 14:49:29 +0300 Subject: [PATCH 3/3] fix: simplify user-defined slot cleanup --- src/attr/_make.py | 3 +-- tests/test_slots.py | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/attr/_make.py b/src/attr/_make.py index c12c9b8c1..1c633d901 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -956,8 +956,7 @@ def _create_slots_class(self): slot_names.append(slot) for slot in original_slots: - if slot in slot_names: - cd.pop(slot, None) + 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 5c4a3326b..953916cb4 100644 --- a/tests/test_slots.py +++ b/tests/test_slots.py @@ -355,6 +355,28 @@ class Child(Parent): 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."""