simvx.editor.scene_file_ops

Scene file operations mixin for State.

Module Contents

Classes

ScenePlan

A save worked out in full, with nothing yet written.

SceneFileOps

Mixin providing scene lifecycle and file dialog operations.

Functions

record_source_baseline

Record what the file tab now describes says about the scene in it.

Data

log

API

simvx.editor.scene_file_ops.log

‘getLogger(…)’

class simvx.editor.scene_file_ops.ScenePlan(path: pathlib.Path, report: list[simvx.editor.scene_diff.ReportEntry], *, document: simvx.core.scene_io.SceneFile | simvx.core.scene_io.SceneModule, root: simvx.core.Node, tab: simvx.editor.workspace_tabs.SceneTabState | None, baseline: dict | None, fresh: dict | None, kept: dict[simvx.core.Node, set[str]], target: str | None)[source]

A save worked out in full, with nothing yet written.

A scene file is not always a thing the editor can write back exactly as the author left it. Children built in a loop, in a conditional or in a helper method have no statement the save can match against the scene, so it writes them out again beside the ones the file already builds; removing a child takes the author’s own statements that stood on it; and a value with no source form never reaches the file at all. All of that is known before anything is written – the reconciliation works on a parsed copy in memory – so it is offered here rather than reported afterwards over a file that has already been overwritten.

Meth:

SceneFileOps.plan_save produces one and writes nothing;

Meth:

SceneFileOps.commit_save writes it. The two together are

Meth:

SceneFileOps.save_scene, which commits a clean plan itself and puts a plan with a report in front of the user first – or commits that too, when there is no dialog wired to ask with and so nobody to ask.

A plan is only good for the file and the tree it was made from; see

Meth:

overtaken, which the commit consults before it writes anything.

Initialization

__slots__

(‘path’, ‘report’, ‘_document’, ‘_root’, ‘_tab’, ‘_baseline’, ‘_fresh’, ‘_kept’, ‘_target’)

property clean: bool[source]

Would this save carry the whole scene and change nothing else?

property destructive: bool[source]

Would this save change the author’s own file beyond their edits?

True when any entry of the report is

Data:

~simvx.editor.scene_diff.DESTRUCTIVE: statements swept out of __init__ with a removed child, an attribute left unbound, a child written a second time because the file builds it where the reconciliation cannot see it. That is the class worth stopping a user for, and it is what :meth:SceneFileOps.save_scene puts a prompt in front of.

A report of nothing but the other class is still a report – the save did not carry everything, the tab stays modified and every entry reaches the log – but it is not a question: the file keeps the line it had, and asking on every save of a scene holding one such slot would teach the user to click through the prompt without reading it.

overtaken() str | None[source]

What has moved since this plan was worked out, if anything.

A plan holds a document parsed from one copy of a file and bookkeeping keyed by the nodes of one tree, and it is written some time after both were read: the user is asked about it first, and the question invites them to go and edit those lines by hand. Either can move in the meantime. Writing a document parsed from text nobody has any more would drop whatever the author has since written; recording a baseline keyed by nodes the tab no longer holds – the file watcher reloads a tab by replacing its root outright – would leave every later save reporting every kept slot, and the tab could never go clean again.

So the answer is a sentence naming what moved, or None when the plan still describes the world it was made in.

class simvx.editor.scene_file_ops.SceneFileOps[source]

Mixin providing scene lifecycle and file dialog operations.

Methods in this class are designed to be mixed into State, which provides the workspace, signals, and delegating properties they depend on.

new_scene(root_type: type = Node, *, populate: bool = False)[source]

Create a new scene tab with the given root type.

open_scene(path: str | pathlib.Path)[source]

Load a scene from disk into the active tab, or a new tab.

save_scene(path: str | pathlib.Path | None = None, *, force: bool = False)[source]

Save the current scene.

Dispatches based on the active workspace tab:

  • Untitled scratch buffer: opens a file-save dialog. On accept the buffer’s text is written to disk and the tab is promoted to a regular file-backed script tab.

  • Script tab: forwards to workspace.save_current_script, which writes the editor text back to the file the tab edits.

  • Scene tab (or no active tab): falls through to the scene-save path.

