Skip to content

Adapter Registries

The dependency injection layer, where adapters are registered against the ports declared in hololinked.core.

Each registry below resolves an implementation by name, on first use.

Registry Resolves Adapters live in
Serializers serializers hololinked.serializers
SchemaValidators schema validators hololinked.schema_validators
StorageBackends storage backends hololinked.storage
MetadataFormats device description languages hololinked.metadata

The implementation is either already available in the package or can be supplied by the end user. All four classes above are singletons, so anything set on them applies process-wide.

hololinked.injection.AdapterRegistry

Bases: MappableSingleton

Metaclass that imports an adapter the first time it is asked for (i.e. a lazy import).

Adapter is a concrete implementation of a specific dependency, for example, JSONSchemaValidator is an adapter for schema validation based on JSON schema, whereas pydantic is a different implementation. However, their interfaces or external behaviour is similar, so they can be used interchangeably or for different technological reasons.

Each adapter lives in its own module, so resolving one never imports the dependencies of the others. This lets one install only the dependencies you need. For individual adapters types, for example schema validators or serializers, a registry class is defined that uses this metaclass to resolve adapters by name, as a lazy import at runtime.

This metaclass providers lazy import functionality.

Source code in repo/hololinked/hololinked/injection.py
class AdapterRegistry(MappableSingleton):
    """
    Metaclass that imports an adapter the first time it is asked for (i.e. a lazy import).

    Adapter is a concrete implementation of a specific dependency, for example, `JSONSchemaValidator` is an adapter for
    schema validation based on JSON schema, whereas pydantic is a different implementation. However, their interfaces
    or external behaviour is similar, so they can be used interchangeably or for different technological reasons.

    Each adapter lives in its own module, so resolving one never imports the dependencies of the others. This lets
    one install only the dependencies you need. For individual adapters types, for example schema validators or
    serializers, a registry class is defined that uses this metaclass to resolve adapters by name, as a lazy import
    at runtime.

    This metaclass providers lazy import functionality.
    """

    package: ClassVar[str] = ""
    """Adapter package the implementations are served from."""

    modules: ClassVar[dict[str, str | tuple[str, str]]] = {}
    """Name of an implementation mapped to the class that provides it."""

    tables: ClassVar[tuple[str, ...]] = ()
    """
    Names of the registry's lookup dictionaries, snapshotted at class creation. 
    One can clear the cache using `forget_adapters()`.
    """

    adapter_kind: ClassVar[str] = "adapter"
    """What this registry holds ("serializers", "schema-validators", "ddl" etc.), used in error messages."""

    instantiate: ClassVar[bool] = False
    """Whether a resolved adapter class needs to be instantiated, or whether the class itself is what is used."""

    def __init__(cls, name: str, bases: tuple[type, ...], namespace: dict[str, Any], **kwargs: Any) -> None:
        """Initialize the registry."""
        super().__init__(name, bases, namespace, **kwargs)
        cls._installed = set()  # type: set[str]
        cls._adapters = {}  # type: dict[tuple[str, str], Any]
        cls._pristine = {table: dict(getattr(cls, table)) for table in cls.tables}

    def __getattr__(cls, name: str) -> Any:
        if name not in cls.modules:
            raise AttributeError(f"no {cls.adapter_kind} is registered under the name {name!r}")
        import importlib

        target = cls.modules[name]
        module_path, attribute = target if isinstance(target, tuple) else (cls.package, target)
        try:
            adapter = getattr(importlib.import_module(module_path), attribute)
        except ModuleNotFoundError as ex:
            if ex.name and not ex.name.startswith("hololinked"):
                raise ModuleNotFoundError(
                    f"the {name!r} {cls.adapter_kind} needs {ex.name!r}, which is not installed. "
                    + "Please install first."
                ) from ex
            raise
        if cls.instantiate:
            # one instance per adapter class, so that aliases resolving to the same class - Serializers.default
            # and Serializers.json, say - also resolve to the same object
            if (module_path, attribute) not in cls._adapters:
                cls._adapters[(module_path, attribute)] = adapter()
            adapter = cls._adapters[(module_path, attribute)]
        cls.install(name, adapter)
        return adapter

    def install(cls, name: str, adapter: Any) -> None:
        """
        Caching the adapter so that subsequent access skips `__getattr__`.

        Parameters
        ----------
        name: str
            the name to serve the adapter under
        adapter: Any
            the adapter, a class or an instance depending on the registry
        """
        setattr(cls, name, adapter)
        cls._installed.add(name)

    def forget_adapters(cls) -> None:
        """Drop every resolved adapter and restore the lookup tables, leaving the registry as it was on import."""
        for name in cls._installed | set(cls.modules):
            if name in cls.__dict__:
                delattr(cls, name)
        cls._installed.clear()
        cls._adapters.clear()
        for table, pristine in cls._pristine.items():
            current = getattr(cls, table)
            current.clear()
            current.update(pristine)

Attributes

adapter_kind class-attribute

adapter_kind: str = 'adapter'

What this registry holds ("serializers", "schema-validators", "ddl" etc.), used in error messages.

package class-attribute

package: str = ''

Adapter package the implementations are served from.

modules class-attribute

modules: dict[str, str | tuple[str, str]] = {}

Name of an implementation mapped to the class that provides it.

tables class-attribute

tables: tuple[str, ...] = ()

Names of the registry's lookup dictionaries, snapshotted at class creation. One can clear the cache using forget_adapters().

instantiate class-attribute

instantiate: bool = False

Whether a resolved adapter class needs to be instantiated, or whether the class itself is what is used.

Functions

install

install(name: str, adapter: Any) -> None

Caching the adapter so that subsequent access skips __getattr__.

Parameters:

Name Type Description Default
name
str

the name to serve the adapter under

required
adapter
Any

the adapter, a class or an instance depending on the registry

required
Source code in repo/hololinked/hololinked/injection.py
def install(cls, name: str, adapter: Any) -> None:
    """
    Caching the adapter so that subsequent access skips `__getattr__`.

    Parameters
    ----------
    name: str
        the name to serve the adapter under
    adapter: Any
        the adapter, a class or an instance depending on the registry
    """
    setattr(cls, name, adapter)
    cls._installed.add(name)

forget_adapters

forget_adapters() -> None

Drop every resolved adapter and restore the lookup tables, leaving the registry as it was on import.

Source code in repo/hololinked/hololinked/injection.py
def forget_adapters(cls) -> None:
    """Drop every resolved adapter and restore the lookup tables, leaving the registry as it was on import."""
    for name in cls._installed | set(cls.modules):
        if name in cls.__dict__:
            delattr(cls, name)
    cls._installed.clear()
    cls._adapters.clear()
    for table, pristine in cls._pristine.items():
        current = getattr(cls, table)
        current.clear()
        current.update(pristine)

hololinked.injection.Registry

Base for the registries below, so that a registry instance resolves adapters the way its class does.

All members are class attributes, and only the metaclass knows how to resolve an unresolved adapter

Source code in repo/hololinked/hololinked/injection.py
class Registry(metaclass=AdapterRegistry):
    """
    Base for the registries below, so that a registry instance resolves adapters the way its class does.

    All members are class attributes, and only the metaclass knows how to resolve an unresolved adapter
    """

    def __getattr__(self, name: str) -> Any:
        return getattr(type(self), name)