Extend Antidote#
Inner working#
Antidote consists roughly of a core mechanism handling the injection and providers which
actually provide the dependencies. The injection goes trough the Container
,
roughly world
, to request dependencies. The latter regroups all registered providers.
It short it looks like the following
+-------------+
tag=... +-----> TagProvider +----+
+-------------+ |
|
+------------------+ | +--------------------+
@implementation +--> IndirectProvider +--+----> Container (~world) +---> @inject
+------------------+ | +--------------------+
|
+-----------------+ |
Service +---> ServiceProvider +--+
+-----------------+
The Container
never handles the instantiation of the dependencies itself, it
relies on providers. But it does handle thread-safety, cycle detection and singletons. When
a new dependency is requested it will try all provider to see if one of them can provide it.
If not a DependencyNotFoundError
is raised. If yes, the container
will cache it if it’s a singleton.
Adding a Provider#
For simplicity, we will add a very simple provider, one that generates a random number when
'random'
is requested. The most important methods are exists()
and provide()
. Both a are called by the Container
.
exists()
is used to check whether a dependency is supported or not
and if yes provide()
will be called to retrieve the dependency value.
It is expected to return a DependencyInstance
which specifies whether the
returned instance is a singleton or not. If yes, the Container
will cache
the result and the provider will never be called again.
import random
from typing import Optional
from antidote import world
from antidote.core import StatelessProvider, DependencyValue, Container
@world.provider
class RandomProvider(StatelessProvider[str]):
def exists(self, dependency: object) -> bool:
return dependency == 'random'
def provide(self, dependency: str, container: Container) -> DependencyValue:
return DependencyValue(random.random(), scope=None)
>>> from antidote import world
>>> world.get[float]('random')
0...
>>> world.get('random') == world.get('random')
False
Note that we’re inheriting from StatelessProvider
as we don’t handle
any state. If you do handle state, you’ll need a bit more work. For example, let’s say we
want to add different kinds of random values such as age or names. But we do not have
them out of the box, we expect someone else to provide the examples:
import random
from typing import Optional, Dict, List
from antidote import world, inject, Provide
from antidote.core import Provider, DependencyValue, Container
@world.provider
class RandomProvider(Provider[str]):
# The provider must be instantiable without any arguments.
def __init__(self, kind_to_values: Dict[str, List[object]] = None):
super().__init__()
self._kind_to_values: Dict[str, List[object]] = kind_to_values or dict()
def clone(self, keep_singletons_cache: bool) -> 'RandomProvider':
# A clone should be independent, so we copy values as new registrations should
# not impact the clone. We don't need a deep copy as we never change the values
# themselves.
return RandomProvider(self._values.copy())
def exists(self, dependency: object) -> bool:
return dependency in self._kind_to_values
def provide(self, dependency: str, container: Container) -> DependencyValue:
return DependencyValue(random.choice(self._kind_to_values[dependency]),
scope=None)
def add_random(self, kind: str, values: List[object]) -> None:
dependency = f"random:{kind}"
# Ensures that no other provider conflicts with the dependency.
# It roughly checks exists() on all of them.
self._assert_not_duplicate(dependency)
self._kind_to_values[dependency] = values
# The recommend way is not to expose the provider directly, but to expose utility
# functions which have the provider injected. Making them easier to use and maintain.
# Often those would be decorators, like... @injectable !
@inject
def add_random(kind: str,
values: List[object],
provider: Provide[RandomProvider] = None):
assert provider is not None
provider.add_random(kind, values)
>>> names = ['John', 'Karl', 'Anna', 'Sophie']
>>> add_random('name', names)
>>> world.get[str]('random:name') in names
True
Note that we still dont’ handle anywhere thread-safety ! The methods exists()
, provide()
, and clone()
are always called
in a thread-safe environment. This also means that you’re not expected to call them yourself.
world.freeze()
is automatically taken into account:
>>> world.freeze()
>>> add_random('random:city', ['Paris', 'Berlin'])
Traceback (most recent call last):
File "<stdin>", line 1, in ?
FrozenWorldError
If your method does not add any dependencies and is only used for instantiation, you can tell
Antidote to avoid it by decorating it with does_not_freeze()
.
Test extensions#
You can test a new kind of dependency with world.test.new()
. It creates a
new world with the same providers and scopes but without any of the existing dependencies.
For a new Provider
you should usually use world.test.empty()
. It
creates an almost empty world. To test the provide()
you should rely on
world.test.maybe_provide_from()
Both world provide a simple way to define a singleton with world.test.singleton()
and a
factory with world.test.factory()
. They will behave like any other dependency,
contrary to the overrides available in world.test.clone()
.