Defining dependencies#

Antidote provides out of the box 4 different kinds of dependencies:

  • injectable() classes for which an instance is provided.

  • const for defining simple constants.

  • lazy function calls (taking into account arguments) used for (stateful-)factories, parameterized dependencies, complex constants, etc.

  • interface for a function, class or even lazy function call for which one or multiple implementations can be provided.

Each Dependency has a lifetime defining how long world holds their value alive. The most important are:

  • singleton: value is created at most once and re-used afterwards

  • transient: value is never cached and re-computed each time.

By default, all dependencies are singleton but it can be easily changed:

from antidote import injectable, world

@injectable(lifetime='transient')
class SingleUse:
    pass

assert world[SingleUse] is not world[SingleUse]

A third lifetime exist, scoped, but its usage will presented later.

Injectable#

As shown multiple times before injectable() defines a class as a dependency for which an instance of the said class will be provided. By default, the decorated class will be automatically wired, like wire(), with a Wiring that can be configured. It is also possible to configure how the instance is created by specifying a factory_method:

from dataclasses import dataclass
from antidote import injectable, world, inject

@injectable
class Service:
    pass

@injectable(factory_method='load')
@dataclass
class ConfiguredService:
    config: object
    service: Service

    @classmethod
    def load(cls, service: Service = inject.me()) -> 'ConfiguredService':
        # load configuration from somewhere
        return cls(config='config', service=service)
>>> world[ConfiguredService].config
'config'
>>> world[ConfiguredService].service is world[Service]
True

Const#

const defines either static constants or ones retrieved, lazily, from environment variables:

from antidote import const, inject

TMP_DIR = const("/tmp")

# From environment variables
LOCATION = const.env("PWD")
USER = const.env()  # uses the name of the variable
PORT = const.env(convert=int)  # convert the environment variable to a given type
UNKNOWN = const.env(default='unknown')

# A class provides a convenient namespace
class Conf:
    TMP_DIR = const("/tmp")
    USER = const.env()

@inject
def f(tmp_dir: str = inject[Conf.TMP_DIR]) -> str:
    return tmp_dir
>>> from antidote import world, inject
>>> world[Conf.TMP_DIR]
'/tmp'
>>> world[UNKNOWN]
'unknown'
>>> import os
>>> os.environ['PORT'] = '80'
>>> world[PORT]
80
>>> f()
'/tmp'

Lazy#

lazy defines a function call as a dependency. As such using the same arguments will return the same dependency. By default, the decorated function will be injected with inject.

from antidote import lazy, world, inject, injectable

@injectable
class Service:
    pass

@lazy
def template(name: str, service: Service = inject.me()) -> str:
    return f"Template {name}"

@inject
def f(main_template = inject[template(name="main")]) -> str:
    return main_template
>>> world[template(name="main")]
'Template main'
>>> f() is world[template(name="main")]  # singleton by default
True

The original function can still be accessed through __wrapped__. lazy also exposes several variations of itself:

  • value() allows the function to be used like a variable:

    from antidote import lazy
    
    class Redis:  # from another library, can't decorate with @injectable
        pass
    
    @lazy.value
    def app_redis() -> Redis:
        return Redis()
    
    >>> from antidote import world
    >>> world[app_redis]
    <Redis object ...>
    
  • method() will inject self like @inject.method. However, contrary to the latter it keeps the same behavior whether called from the class or an instance:

    from dataclasses import dataclass
    from antidote import lazy, world, injectable
    
    @dataclass
    class Dummy:
        name: str
    
    @injectable
    @dataclass
    class Factory:
        prefix: str = 'Mr. '
    
        @lazy.method  # used as stateful factory
        def dummy(self, name: str) -> Dummy:
            return Dummy(name=f'{self.prefix}{name}')
    
    >>> from antidote import world
    >>> world[Factory.dummy(name='John')]
    Dummy(name='Mr. John')
    >>> factory = Factory(prefix="Ms. ")
    >>> # calling from an instance does not change its behavior
    ... world[factory.dummy(name='John')]
    Dummy(name='Mr. John')
    
  • property() behaves like a property and will inject self like method():

    from antidote import lazy, injectable
    
    @injectable
    class Conf:
    
        @lazy.property
        def host(self) -> str:
            return 'localhost'
    
    >>> from antidote import world
    >>> world[Conf.host]
    'localhost'
    

