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