Source code for modelly.studio.loader

"""Find the model class and the portrayal a ``modelly studio`` command line names.

A ``path/to/model.py`` is imported under its package's dotted name when its folders are
packages (``modelly/examples/basic/schelling/model.py`` is
``modelly.examples.basic.schelling.model``), else under its own name with its folder on
``sys.path``. Either way the classes are importable by name, so checkpoints of the model
load again, and a model's ``from agents import Sheep`` finds the file next to it.
"""

from __future__ import annotations

import importlib
import inspect
import sys
from collections.abc import Callable
from pathlib import Path
from types import ModuleType

from modelly.core.model import Model
from modelly.profile.loader import TargetError

INIT = "__init__.py"


[docs] def module_name(path: Path) -> tuple[Path, str]: """The folder to put on ``sys.path`` and the dotted name to import ``path`` by.""" parts = [path.stem] folder = path.parent while (folder / INIT).is_file(): parts.insert(0, folder.name) folder = folder.parent return folder, ".".join(parts)
[docs] def import_source(source: str) -> ModuleType: """Import ``source``: a ``.py`` file or a dotted module name. Raises: TargetError: When the file does not exist or the module cannot be imported. """ path = Path(source) if path.suffix == ".py": if not path.is_file(): raise TargetError(f"no such file: {source}") root, source = module_name(path.resolve()) if str(root) not in sys.path: sys.path.insert(0, str(root)) try: return importlib.import_module(source) except ImportError as error: raise TargetError(f"cannot import {source!r}: {error}") from error
def _split(target: str, what: str) -> tuple[str, str]: """``source:name`` to ``(source, name)``.""" source, sep, name = target.rpartition(":") if not sep or not source or not name: raise TargetError( f"expected path/to/file.py:{what} or package.module:{what}, got {target!r}" ) return source, name
[docs] def load_model_class(target: str) -> type[Model]: """The Model subclass ``path/to/model.py:Class`` or ``package.module:Class`` names. Raises: TargetError: When it cannot be found or is not a Model subclass. """ source, name = _split(target, "ModelClass") cls = getattr(import_source(source), name, None) if not (inspect.isclass(cls) and issubclass(cls, Model)): raise TargetError(f"{name!r} in {source} is not a Model subclass") return cls
[docs] def load_portrayal(target: str) -> Callable: """The agent portrayal function ``path/to/app.py:function`` names. Raises: TargetError: When it cannot be found or is not callable. """ source, name = _split(target, "function") function = getattr(import_source(source), name, None) if not callable(function): raise TargetError(f"{name!r} in {source} is not a function") return function
__all__ = ["import_source", "load_model_class", "load_portrayal", "module_name"]