Source code for pyzeta.framework.ioc.container
"""
Module container.py from the package `pyzeta.framework.ioc`.
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 Any, Callable, Dict, Type, TypeVar
from typing_extensions import ParamSpec
from pyzeta.framework.aop.aspect import Aspect
from pyzeta.framework.ioc.configuration_exception import (
InvalidServiceConfiguration,
)
# type variable for the various signatures of Container methods
S = TypeVar("S")
T = TypeVar("T")
P = ParamSpec("P")
[docs]
class Container:
"""
Container class used to add and resolve services as part of dependency
inversion.
"""
__slots__ = (
"_transientServices",
"_singletonServices",
"_aspects",
"_sealed",
)
[docs]
def __init__(self) -> None:
"Initialize a new container instance."
self._transientServices: Dict[Type[object], Callable[..., object]] = {}
self._singletonServices: Dict[Type[object], object] = {}
self._aspects: Dict[Type[object], Aspect[Any, Any, Any]] = {}
self._sealed: bool = False
[docs]
def registerAsSingleton(
self, serviceType: Type[T], instance: T
) -> Container:
"""
Register service `instance` as a singleton service. The instance
resolved will be identical to the instance registered.
:param serviceType: Type of service
:param instance: Service instance
:raises ValueError: If the container is sealed, no new services can be
registered.
:raises InvalidServiceConfiguration: Given instance must implement the
given type
:return: Return the container instance itself for method chaining
"""
if self.isSealed():
raise ValueError(
f"can't register singleton {serviceType} in sealed container!"
)
if isinstance(instance, serviceType):
self._singletonServices[serviceType] = instance
return self
raise InvalidServiceConfiguration(serviceType)
[docs]
def registerAsTransient(
self, serviceType: Type[T], factory: Callable[..., T]
) -> Container:
"""
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 container is sealed, no new services can be
registered.
:return: Return the container instance itself for method chaining
"""
if self.isSealed():
raise ValueError(
f"cannot register transient {serviceType} on sealed container!"
)
self._transientServices[serviceType] = factory
return self
[docs]
def tryResolve(self, 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 = self._singletonServices.get(serviceType, None)
if isinstance(instance, serviceType):
if aspect := self._aspects.get(serviceType, None):
aspect(type(instance))
return instance
# try to resolve as transient
factory = self._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] = self.tryResolve(
sig.parameters[param].annotation
)
instance = factory(**arguments)
if isinstance(instance, serviceType):
if aspect := self._aspects.get(serviceType, None):
aspect(type(instance))
return instance
# resolving failed
raise InvalidServiceConfiguration(serviceType)
[docs]
def seal(self) -> None:
"""
Seal the container to prevent additional services from being
registered.
:raises ValueError: Raises an exception if the container has already
been sealed.
"""
if self._sealed:
raise ValueError("cannot re-seal an already sealed container!")
self._sealed = True
[docs]
def isSealed(self) -> bool:
"""
Return `True` if the container is sealed.
:return: `True` if container is sealed
"""
return self._sealed
[docs]
def clearConfigurations(self) -> None:
"""
Clear all previous configurations, unsealing the container in the
process.
"""
self._transientServices.clear()
self._singletonServices.clear()
self._sealed = False
[docs]
def registerAspect(
self, aspect: Aspect[S, T, P], serviceType: Type[S]
) -> Container:
"""
Register an aspect for a given type of service. The aspect intercepts
calls to resolve the service type and adorns the concrete return
types lazily.
:param aspect: Aspect to apply to the service
:param serviceType: The type of service whose implementations gets
aspect gets applied to
:return: Return the container instance for method chaining
"""
if self.isSealed():
raise ValueError(
f"cannot register aspect {aspect} for {serviceType} if sealed!"
)
self._aspects[serviceType] = aspect
return self