Scene save:

The save is worked out first and written second (:meth:plan_save,

Meth:

commit_save). A plan that would change the author’s own file beyond carrying the scene across – take their statements out of __init__, leave an attribute unbound, write a second construction for a child it could not account for – is put in front of them first, and the file on disk is untouched until they accept (:attr:ScenePlan.destructive). Everything else is written straight away, including a save that could not carry a value into a line it will not overwrite: nothing of the author’s is lost there, the file keeps what it said, and the message reaches the log and the tab’s modified mark rather than a modal. The prompt needs a dialog wired to the editor (_save_report_dialog, installed by

Class:

~simvx.editor.root.Root); a caller with none, which means a headless or scripted save, commits either way and the report goes to the log as it always did.

Returns True only for a save that has been written and carried the whole scene. A save waiting on the user answers False, as does one that wrote a file the scene is still ahead of.

force=True skips the unresolved-reference warning dialog (used by the dialog’s “Save anyway” callback to avoid re-prompting).

plan_save(path: str | pathlib.Path | None = None) simvx.editor.scene_file_ops.ScenePlan | None[source]

Work out the save to path in full, writing nothing.

For an existing on-disk source: parse it and reconcile the runtime tree against the parsed source via :func:apply_runtime_diff, which preserves comments, blank lines, hand-written code, and import ordering. For a brand-new scene (the path does not exist): emit greenfield via :meth:SceneFile.from_runtime. Folder scenes (when path is a directory) route through :class:SceneModule. In every case the result is a parsed document held in memory and a

Attr:

ScenePlan.report of everything the save would do beyond carrying the user’s edits across; the file itself is not touched until

Meth:

commit_save.

A value the file would end up without is one entry of that report. Both routes can produce one. Greenfield produces one when the emitter cannot write a value at all – a texture built from pixels in memory, say. Preserve mode produces one for that same case, since a value with no source form has none here either, and additionally when the file spells a slot as an expression this layer will not overwrite; the test is then whether the file yields what the scene holds, so the message stops once the author either edits that expression or makes it yield the value the scene has – repointing the binding it names counts, and leaves the line itself untouched.

Answering that question costs a read of the file being written, which for a scene means running it: scenes are code, so there is no way to learn what an expression yields without executing the module. It is paid only when the reconciliation actually declined something, and it is paid here, while the copy on disk is still the one the author last saw.

None when there is nothing to plan: no destination and none recorded on the tab, or no scene open. Neither opens a dialog – that is

Meth:

save_scene’s business, not the planning’s.

commit_save(plan: simvx.editor.scene_file_ops.ScenePlan) bool[source]

Write what plan described, and bring the tab into line with it.

Returns True for a save that carried the whole scene. A plan with a report answers False and leaves the tab marked modified: the file on disk no longer holds everything the scene has, and calling that clean would tell the user their work is safe when part of it is not. Each entry goes to the log as an error, which is how it reaches the console panel.

A plan the world has moved under (:meth:ScenePlan.overtaken) is not written at all: it answers False and says why in the log. Work the plan out again to save that scene – which is what

Meth:

save_scene does when the user approves a plan that has gone stale while they were reading it.

simvx.editor.scene_file_ops.record_source_baseline(tab, root: simvx.core.Node, path: pathlib.Path | None = None, *, previous: dict | None = None, kept: dict[simvx.core.Node, set[str]] | None = None) None[source]

Record what the file tab now describes says about the scene in it.

Every route by which a tab comes to stand for a file goes through here –

Meth:

SceneFileOps.open_scene, the live-file import and the reload the file watcher triggers (:meth:LiveFileOps.open_file), the project session’s own open, and each save. Two things are kept, both keyed by runtime object so they survive renames and property edits: which source var each child came from, and what the file said about every constructor kwarg. Miss a route and the keys are objects from a tree that no longer exists, at which point every save reports every kept slot and the tab can never go clean again.

After a save, previous and kept correct the one place the file and the tree disagree. The tree is otherwise the right answer – the save just wrote it out – but a slot the diff declined to write still says whatever it said before, so its old value is carried forward. Recording the live one there would have the file claiming to hold the very edit it dropped, and the next save would go clean over it.