Source code for factorytx.registry
"""This module provides a way to extend FactoryTX with custom components
(eg. custom receivers, transforms, and transmits.) Each component has an
associated name and type by which is may be referenced in a FactoryTX
configuration.
"""
import importlib
import pkgutil
from typing import Callable, Dict, Generic, List, NamedTuple, Set, Tuple, TypeVar
[docs]class MissingComponentError(ValueError):
    """Thrown when `Registry.get` fails because there was no component with the
    requested name.
    """
    pass 
[docs]class MissingVersionError(ValueError):
    """Thrown when `Registry.get` fails because there was no component with the
    requested name and version, though a component with the requested name was
    found.
    """
    pass 
class ComponentInfo(NamedTuple):
    name: str
    version: int
# Load components from top-level Python modules that begin with MODULE_PREFIX.
MODULE_PREFIX = 'factorytx'
_initialized = False
[docs]def initialize() -> None:
    """Imports all subpackages of factorytx plugins (packages named 'factorytx*.*')
    in order to trigger component registration.
    """
    global _initialized
    if _initialized:
        return
    for mod_info in pkgutil.iter_modules():
        if mod_info.name.startswith(MODULE_PREFIX):
            module = importlib.import_module(mod_info.name)
            module_path: List[str] = module.__path__   # type: ignore
            for pkg_info in pkgutil.walk_packages(module_path, module.__name__ + '.'):
                importlib.import_module(pkg_info.name)
    _initialized = True 
T = TypeVar('T', bound=type)
[docs]class Registry(Generic[T]):
    """Creates a registry for named subclasses of a given type.
    Each class in the registry is identified by a (name, version) pair.
    The version may be omitted; omitted versions are interpreted as version 1.
    Example:
        >>> class Actuator: pass
        >>> actuators = Registry(Actuator)
        >>> @actuators.register('upgoer')  # Same as .register('upgoer', version=1).
        ... class UpGoer1(Actuator): pass
        >>> @actuators.register('upgoer', version=2)
        ... class UpGoer2(Actuator): pass
        >>> actuators.list()
        [ComponentInfo(name='upgoer', version=1),
         ComponentInfo(name='upgoer', version=2)]
        >>> actuators.get('upgoer')
        <class 'factorytx.registry.UpGoer1'>
        >>> actuators.get('upgoer', version=1)
        <class 'factorytx.registry.UpGoer1'>
        >>> actuators.get('upgoer', version=2)
        <class 'factorytx.registry.UpGoer2'>
        >>> try: actuators.get('parser')
        ... except Exception as e: e
        MissingComponentError('There is no component named "parser"')
        >>> try: actuators.get('upgoer', version=3)
        ... except Exception as e: e
        MissingVersionError('There is no component named "upgoer" version 3')
    """
    def __init__(self, base_class: T) -> None:
        """Creates a new Registry instance for subclasses of `base_class`."""
        self.base_class = base_class
        self._registered: Dict[Tuple[str, int], T] = {}
        self._components: Set[str] = set()
    def __repr__(self) -> str:
        return f'Registry({self.base_class})'
[docs]    def register(self, name: str, version: int = 1) -> Callable[[T], T]:
        """Registers a component class with a given name and version."""
        def decorator(cls: T) -> T:
            key = (name, version)
            existing = self._registered.get(key)
            if existing is not None:
                raise ValueError(f'{key} is already used for {existing!r}')
            if not issubclass(cls, self.base_class):
                raise TypeError(f'must be a subclass of {self.base_class!r}')
            self._registered[key] = cls # type: ignore
            self._components.add(name)
            return cls # type: ignore
        return decorator 
[docs]    def get(self, name: str, version: int = 1) -> T:
        """Returns the class registered for the component named `name` with
        version `version`.
        :raises MissingComponentError: if there is no component registered with
            the name `name`.
        :raises MissingVersionError: if there is a component registered with the
            name `name`, but no component matches both `name` and `version`.
        """
        initialize()
        if name not in self._components:
            raise MissingComponentError(f'There is no component named "{name}"')
        key = (name, version)
        if key not in self._registered:
            raise MissingVersionError(f'There is no component named "{name}" version {version}')
        return self._registered[key] 
[docs]    def list(self) -> List[ComponentInfo]:
        """Returns a list of all registered component versions."""
        initialize()
        results = [ComponentInfo(name, ver) for name, ver in self._registered.keys()]
        return results