From dc3658d6c22495fcc06bde5d94968f01854b052a Mon Sep 17 00:00:00 2001 From: Priyansh Date: Tue, 13 Oct 2026 22:21:25 +0000 Subject: [PATCH 1/2] fix: preserve slots declared on slotted attrs classes --- src/attr/_make.py | 15 +++++++++++++++ tests/test_slots.py | 18 ++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/src/attr/_make.py b/src/attr/_make.py index e5b3760c1..ec06fbe8b 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__", ())) + slot_names = [ + name + for name in own_slots + if name not in slot_names and name not in existing_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_slots: + if name in slot_names: + 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 From 8ce3ee7defe52d53e719ea6b989b8b24dcbe9959 Mon Sep 17 00:00:00 2001 From: Priyansh Date: Fri, 16 Oct 2026 08:13:00 +0000 Subject: [PATCH 2/2] fix: avoid an uncoverable branch when preserving own __slots__ Iterate the already-filtered list of the class's own slots instead of re-testing each own slot against the combined slot_names in the descriptor cleanup loop. The popped set is unchanged (a name is only in own_new_slots when it is added to slot_names), but the redundant membership test introduced a branch that is never taken for valid input, which the 100% branch-coverage gate rejects. --- changelog.d/1642.change.md | 1 + src/attr/_make.py | 10 +++++----- 2 files changed, 6 insertions(+), 5 deletions(-) create mode 100644 changelog.d/1642.change.md 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 ec06fbe8b..85036e0a0 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -950,17 +950,17 @@ def _create_slots_class(self): # 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__", ())) - slot_names = [ + own_new_slots = [ name for name in own_slots if name not in slot_names and name not in existing_slots - ] + slot_names + ] + 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_slots: - if name in slot_names: - cd.pop(name, None) + for name in own_new_slots: + cd.pop(name, None) # There are slots for attributes from current class # that are defined in parent classes.