3. Dependency Injection container plugins¶
Grundzeug containers can be extended using container resolution plugins. Container resolution plugins can handle specific types of contracts and implement their own bean resolution logic.
3.1. Adding plugins to a container¶
To add a plugin to a container hierarchy, call add_plugin()
on the
root container:
container = Container()
container.add_plugin(ContainerConverterResolutionPlugin())
Warning
Plugins are shared by the whole container hierarchy! This means that any descendant container is always going to have the same list of plugins as the container itself!
Note
Plugin order matters. The first plugin to be added to the container will be the last plugin to be executed during
registration and resolution. You may review the order of the plugins by inspecting the plugins
field on the
container, which contains a list of plugins that were added to this container hierarchy in the order that they will
be executed (i.e. in reverse order to the order they were added in).
3.2. Built-in plugins¶
3.2.1. ContainerSingleValueResolutionPlugin¶
The default bean resolution logic that was demonstrated in the dependency container basics and dependency injection
tutorials is provided by the
ContainerSingleValueResolutionPlugin
plugin. In other words, this plugin provides the default bean resolution logic that you may see in other IoC containers
such as Unity.
This plugin is registered in containers by default and acts as the fallback for any bean registrations.
3.2.2. ContainerBeanListResolutionPlugin¶
The ContainerBeanListResolutionPlugin
handles situations where you want to
register multiple implementations with the same contract and retrieve all of them at once during bean resolution. This
plugin is also registered in containers by default, so you don’t have to do anything extra to use it, just register
multiple beans under a BeanList[...]
contract:
class IBean:
...
class Bean1(IBean):
...
class Bean2(IBean):
...
container = Container()
container.register_factory[BeanList[IBean]](lambda: Bean1())
container.register_factory[BeanList[IBean]](lambda: Bean2())
beans = container.resolve[BeanList[IBean]]()
assert isinstance(beans, BeanList)
assert len(beans) == 2
assert all(isinstance(b, IBean) for b in beans)
3.2.3. ContainerConverterResolutionPlugin¶
The ContainerConverterResolutionPlugin
is a plugin that handles functions
to convert objects from one type from another. It attempts to resolve the most specific converter according to
Liskov substitution:
container = Container()
container.add_plugin(ContainerConverterResolutionPlugin())
def _assert_false(x):
assert False
container.register_instance[Converter[Any, Any]](_assert_false)
container.register_instance[Converter[Any, int]](_assert_false)
container.register_instance[Converter[str, object]](Converter[str, object].identity())
container.register_instance[Converter[str, int]](lambda x: int(x))
# Should resolve the last converter, since it's the most specific:
str_to_int = container.resolve[Converter[str, int]]()
assert str_to_int("3") == 3
# Should resolve the second last converter, since it's the most specific:
str_to_obj = container.resolve[Converter[str, object]]()
assert str_to_obj("3") == "3"
3.2.4. Configuration¶
Grundzeug’s configuration capabilities are implemented as a DI container plugin.
Warning
TODO: Add a link to the documentation once the documentation is ready.
3.3. Writing a container resolution plugin¶
A container resolution plugin has 3 groups of members: one with members pertaining to registration, one pertaining to resolution, and one related to registration listing.
In this tutorial, we’ll rebuild the ContainerSingleValueResolutionPlugin
:
class ContainerSingleValueResolutionPlugin(ContainerResolutionPlugin):
3.3.1. Registration¶
The first step is to implement the method that will be called when a bean is being registered:
def register(
self,
key: RegistrationKey,
registration: ContainerRegistration,
container: IContainer
) -> bool:
registry = container.get_plugin_storage(self)
if key in registry:
raise ContainerAlreadyHasRegistrationError()
registry[key] = registration
return True
The registration key is a pair consisting of the contract and the bean’s name (or None
if the bean is not named).
The registration is a class that handles the lifetimes of the bean instances. This is precisely what the plugin should return during resolution.
If this plugin does not support the contract specified in the key, this method should return False
as soon as
possible.
Inside the implementation, we retrieve a dictionary that will be used as storage for the container we’re registering
the bean in. A naive approach would be to store the registrations in a dictionary with containers as keys, but this
would lead to memory leaks. This is precisely why the get_plugin_storage()
is needed — it provides a storage mechanism that will not cause challenging situations for the garbage collector.
On successful registration, the plugin should return True
.
3.3.2. Resolution¶
Grundzeug containers resolve beans by starting at the container on which
resolve_bean()
was called and ascending up the container tree until
one of the container plugins returns a ReturnMessage
.
At the beginning of the bean resolution procedure, the Grundzeug container calls
resolve_bean_create_initial_state()
on each plugin,
which allows the plugins to initialize the initial (seed) state for the bean resolution.
def resolve_bean_create_initial_state(
self,
key: RegistrationKey,
container: IContainer
) -> Any:
return None
The state created by
resolve_bean_create_initial_state()
will be the
initial state passed into the reducer that will be called for each container in the chain of containers:
def resolve_bean_reduce(
self,
key: RegistrationKey,
local_state: Any,
container: IContainer,
ancestor_container: IContainer
) -> Union[ReturnMessage, ContinueMessage, NotFoundMessage]:
registry = ancestor_container.get_plugin_storage(self)
if key in registry:
registration = registry[key]
return ReturnMessage(RegistrationBeanResolver(registration=registration, container=container))
return NotFoundMessage(None)
def resolve_bean_postprocess(
self,
key: RegistrationKey,
local_state: Any,
container: IContainer
) -> Any:
return NotFoundMessage(None)
3.3.3. Registration listing¶
def registrations(
self,
container: IContainer
) -> typing.Iterable[typing.Tuple[RegistrationKey, ContainerRegistration]]:
registry = container.get_plugin_storage(self)
return registry.items()