Source code for matmelt.backends.registry
"""The backend registry: ``kind`` string -> ``ComputeBackend`` class.
The single seam where *config* becomes a *running backend*. A user's YAML names
each backend only by ``kind`` (``local``, ``ssh_exec``, ...); this module maps
that string to the concrete class and instantiates it. Because the lookup is by
string, adding a backend is one subclass + one ``register`` call -- no stage,
orchestrator, or CLI change.
Registration is an **explicit call**, not an import-time decorator, so there is
no hidden import-order dependency: a concrete backend module calls
``register("local", LocalBackend)`` at the bottom of its file, and whoever wires
the tool imports the backend modules it intends to offer.
**Construction contract:** a registered class is instantiated as
``cls(name, options)`` -- the backend's configured name plus its free-form
options dict. The dict (rather than unpacked kwargs) keeps the contract stable
as backends grow new options; each backend validates what it needs in its own
``__init__`` and raises ``BackendError`` for a missing/invalid option.
"""
from __future__ import annotations
from typing import Any
from matmelt.backends.base import ComputeBackend
from matmelt.core.jobspec import BackendError
#: The live ``kind`` -> class table. Module-global and intentionally mutable:
#: concrete backend modules register into it when imported. Starts empty.
_REGISTRY: dict[str, type[ComputeBackend]] = {}
[docs]
def register(kind: str, cls: type[ComputeBackend]) -> None:
"""Register ``cls`` as the implementation of ``kind``.
Called explicitly at the bottom of each concrete backend module.
Parameters
----------
kind
The string a config's ``backends.<name>.kind`` names.
cls
The implementing class, which must be a ``ComputeBackend`` subclass.
Raises
------
BackendError
If ``cls`` is not a ``ComputeBackend`` subclass, or if ``kind`` is
already registered to a *different* class. Re-registering the same
class is idempotent, so a module re-import is a no-op, but silently
shadowing an existing kind is refused.
"""
if not (isinstance(cls, type) and issubclass(cls, ComputeBackend)):
raise BackendError(
f"cannot register {cls!r} for kind {kind!r}: not a ComputeBackend subclass"
)
existing = _REGISTRY.get(kind)
if existing is not None and existing is not cls:
raise BackendError(
f"kind {kind!r} is already registered to {existing.__name__}; "
f"refusing to shadow it with {cls.__name__}"
)
_REGISTRY[kind] = cls
[docs]
def get(kind: str) -> type[ComputeBackend]:
"""Return the class registered under ``kind``.
Parameters
----------
kind
The backend kind to look up.
Returns
-------
type
The registered ``ComputeBackend`` subclass.
Raises
------
BackendError
If nothing is registered under ``kind``. The message names the kinds
that *are* known, so a config typo produces an actionable message
rather than a ``KeyError``.
"""
try:
return _REGISTRY[kind]
except KeyError:
known = ", ".join(sorted(_REGISTRY)) or "(none registered)"
raise BackendError(f"unknown backend kind {kind!r}; known kinds: {known}") from None
[docs]
def create_backend(name: str, kind: str, options: dict[str, Any]) -> ComputeBackend:
"""Instantiate the backend ``name`` of ``kind`` with its ``options`` dict.
The one seam where a config block becomes a live backend.
Parameters
----------
name
The user's configured name for this instance, carried into every
``Handle`` it issues.
kind
Which registered implementation to construct.
options
The backend's free-form ``options:`` block, passed through per the
``cls(name, options)`` contract.
Returns
-------
ComputeBackend
The constructed, option-validated backend.
Raises
------
BackendError
If ``kind`` is unknown, or the backend's own ``__init__`` validation
rejects an option -- a missing required value or a bad one.
Examples
--------
>>> from matmelt.backends import local # registers kind "local"
>>> backend = create_backend("laptop", "local", {"max_parallel": 4})
>>> backend.name, backend.capabilities().max_parallel
('laptop', 4)
"""
cls = get(kind)
return cls(name, options)
[docs]
def known_kinds() -> list[str]:
"""List the backend kinds currently registered.
Returns
-------
list of str
Sorted kinds, used in diagnostics and error messages. Which kinds are
present depends on which backend modules have been imported.
"""
return sorted(_REGISTRY)