Source code for simvx.core.scene_io.loader

"""Runtime loading: import a scene `.py` file or folder and instantiate its primary Node.

`load_scene` is the canonical way to take a path on disk and return a live
:class:`~simvx.core.Node` tree. For *editing* the source, use
:class:`SceneFile` / :class:`SceneModule`; this module is the runtime side.
"""

from __future__ import annotations

import importlib
import importlib.util
import sys
from pathlib import Path
from typing import TYPE_CHECKING

from .detection import primary_node_class_from_source
from .scene_module import SceneModule

if TYPE_CHECKING:
    from ..node import Node

__all__ = ["load_scene"]


[docs] def load_scene(path: str | Path) -> Node: """Load a scene from a ``.py`` file or scene-module folder. File path → imports the module (under a unique synthetic name) and instantiates the primary :class:`Node` subclass. Folder path → imports the package via :func:`importlib.import_module` (after pinning ``path.parent`` onto :data:`sys.path`) and instantiates the primary class declared in ``__init__.py`` or, as a fallback, ``<folder>/<folder>.py``. In both cases :meth:`ScriptManager.load_tree` is run on the result so nodes carrying ``script`` references resolve relative to the project layout (``project/scenes/foo.py`` → ``project/`` is the script root). """ p = Path(path) if p.is_dir(): return _load_folder(p) return _load_file(p)
def _load_file(path: Path) -> Node: from ..node import Node from ..script import ScriptManager class_name = primary_node_class_from_source(path.read_text(encoding="utf-8"), path=path) if class_name is None: raise ValueError(f"No Node subclass found in {path}") module_name = f"_simvx_scene_{path.stem}_{id(path)}" spec = importlib.util.spec_from_file_location(module_name, str(path)) if spec is None or spec.loader is None: raise ValueError(f"Cannot load scene from {path}") module = importlib.util.module_from_spec(spec) sys.modules[module_name] = module try: spec.loader.exec_module(module) except Exception: sys.modules.pop(module_name, None) raise node_cls = getattr(module, class_name, None) if node_cls is None or not isinstance(node_cls, type) or not issubclass(node_cls, Node): raise ValueError(f"Class {class_name!r} in {path} is not a Node subclass") root = node_cls() project_dir = str(path.parent.parent) if path.parent.name == "scenes" else str(path.parent) ScriptManager.load_tree(root, project_dir) return root def _load_folder(path: Path) -> Node: from ..node import Node from ..script import ScriptManager if not SceneModule.is_folder_scene(path): raise ValueError(f"{path} is not a scene module") parent_str = str(path.parent.resolve()) if parent_str not in sys.path: sys.path.insert(0, parent_str) package = importlib.import_module(path.name) init = path / "__init__.py" namespaced = path / f"{path.name}.py" class_name: str | None = None holder = package if init.is_file(): text = init.read_text(encoding="utf-8") candidate = primary_node_class_from_source(text, path=init) if candidate is not None and hasattr(package, candidate): class_name = candidate if class_name is None and namespaced.is_file(): text = namespaced.read_text(encoding="utf-8") candidate = primary_node_class_from_source(text, path=namespaced) if candidate is not None: sub = importlib.import_module(f"{path.name}.{path.name}") if hasattr(sub, candidate): class_name = candidate holder = sub if class_name is None: raise ValueError(f"No Node subclass resolvable in scene module {path}") node_cls = getattr(holder, class_name) if not isinstance(node_cls, type) or not issubclass(node_cls, Node): raise ValueError(f"Class {class_name!r} in {path} is not a Node subclass") root = node_cls() ScriptManager.load_tree(root, str(path)) return root