simvx.core.scene_io.scene_file

High-level scene-shaped editing surface for parso-parsed sources.

This is Tier 3b of the scene I/O layer. It composes the lossless parse (:mod:source_tree) and prefix-preserving primitives (:mod:edits) with greenfield emission (:mod:emitter) and structural detection (:mod:detection) into a small public API:

SceneFile : a parsed Python file with byte-perfect round-trip save.
SceneClass: an editable view of one Node-subclass in the file.
ImportSet : an editable view of the file's top-level imports.

The editor’s save/load path and the IDE’s refactor tools build on this surface. Lower tiers remain available for callers that need finer control.

Module Contents

Classes

Removed

What taking a child out of __init__ carried off besides its own lines.

SceneFile

A parsed Python scene file with byte-perfect round-trip save.

SceneClass

An editable view of one Node-subclass class definition inside a scene.

ImportSet

Editable view of the file’s top-level imports.

Data

API

simvx.core.scene_io.scene_file.__all__

[‘ImportSet’, ‘Removed’, ‘SceneClass’, ‘SceneFile’]

class simvx.core.scene_io.scene_file.Removed[source]

Bases: typing.NamedTuple

What taking a child out of __init__ carried off besides its own lines.

A removal takes every statement standing on the child, and a caller with somewhere to say so should say what those were: the author wrote them and they are not coming back (:meth:SceneClass.remove_child).

statements: list[str]

None

attributes: list[str]

None

class simvx.core.scene_io.scene_file.SceneFile(source_tree: simvx.core.scene_io.source_tree.SourceTree, *, path: pathlib.Path | None)[source]

A parsed Python scene file with byte-perfect round-trip save.

Holds a parso tree plus the original on-disk text. All edits operate on the parso tree; :meth:save writes tree.get_code(). Round-trip identity is guaranteed when no edits were made.

Initialization

__slots__

(‘_source_tree’, ‘_path’, ‘_imports’, ‘_original_text’)

classmethod load(path: str | pathlib.Path) simvx.core.scene_io.scene_file.SceneFile[source]

Read path and parse it.

A scene using one of the constructs the format documents as unreadable (:mod:simvx.core.scene_io.syntax) opens here and refuses its edits; source broken any other way raises

Class:

~simvx.core.scene_io.syntax.UnsupportedSceneSyntaxError, naming the place rather than surfacing parso’s own token position. Raises :class:FileNotFoundError when the file is absent.

classmethod from_source(text: str, *, path: pathlib.Path | None = None) simvx.core.scene_io.scene_file.SceneFile[source]

Parse already-loaded source text.

path is recorded for :meth:save and error messages but is not read. Refuses unreadable source the way :meth:load does.

classmethod from_runtime(root: simvx.core.node.Node, *, class_name: str | None = None, report: list[str] | None = None) simvx.core.scene_io.scene_file.SceneFile[source]

Greenfield: emit source for a live :class:Node tree, then parse it.

The returned :class:SceneFile has no path until :meth:save is called with one.

A value the emitter cannot write raises

Class:

~simvx.core.scene_io.UnemittableValueError, because a file saved without it would load back as a different scene. Pass report to take the other policy instead: the file is built without those values and one message per refusal is appended to the list, which is what an editor wants – it can show them and leave the document marked unsaved rather than lose the user’s work to an exception.

property path: pathlib.Path | None[source]

Path the file was loaded from / will be saved to, or None.

property source_tree: simvx.core.scene_io.source_tree.SourceTree[source]

Underlying lossless :class:SourceTree.

property imports: simvx.core.scene_io.scene_file.ImportSet[source]

Editable view of the file’s top-level imports.

scene_class() simvx.core.scene_io.scene_file.SceneClass[source]

The single primary Node subclass in the file.

Raises :class:AmbiguousSceneError if the file contains multiple Node subclasses; raises :class:ValueError if it contains none.

all_scene_classes() list[simvx.core.scene_io.scene_file.SceneClass][source]

Every Node subclass defined in the file, in source order.

Used for diagnostics and IDE features. The typical scene has one. Detection mirrors :func:primary_node_class_from_source’s rule (Node base + __init__ or class-body Property descriptors).

insert_top_level_class(name: str, base: str, *, body: str = 'pass', before: str | None = None) simvx.core.scene_io.scene_file.SceneClass[source]

