Source code for pyzeal.utils.service_locator
"""
Module service_locator.py from the package PyZEAL.
This module provides a way to register and locate services used throughout
the project, to enable loading additional services for the plugin system.
Authors:\n
- Philipp Schuette\n
"""
from __future__ import annotations
from inspect import Parameter, signature
from typing import Callable, Dict, Type, TypeVar
from pyzeal.utils.configuration_exception import InvalidServiceConfiguration
# type variable for the various signatures of ServiceLocator
T = TypeVar("T")
[docs]
class ServiceLocator:
"""
Static locator class used to add and resolve services.
"""
_transientServices: Dict[Type[object], Callable[..., object]] = {}
_singletonServices: Dict[Type[object], object] = {}
_sealed: bool = False
[docs]
@staticmethod
def registerAsSingleton(
serviceType: Type[T], instance: T
) -> Type[ServiceLocator]:
"""
Register service `instance` as a singleton service, meaning only one
instance exists.
:param serviceType: Type of service
:param instance: Service instance
:raises ValueError: If the service locator is sealed, no new services
can be registered.
:raises InvalidServiceConfiguration: Given instance must implement the
given type
:return: Return the static `ServiceLocator` for method chaining
"""
if ServiceLocator.isSealed():
raise ValueError(
f"cannot register singleton {serviceType} on sealed locator!"
)
if isinstance(instance, serviceType):
ServiceLocator._singletonServices[serviceType] = instance
return ServiceLocator
raise InvalidServiceConfiguration(serviceType)
[docs]
@staticmethod
def registerAsTransient(
serviceType: Type[T], factory: Callable[..., T]
) -> Type[ServiceLocator]:
"""
Register a transient service. Note that you MUST implement a (dummy)
default constructor if you want to register a class without constructor
as an instance factory and that class inherits from `typing.Protocol`.
:param serviceType: Type of service to register.
:param factory: Factory for the given service
:raises ValueError: If the service locator is sealed, no new services
can be registered.
:return: Return the static `ServiceLocator` for method chaining
"""
if ServiceLocator.isSealed():
raise ValueError(
f"cannot register transient {serviceType} on sealed locator!"
)
ServiceLocator._transientServices[serviceType] = factory
return ServiceLocator
[docs]
@staticmethod
def tryResolve(serviceType: Type[T], **kwargs: object) -> T:
"""
Try to resolve the requested service type by first searching registered
singleton and then registered transient configurations.
:param serviceType: Type of service to resolve
:raises InvalidServiceConfiguration: If the given `serviceType` can
not be resolved, an exception is raised
:return: An instance of the given service. If the service is
transient, the factory is called with the parameters given
by `**kwargs`.
"""
# try to resolve as singleton
instance = ServiceLocator._singletonServices.get(serviceType, None)
if isinstance(instance, serviceType):
return instance
# try to resolve as transient
factory = ServiceLocator._transientServices.get(serviceType, None)
if factory is None:
raise InvalidServiceConfiguration(serviceType)
sig = signature(factory)
arguments: Dict[str, object] = {}
# try to instantiate an instance from the factory and given kwargs or
# default parameters of the factory - additional kwargs are ignored
for param in sig.parameters:
try:
arguments[param] = kwargs[param]
except KeyError:
if (arg := sig.parameters[param].default) != Parameter.empty:
arguments[param] = arg
else:
arguments[param] = ServiceLocator.tryResolve(
sig.parameters[param].annotation
)
instance = factory(**arguments)
if isinstance(instance, serviceType):
return instance
# resolving failed
raise InvalidServiceConfiguration(serviceType)
[docs]
@staticmethod
def seal() -> None:
"""
Seal the service locator to prevent additional services from
being registered.
:raises ValueError: Raises an exception if the service locator
has already been sealed.
"""
if ServiceLocator._sealed:
raise ValueError("cannot re-seal an already sealed locator!")
ServiceLocator._sealed = True
[docs]
@staticmethod
def isSealed() -> bool:
"""
Return `True` if the service locator is sealed.
:return: `True` if the service locator is sealed.
"""
return ServiceLocator._sealed
[docs]
@staticmethod
def clearConfigurations() -> None:
"""
Clear all service locator configurations, unsealing the locator in the
process.
"""
ServiceLocator._transientServices.clear()
ServiceLocator._singletonServices.clear()
ServiceLocator._sealed = False