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())