Source code for simvx.core.scene_io

"""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__))