Expand description
sphinx.pycode — the ModuleAnalyzer/DefinitionFinder surface the
literalinclude :pyobject: filter consumes.
find_tags is the port of ModuleAnalyzer.find_tags()
(SP/pycode/__init__.py:167-170 → Parser.parse_definition,
SP/pycode/parser.py:623-627 → DefinitionFinder,
SP/pycode/parser.py:514-588, sphinx 9.1.0). Ground truth: research spec
§3.5 (cited as [INC §3.5]),
docs/superpowers/plans/2026-09-01-m2-wave4.5-research-spec-include-literalinclude.md.
DefinitionFinder is a token-stream scanner, not an AST walk: it reads
the tokenize stream and keeps two stacks — a dotted context of
class/def names and an indents stack of open blocks. That mechanic, not
Python’s grammar, is what decides every line number, so the port keeps the
same shape:
- a definition’s start line is the
@line of its FIRST decorator when one is pending, else the line of the NAME afterclass/def(:563-567); - a one-liner (
def f(): return 1) ends at the NAME’s line — even when its signature spans continuation lines (:573-576, probeoneliner_multiline_sig); - a block definition ends at
DEDENT.end_row - 1, walked back over every line matchingemptyline_re = ^\s*(#.*)?$— blank lines AND comment-only lines (:578-588, probecomment_trap); add_definitiondrops adefwhose immediately enclosing open block is also adef(:526-532), so nested functions vanish but methods survive asClass.method— and adefnested in adefthrough an interveningif/for/withblock SURVIVES, because that block pushed an'other'entry (probedef_in_if_in_def:outer.inneris recorded);- the context stack tracks only class/def names, so a
definside a module-levelifis recorded under its bare name (probedef_in_if); async defneeds no special case:asyncis an ordinary NAME token anddefdrives the scan (probedeco_async).
§The tokenizer simplification and its bounds
Sphinx runs CPython’s tokenize; this module runs a line-oriented scanner
that produces only what DefinitionFinder consumes: INDENT/DEDENT with
CPython’s two-column (tabs-as-8 and tabs-as-1) bookkeeping, NEWLINE vs
NL, COMMENT, NAME, NUMBER, STRING and maximal-munch OP. It is
deliberately NOT a Python parser. Tested bounds (each pinned below against
the real ModuleAnalyzer):
- logical vs physical lines: a bracketed continuation emits
NL, notNEWLINE, and suppresses indentation processing (probecontinuation_sig); so does a backslash continuation, which makesdef f(a): \+return aa ONE-LINER (probebackslash_continuation); - blank and comment-only lines never produce
INDENT/DEDENT, which is why a# commentat column 0 inside a class does not close it (probedef_then_dedent_to_comment_col0); - strings (all prefixes, triple quotes, escapes) are skipped whole, so
def/classinside a docstring creates no tag (probetriple_quoted_trap); @only starts a decorator when the previous token isNEWLINE/NL/INDENT/DEDENTor the stream just started — the matrix-multiplication operator inside a statement is not a decorator (probematmul_at), but one after an in-bracketNLIS mistaken for one by sphinx, and this port reproduces that (probeat_after_nl_in_parens: start line 2, not 3);filter_whitespace(parser.py:31-32) replaces form feeds with spaces BEFORE lines are split, and line splitting isstr.splitlines(True), both mirrored here so line numbers agree (probeformfeed).
Three places where the scanner is knowingly coarser than tokenize, none of
which can change a tag in a file that is valid Python:
- number munching is greedy over alphanumerics, so
1if x else 2becomes ONENUMBERwhere CPython emitsNUMBER 1+NAME if. Nothing the finder keys on (def/class/@/:) can be swallowed that way, since a number never precedes them at a header’s top level; - a name of 1-2 letters from
rRbBuUfFimmediately before a quote is taken for a string prefix, so an INVALID combination (bb"x",uu'y') is lexed as oneSTRINGwhere CPython emitsNAME+STRING. Such a file is a syntax error in Python, so sphinx warns on it anyway (the tokenizes-but-does-not-parse class below); - the dedent branch checks CPython’s two conditions in CPython’s order —
unindent-matches-no-outer-level first, tab inconsistency second — so which
failure fires is faithful; only the rendered detail differs, since sphinx
surfaces them wrapped in
IndentationError/TabErrorreprs (below).
§Errors, and the divergence they carry
Sphinx’s Parser.parse() runs ast.parse BEFORE DefinitionFinder
(parser.py:607-621), so find_tags fails for EVERY file that is not
valid Python, with PycodeError(f'parsing {srcname!r} failed: {exc!r}')
(__init__.py:158-160) — rendered, probed verbatim, as
parsing '/abs/broken.py' failed: SyntaxError('invalid syntax', ('<unknown>', 1, 7, 'def f(:\n', 1, 8)). That tail is a CPython
SyntaxError repr: version-specific wording, '<unknown>' from
ast.parse’s default filename, and byte offsets. This port has no Python
parser, so it cannot reproduce either the tail or the full failure SET.
The decision (task 15, evidence above):
- our failures are a strict SUBSET of sphinx’s — only what the scanner
itself cannot get past (unterminated string, unclosed bracket at EOF,
unindent that matches no outer level, inconsistent tabs/spaces), each of
which also fails
ast.parse; - the caller (
crate::rst’s literalinclude reader) keeps sphinx’sparsing %r failed:prefix and substitutes this error’s [Display] for the un-reproducible{exc!r}tail; - a file that tokenizes but does not PARSE (
x = = 1after a cleandef) yields tags here and a warning in sphinx. Documented divergence, not a bug to fix without a Python parser.
Other documented divergences from the sphinx path:
- sphinx reads the file itself with
tokenize.open(BOM + PEP 263 coding cookie); this interface takes the reader’s already-decoded text, so a non-UTF-8 file with a coding cookie and no:encoding:option differs; - the reader has already applied
:tab-width:expansion when it hands the text over (read_file,SP/directives/code.py:221-240), while sphinx’s analyzer re-reads the RAW file. Expansion never changes the line COUNT, but at atab-widthother than 8 it can change the indentation COLUMNS this scanner measures, and with them the block structure and every end line derived from it: a\tis column 8 to CPython’s tokenizer but four spaces afterexpandtabs(4), so against a six-space line it flips from deeper to shallower — anINDENTwhere sphinx sees aDEDENT, or a clean parse here where sphinx raisesIndentationError. Triggering it takes all three of:tab-width:(≠ 8),:pyobject:, and a file that mixes tab and space indentation; - PEP 701 f-strings that reuse the outer quote inside a replacement field
(
f"{"a"}") are lexed here as pre-3.12 string literals.
Structs§
- Pycode
Error sphinx.errors.PycodeError— every analyzer failure funnels throughLiteralInclude.run()’s broadexceptinto a reporter warning whose text is this error’sDisplay.
Enums§
- TagKind
- The first member of a
find_tagsentry — sphinx’s'class'/'def'tag strings.
Functions§
- find_
tags ModuleAnalyzer.find_tags()over already-decoded source text:dotted_name -> (kind, start_line, end_line), both line numbers 1-based inclusive, exactly aslines[start - 1:end]expects.