Source code for quchip.utils.registry

"""Subclass registration and serialization dispatch.

The declaration ``class BaseThing(Registrable, registry_root=True)`` makes a
registry for that family. Concrete subclasses register under their qualified
names. The registry excludes registry roots and abstract classes.

``to_dict`` supplies the ``type`` tag. A registry root's ``from_dict`` finds
the concrete class and passes it the payload and all extra arguments. Concrete
classes implement ``from_dict`` or ``_from_dict_payload``. The default payload
constructor is ``cls()``.
"""

from __future__ import annotations

from typing import Any, ClassVar


def _is_abstract(cls: type) -> bool:
    """Check for abstract methods before ``ABCMeta`` sets ``__abstractmethods__``.

    Registration runs inside ``__init_subclass__``, before that attribute exists.
    Inspect the most-derived definition of each abstract name in the MRO.
    """
    abstract_names = {
        name
        for klass in cls.__mro__
        for name, value in vars(klass).items()
        if getattr(value, "__isabstractmethod__", False)
    }
    for name in abstract_names:
        for klass in cls.__mro__:
            if name in vars(klass):
                if getattr(vars(klass)[name], "__isabstractmethod__", False):
                    return True
                break
    return False


[docs] class Registrable: """Mixin that owns a subclass registry and the shared (de)serialization contract.""" #: Per-root mapping ``fully-qualified-name -> concrete subclass``. Installed #: fresh on each registry root; inherited (shared) by every subclass below it. _registry: ClassVar[dict[str, type["Registrable"]]] #: The class that declared the registry (``registry_root=True``). Used by #: :meth:`from_dict` to tell the dispatching root from a concrete leaf. _registry_root: ClassVar[type["Registrable"]] def __init_subclass__(cls, *, registry_root: bool = False, **kwargs: Any) -> None: """Register *cls*, or start a new registry when *registry_root* is set. ``registry_root=True`` installs a new :attr:`_registry` on *cls* and excludes *cls* from that registry. Otherwise, concrete subclasses register under their fully qualified name (:meth:`_type_key`). Abstract subclasses are skipped. """ super().__init_subclass__(**kwargs) if registry_root: cls._registry = {} cls._registry_root = cls return # Abstract subclasses can't be instantiated → never serialized → skip. if _is_abstract(cls): return cls._registry[cls._type_key()] = cls @classmethod def _type_key(cls) -> str: """Return the fully-qualified registry key for *cls* (its serialization ``type``).""" return f"{cls.__module__}.{cls.__qualname__}"
[docs] def to_dict(self) -> dict[str, Any]: """Serialize the type tag; subclasses add their own fields.""" return {"type": type(self)._type_key()}
[docs] @classmethod def from_dict(cls, data: dict[str, Any], *args: Any, **kwargs: Any) -> Any: r"""Reconstruct the object from the output of :meth:`to_dict`. On the registry root, dispatch to the concrete subclass that ``data["type"]`` names, and send ``*args`` / ``**kwargs`` to it. On a concrete subclass, use :meth:`_from_dict_payload`. Concrete subclasses that carry a payload override this method directly. Parameters ---------- data : dict Serialized declaration that contains the fully qualified type key. *args : object Additional positional arguments for reconstruction, for example coupling endpoints. **kwargs : object Keyword arguments for reconstruction that the registered class accepts. """ if cls is cls._registry_root: type_key = str(data["type"]) try: target_cls = cls._registry[type_key] except KeyError: raise ValueError( f"Unknown {cls.__name__} type {type_key!r}. " f"Registered types: {sorted(cls._registry)}" ) from None return target_cls.from_dict(data, *args, **kwargs) return cls._from_dict_payload(data, *args, **kwargs)
@classmethod def _from_dict_payload(cls, data: dict[str, Any], *args: Any, **kwargs: Any) -> Any: """Reconstruct a concrete instance (default: parameter-less ``cls()``).""" return cls()