Source code for simvx.core.scene_io

"""Scene I/O: round-trip parsing and editing of scene `.py` files.

Tier 1 (`source`) is the seam the round-trip reads and edits source through: a
document of statements, trivia and arguments, backed by a parser the tier above
never sees. Tier 2 (`scene_file`, `scene_module`) is the scene the editor holds.
Tier 3 (`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 the
editing surface is far more than the browser runtime needs to run a scene: an
eager import here would make every consumer pay for it. Loading a scene *file*
costs the loader alone; the 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 the import chain stays exactly as cheap as before.
    from . import source  # 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,
        import_module_for_type,
        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 .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. "source" maps to itself: the submodule is
# the exported object. Importing it costs nothing -- it resolves its backend on
# the first parse, not on import -- so it keeps the cheap-import property this
# whole module exists for.
_EXPORTS = {
    "source": "source",
    "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",
    "import_module_for_type": "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",
    "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__))