Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,23 @@ $ uv add gp-sphinx --prerelease allow

<!-- To maintainers and contributors: Please add notes for the forthcoming version below -->

### 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
Expand Down
30 changes: 30 additions & 0 deletions docs/packages/sphinx-autodoc-typehints-gp/how-to.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
<Connection ...>
"""
```

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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -317,6 +352,8 @@ def process_docstring(
>>> process_docstring # doctest: +ELLIPSIS
<function process_docstring at 0x...>
"""
_strip_hidden_doctest_examples(lines)

from sphinx_autodoc_typehints_gp._numpy_docstring import process_numpy_docstring

lines[:] = process_numpy_docstring(lines)
Expand Down