"""Scene I/O: parso-based round-trip parsing and editing of scene `.py` files.
Tier 1 (`source_tree`) provides the lossless parse/dump primitive; Tier 2
(`edits`) provides prefix-preserving editing primitives that operate on the
parso tree returned by Tier 1. Tier 3a (`emitter`, `detection`) provides
greenfield Python emission for live node trees and lightweight scene-file
detection.
Every name resolves lazily (PEP 562). This package sits on the eager
``import simvx.core`` chain (``hot_reload`` imports ``loader``), and five of
its modules need parso, which the browser runtime does not ship: an eager
import here makes the whole engine unimportable in a web export. Loading a
scene *file* stays parso-free; the parso-backed editing surface pays its
import cost on first use.
"""
from typing import TYPE_CHECKING, Any
if TYPE_CHECKING:
# Restated for type checkers, which cannot follow the ``__getattr__``
# below and otherwise see every one of these names as ``Any``. Nothing
# here runs, so parso stays off the import chain exactly as before.
from . import edits # noqa: F401
from .detection import ( # noqa: F401
AmbiguousSceneError,
has_procedural_construction,
is_scene_path,
primary_node_class_from_source,
)
from .emitter import ( # noqa: F401
UnemittableValueError,
emit_node_construction,
emit_scene,
emit_value,
engine_computed,
expression_describes,
expression_is_opaque,
expression_reads_a_shape,
helper_import_module,
iter_runtime_kwargs,
structural_type_name,
)
from .loader import ( # noqa: F401
load_scene,
)
from .scene_file import ( # noqa: F401
ImportSet,
SceneClass,
SceneFile,
)
from .scene_module import ( # noqa: F401
NotASceneModuleError,
SceneModule,
)
from .source_tree import ( # noqa: F401
SourceTree,
parse_snippet,
parse_source,
)
from .symbols import ( # noqa: F401
ClassDefRef,
UseSiteRef,
find_class_definitions,
find_class_uses,
rename_class_in_source,
rename_module_in_imports,
)
from .syntax import ( # noqa: F401
SyntaxIssue,
UnsupportedSceneSyntaxError,
)
# name -> submodule that defines it. "edits" maps to itself: the submodule is
# the exported object.
_EXPORTS = {
"edits": "edits",
"AmbiguousSceneError": "detection",
"has_procedural_construction": "detection",
"is_scene_path": "detection",
"primary_node_class_from_source": "detection",
"UnemittableValueError": "emitter",
"emit_node_construction": "emitter",
"emit_scene": "emitter",
"emit_value": "emitter",
"engine_computed": "emitter",
"expression_describes": "emitter",
"expression_is_opaque": "emitter",
"expression_reads_a_shape": "emitter",
"helper_import_module": "emitter",
"iter_runtime_kwargs": "emitter",
"structural_type_name": "emitter",
"load_scene": "loader",
"ImportSet": "scene_file",
"SceneClass": "scene_file",
"SceneFile": "scene_file",
"NotASceneModuleError": "scene_module",
"SceneModule": "scene_module",
"SourceTree": "source_tree",
"parse_snippet": "source_tree",
"parse_source": "source_tree",
"ClassDefRef": "symbols",
"UseSiteRef": "symbols",
"find_class_definitions": "symbols",
"find_class_uses": "symbols",
"rename_class_in_source": "symbols",
"rename_module_in_imports": "symbols",
"SyntaxIssue": "syntax",
"UnsupportedSceneSyntaxError": "syntax",
}
__all__ = sorted(_EXPORTS)
if not TYPE_CHECKING:
# Hidden from type checkers on purpose. A module-level ``__getattr__`` makes
# a checker accept ANY attribute name, which would leave the block above
# decorative: a name dropped from ``_EXPORTS``, or a typo at a call site,
# would still check clean. With it hidden, the restated names are the whole
# published surface and anything else is an error.
def __getattr__(name: str) -> Any:
module_name = _EXPORTS.get(name)
if module_name is None:
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
from importlib import import_module
module = import_module(f".{module_name}", __name__)
value = module if name == module_name else getattr(module, name)
globals()[name] = value
return value
[docs]
def __dir__() -> list[str]:
return sorted(set(globals()) | set(__all__))