Source code for srdatalog.viz.introspect

'''Import a user's .py program file and find its Program instance.

The auto-generated files produced by `tools/nim_to_dsl.py` all follow
the same shape:

    def build_<name>_program(meta: dict[str, int] | None = None) -> Program:
        ...

Users can also write programs that just construct a Program at module
top level. This module handles both shapes and plumbs optional
`meta.json` + `entry` overrides through.
'''

from __future__ import annotations

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

if TYPE_CHECKING:
  from srdatalog.dsl import Program


[docs] class ProgramDiscoveryError(RuntimeError): '''Raised when `load_program` can't find or build a Program.'''
[docs] def load_program( path: str | Path, *, entry: str | None = None, meta: dict | str | Path | None = None, ) -> Program: '''Import the .py file, discover and invoke a Program builder. Discovery order (stops at first match): 1. `entry` arg — explicit function name; takes precedence. 2. A top-level callable matching `build_*_program`. 3. A module-level `Program` instance (non-callable). `meta` — dataset_const metadata. Passed to the builder if the builder accepts a positional arg. Can be: - dict: passed as-is - str / Path: loaded as JSON - None: builder called with no args ''' from srdatalog.dsl import Program # local import to avoid cycles path = Path(path).resolve() if not path.exists(): raise ProgramDiscoveryError(f"source file not found: {path}") module = _import_file(path) # 1. Explicit entry if entry is not None: if not hasattr(module, entry): raise ProgramDiscoveryError(f"{path}: no attribute {entry!r}") candidate = getattr(module, entry) else: # 2. Auto-discovered build_*_program callable candidate = None for name in dir(module): if name.startswith("build_") and name.endswith("_program"): fn = getattr(module, name) if callable(fn): candidate = fn break # 3. Module-level Program instance if candidate is None: for name in dir(module): if name.startswith("_"): continue val = getattr(module, name) if isinstance(val, Program): return val raise ProgramDiscoveryError( f"{path}: no Program found. Expected `build_<name>_program()` or a top-level Program." ) if not callable(candidate): if isinstance(candidate, Program): return candidate raise ProgramDiscoveryError( f"{path}: {entry or candidate.__name__!r} is not callable and not a Program" ) meta_dict = _resolve_meta(meta) result = _invoke_builder(candidate, meta_dict) if not isinstance(result, Program): raise ProgramDiscoveryError(f"{path}: builder returned {type(result).__name__}, not Program") return result
def _import_file(path: Path): # Use a sanitized module name so repeated loads don't collide in sys.modules. mod_name = f"_srdatalog_viz_user_{path.stem}_{abs(hash(str(path))):x}" spec = importlib.util.spec_from_file_location(mod_name, path) if spec is None or spec.loader is None: raise ProgramDiscoveryError(f"could not create import spec for {path}") module = importlib.util.module_from_spec(spec) sys.modules[mod_name] = module # Let the user's file import relative to its directory (doop_run.py style). sys.path.insert(0, str(path.parent)) try: spec.loader.exec_module(module) finally: if sys.path and sys.path[0] == str(path.parent): sys.path.pop(0) return module def _resolve_meta(meta): if meta is None: return None if isinstance(meta, dict): return meta return json.loads(Path(meta).read_text()) def _invoke_builder(fn, meta_dict): '''Call fn(). If it accepts a positional arg and meta is provided, pass meta. If fn needs a required arg and meta is None, raise a helpful error rather than TypeError-ing inside the user code.''' sig = inspect.signature(fn) positional = [ p for p in sig.parameters.values() if p.kind in (p.POSITIONAL_ONLY, p.POSITIONAL_OR_KEYWORD) ] required = [p for p in positional if p.default is inspect.Parameter.empty] if required: if meta_dict is None: raise ProgramDiscoveryError( f"{fn.__name__} requires {len(required)} arg(s) — pass --meta <json>" ) return fn(meta_dict) if positional and meta_dict is not None: return fn(meta_dict) return fn()