From 2751a66ea88aff146136db11e91b6fdf6476c909 Mon Sep 17 00:00:00 2001 From: Samuel-Hills Date: Fri, 2 Oct 2026 21:12:24 -0400 Subject: [PATCH 1/4] fix: preserve explicitly declared slots --- changelog.d/1637.change.md | 1 + src/attr/_make.py | 18 ++++++++++++++++-- tests/test_slots.py | 17 +++++++++++++++++ 3 files changed, 34 insertions(+), 2 deletions(-) create mode 100644 changelog.d/1637.change.md diff --git a/changelog.d/1637.change.md b/changelog.d/1637.change.md new file mode 100644 index 000000000..9c530dfe0 --- /dev/null +++ b/changelog.d/1637.change.md @@ -0,0 +1 @@ +Preserve slots declared directly on a class decorated with `@attrs.define` or `@attr.s`. diff --git a/src/attr/_make.py b/src/attr/_make.py index afbca4635..8404166d4 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -860,10 +860,22 @@ def _create_slots_class(self): """ Build and return a new class with a `__slots__` attribute. """ + own_slots = self._cls.__dict__.get("__slots__", ()) + if isinstance(own_slots, str): + own_slots = (own_slots,) + else: + own_slots = tuple(own_slots) + cd = { k: v for k, v in self._cls_dict.items() - if k not in (*tuple(self._attr_names), "__dict__", "__weakref__") + if k + not in ( + *tuple(self._attr_names), + *own_slots, + "__dict__", + "__weakref__", + ) } # 3.14.0rc2+ @@ -903,7 +915,9 @@ def _create_slots_class(self): base_names = set(self._base_names) - names = self._attr_names + names = self._attr_names + tuple( + name for name in own_slots if name not in self._attr_names + ) if ( self._weakref_slot and "__weakref__" not in getattr(self._cls, "__slots__", ()) diff --git a/tests/test_slots.py b/tests/test_slots.py index a74c32b03..07a577d49 100644 --- a/tests/test_slots.py +++ b/tests/test_slots.py @@ -104,6 +104,23 @@ def test_slots_being_used(): assert attr.asdict(slot_instance) == attr.asdict(non_slot_instance) +def test_explicit_slots_are_preserved(): + """Slots declared on the class are retained when attrs creates the class.""" + + @attrs.define + class ExplicitSlots: + __slots__ = ("private",) + + public: str = "foo" + + instance = ExplicitSlots() + instance.private = 1 + + assert instance.private == 1 + assert instance.public == "foo" + assert {"__weakref__", "private", "public"} == set(ExplicitSlots.__slots__) + + def test_slots_base_of_slotted(): """ The (hopefully gc'ed) temporary base class of a slotted class contains a From a514fb9d7b4c58d524396eccd3066b5d68ad8ed3 Mon Sep 17 00:00:00 2001 From: Samuel-Hills Date: Fri, 23 Oct 2026 07:24:07 +0200 Subject: [PATCH 2/4] test: cover declared slot variants --- tests/test_slots.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/tests/test_slots.py b/tests/test_slots.py index 07a577d49..0a3a13011 100644 --- a/tests/test_slots.py +++ b/tests/test_slots.py @@ -121,6 +121,36 @@ class ExplicitSlots: assert {"__weakref__", "private", "public"} == set(ExplicitSlots.__slots__) +def test_explicit_string_slot_is_preserved(): + @attrs.define + class ExplicitStringSlot: + __slots__ = "private" + + public: str = "foo" + + instance = ExplicitStringSlot() + instance.private = 1 + + assert instance.private == 1 + assert instance.public == "foo" + assert {"__weakref__", "private", "public"} == set( + ExplicitStringSlot.__slots__ + ) + + +def test_explicit_slot_for_attribute_is_preserved(): + @attrs.define + class ExplicitAttributeSlot: + __slots__ = ("public",) + + public: str + + instance = ExplicitAttributeSlot("foo") + + assert instance.public == "foo" + assert {"__weakref__", "public"} == set(ExplicitAttributeSlot.__slots__) + + def test_slots_base_of_slotted(): """ The (hopefully gc'ed) temporary base class of a slotted class contains a From e80642262f067b61486008792e56381299615041 Mon Sep 17 00:00:00 2001 From: Samuel-Hills Date: Fri, 23 Oct 2026 07:27:19 +0200 Subject: [PATCH 3/4] test: satisfy lint for string slots case --- tests/test_slots.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_slots.py b/tests/test_slots.py index 0a3a13011..70037a2e2 100644 --- a/tests/test_slots.py +++ b/tests/test_slots.py @@ -124,7 +124,7 @@ class ExplicitSlots: def test_explicit_string_slot_is_preserved(): @attrs.define class ExplicitStringSlot: - __slots__ = "private" + __slots__ = "private" # noqa: PLC0205 - exercise the supported string form public: str = "foo" From d96c51fef05df5e6715d7c1b4f6afec4491e449e Mon Sep 17 00:00:00 2001 From: Samuel-Hills Date: Fri, 23 Oct 2026 07:32:24 +0200 Subject: [PATCH 4/4] test: document declared slot coverage --- tests/test_slots.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/test_slots.py b/tests/test_slots.py index 70037a2e2..2ab11fd28 100644 --- a/tests/test_slots.py +++ b/tests/test_slots.py @@ -122,6 +122,8 @@ class ExplicitSlots: def test_explicit_string_slot_is_preserved(): + """A single string slot declaration survives attrs class rebuilding.""" + @attrs.define class ExplicitStringSlot: __slots__ = "private" # noqa: PLC0205 - exercise the supported string form @@ -139,6 +141,8 @@ class ExplicitStringSlot: def test_explicit_slot_for_attribute_is_preserved(): + """A declared slot matching an attrs field is not duplicated.""" + @attrs.define class ExplicitAttributeSlot: __slots__ = ("public",)