Source code for rath.backend.registry
"""Backend registry: register / lookup / select.
Backends register a class under a string name. Public lookup helpers operate
on classes; ``get(name)`` instantiates a backend on demand.
"""
from __future__ import annotations
import threading
from collections.abc import Callable
from typing import TypeVar
from rath.backend.abc import Backend
from rath.backend.errors import BackendNotFound
_REGISTRY: dict[str, type[Backend]] = {}
_DEFAULT: dict[str, str] = {}
_INSTANCES: dict[str, Backend] = {}
_INSTANCE_LOCK = threading.Lock()
B = TypeVar("B", bound=Backend)
def register(name: str) -> Callable[[type[B]], type[B]]:
"""Decorator: register a :class:`Backend` subclass under ``name``."""
def decorator(cls: type[B]) -> type[B]:
if name in _REGISTRY:
raise ValueError(f"backend {name!r} is already registered")
cls.name = name
_REGISTRY[name] = cls
return cls
return decorator
def list_names() -> list[str]:
"""Return the names of all registered backends, in registration order."""
return list(_REGISTRY)
[docs]
def get(name: str) -> Backend:
"""Look up a backend by name and return the per-process singleton instance.
The same instance is returned for repeated calls with the same ``name``,
so per-backend caches (e.g. ``OpenSandboxBackend._natives``) and the
sandbox refcount are coherent across all sessions in the process.
"""
cls = _get_class(name)
with _INSTANCE_LOCK:
inst = _INSTANCES.get(name)
if inst is None:
inst = cls()
_INSTANCES[name] = inst
return inst
def get_class(name: str) -> type[Backend]:
"""Look up the registered class for ``name`` without instantiating it."""
return _get_class(name)
def is_available(name: str) -> bool:
"""Return ``True`` iff a backend named ``name`` is registered and available."""
if name not in _REGISTRY:
return False
return _REGISTRY[name].is_available()
[docs]
def preferred(names: list[str]) -> Backend:
"""Return an instance of the first available backend in ``names``.
Raises :class:`BackendNotFound` if none of the listed backends are
registered and available.
"""
for n in names:
if n in _REGISTRY and _REGISTRY[n].is_available():
return get(n)
available = [n for n in _REGISTRY if _REGISTRY[n].is_available()]
raise BackendNotFound(name=", ".join(names), available=available)
def set_default(name: str) -> None:
"""Set the default backend used by :func:`current`."""
_get_class(name) # Raises if ``name`` is unknown.
_DEFAULT["name"] = name
def current() -> Backend:
"""Return a fresh instance of the default backend.
Raises :class:`BackendNotFound` if no default has been set.
"""
if "name" not in _DEFAULT:
raise BackendNotFound(name="<default>", available=list_names())
return get(_DEFAULT["name"])
def _get_class(name: str) -> type[Backend]:
if name not in _REGISTRY:
raise BackendNotFound(name=name, available=list_names())
return _REGISTRY[name]
def _reset() -> None:
"""Clear the registry. Test-only helper."""
_REGISTRY.clear()
_DEFAULT.clear()
with _INSTANCE_LOCK:
_INSTANCES.clear()
def _reset_instances() -> None:
"""Drop cached backend singletons without clearing class registrations.
Test-only helper: keeps ``LocalBackend`` / ``OpenSandboxBackend`` registered
but forces ``get(name)`` to construct fresh instances on next call. Useful
when a test wants an isolated backend (e.g. its own ``_open_handles`` set)
without re-registering classes.
"""
with _INSTANCE_LOCK:
_INSTANCES.clear()