From 8bfb9f1f3aaa2b561651445659d3c2dbdf0a46ff Mon Sep 17 00:00:00 2001 From: Zachary Spector Date: Fri, 20 Feb 2026 20:54:37 +1300 Subject: [PATCH 1/7] Generate `__slots__` dictionary with Sphinx-friendly docstrings I have not worked out how to make the annotation really show up in generated Sphinx output. At least the docstring is there. --- src/attr/_make.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/attr/_make.py b/src/attr/_make.py index 32e42976e..5b240ae25 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -10,6 +10,7 @@ import itertools import linecache import sys +import textwrap import types import unicodedata import weakref @@ -893,14 +894,14 @@ def _create_slots_class(self): base_names = set(self._base_names) - names = self._attr_names + names = dict.fromkeys(self._attr_names) if ( self._weakref_slot and "__weakref__" not in getattr(self._cls, "__slots__", ()) and "__weakref__" not in names and not weakref_inherited ): - names += ("__weakref__",) + names["__weakref__"] = "" cached_properties = { name: cached_prop.func @@ -915,13 +916,18 @@ def _create_slots_class(self): class_annotations = _get_annotations(self._cls) for name, func in cached_properties.items(): # Add cached properties to names for slotting. - names += (name,) # Clear out function from class to avoid clashing. del cd[name] additional_closure_functions_to_update.append(func) annotation = inspect.signature(func).return_annotation if annotation is not inspect.Parameter.empty: class_annotations[name] = annotation + doclines = [f" :type: {annotation}"] + else: + doclines = [] + if func.__doc__ is not None: + doclines.extend(textwrap.indent(textwrap.dedent(func.__doc__), " ").splitlines()) + names[name] = "\n".join(doclines) original_getattr = cd.get("__getattr__") if original_getattr is not None: @@ -949,7 +955,7 @@ def _create_slots_class(self): if self._cache_hash: slot_names.append(_HASH_CACHE_FIELD) - cd["__slots__"] = tuple(slot_names) + cd["__slots__"] = {slot: names.get(slot) for slot in slot_names} cd["__qualname__"] = self._cls.__qualname__ From 247dd6680c2a13eea746983945a53906736adba5 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 20 Feb 2026 08:15:37 +0000 Subject: [PATCH 2/7] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/attr/_make.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/attr/_make.py b/src/attr/_make.py index 5b240ae25..f2af5a2e8 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -926,7 +926,11 @@ def _create_slots_class(self): else: doclines = [] if func.__doc__ is not None: - doclines.extend(textwrap.indent(textwrap.dedent(func.__doc__), " ").splitlines()) + doclines.extend( + textwrap.indent( + textwrap.dedent(func.__doc__), " " + ).splitlines() + ) names[name] = "\n".join(doclines) original_getattr = cd.get("__getattr__") From 54adf3c40d2c18e221190badd4d95529ea4647d5 Mon Sep 17 00:00:00 2001 From: Zachary Spector Date: Fri, 20 Feb 2026 21:49:55 +1300 Subject: [PATCH 3/7] Improve the display of the type annotations They still don't say they're `property`s --- src/attr/_make.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/attr/_make.py b/src/attr/_make.py index 5b240ae25..6aa5d00ad 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -922,7 +922,7 @@ def _create_slots_class(self): annotation = inspect.signature(func).return_annotation if annotation is not inspect.Parameter.empty: class_annotations[name] = annotation - doclines = [f" :type: {annotation}"] + doclines = [f" :type:`{annotation}`", ""] else: doclines = [] if func.__doc__ is not None: From e060431f9062b6e13c2ad7b63964cf0ba96decc3 Mon Sep 17 00:00:00 2001 From: Zachary Spector Date: Fri, 20 Feb 2026 22:09:50 +1300 Subject: [PATCH 4/7] Remove some pointless indentation --- src/attr/_make.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/attr/_make.py b/src/attr/_make.py index 22db01e83..7f642cc15 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -922,14 +922,12 @@ def _create_slots_class(self): annotation = inspect.signature(func).return_annotation if annotation is not inspect.Parameter.empty: class_annotations[name] = annotation - doclines = [f" :type:`{annotation}`", ""] + doclines = [f":type:`{annotation}`", ""] else: doclines = [] if func.__doc__ is not None: doclines.extend( - textwrap.indent( - textwrap.dedent(func.__doc__), " " - ).splitlines() + textwrap.dedent(func.__doc__).splitlines() ) names[name] = "\n".join(doclines) From e42576403fdd665571134f335228aa00a2a8f7e9 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 20 Feb 2026 09:10:05 +0000 Subject: [PATCH 5/7] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/attr/_make.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/attr/_make.py b/src/attr/_make.py index 7f642cc15..9c84546e8 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -926,9 +926,7 @@ def _create_slots_class(self): else: doclines = [] if func.__doc__ is not None: - doclines.extend( - textwrap.dedent(func.__doc__).splitlines() - ) + doclines.extend(textwrap.dedent(func.__doc__).splitlines()) names[name] = "\n".join(doclines) original_getattr = cd.get("__getattr__") From 5fd6218faac972a3afb0bd855aa445fe0888544d Mon Sep 17 00:00:00 2001 From: Zachary Spector Date: Sat, 21 Feb 2026 09:38:51 +1300 Subject: [PATCH 6/7] Fix a couple tests that assumed tuple __slots__ --- tests/test_slots.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_slots.py b/tests/test_slots.py index a74c32b03..d9be34066 100644 --- a/tests/test_slots.py +++ b/tests/test_slots.py @@ -1034,7 +1034,7 @@ def f(self): return self.x * 2 assert B(1).f == 2 - assert B.__slots__ == () + assert list(B.__slots__) == [] def test_slots_sub_class_with_actual_slot(): @@ -1055,7 +1055,7 @@ class B(A): f: int = attr.ib() assert B(1, 2).f == 2 - assert B.__slots__ == () + assert list(B.__slots__) == [] def test_slots_cached_property_is_not_called_at_construction(): From e0df48411de2c6f0dcb91295805339e06d6d493e Mon Sep 17 00:00:00 2001 From: Zachary Spector Date: Sat, 21 Feb 2026 10:02:18 +1300 Subject: [PATCH 7/7] Perform a very silly hack to make Sphinx document `@cached_property` The property objects do nothing, because they're not accessible from the instance -- slotted instances have no __dict__, but their *class object* does have __dict__, and that's the __dict__ that Sphinx looks at to find class members to document. --- src/attr/_make.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/attr/_make.py b/src/attr/_make.py index 9c84546e8..c8e357bf6 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -922,9 +922,7 @@ def _create_slots_class(self): annotation = inspect.signature(func).return_annotation if annotation is not inspect.Parameter.empty: class_annotations[name] = annotation - doclines = [f":type:`{annotation}`", ""] - else: - doclines = [] + doclines = [] if func.__doc__ is not None: doclines.extend(textwrap.dedent(func.__doc__).splitlines()) names[name] = "\n".join(doclines) @@ -993,6 +991,10 @@ def _create_slots_class(self): else: if match: cell.cell_contents = cls + # Add the cached properties back to the __dict__ again -- + # they won't be used, not being in the *instance* __dict__, but + # Sphinx checks __dict__ directly, and will therefore see these. + cd.update(cached_properties) return cls def add_repr(self, ns):