Insert class <name>(<base>): <body> at module scope.

Auto-imports base via :class:ImportSet (defaults to simvx.core). Placement is just before the existing scene class (or before before when given) so the new definition sits between imports and the scene that uses it. Returns the new

Class:

SceneClass view.

Raises :class:ValueError if a top-level class with the same name already exists.

dump() str[source]

Current source as a string.

is_dirty() bool[source]

True iff :meth:dump differs from the original input text.

Used by :class:SceneModule to skip writes for files that were opened but never edited.

save(path: str | pathlib.Path | None = None) pathlib.Path[source]

Write to path (or to self.path if not given).

Returns the path written. Raises :class:ValueError if neither is set. self._path is updated on success so subsequent saves without an argument reuse the last destination.

assert_idempotent() None[source]

Assert that :meth:dump equals the original input text.

Useful for tests that verify no accidental edits leaked into a load/save round-trip.

class simvx.core.scene_io.scene_file.SceneClass(file: simvx.core.scene_io.scene_file.SceneFile, class_node: parso.python.tree.Class)[source]

An editable view of one Node-subclass class definition inside a scene.

Initialization

__slots__

(‘_file’, ‘_class’)

property name: str[source]

Class name (e.g. "Arena").

property node: parso.python.tree.Class[source]

Underlying parso :class:Class node.

unreadable_syntax() simvx.core.scene_io.syntax.SyntaxIssue | None[source]

The first construct in __init__ the parser could not read, if any.

None for the ordinary scene, which is every scene that stays inside the documented subset (:mod:simvx.core.scene_io.syntax). A caller about to rewrite __init__ as a whole – the editor’s save – asks this first and leaves the file alone, because the individual edits it would make each look reasonable and the file they add up to does not run.

has_property(name: str) bool[source]
get_property_default(name: str) str | None[source]

Source text of the Property’s default expression, or None.

Inherited Properties are not visible: use the runtime tree to observe inherited values.

add_property(name: str, default_expr: str) None[source]

Insert name = Property(default_expr) into the class body.

Inserted after existing class-level Property declarations. Auto- imports Property via the file’s :class:ImportSet.

remove_property(name: str) None[source]
set_property_default(name: str, default_expr: str) None[source]
get_root_kwarg(name: str) str | None[source]
set_root_kwarg(name: str, value_expr: str) None[source]

Update or insert a kwarg in the root super().__init__(...) call.

remove_root_kwarg(name: str) None[source]
has_child(var_name: str) bool[source]
child_var_names(*, receiver: str | None = 'self') list[str][source]

Variable names of all children added via <receiver>.add_child(<var>), in source order.

receiver defaults to "self", the root’s own children. Pass None for the whole tree the file builds: a grandchild is added on the variable its parent is bound to (panel.add_child(label)), so a reader that asks only about self reports a file with children as a file with none.

add_child(var_name: str, type_name: str, *, before: str | None = None, after: str | None = None, from_module: str | None = 'simvx.core', receiver: str = 'self', **kwarg_exprs: str) None[source]

Insert a child construction + <receiver>.add_child pair into __init__.

Position: appended at the end of the existing child block by default; before= or after= (mutually exclusive) places relative to another child.

receiver is the local the add_child call is written on, and "self" – the root’s own children – is the default. Anything else is a local __init__ already binds to a child, and the pair is appended after the last child that local already has, or after the statement that parented the local itself when it has none yet. A local the file does not bind is refused: the file would name a variable that does not exist.

Auto-imports type_name from from_module (defaults to simvx.core) when the name is not already imported under any alias. Callers placing user classes should pass from_module=type(node).__module__. from_module=None imports nothing, which is what a class this file defines itself is owed: an import of it either names a module nothing can resolve or runs this very file a second time, and the class the second run defines is not the one the scene was built from.

Raises :class:ValueError if var_name already exists in the __init__ body or if both before and after are passed.

A construction too wide for the line limit is written across several lines, one keyword argument each, as a formatter would leave it: the column it is being inserted at is known here, and that is what the layout is measured against (:func:~simvx.core.scene_io.layout.wrap_statement).

Note: when the source is procedural (children built inside loops or conditionals: see :func:has_procedural_construction), the inserted statements are appended at the top level of __init__ and may execute in a surprising order relative to the procedural code.