To customize the injections, just apply inject yourself:

from antidote import lazy, inject, injectable

@injectable
class Service:
    pass

@lazy
@inject(kwargs=dict(service=Service))
def f(service):
    ...

Interface#

interface defines a contract for which one or multiple implementations can be registered. The interface itself can be a class, a function or even a lazy call. Implementations won’t be directly accessible unless explicitly defined as such.

Class#

For a class implements ensures that all implementations are subclasses of it.

from antidote import implements, inject, interface, world, instanceOf


@interface
class Task:
    pass


@implements(Task)
class CustomTask(Task):  # CustomTask must inherit Task
    pass


assert world.get(CustomTask) is None  # CustomTask not directly accessible
assert isinstance(world[Task], CustomTask)
assert world[Task] is world[Task]  # CustomTask is a singleton

# More on this latter, constraints can be passed down to single() and all() to filter implementations
assert world[instanceOf(Task)] is world[Task]
assert world[instanceOf(Task).single()] is world[Task]
assert world[instanceOf(Task).all()] == [world[Task]]


@inject
def f(task: Task = inject.me()) -> Task:
    return task

@inject  #   ⯆ Iterable[Task] / Sequence[Task] / List[Task] would also work
def g(tasks: list[Task] = inject.me()) -> list[Task]:
    return tasks

assert f() is world[Task]
assert g() == world[instanceOf(Task).all()]

