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..2ab11fd28 100644 --- a/tests/test_slots.py +++ b/tests/test_slots.py @@ -104,6 +104,57 @@ 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_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 + + 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(): + """A declared slot matching an attrs field is not duplicated.""" + + @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