Skip to main content

Module annotations

Module annotations 

Source
Expand description

Annotation rendering: the _parse_annotation / type_to_xref port (sphinx/domains/python/_annotations.py:30-251, sphinx 9.1.0).

parse_annotation turns one annotation string into the flat node list Sphinx splices into a desc_sig_name wrapper (parameter annotations), a desc_returns (return annotations) or a desc_annotation (:type: options): pending_xref for every name, desc_sig_* leaves for the punctuation/constants between them. The walk runs over the wave-4.5 super::expr::PyExpr AST, parsed the way _parse_annotation parses it: ast.parse(annotation, type_comments=True) — exec mode, not eval (parse_py_expr_stmt, _annotations.py:232). That is what makes a PEP 646 *Ts annotation a legal Expr(Starred(…)) statement, what makes an empty annotation an empty node list rather than an empty-target xref, and what makes a leading indent an IndentationError whose xref keeps the unstripped text. Anything parse_py_expr_stmt rejects — and any node shape Sphinx’s own unparse has no branch for (ast.Add, ast.Not, ast.BoolOp, sets, dicts, …, which raise SyntaxError there) — falls back to a single type_to_xref of the whole annotation text, exactly like Sphinx’s except SyntaxError arm (_annotations.py:250-251).

Ground truth, cited throughout as [PY §n] / [SIG §n]:

  • [PY] docs/superpowers/plans/2026-09-01-m2-wave4.5-research-spec-py-domain.md (§2.3 semantics, §2.6 leaf classes, §1.6 probe outputs);
  • [SIG] docs/superpowers/plans/2026-09-01-m2-wave4.5-research-spec-signature-config.md (§4.2 pending_xref_condition pair, §5.1 short literal chains).

Every expected pformat in the test module is copied verbatim from probe runs against the pinned toolchain (sphinx 9.1.0 / docutils 0.22.4, harness3 conventions from tools/gen_sphinx_fixture.py; cases cited as probe <app>/<case>, logged in the task-4 report).

Documented divergences (all conservative — we fall back to the same single-xref shape Sphinx uses for SyntaxError, never print something different):

  • constructs parse_py_expr_stmt rejects but ast.parse accepts and Sphinx would render (complex literals, multi-statement strings such as int; or int\nstr) fall back to one xref;
  • an ast.Attribute whose value’s first fragment is not a text node ((1).x, 's'.x) falls back instead of reproducing Python’s str(Element) garbage (f'{unparse(node.value)[0]}.{node.attr}', _annotations.py:101);
  • Union[()] / Optional[()] fall back where Sphinx raises an uncaught IndexError (_annotations.py:217).

Structs§

PyRefContext
The slice of env.ref_context that type_to_xref copies onto every annotation xref (_annotations.py:62-66): the enclosing py:module / py:class, or Python None when unset — which docutils pformat renders as the "True" sentinel (same convention as the inline parser’s role xrefs, src/rst/inline.rs) — plus the provenance the built nodes carry.

Functions§

parse_annotation
Port of _parse_annotation (_annotations.py:95-251): parse one annotation string and render it as a flat node list. On any parse or unparse failure the whole string becomes a single type_to_xref (_annotations.py:250-251) [PY §2.3].
parse_reftarget
Port of parse_reftarget (_annotations.py:30-55), suppress_prefix fixed to False: returns (reftype, reftarget, title, refspecific).
type_to_xref
Port of type_to_xref (_annotations.py:58-92): one pending_xref carrying refdomain/reftype/reftarget/refspecific plus the py:module/py:class ref-context attrs, with a Text(title) child — or, under python_use_unqualified_type_names, the two pending_xref_condition children (condition="resolved" short name / condition="*" full title) [SIG §4.2].