Skip to content

Commit c7d3bdf

Browse files
committed
docs(conf) Stop double-documenting NamedTuple fields
why: a NumPy Attributes section on a NamedTuple is documented twice -- once as an .. attribute:: block from the docstring, and again by autodoc, whose "Alias for field number N" boilerplate counts as a real docstring and so survives regardless of undoc-members. The Python domain warns on each duplicate, and the warning is untyped, so suppress_warnings cannot reach it. what: - Skip a NamedTuple field member when the class docstring already documents it in an Attributes section, leaving the prose and type from the docstring as the sole rendering - Leave fields no Attributes section documents exactly as they were
1 parent e2375d6 commit c7d3bdf

1 file changed

Lines changed: 73 additions & 0 deletions

File tree

docs/conf.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from __future__ import annotations
44

55
import pathlib
6+
import re
67
import sys
78
import typing as t
89

@@ -54,10 +55,82 @@
5455

5556
_gp_setup = conf.pop("setup")
5657

58+
_NUMPY_UNDERLINE = re.compile(r"^\s*-{2,}\s*$")
59+
60+
61+
def _numpy_attribute_names(doc: str | None) -> frozenset[str]:
62+
"""Return field names a NumPy ``Attributes`` section of *doc* documents."""
63+
if not doc:
64+
return frozenset()
65+
66+
lines = doc.expandtabs().splitlines()
67+
names: set[str] = set()
68+
index = 0
69+
while index + 1 < len(lines):
70+
heading = lines[index].strip() == "Attributes"
71+
if not (heading and _NUMPY_UNDERLINE.match(lines[index + 1])):
72+
index += 1
73+
continue
74+
75+
indent = len(lines[index]) - len(lines[index].lstrip())
76+
cursor = index + 2
77+
while cursor < len(lines):
78+
entry = lines[cursor]
79+
if not entry.strip():
80+
cursor += 1
81+
continue
82+
entry_indent = len(entry) - len(entry.lstrip())
83+
if entry_indent < indent:
84+
break
85+
if entry_indent == indent:
86+
# The next NumPy section header is underlined; stop before it.
87+
if cursor + 1 < len(lines) and _NUMPY_UNDERLINE.match(
88+
lines[cursor + 1]
89+
):
90+
break
91+
names.add(entry.split(":", 1)[0].strip())
92+
cursor += 1
93+
index = cursor
94+
95+
return frozenset(names)
96+
97+
98+
def _skip_documented_namedtuple_fields(
99+
app: Sphinx,
100+
what: str,
101+
name: str,
102+
obj: object,
103+
skip: bool,
104+
options: object,
105+
) -> bool | None:
106+
"""Drop NamedTuple field stubs the class docstring already documents.
107+
108+
``typing.NamedTuple`` fields are descriptors whose ``__doc__`` is
109+
``"Alias for field number N"``. Autodoc counts that boilerplate as a real
110+
docstring, so the field is documented no matter how ``undoc-members`` is
111+
set. When the class docstring carries a NumPy ``Attributes`` section, the
112+
docstring preprocessor has already emitted an ``.. attribute::`` block for
113+
the same dotted name, and the Python domain warns about the duplicate.
114+
"""
115+
if skip or what != "class":
116+
return None
117+
118+
current = app.env.current_document
119+
module = sys.modules.get(getattr(current, "autodoc_module", "") or "")
120+
owner_name = (getattr(current, "autodoc_class", "") or "").partition(".")[0]
121+
owner = getattr(module, owner_name, None)
122+
123+
fields = getattr(owner, "_fields", None)
124+
if not isinstance(fields, tuple) or name not in fields:
125+
return None
126+
127+
return name in _numpy_attribute_names(owner.__doc__)
128+
57129

58130
def setup(app: Sphinx) -> None:
59131
"""Configure Sphinx app hooks and register vcspull-specific lexers."""
60132
_gp_setup(app)
133+
app.connect("autodoc-skip-member", _skip_documented_namedtuple_fields)
61134

62135
from vcspull_console_lexer import VcspullConsoleLexer
63136
from vcspull_output_lexer import VcspullOutputLexer

0 commit comments

Comments
 (0)