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_conditionpair, §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_stmtrejects butast.parseaccepts and Sphinx would render (complex literals, multi-statement strings such asint;orint\nstr) fall back to one xref; - an
ast.Attributewhose value’s first fragment is not a text node ((1).x,'s'.x) falls back instead of reproducing Python’sstr(Element)garbage (f'{unparse(node.value)[0]}.{node.attr}',_annotations.py:101); Union[()]/Optional[()]fall back where Sphinx raises an uncaughtIndexError(_annotations.py:217).
Structs§
- PyRef
Context - The slice of
env.ref_contextthattype_to_xrefcopies onto every annotation xref (_annotations.py:62-66): the enclosingpy:module/py:class, or PythonNonewhen 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 singletype_to_xref(_annotations.py:250-251) [PY §2.3]. - parse_
reftarget - Port of
parse_reftarget(_annotations.py:30-55),suppress_prefixfixed toFalse: returns(reftype, reftarget, title, refspecific). - type_
to_ xref - Port of
type_to_xref(_annotations.py:58-92): onepending_xrefcarryingrefdomain/reftype/reftarget/refspecificplus thepy:module/py:classref-context attrs, with aText(title)child — or, underpython_use_unqualified_type_names, the twopending_xref_conditionchildren (condition="resolved"short name /condition="*"full title) [SIG §4.2].