|
3 | 3 | from __future__ import annotations |
4 | 4 |
|
5 | 5 | import pathlib |
| 6 | +import re |
6 | 7 | import sys |
7 | 8 | import typing as t |
8 | 9 |
|
|
54 | 55 |
|
55 | 56 | _gp_setup = conf.pop("setup") |
56 | 57 |
|
| 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 | + |
57 | 129 |
|
58 | 130 | def setup(app: Sphinx) -> None: |
59 | 131 | """Configure Sphinx app hooks and register vcspull-specific lexers.""" |
60 | 132 | _gp_setup(app) |
| 133 | + app.connect("autodoc-skip-member", _skip_documented_namedtuple_fields) |
61 | 134 |
|
62 | 135 | from vcspull_console_lexer import VcspullConsoleLexer |
63 | 136 | from vcspull_output_lexer import VcspullOutputLexer |
|
0 commit comments