Source code for simvx.core.run

"""simvx.run() — unified launch function for SimVX projects.

Handles all launch modes:
    import simvx
    simvx.run()                          # Read simvx.toml, find Main class
    simvx.run(MyNode)                    # Run specific class
    simvx.run(MyNode, width=800)         # With explicit settings
"""

import importlib
import importlib.util
import inspect
import logging
import sys
from pathlib import Path
from typing import Any

from .project import ProjectSettings, find_project, load_project

log = logging.getLogger(__name__)

__all__ = ["run"]

def _find_node_classes(module) -> list[tuple[str, type]]:
    """Find all Node subclass definitions in a module."""
    from .node import Node

    results = []
    for name, obj in inspect.getmembers(module, inspect.isclass):
        if issubclass(obj, Node) and obj is not Node and obj.__module__ == module.__name__:
            results.append((name, obj))
    return results

def _resolve_class_from_module(module, filename: str = "", class_name: str | None = None) -> type:
    """Resolve which Node subclass to run from a module.

    Priority:
        1. class_name if specified
        2. 'Main' class
        3. Class matching filename (case-insensitive)
        4. Sole Node subclass
        5. Error
    """
    classes = _find_node_classes(module)
    if not classes:
        raise RuntimeError(f"No Node subclasses found in {module.__name__}")

    class_map = {name: cls for name, cls in classes}

    # 1. Explicit class name
    if class_name:
        if class_name in class_map:
            return class_map[class_name]
        raise RuntimeError(f"Class {class_name!r} not found in {module.__name__}. Available: {list(class_map)}")

    # 2. 'Main' class
    if "Main" in class_map:
        return class_map["Main"]

    # 3. Filename match (case-insensitive)
    if filename:
        stem = Path(filename).stem.lower()
        for name, cls in classes:
            if name.lower() == stem:
                return cls

    # 4. Sole class
    if len(classes) == 1:
        return classes[0][1]

    # 5. Error
    raise RuntimeError(
        f"Multiple Node subclasses in {module.__name__}, cannot determine which to run. "
        f"Available: {list(class_map)}. Define a 'Main' class or use simvx.run(ClassName)."
    )

def _import_file(file_path: str) -> Any:
    """Import a Python file as a module."""
    path = Path(file_path).resolve()
    if not path.exists():
        raise FileNotFoundError(f"File not found: {path}")
    module_name = f"_simvx_run_{path.stem}"
    spec = importlib.util.spec_from_file_location(module_name, str(path))
    if spec is None or spec.loader is None:
        raise ImportError(f"Cannot import {path}")
    module = importlib.util.module_from_spec(spec)
    sys.modules[module_name] = module
    spec.loader.exec_module(module)
    return module

[docs] def run( node_class=None, *, width: int | None = None, height: int | None = None, title: str | None = None, file: str | None = None, class_name: str | None = None, **overrides, ) -> None: """Run a SimVX application. Args: node_class: Node subclass to instantiate and run. If None, resolves from simvx.toml or ``file`` parameter. width: Window width override. height: Window height override. title: Window title override. file: Python file to import and find a Node class in. class_name: Specific class name to run from the file/module. **overrides: Additional App kwargs (vsync, physics_fps, etc.). """ # Load project settings if available settings: ProjectSettings | None = None toml_path = find_project() if toml_path: settings = load_project(toml_path) log.debug("run: loaded project %s from %s", settings.name, toml_path) # Resolve the node class if node_class is None: if file: # Import specific file module = _import_file(file) node_class = _resolve_class_from_module(module, filename=file, class_name=class_name) elif settings and settings.main: # Use main from simvx.toml main_path = settings.resolve_path(settings.main) module = _import_file(main_path) node_class = _resolve_class_from_module(module, filename=main_path, class_name=class_name) else: raise RuntimeError( "No node_class specified and no simvx.toml found (or no 'main' set). " "Use simvx.run(MyNode) or create a simvx.toml with main = 'path/to/main.py'." ) # Build App kwargs from settings + overrides app_kwargs: dict[str, Any] = {} if settings: app_kwargs["width"] = settings.display.get("width", 1280) app_kwargs["height"] = settings.display.get("height", 720) app_kwargs["title"] = settings.name app_kwargs["vsync"] = settings.display.get("vsync", True) app_kwargs["physics_fps"] = settings.physics.get("fps", 60) # Apply input actions settings.apply_input_actions() # Explicit args override settings if width is not None: app_kwargs["width"] = width if height is not None: app_kwargs["height"] = height if title is not None: app_kwargs["title"] = title app_kwargs.update(overrides) # Lazy import graphics from simvx.graphics import App app = App(**app_kwargs) app.run(node_class())