simvx.core.scene_io.source._source_ast.spans¶
Where things are: ast positions converted to str indices, once, here.
This is the one module in the engine that knows CPython reports a column as a
utf-8 byte offset into its line while Python strings are indexed in code
points. Every other module deals in :class:Span values, which are plain str
slice bounds, so a scene holding an em-space in a comment or an emoji in a label
is not a special case anywhere above this file.
Four hazards live here, each with a named test in
packages/core/tests/scene_io/test_source_ast_spans.py:
Byte columns.
col_offsetcounts utf-8 bytes;tokenizecounts characters. :class:LineTableconverts from either convention and never guesses which one it was handed.Decorators.
astputs a decorated statement’slinenoon thedeforclassline, so the decorators above it are outside the node’s own span.- func:
statement_spanwidens to the first@.
Redundant parentheses. An expression node’s span excludes parentheses the author wrote around it, so
f(x=(1 + 2))reports1 + 2. Splicing that span alone therefore keeps the author’s parentheses, which is what we want;- class:
ValueSlotrecords where they are so a caller can also tell when a replacement needs parentheses it does not have.
Position-less nodes.
ast.Module,ast.argumentsand the context markers carry no position at all. :func:node_spanrefuses them by name instead of raisingAttributeErrorthree layers up.
The line grid is the one the language reference defines: a physical line ends at
\n or \r\n. A bare \r terminator is refused (see
- class:
UnsupportedLineEnding) becauseasttreats it as a line break andtokenizedoes not, and no correct answer can be built on two parsers that disagree about where the lines are.
Module Contents¶
Classes¶
A half-open range of |
|
The line grid of one source text, and the two column conventions on it. |
|
One occurrence of a bare name, and whether it reads or writes it. |
|
A place a value expression can be replaced, and what a replacement owes. |
|
One argument of one call, as written. |
|
Every span an argument-level edit of one call expression needs. |
Functions¶
The span |
|
The full span of one statement, decorators included, indentation excluded. |
|
|
|
Every |
|
Resolve every span of one call expression. |
|
A :class: |
|
Every suite hanging off |
Data¶
API¶
- simvx.core.scene_io.source._source_ast.spans.__all__¶
[‘ArgumentSpans’, ‘CallSpans’, ‘LineTable’, ‘NameSpan’, ‘Span’, ‘UnsupportedLineEnding’, ‘ValueRole’…
- exception simvx.core.scene_io.source._source_ast.spans.UnsupportedLineEnding[source]¶
Bases:
ValueErrorRaised for source whose lines end with a bare carriage return.
ast.parseacceptsx = 1\ry = 2as two statements on two lines;tokenizereads the same text as one line containing a stray operator. Trivia is derived from the token stream and spans from the parse tree, so a file the two disagree about cannot be edited faithfully by any amount of care further up. Refusing it here is the only honest answer, and it is reachable in practice only from classic Mac OS files.Initialization
Initialize self. See help(type(self)) for accurate signature.
- class __cause__¶
- class __context__¶
- __delattr__()¶
- __dir__()¶
- __eq__()¶
- __format__()¶
- __ge__()¶
- __getattribute__()¶
- __getstate__()¶
- __gt__()¶
- __hash__()¶
- __le__()¶
- __lt__()¶
- __ne__()¶
- __new__()¶
- __reduce__()¶
- __reduce_ex__()¶
- __repr__()¶
- __setattr__()¶
- __setstate__()¶
- __sizeof__()¶
- __str__()¶
- __subclasshook__()¶
- class __suppress_context__¶
- class __traceback__¶
- add_note()¶
- class args¶
- with_traceback()¶
- simvx.core.scene_io.source._source_ast.spans.ValueRole¶
None
Where a replaceable expression sits, which decides what parentheses it owes.
- class simvx.core.scene_io.source._source_ast.spans.Span[source]¶
A half-open range of
strindices into one source text.Spans are compared and sorted by
(start, stop), which puts them in source order. They carry no reference to the text they index, so a span outlives the edit that invalidated it: the document layer above is responsible for shifting or discarding spans across a splice.- start: int¶
None
- stop: int¶
None
- __bool__() bool[source]¶
True when the span covers at least one character.
Defined explicitly because
__len__alone would make every empty span falsy in a way that reads as “missing” at the call sites that askif trivia.leading:. Empty is the answer there, so keep it.
- shift(delta: int) simvx.core.scene_io.source._source_ast.spans.Span[source]¶
The same span moved
deltacharacters along the text.
- contains(other: simvx.core.scene_io.source._source_ast.spans.Span) bool[source]¶
True when
otherlies wholly inside this span.
- class simvx.core.scene_io.source._source_ast.spans.LineTable(text: str)[source]¶
The line grid of one source text, and the two column conventions on it.
Built once per parse. Converting a position costs a list index for ASCII lines and a cached scan for the rest, so the utf-8 hazard is paid for only by the files that actually contain non-ASCII text.
Line numbers are 1-indexed to match
astandtokenize. A line’s span includes its terminator, soline_span(n).stopis where linen + 1begins and the spans tile the text with no gaps.Initialization
- __slots__¶
(‘_text’, ‘_starts’, ‘_byte_maps’)
- property line_count: int[source]¶
Number of physical lines.
A file ending with a newline has one more line than it has terminators: the empty last one, which is where
tokenizeputs its end marker and whereline_of(len(text))lands. Counting it is what keeps every offset in the text, including the one past the end, on a real line.
- line_span(lineno: int) simvx.core.scene_io.source._source_ast.spans.Span[source]¶
Span of line
linenoincluding its terminator.
- offset(lineno: int, col_offset: int) int[source]¶
strindex for anastposition, whose column counts utf-8 bytes.This is the conversion the whole module exists for. Pass
linenoandcol_offset(orend_lineno/end_col_offset) straight off anastnode; never add a column to a line start yourself.
- char_offset(lineno: int, column: int) int[source]¶
strindex for atokenizeposition, whose column counts characters.The token stream reports columns in code points, so this is a plain addition. It exists as a named method so that a call site states which convention its numbers came from.
- position(offset: int) tuple[int, int][source]¶
(lineno, col_offset)in theastconvention for astrindex.
- char_position(offset: int) tuple[int, int][source]¶
(lineno, column)in thetokenizeconvention for astrindex.
- indent_of(offset: int) simvx.core.scene_io.source._source_ast.spans.Span[source]¶
The leading whitespace of
offset’s line, empty when code precedes it.A statement written after a semicolon has no indentation of its own, and this answers with an empty span there rather than with the indentation of the statement it shares a line with.
- simvx.core.scene_io.source._source_ast.spans.node_span(table: simvx.core.scene_io.source._source_ast.spans.LineTable, node: ast.AST) simvx.core.scene_io.source._source_ast.spans.Span[source]¶
The span
astreports fornode, converted tostrindices.Raises :class:
TypeErrorfor the nodes that carry no position at all (ast.Module,ast.arguments, theast.expr_contextmarkers), which is a caller bug rather than a source the layer cannot read.
- simvx.core.scene_io.source._source_ast.spans.statement_span(table: simvx.core.scene_io.source._source_ast.spans.LineTable, node: ast.stmt) simvx.core.scene_io.source._source_ast.spans.Span[source]¶
The full span of one statement, decorators included, indentation excluded.
astreports a decorated function or class from itsdef/classkeyword, leaving@propertyabove it outside the node. A caller that replaced or removed the node’s own span would leave the decorators behind attached to whatever followed, so the span starts at the first@.Leading whitespace is not part of the statement. Indentation, blank lines and the comments above belong to the statement’s trivia record, which is what carries them through a move (see :mod:
.trivia).
- simvx.core.scene_io.source._source_ast.spans.statement_spans(table: simvx.core.scene_io.source._source_ast.spans.LineTable, body: collections.abc.Sequence[ast.stmt]) list[simvx.core.scene_io.source._source_ast.spans.Span][source]¶
- Func:
statement_spanfor every statement of one suite, in source order.
- class simvx.core.scene_io.source._source_ast.spans.NameSpan[source]¶
One occurrence of a bare name, and whether it reads or writes it.
- name: str¶
None
- context: Literal[load, store, delete]¶
None
- simvx.core.scene_io.source._source_ast.spans.name_spans(table: simvx.core.scene_io.source._source_ast.spans.LineTable, node: ast.AST, *, name: str | None = None) list[simvx.core.scene_io.source._source_ast.spans.NameSpan][source]¶
Every
ast.Nameoccurrence insidenode, in source order.This is the rename surface: replacing each span with a new identifier renames the variable and touches nothing else. Attribute names, keyword argument names and string contents are deliberately absent, because none of them is the same binding as the name that spells them.
Occurrences inside a PEP 701 f-string are included and carry real positions, so
f"{hero}"renames with the rest.
- class simvx.core.scene_io.source._source_ast.spans.ValueSlot[source]¶
A place a value expression can be replaced, and what a replacement owes.
- Attr:
spanis the expression asastsees it, which excludes any parentheses the author wrote around it. Splicing over :attr:spantherefore preserves those parentheses, which is the behaviour we want: an author who wrapped a value keeps the wrapping. :attr:outeris the same value with them, so a caller that needs to remove the whole argument knows how far it reaches, and :attr:parenthesisedsays whether the two differ.
The reverse case is a replacement that needs parentheses the slot does not have: a bare tuple in a positional slot would change the call’s arity, and a walrus or a
yieldbeside a keyword’s=does not parse at all. That is- Meth:
requires_parentheses, kept as a question rather than an automatic rewrite so a caller can decide to reject the value instead.
- requires_parentheses(replacement: str) bool[source]¶
True when
replacementwould not parse, or would not mean the same, bare here.Raises :class:
SyntaxError(CPython’s own) whenreplacementis not a single expression, which is the check every emitted value should be passing anyway.
- splice(replacement: str) tuple[simvx.core.scene_io.source._source_ast.spans.Span, str][source]¶
The span to overwrite and the text to write, parenthesised if it must be.
- class simvx.core.scene_io.source._source_ast.spans.ArgumentSpans[source]¶
One argument of one call, as written.
- Attr:
spancovers the argument whole:colour=REDfor a keyword,*restfor an unpacking, the bare expression for a positional. It is what a removal deletes. :attr:valueis the part a value edit rewrites, which for*restisrestand forcolour=REDisRED.
- role: Literal[positional, keyword, star_args, star_kwargs]¶
None
- name: str | None¶
None
- class simvx.core.scene_io.source._source_ast.spans.CallSpans[source]¶
Every span an argument-level edit of one call expression needs.
- Attr:
argumentsis in source order, which is notast’s order:astlists positionals and keywords separately, and a call may interleave them (f(a, k=1, *rest)is legal). Editing by position demands source order, so it is sorted here once.
- open_paren: int¶
None
- close_paren: int¶
None
- arguments: tuple[simvx.core.scene_io.source._source_ast.spans.ArgumentSpans, ...]¶
None
- keyword(name: str) simvx.core.scene_io.source._source_ast.spans.ArgumentSpans | None[source]¶
The keyword argument written as
name=..., or None.
- positional(index: int) simvx.core.scene_io.source._source_ast.spans.ArgumentSpans | None[source]¶
The
index-th positional argument, counting unpackings, or None.
- simvx.core.scene_io.source._source_ast.spans.call_spans(table: simvx.core.scene_io.source._source_ast.spans.LineTable, call: ast.Call, *, comments: collections.abc.Iterable[simvx.core.scene_io.source._source_ast.spans.Span] = ()) simvx.core.scene_io.source._source_ast.spans.CallSpans[source]¶
Resolve every span of one call expression.
commentsis the comment span list from the trivia pass. It is optional and only ever makes the answer better: without it, a value whose author parentheses are separated from it by a comment is reported unparenthesised, which costs a caller a redundant pair of parentheses and never a wrong splice. With it the answer is exact.
- simvx.core.scene_io.source._source_ast.spans.value_slot(table: simvx.core.scene_io.source._source_ast.spans.LineTable, node: ast.expr, *, role: simvx.core.scene_io.source._source_ast.spans.ValueRole, bounds: simvx.core.scene_io.source._source_ast.spans.Span, comments: collections.abc.Iterable[simvx.core.scene_io.source._source_ast.spans.Span] = ()) simvx.core.scene_io.source._source_ast.spans.ValueSlot[source]¶
A :class:
ValueSlotfor a free-standing expression insidebounds.boundsis the region the author’s parentheses may not escape: for a call argument that is the inside of the call’s own parentheses, so that the sole argument off((1))reports one pair of author parentheses and not two.
- simvx.core.scene_io.source._source_ast.spans.iter_child_statements(node: ast.AST) collections.abc.Iterator[tuple[str, list[ast.stmt]]][source]¶
Every suite hanging off
node, as(field name, statement list).A suite is any field holding a list of statements: a body, an
else, afinally, the arm of amatchcase, the handler of anexcept*. Naming them by field rather than by statement type is what keeps this function from needing an update when the grammar grows another block.