remove_child(var_name: str) simvx.core.scene_io.scene_file.Removed[source]

Remove the child’s own statements and everything left standing on them.

The assignment and the self.add_child line are the two the emitter writes, but they are not the whole of what a child can own. A child of its own is written as a statement on its variable (child_0.add_child(label_0)), and so is anything else the author hung off it, so taking only the two would leave statements naming a variable that is gone and a file that raises on import. Every statement written on __init__’s own body goes if it names the variable, and so do the constructions those statements were the only use of, however far that propagates.

A block is where this stops. A name used inside a for, an if, a with or a try is not one statement to take out, and taking the block whole would delete work of the author’s that the child is only a part of, so the removal is refused: nothing is taken out, and

Class:

ValueError names the blocks that hold it back.

Meth:

_removal_blockers asks the same question without attempting the removal, for a caller that would rather announce it than raise.

Returns what went with it beyond the child’s own two lines (:class:Removed): every other statement the sweep took, and the attribute bindings they carried off. Both are what a caller with somewhere to say so should say, since the author wrote those lines and a method reading self.<name> no longer has one.

Does not auto-clean unused imports: that is the caller’s responsibility (use :meth:ImportSet.remove). Raises

Class:

ValueError if var_name is absent.

rename_child(old: str, new: str) None[source]

Rename the local variable a child is bound to, everywhere __init__ names it.

Everywhere, not just on the two statements the emitter writes: an author may have hung a child of its own off the variable (child_0.add_child(label_0)), read it further down, or used it inside a for or an if, and a rename that reached only the binding and the add_child call would leave those naming a variable the file no longer has. A local is one name for the whole function, so every place __init__ writes it, at whatever depth, is the same variable and follows.

get_child_kwarg(var_name: str, kwarg: str) str | None[source]
set_child_kwarg(var_name: str, kwarg: str, value_expr: str) None[source]
remove_child_kwarg(var_name: str, kwarg: str) None[source]
reorder_children(order: list[str], *, receiver: str = 'self') None[source]

Reorder child assignment + add_child line pairs to match order.

Every existing child of receiver that the file binds a variable to must appear in order exactly once. A child the file constructs inside its own add_child call binds no variable and so cannot be named here at all: :meth:reorder_children_by_statement is the mover that can move one, and this is a thin way of asking for it by name.

receiver says whose children are being reordered.

reorder_children_by_statement(order: list[parso.tree.NodeOrLeaf], *, receiver: str = 'self') None[source]

Reorder receiver’s children so their add_child statements run in order.

order holds every add_child statement written on receiver, exactly once each, in the order the file should call them. What moves with each is the child’s own lines and nothing else: the construction the file binds to a variable, where there is one, and the add_child line itself.

A child the file constructs inside the call is movable too, which is why this exists beside :meth:reorder_children. self.add_child( make_sprite("A")) binds no variable to name it by, but it is a whole line and moving it is exactly as safe as moving a bound child’s pair. What genuinely blocks a move is a statement sharing a line with another one, since the line cannot follow the child without taking that statement along, and that is refused here rather than written wrong.

Meth:

_children_share_lines asks the same question without attempting the move.

A statement of the author’s written between two children ends up after all of them: the children are re-inserted contiguously, which is the shape the emitter writes and the only one this can restore. That is a real cost and the round-trip design turns it into a refusal; until then it is what a reorder does.

class simvx.core.scene_io.scene_file.ImportSet(source_tree: simvx.core.scene_io.source_tree.SourceTree)[source]

Editable view of the file’s top-level imports.

Initialization

__slots__

(‘_source_tree’,)

has(name: str, *, from_: str | None = None) bool[source]
has_any_alias(name: str) bool[source]

True iff name is imported from anywhere (any module).

ensure(name: str, *, from_: str | None = None) None[source]

Add import name or from <from_> import name if absent.

When from_ matches an existing from <from_> import line, the new name is merged into that line (sorted, deduplicated) instead of creating a separate import line, and that line is laid out to the width the rest of the file is written at.

remove(name: str, *, from_: str | None = None) None[source]

Remove an import. If the line becomes empty, remove it.

No-op if name is not imported (with the given from_).

names() list[tuple[str | None, str]][source]

List of (from_, name) pairs in source order.