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 Hashable, Optional

from antidote import world
from antidote.core import StatelessProvider, DependencyValue, Container

@world.provider
class RandomProvider(StatelessProvider[str]):
    def exists(self, dependency: Hashable) -> 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 Hashable, 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: Hashable) -> 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... @factory !
@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().