Underneath implements uses :py:func:.injectable`, so you can customize the implementation however you wish through it. The following implementation is strictly equivalent to the previous one:

from antidote import injectable

@implements(Task)  #      ⯆ More on this later, this is what "hides" CustomTask
@injectable(catalog=world.private)
class CustomTask(Task):
    pass

“Hiding” the implementation is not a necessity though:

@implements(Task)
@injectable  # not hidden anymore
class CustomTask(Task):
    pass

When using a Protocol as an interface, implementations will only be checked if runtime_checkable was applied on the protocol. For proper static-typing alternative syntaxes are also provided:

from typing import Protocol, runtime_checkable

from antidote import implements, interface, world, instanceOf


@interface
@runtime_checkable  # if present, implementations will be checked
class Base(Protocol):
    def get(self) -> object:
        pass


@implements.protocol[Base]()
class BaseImpl:
    def get(self) -> object:
        pass


assert isinstance(world[instanceOf[Base]], BaseImpl)

Function & Lazy#

For a function implements ensures the signature of the implementation matches the interface.

from typing import Callable, List

from antidote import implements, interface, world, inject


@interface
def validator(name: str) -> bool:
    ...

@implements(validator)
def not_too_long(name: str) -> bool:
    return len(name) < 10


# returning the function itself
assert world[validator] is not_too_long


@implements(validator)
def lower_case_only(name: str) -> bool:
    return name.lower() == name


@inject
def validate(name: str, validators: List[Callable[[str], bool]] = inject[validator.all()]) -> bool:
    return all(v(name) for v in validators)


assert not validate("CAPITAL")
assert not validate("this is too long to be validated")
assert validate("antidote")

Like lazy, it is also possible to define the function call as the dependency:

from antidote import implements, inject, interface, world


@interface.lazy
def template(name: str) -> str:
    ...


@implements.lazy(template)
def my_template(name: str) -> str:
    return f"My Template {name}"


# Contrary to a function interface, here the template by itself is not a dependency.
assert template not in world
# Only the function call is
assert world[template(name="world")] == "My Template world"
# Retrieving a single implementation and calling it with specified arguments
assert world[template.single()(name="world")] == "My Template world"


@inject
def f(world_template: str = inject[template(name="world")]) -> str:
    return world_template


assert f() == "My Template world"

Similar to what we have seen for interface classes and injectable(), @implements.lazy applies lazy underneath. The following is strictly equivalent to the previous definition:

from antidote import lazy

@implements.lazy(template)
@lazy(catalog=world.private)
def my_template(name: str) -> str:
    return f"My Template {name}"

Multiple implementations#

Three different mechanisms exist to select one or multiple implementations among many. First let’s define a simple interface:

from antidote import interface

@interface
class CloudAPI:
    pass
  1. At declaration, conditions define whether an implementation is registered or not:

from antidote import inject, const, world, implements

CLOUD = const('gcp')


@inject
def in_cloud(name: str, current_cloud: str = inject[CLOUD]) -> bool:
    return name == current_cloud


@implements(CloudAPI).when(in_cloud('gcp'))
class GCPapi(CloudAPI):
    pass


@implements(CloudAPI).when(in_cloud('aws'))
class AWSapi(CloudAPI):
    pass


assert isinstance(world[CloudAPI], GCPapi)
  1. A condition can also be a Predicate which allows adding metadata to an implementation. At request, it is then possible to filter implementations based on this metadata with a PredicateConstraint. Here is an example with the QualifiedBy predicate:

from antidote import world, implements, instanceOf, QualifiedBy


@implements(CloudAPI).when(QualifiedBy('aws'))
class AWSapi(CloudAPI):
    pass


@implements(CloudAPI).when(qualified_by='gcp')  # shortcut definition
class GCPapi(CloudAPI):
    pass


assert isinstance(world[instanceOf(CloudAPI).single(qualified_by='aws')], AWSapi)
  1. A condition not only defines whether an implementation is used or not, but also their ordering with an ImplementationWeight. By default the NeutralWeight is used, which as the name implies has no effect. It’s possible to define one’s own weight and use it in combination with the NeutralWeight, but two custom weight implementations cannot be used together:

from typing import Any
from dataclasses import dataclass
from antidote import world, implements, Predicate


@dataclass
class Weight:
    value: float

    @classmethod
    def neutral(cls) -> 'Weight':
        return Weight(0)

    @classmethod
    def of_neutral_predicate(cls, predicate: Predicate[Any]) -> 'Weight':
        return cls.neutral()

    def __lt__(self, other: 'Weight') -> bool:
        return self.value < other.value

    def __add__(self, other: 'Weight') -> 'Weight':
        return Weight(self.value + other.value)


@implements(CloudAPI).when(Weight(1))
class GCPapi(CloudAPI):
    pass


@implements(CloudAPI)
class AWSapi(CloudAPI):
    pass


assert isinstance(world[CloudAPI], GCPapi)

Default implementation#

A default implementation is used whenever no alternative implementation can be used. You can either define it to be the interface itself or an implementation:

from antidote import interface, implements, world


@interface.as_default
def callback() -> None:
    pass


assert world[callback] is callback.__wrapped__


@implements(callback)
def custom_callback() -> None:
    pass


assert world[callback] is custom_callback


@interface
class Service:
    pass


@implements(Service).as_default
class ServiceDefaultImpl(Service):
    pass


assert isinstance(world[Service], ServiceDefaultImpl)


@implements(Service)
class CustomServiceImpl(Service):
    pass


assert isinstance(world[Service], CustomServiceImpl)

Overriding an implementation#

An (default) implementation can be overridden by another one. It’ll be used in exactly the same conditions as the original one.

from antidote import implements, interface, world


@interface
class Service:
    pass


@implements(Service)
class ServiceImpl(Service):
    pass


@implements(Service).overriding(ServiceImpl)
class Override(Service):
    pass


assert isinstance(world[Service], Override)

Freezing dependencies definitions#

The catalog, world, can be frozen in order to prevent any new dependency definitions with freeze(), a FrozenCatalogError will be raised instead:

from antidote import world

world.freeze()