Expand description
Arglist and PEP-695 type-parameter-list parsing for the py domain: the
_parse_arglist / _pseudo_parse_arglist / _parse_type_list port
(sphinx/domains/python/_annotations.py:254-619, sphinx 9.1.0) plus the
signature_from_str grammar (sphinx/util/inspect.py:967-1038) and the
multi_line_parameter_list measurement (_object.py:291-312).
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.1 arglist node shapes, §2.2 pseudo fallback, §2.4 type parameter lists, §1.6 probe outputs);
- [SIG] docs/superpowers/plans/2026-09-01-m2-wave4.5-research-spec-signature-config.md (§1.3 measurement, §1.4 attr placement, appendix A.1 matrix).
Expected pformats in the test module are verbatim probe output against the
pinned toolchain (sphinx 9.1.0 / docutils 0.22.4, harness3 conventions);
cases cited as probe <case> come from the spec’s §1.6/§A.1 blocks or the
task-5 probe run (probe task5/<case>, logged in the task-5 report).
§The two render pipelines (trap)
Defaults and annotations do NOT round through CPython’s ast.unparse
(which task 3’s expr::unparse mirrors): signature_from_str routes
them through sphinx.pycode.ast.unparse — “a greatly cut-down version of
ast._Unparser” — whose rules differ observably (probe
task5/default_normalized: f(x=0xFF, y=[1,2]) renders 0xFF and
[1, 2]):
- int/float constants keep their SOURCE text via
ast.get_source_segment(0xFF,1_000,1e5), falling back torepronly when the segment is unavailable; - no precedence parentheses at all (
(a+b)*c→a + b * c); **is rendered without surrounding spaces (a**b);- unary operators never parenthesize (
-(a+b)→-a + b); - a
u''string prefix is dropped (reprof the value).
[pycode_unparse] implements those rules over the task-3 PyExpr AST;
annotation nodes are still rendered by task 4’s
parse_annotation, which re-parses the pycode-normalized string exactly
as _parse_annotation(param.annotation) does (_annotations.py:495).
§Documented divergences (all conservative)
- Expressions outside the task-3 subset (lambdas, comparisons, ternaries,
slices, f-strings, starred
**dict unpacks, complex literals) makesignature_from_strreturnSigParseError::Syntax, routing task 6 into the silent pseudo fallback. Sphinx renders most of these (or warns viaNotImplementedError/ValueErrorfora == b,a if b else c, f-strings and{**a}), so the fallback shape — and a missing warning — can diverge for such signatures. - A top-level
lambdakeyword in the arglist is rejected before comma splitting (its parameter commas are not bracket-protected, so no naive split is faithful); sphinx parses it and printslambda a, b: .... - Numeric source recovery maps number tokens to constants in render
order and verifies each token re-parses to the same value; when the
mapping is ambiguous the whole fragment falls back to
reprform (normalized digits) where sphinx would keep source text.visit_Constantrecovers its text by AST POSITION (ast.get_source_segment), so render order only has to agree with source order — which it now does everywhere except one shape:ast.Callsplitsargsfromkeywords, losing their interleaving, so a positional (necessarily a*unpack) written AFTER a keyword argument is rendered before it and consumes the earlier token. The text order still matches sphinx (visit_Callreorders identically), but a non-decimal spelling in such a fragment lands in thereprfallback:f(k=0x1, *a(0x2))printsf(*a(2), k=1)where sphinx keepsf(*a(0x2), k=0x1). Fixing it needs source spans onPyConst, not a render-order tweak. - Type-parameter tokenization errors surface as
SigParseError::Syntaxwith an approximate message where sphinx’s warning embeds the exacttokenize.TokenErrortext. pseudo_parse_arglisttakes(ctx, cfg)in addition to the brief’s(arglist, multi_line): the pseudo path renders annotation xrefs through_parse_annotationand stampsmulti_line_trailing_commafrompython_trailing_comma_in_multi_line_signatures(_object.py:363-380), neither of which is derivable without them.multi_line_flagsmeasures Pythonlen(sig)— Unicode scalar count — whilePySigMatchspans are byte offsets; widths are computed as the char count of the spanned slice, so non-ASCII signatures measure exactly as CPython does.
Structs§
- PyParam
- One parameter of a parsed arglist.
annotationanddefaultaresphinx.pycode.ast.unparse-normalized source strings, exactly whatinspect.Parameter.annotation/DefaultValuecarry in sphinx (util/inspect.py:1027-1038). - PySig
Match - The
py_sig_rematch carrier (_object.py:41-50): groups (prefix, name, tp_list, arglist, retann) plus the two inner-text spans the multi-line measurement subtracts (_object.py:300-311). Task 6’shandle_py_signatureconstructs this from its matcher; spans are BYTE offsets of the group’s inner text within the stripped signature,(0, 0)when the group did not participate (Python’s(-1, -1)span normalizes to width 0 either way). - Single
Line Opts - The two
single-line-*directive flags (_object.py:180-181); each suppresses only its own list ([SIG §1.5], probes D1/D2).
Enums§
- Param
Kind inspect._ParameterKind, as classified bysignature_from_ast(sphinx/util/inspect.py:976-1025).- SigParse
Error - Why an arglist / type-parameter list failed to parse. The two variants
carry sphinx’s two observable failure channels ([PY §1.3 step 5],
_object.py:355-381):
Functions§
- multi_
line_ flags - The
(multi_line_parameter_list, multi_line_type_parameter_list)pair (_object.py:300-311, [SIG §1.3]): each flag is!single-line-option && (len(sig) - width(other group)) > max_len > 0, strictly greater, withmax_lenresolved byPySigConfig::max_len. Lengths are Pythonlen()— Unicode scalar counts — computed from the byte spans’ slices; an out-of-range span counts as width 0. - parse_
arglist - Port of
_parse_arglist(_annotations.py:462-516, [PY §2.1]): adesc_parameterlistcarryingmulti_line_parameter_list/multi_line_trailing_commaUNCONDITIONALLY ([SIG §1.4]), onedesc_parameterper param,/and*separator parameters between kind transitions, and the trailing-/epilogue when the list ends positional-only. - parse_
type_ list - Port of
_parse_type_list+_TypeParameterListParser(_annotations.py:254-459, [PY §2.4]): adesc_type_parameter_listwith the same two multi_line attrs, onedesc_type_parameterper param,*/**operators for variadics, bounds/constraints after:+space inside adesc_sig_namewrapper (constraints re-parenthesized), and␣=␣+default_valueinline defaults. - pseudo_
parse_ arglist - Port of
_pseudo_parse_arglist(_annotations.py:541-619, [PY §2.2]): comma-split fallback with[/]push/pop ofdesc_optional,name[:annotation][=default]partition,=always a baredesc_sig_operator(space-wrapped only when annotated), and — on total bracket imbalance — a fresh paramlist containing the raw arglist as onedesc_parameter, with NO multi_line attributes (the one attr-less exception, [SIG §1.4]). - signature_
from_ str - Parse
arglistwith the grammar ofdef func(<arglist>): pass(sphinx/util/inspect.py:967-974): positional-only via/, keyword-only after a bare*or*args,**kwargslast. Defaults and annotations are validated by task 3’sparse_py_exprand rendered by [pycode_unparse]; any rejected fragment or grammar violation isSigParseError::Syntax, and duplicate parameter names — whichast.parseaccepts — areSigParseError::Duplicateexactly asinspect.Signature.__init__raisesValueErrorafter a clean parse.