diff --git a/CHANGES b/CHANGES index 32a5ad2b..4a32eccc 100644 --- a/CHANGES +++ b/CHANGES @@ -18,6 +18,23 @@ $ uv add gp-sphinx --prerelease allow +### What's new + +#### Hide `+HIDE` doctest setup from rendered docs + +Doctest lines tagged `# doctest: +HIDE` still execute as tests but are +dropped from the rendered autodoc output, keeping incidental setup — an +`env` mapping, a socket path — out of the published example. (#65) + +### Fixes + +#### `sphinx-autodoc-api-style`: Modifier no longer renders twice + +Members marked `classmethod`, `staticmethod`, `async`, or `abstract` +showed the modifier twice — once as a signature prefix, once as the +badge beside it. The redundant prefix is dropped, so each modifier +appears only in its badge. (#65) + ## gp-sphinx 0.0.1a33 (2026-07-05) ### What's new diff --git a/docs/packages/sphinx-autodoc-typehints-gp/how-to.md b/docs/packages/sphinx-autodoc-typehints-gp/how-to.md index 6527dbe4..24a1e43f 100644 --- a/docs/packages/sphinx-autodoc-typehints-gp/how-to.md +++ b/docs/packages/sphinx-autodoc-typehints-gp/how-to.md @@ -30,9 +30,39 @@ and skips its own plain-text duplicates — cooperation, not conflict. - Resolves type hints statically without `exec()` or {py:func}`typing.get_type_hints`. - Works with `TYPE_CHECKING` blocks because annotations are stringified at Sphinx build time. - No text-level race conditions with Napoleon. +- Hides incidental doctest setup marked `# doctest: +HIDE` from rendered + docstrings, so plumbing can execute without cluttering the example. - Exposes reusable helpers for annotation display classification and rendered type paragraphs used by the other autodoc packages. +## Hiding incidental doctest setup + +A docstring example often needs plumbing to run — building an environment +mapping, opening a socket path — that means nothing to the reader. Mark the +setup line with `# doctest: +HIDE` and this extension drops it from the +rendered docstring, together with any `...` continuation lines, leaving the +meaningful call and its output in place: + +```python +def connect(url: str) -> Connection: + """Open a connection to ``url``. + + >>> socket = "/run/gp/app.sock" # doctest: +HIDE + >>> connect("unix://" + socket) + + """ +``` + +The rendered page shows only the `connect(...)` call and its result; the +`socket` line is gone. Nothing rewrites the source docstring — the strip runs +at Sphinx build time, on the `autodoc-process-docstring` event — so the example +your doctest runner executes is unchanged. + +Because `# doctest: +HIDE` is a doctest optionflag, the runner has to recognize +it: register it once with `doctest.register_optionflag("HIDE")`, or an +unregistered flag raises `ValueError: invalid option '+HIDE'` when the +docstring runs. + ## Shared layer `sphinx_autodoc_typehints_gp` serves as the shared internal annotation normalization diff --git a/packages/sphinx-autodoc-api-style/src/sphinx_autodoc_api_style/_transforms.py b/packages/sphinx-autodoc-api-style/src/sphinx_autodoc_api_style/_transforms.py index 5cc5736e..913b9442 100644 --- a/packages/sphinx-autodoc-api-style/src/sphinx_autodoc_api_style/_transforms.py +++ b/packages/sphinx-autodoc-api-style/src/sphinx_autodoc_api_style/_transforms.py @@ -52,6 +52,10 @@ "final": "final", } +# Signature-prefix keywords the Python domain emits that a modifier badge now +# conveys. Keyed off _KEYWORD_TO_MOD so detection and stripping never drift. +_REDUNDANT_PREFIX_KEYWORDS: frozenset[str] = frozenset(_KEYWORD_TO_MOD) + def _detect_modifiers(sig_node: addnodes.desc_signature) -> frozenset[str]: """Detect modifier keywords from a signature node's annotations. @@ -131,6 +135,45 @@ def _detect_deprecated(desc_node: addnodes.desc) -> bool: return False +def _strip_redundant_prefix(sig_node: addnodes.desc_signature) -> None: + """Remove leading modifier keywords already shown as a badge. + + The Python domain renders ``classmethod``/``staticmethod``/``async``/ + ``abstractmethod``/``final`` as a ``desc_sig_keyword`` inside the signature's + leading ``desc_annotation``. The badge group conveys the same modifier, so + the prefix keyword duplicates it. Remove each such keyword and its trailing + space; drop the annotation if it empties out. Type-word prefixes + (``property``/``type``) are left alone. + + Examples + -------- + >>> from sphinx import addnodes + >>> sig = addnodes.desc_signature() + >>> ann = addnodes.desc_annotation() + >>> ann += addnodes.desc_sig_keyword("", "classmethod") + >>> ann += addnodes.desc_sig_space() + >>> sig += ann + >>> sig += addnodes.desc_name("", "from_env") + >>> _strip_redundant_prefix(sig) + >>> any(isinstance(n, addnodes.desc_annotation) for n in sig.children) + False + """ + for ann in list(sig_node.findall(addnodes.desc_annotation)): + removed = False + for kw in list(ann.findall(addnodes.desc_sig_keyword)): + if kw.astext().strip() not in _REDUNDANT_PREFIX_KEYWORDS: + continue + idx = kw.parent.index(kw) + if idx + 1 < len(kw.parent) and isinstance( + kw.parent[idx + 1], addnodes.desc_sig_space + ): + kw.parent.pop(idx + 1) + kw.parent.remove(kw) + removed = True + if removed and not ann.astext().strip() and ann.parent is not None: + ann.parent.remove(ann) + + def _inject_badges(sig_node: addnodes.desc_signature, objtype: str) -> None: """Inject structured layout slots containing badges and source links. @@ -163,6 +206,8 @@ def _inject_badges(sig_node: addnodes.desc_signature, objtype: str) -> None: marker_attr="sab_badges_injected", badge_node=badge_group, ) + # The badge now conveys the modifier; drop the duplicate signature prefix. + _strip_redundant_prefix(sig_node) def _prune_empty_desc_content(desc_node: addnodes.desc) -> None: diff --git a/packages/sphinx-autodoc-typehints-gp/src/sphinx_autodoc_typehints_gp/extension.py b/packages/sphinx-autodoc-typehints-gp/src/sphinx_autodoc_typehints_gp/extension.py index 3b128335..363a6759 100644 --- a/packages/sphinx-autodoc-typehints-gp/src/sphinx_autodoc_typehints_gp/extension.py +++ b/packages/sphinx-autodoc-typehints-gp/src/sphinx_autodoc_typehints_gp/extension.py @@ -283,6 +283,41 @@ def _enhance_existing_type_field( para.extend(_annotation_to_nodes(text, env)) +def _strip_hidden_doctest_examples(lines: list[str]) -> None: + """Drop doctest examples flagged ``# doctest: +HIDE`` from rendered output. + + The example still runs as a test — the doctest runner reads ``__doc__`` from + source, which this never touches. This only removes it from the + Sphinx-rendered docstring, so incidental setup (building an ``env`` mapping, + a socket path) can execute without cluttering the docs. Each ``>>>`` line + carrying the flag is dropped together with its ``...`` continuation lines. + + Parameters + ---------- + lines : list[str] + The docstring lines, modified in place. + + Examples + -------- + >>> body = [">>> x = 1 # doctest: +HIDE", ">>> x", "1"] + >>> _strip_hidden_doctest_examples(body) + >>> body + ['>>> x', '1'] + """ + kept: list[str] = [] + dropping = False + for line in lines: + stripped = line.lstrip() + if stripped.startswith(">>>") and "doctest:" in line and "+HIDE" in line: + dropping = True + continue + if dropping and stripped.startswith("..."): + continue + dropping = False + kept.append(line) + lines[:] = kept + + def process_docstring( app: Sphinx, what: str, @@ -317,6 +352,8 @@ def process_docstring( >>> process_docstring # doctest: +ELLIPSIS """ + _strip_hidden_doctest_examples(lines) + from sphinx_autodoc_typehints_gp._numpy_docstring import process_numpy_docstring lines[:] = process_numpy_docstring(lines)