Core#

Injection#

inject: antidote.core.Inject#

Singleton instance of Inject

class Inject#

Use the singleton inject.

__call__(_Inject__arg=None, *, args=None, kwargs=None, fallback=None, ignore_type_hints=False, ignore_defaults=False, type_hints_locals=Default.sentinel, app_catalog=None)#

Wrap a function to inject dependencies when executed. Dependencies can be tight to arguments in multiple ways. The priority is defined as follows:

  1. args and kwargs: argument name to dependencies.

  2. Defaults (ex: me()) or PEP-593 Annotated type hints (ex: InjectMe)

  3. fallback: argument name to dependencies.

>>> from antidote import inject, injectable, InjectMe
>>> @injectable
... class A:
...     pass
>>> @injectable
... class B:
...     pass
>>> @inject
... def f1(a: A = inject.me()):
...     return a
>>> f1() is world[A]
True
>>> @inject
... def f2(a: InjectMe[A]):  # PEP-593 annotation
...     return a
>>> f2() is world[A]
True
>>> @inject(kwargs=dict(a=A))
... def f3(a):
...     return a
>>> f3() is world[A]
True
>>> @inject(fallback=dict(a=A))
... def f4(a):
...     return a
>>> f4() is world[A]
True

The decorator can be applied on any kind of function:

>>> class Dummy:
...     @staticmethod
...     @inject
...     def static_method(a: A = inject.me()) -> object:
...         return a
...
...     @inject
...     @classmethod
...     def class_method(cls, a: A = inject.me()) -> object:
...         return a
>>> Dummy.static_method() is world[A]
True
>>> Dummy.class_method() is world[A]
True
>>> @inject
... async def f(a: A = inject.me()) -> A:
...     return a

Note

To inject the first argument of a method, commonly self, see method().

Parameters
  • __arg/positional-only/ Callable to be wrapped, which may also be a static or class method. If used as a decorator, it can be a sequence of dependencies or a mapping of argument names to their respective dependencies. For the former, dependencies are associated with the arguments through their position None can be used as a placeholder to ignore certain arguments.

  • kwargs (Mapping[str, object] | None) – Mapping of argument names to dependencies. This has the highest priority.

  • fallback (Mapping[str, object] | None) – Mapping of argument names to dependencies. This has the lowest priority.

  • ignore_type_hints (bool) – If True, neither type hints nor type_hints_locals will not be used at all. Defaults to False.

  • ignore_defaults (bool) – If True, default values such as inject.me() are ignored. Defaults to False.

  • type_hints_locals (TypeHintsLocals) – Local variables to use for typing.get_type_hints(). They can be explicitly defined by passing a dictionary or automatically detected with inspect and frame manipulation by specifying 'auto'. Specifying None will deactivate the use of locals. When ignore_type_hints is True, this features cannot be used. The default behavior depends on the config value of auto_detect_type_hints_locals. If True the default value is equivalent to specifying 'auto', otherwise to None.

  • app_catalog (API.Experimental[ReadOnlyCatalog | None]) – Defines the app_catalog to be used by the current injection and nested ones. If unspecified, the catalog used depends on the context. Usually it will be app_catalog defined by a upstream inject. If never never specified, it’s py:obj:.world. However, dependencies such as injectable() use Inject.rewire() to force the use of the catalog in which the dependency is registered. If explicitely specified, it cannot be changed afterwards and can be either a Catalog or app_catalog. The latter forcing the use of the current app_catalog could otherwise be rewired by injectable() and alike.

static me(*constraints, qualified_by=None, qualified_by_one_of=None)#

Injection Marker specifying that the current type hint should be used as dependency.

>>> from antidote import inject, injectable
>>> @injectable
... class MyService:
...     pass
>>> @inject
... def f(s: MyService = inject.me()) -> MyService:
...     return s
>>> f()
<MyService object at ...>

When the type hint is Optional inject() won’t raise DependencyNotFoundError but will provide None instead:

>>> from typing import Optional
>>> from antidote import inject
>>> class MyService:
...     pass
>>> @inject
... def f(s: Optional[MyService] = inject.me()) -> MyService:
...     return s
>>> f() is None
True

interface() are also supported:

>>> from typing import Sequence
>>> from antidote import inject, interface, implements
>>> @interface
... class Alert:
...     pass
>>> @implements(Alert)
... class DefaultAlert(Alert):
...     pass
>>> @inject
... def get_single(alert: Alert = inject.me()) -> Alert:
...     return alert
>>> get_single()
<DefaultAlert object at ...>
>>> @inject
... def get_all(alerts: Sequence[Alert] = inject.me()) -> Sequence[Alert]:
...     return alerts
>>> get_all()
[<DefaultAlert object at ...>]
Parameters
  • *constraintsPredicateConstraint to evaluate for each implementation.

  • qualified_by – All specified qualifiers must qualify the implementation.

  • qualified_by_one_of – At least one of the specified qualifiers must qualify the implementation.

method(_Inject__arg=None, *, args=None, kwargs=None, fallback=None, ignore_type_hints=False, ignore_defaults=False, type_hints_locals=Default.sentinel, app_catalog=None)#

Specialized version of inject for methods to also inject the first argument, commonly named self. More precisely, the dependency for self is defined to be the class itself and so the dependency value associated with it will be injected when not provided. So when called through the class self will be injected but not when called through an instance.

>>> from antidote import inject, injectable, world
>>> @injectable
... class Dummy:
...     @inject.method
...     def get_self(self) -> 'Dummy':
...         return self
>>> Dummy.get_self() is world[Dummy]
True
>>> dummy = Dummy()
>>> dummy.get_self() is dummy
True

The class will not be defined as a dependency magically:

>>> class Unknown:
...     @inject.method
...     def get_self(self) -> 'Unknown':
...         return self
>>> Unknown.get_self()
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
DependencyNotFoundError: ...

For information on all other features, see inject.

Parameters
  • __arg/positional-only/ Callable to be wrapped, which may also be a static or class method. If used as a decorator, it can be a sequence of dependencies or a mapping of argument names to their respective dependencies. For the former, dependencies are associated with the arguments through their position None can be used as a placeholder to ignore certain arguments.

  • kwargs (Mapping[str, object] | None) – Mapping of argument names to dependencies. This has the highest priority.

  • fallback (Mapping[str, object] | None) – Mapping of argument names to dependencies. This has the lowest priority.

  • ignore_type_hints (bool) – If True, neither type hints nor type_hints_locals will not be used at all. Defaults to False.

  • ignore_defaults (bool) – If True, default values such as inject.me() are ignored. Defaults to False.

  • type_hints_locals (TypeHintsLocals) – Local variables to use for typing.get_type_hints(). They can be explicitly defined by passing a dictionary or automatically detected with inspect and frame manipulation by specifying 'auto'. Specifying None will deactivate the use of locals. When ignore_type_hints is True, this features cannot be used. The default behavior depends on the config value of auto_detect_type_hints_locals. If True the default value is equivalent to specifying 'auto', otherwise to None.

  • app_catalog (API.Experimental[ReadOnlyCatalog | None]) – Defines the app_catalog to be used by the current injection and nested ones. If unspecified, the catalog used depends on the context. Usually it will be app_catalog defined by a upstream inject. If never never specified, it’s py:obj:.world. However, dependencies such as injectable() use Inject.rewire() to force the use of the catalog in which the dependency is registered. If explicitely specified, it cannot be changed afterwards and can be either a Catalog or app_catalog. The latter forcing the use of the current app_catalog could otherwise be rewired by injectable() and alike.

rewire(_Inject__func, *, app_catalog, method=Default.sentinel)#

Rewire the function to use the specified catalog if the injection wasn’t hardwired.

>>> from antidote import inject, app_catalog, new_catalog, world, injectable
>>> @injectable
... class Service:
...     pass
>>> @inject
... def f(x: Service = inject.me()) -> Service:
...     return x
>>> @inject(app_catalog=world)
... def g(x: Service = inject.me()) -> Service:
...     return x
>>> catalog = new_catalog()
>>> # f() will now retrieve dependencies from `catalog`
... inject.rewire(f, app_catalog=catalog)
>>> # f() will now retrieve dependencies from `app_catalog`, which was the default
... # behavior
... inject.rewire(f, app_catalog=app_catalog)
>>> # has no impact as the catalog was explicitly hardwired
... inject.rewire(g, app_catalog=catalog)
InjectMe#

Annotation specifying that the type hint itself is the dependency:

>>> from antidote import world, inject, InjectMe, injectable
>>> @injectable
... class Database:
...     pass
>>> @inject
... def load_db(db: InjectMe[Database]):
...     return db
>>> load_db()
<Database ...>

alias of T[T]

class ParameterDependency#

Defines the dependency to inject based on the argument name and type hints when using inject. This is how me() and Inject work underneath.

>>> from typing import Any
>>> from antidote import Dependency, dependencyOf, inject, injectable, ParameterDependency, world
>>> class Auto(ParameterDependency):
...     def __antidote_parameter_dependency__(self, *,
...                                           name: str,
...                                           type_hint: object,
...                                           type_hint_with_extras: object
...                                           ) -> Dependency[Any]:
...         if isinstance(type_hint, type):
...             return dependencyOf(type_hint)
...         raise RuntimeError()
>>> def auto() -> Any:  # for static typing, wrapper that returns Any
...     return Auto()
>>> @injectable
... class Service:
...     pass
>>> @inject
... def f(service: Service = auto()) -> Service:
...     return service
>>> assert f() is world[Service]
class Wiring(*, methods=Methods.ALL, fallback=None, raise_on_double_injection=False, ignore_type_hints=False)#

Defines how a class should be wired, meaning if/how/which methods are injected. This class is intended to be used as a parameter. Consider using wire() to wire classes directly. Instances are immutable.

>>> from antidote import Wiring, injectable, inject
>>> @injectable
... class Database:
...     pass
>>> @injectable(wiring=Wiring(methods=['my_method']))
... class Dummy:
...     def my_method(self, db: Database = inject.me()) -> Database:
...         return db
>>> Dummy().my_method()
<Database ...>
Parameters
  • methods (Methods | Iterable[str]) – Names of methods that must be injected. Defaults to all method.

  • raise_on_double_injection (bool) – Whether an error should be raised if method is already injected. Defaults to False.

  • fallback (Mapping[str, object] | None) – Propagated for every method to inject.

  • ignore_type_hints (bool) – Propagated for every method to inject.

copy(*, methods=Copy.IDENTICAL, fallback=Copy.IDENTICAL, raise_on_double_injection=Copy.IDENTICAL, ignore_type_hints=Copy.IDENTICAL)#

Copies current wiring and overrides only specified arguments. Accepts the same arguments as __init__().

wire(*, klass, app_catalog=None, type_hints_locals=None, class_in_locals=Default.sentinel)#

Used to wire a class with specified configuration. It does not return a new class and modifies the existing one.

Parameters
  • klass (type) – Class to wire.

  • type_hints_locals (Optional[Mapping[str, object]]) – Propagated for every method to inject.

  • app_catalog (ReadOnlyCatalog | None) – Propagated for every method to inject.

  • class_in_locals (bool | Default) – Whether to add the current class as a local variable. This is typically helpful when the class uses itself as a type hint as during the wiring, the class has not yet been defined in the globals/locals. The default depends on the value of ignore_type_hints. If ignored, the class will not be added to the type_hints_locals. Specifying type_hints_locals=None does not prevent the class to be added.

wire(__klass=None, *, methods=Methods.ALL, fallback=None, raise_on_double_injection=False, ignore_type_hints=False, type_hints_locals=Default.sentinel, app_catalog=None)#

Wire a class by injected specified methods. Methods are only replaced if any dependencies were detected. The same class is returned, it only modifies the methods.

>>> from antidote import wire, injectable, inject
>>> @injectable
... class MyService:
...     pass
>>> @wire
... class Dummy:
...     def method(self, service: MyService = inject.me()) -> MyService:
...         return service
>>> Dummy().method()
<MyService object at ...>
Parameters
  • __klass (C | None) – Class to wire.

  • methods (Methods | Iterable[str]) – Names of methods that must be injected. Defaults to all method.

  • raise_on_double_injection (bool) – Whether an error should be raised if method is already injected. Defaults to False.

  • fallback (Mapping[str, object] | None) – Propagated for every method to inject.

  • ignore_type_hints (bool) – Propagated for every method to inject.

  • type_hints_locals (Union[Mapping[str, object], Literal['auto'], Default, None]) – Propagated for every method to inject.

  • app_catalog (ReadOnlyCatalog | None) – Propagated for every method to inject.

Returns

Wired class or a class decorator.

Catalog#

world: antidote.core.PublicCatalog#

Default catalog for all dependencies

app_catalog: antidote.core.ReadOnlyCatalog#

Current catalog used as defined by inject

new_catalog(*, name=Default.sentinel, include=Default.sentinel)#

Creates a new PublicCatalog. It’s recommended to provide a name to the catalog to better differentiate it from others. It’s possible to provide an iterable of functions or public catalogs to include.

class CatalogId(name, test_context_ids)#
class PublicCatalog#

Subclass of ReadOnlyCatalog for dependency access and Catalog for dependency registration.

Can be created with new_catalog().

freeze()#

Freezes the catalog, no additional dependencies, child catalog or providers can be added. Currently, it does not provide any performance improvements but may in the future.

>>> from antidote import world, injectable
>>> world.freeze()
>>> @injectable
... class Dummy:
...     pass
Traceback (most recent call last):
...
FrozenCatalogError
>>> world.is_frozen
True
property test#

See TestContextBuilder.

class Catalog#

Subclass of ReadOnlyCatalog for dependency access.

A catalog is a collection of dependencies that can be provided. It ensures that singletons and bound dependencies are created in a thread-safe manner. But the actual instantiation is handled by one of its Provider.

A catalog may have one or more child catalogs which are traversed after the providers. So any dependency defined in the catalog’s children can be overridden by the catalog’s own providers. To prevent any race conditions on the catalog locks, a catalog can only have one parent and as such be included only once.

Catalogs are always created by pair, a private and a public (PublicCatalog) one. Most of the time one would manipulate the public one. The private’s only purpose is to allow the definition of private dependencies which are not directly accessible from the public catalog. This is only needed for library/framework authors who want to expose only a subset of the dependencies registered. In this case, it’s recommended to freeze the catalog before exposing it to ensure proper isolation.

>>> from antidote import new_catalog, injectable, world
>>> catalog = new_catalog(name='my-catalog')
>>> @injectable(catalog=catalog)
... class Dummy:
...     pass
>>> Dummy in world
False
>>> catalog[Dummy]
<Dummy object at ...>
>>> world.include(catalog)
>>> world[Dummy] is catalog[Dummy]
True
include(_Catalog__obj)#

Include something into the catalog. Multiple inputs are supported:

  • A Provider class will add a new provider to the Catalog. It behaves like a class decorator in this case.

  • A PublicCatalog which will be included as a child. Beware that a catalog can only have one parent. Private catalog cannot be added as a child.

  • A function accepting the current catalog as argument. It’s typically used by an extension to initialize a catalog with child catalogs, providers or dependencies.

>>> from antidote import world, new_catalog, Catalog
>>> my_catalog = new_catalog(name='custom')
>>> world.include(my_catalog)
>>> def my_extension(catalog: Catalog) -> None:
...     hidden_catalog = new_catalog(name='hidden')
...     catalog.include(hidden_catalog)
>>> world.include(my_extension)
property private#

Returns the associated private Catalog, or itself if it’s already the private one.

>>> from antidote import world, injectable, inject
>>> @injectable(catalog=world.private)
... class Dummy:
...     pass
>>> Dummy in world
False
>>> @injectable
... class SuperDummy:
...     def __init__(self, dummy: Dummy = inject.me()) -> None:
...         self.dummy = dummy
>>> world[SuperDummy].dummy
<Dummy object at ...>
property providers#

Returns a Mapping of the providers type to their instance included in the catalog. It’s only needed when creating a custom Provider.

The returned mapping is immutable and changes to the catalog will not be reflected.

Warning

Beware, Provider are at the core of a Catalog. Be sure to understand all the implications of manipulating one. None of the Antidote provided Provider is part of the public API.

class ReadOnlyCatalog#
__contains__(_ReadOnlyCatalog__dependency)#

Returns True if the dependency can be provided, else False.

__getitem__(_DependencyAccessor__dependency)#

Return the value for the dependency. Raises a DependencyNotFoundError (subclass of KeyError) if the dependency cannot be provided.

inject provides an equivalent API to specify a lazily injected dependency.

>>> from antidote import world, const, inject
>>> class Conf:
...     HOST = const('localhost')
...     UNKNOWN = 1
>>> world[Conf.HOST]
'localhost'
>>> world.get(Conf.UNKNOWN) is None
True
>>> @inject
... def f(host: str = inject[Conf.HOST]) -> str:
...     return host
>>> f()
'localhost'
>>> @inject
... def g(unknown: object = inject[Conf.UNKNOWN]) -> object:
...     return unknown
>>> g()
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
DependencyNotFoundError: ...
debug(_ReadOnlyCatalog__obj, *, depth=-1)#

If the object is a dependency that can be provided, a tree representation of all of its transitive dependencies, as the catalog would retrieve them, is returned. Otherwise, if the specified object is a callable with any injected dependencies, a tree representation of their debug output is returned.

The tree is created idendepently from the values, so no singleton or bound dependencies will be instantiated.

>>> from antidote import world, const, injectable, inject
>>> class Conf:
...     HOST = const('localhost')
>>> @injectable
... class Dummy:
...     def __init__(self, host: str = inject[Conf.HOST]) -> None:
...         ...
>>> print(world.debug(Dummy))
🟉 Dummy
└── Dummy.__init__
    └── 🟉 <const> 'localhost'

∅ = transient
↻ = bound
🟉 = singleton
Parameters
  • __obj – Root of the dependency tree.

  • depth (int) – Maximum depth of the result tree. Defaults to -1 which implies not limit.

get(_DependencyAccessor__dependency, default=None)#

Return the value for the specified dependency if in the catalog, else default. If default is not given, it defaults to None, so that this method never raises a DependencyNotFoundError (subclass of KeyError).

inject provides an equivalent API to specify a lazily injected dependency.

>>> from antidote import world, const, inject
>>> class Conf:
...     HOST = const('localhost')
...     UNKNOWN = 1
>>> world.get(Conf.HOST)
'localhost'
>>> world.get(Conf.UNKNOWN) is None
True
>>> world.get(Conf.UNKNOWN, default='something')
'something'
>>> @inject
... def f(host: str = inject.get(Conf.HOST),
...       unknown: object = inject.get(Conf.UNKNOWN),
...       something: object = inject.get(Conf.UNKNOWN, default='something')) -> object:
...     return host, unknown, something
>>> f()
('localhost', None, 'something')
property id#

Unique identifier of the Catalog. There’s no backwards-compatibility guarantee on its value or type. It is only guaranteed to be unique.

Its main purpose is to allow dependencies, such as lazy() ones, to be aware of which catalog they’ve been registered.

property is_frozen#

Returns True if the catalog is frozen, else False. A frozen catalog cannot be changed anymore, all new dependency registrations will fail.

raise_if_frozen()#

Raises an FrozenCatalogError if the catalog is frozen. This is used to prevent any new registrations and have a more predictable catalog once frozen.

is_catalog(catalog)#

Returns whether the specified object is a Catalog or not.

is_readonly_catalog(catalog)#

Returns whether the specified object is a ReadOnlyCatalog or not.

Test Context#

class TestContextBuilder#

Used to create test environments with proper isolation and the possibility to create or override dependencies easily:

>>> from antidote import world
>>> with world.test.new() as overrides:
...     overrides['hello'] = 'world'
...     world['hello']
'world'

Within the context manager (with block), world will behave like a different Catalog and changes will not be propagated back. Four different test environment exist:

  • empty(): the catalog will be totally empty.

  • new(): the catalog will keep all of its Provider and child catalog. But those will be empty as if no dependency was ever registered.

  • clone(): All of the Provider, child catalog and dependency registration are kept. However, the catalog does not keep any of the dependency values (singletons, …) as if it was never used. By default, the catalog will be frozen.

  • copy(): Same strategy than clone() except all existing dependencies are kept. Be careful with this test environment, dependencies values are not copied. The actual singletons values are exposed.

It is also possible to nest the different test enviroments allowing to set up an environment and re-use it for different tests.

>>> from antidote import world
>>> with world.test.new() as overrides:
...     overrides['hello'] = 'world'
...     with world.test.copy() as nested_overrides:
...         print(world['hello'])
...         nested_overrides['hello'] = "new world"
...         print(world['hello'])
...     print(world['hello'])
world
new world
world

Each test environment exposes a CatalogOverride can create/override dependencies:

>>> from antidote import world
>>> with world.test.new() as overrides:
...     # create/override a singleton
...     overrides['hello'] = 'world'
...     # replace previous one
...     overrides['hello'] = 'new world'
...     # delete a dependency
...     del overrides['hello']
...     # create/override multiple dependencies at once
...     overrides.update({'my': 'world'})
...     # or use a factory which by default creates a non-singleton:
...     @overrides.factory('env')
...     def build() -> str:
...         return "test"

When using a test environment on a catalog, it will also be applied on all children, recursively. It’s also possible to override dependencies only within a specific catalog:

>>> from antidote import world, new_catalog
>>> catalog = new_catalog(name='child')
>>> world.include(catalog)
>>> with world.test.clone() as overrides:
...     # overrides.of(catalog) also supports del, update() and factory()
...     overrides.of(catalog)['hello'] = 'child'
...     print(catalog['hello'])
...     # It's also possible to modify the private catalog
...     overrides.of(world.private)['hello'] = 'private'
...     print(world.private['hello'])
child
private
>>> 'hello' in catalog
False
clone(*, frozen=True)#

Creates a test enviroment keeping a copy of all child catalogs, Provider and reigstered dependencies, state ones included. Existing dependency values are not copied. Unscoped dependencies will be reset to their initial value if any. By default, the catalog will be frozen. However, child catalogs will keep their previous state, staying unfrozen if they weren’t and frozen if not.

>>> from antidote import injectable, world
>>> @injectable
... class Service:
...     pass
>>> with world.test.clone():
...     # Existing dependencies are kept
...     Service in world
True
>>> service = world[Service]
>>> with world.test.clone():
...     # dependency values are not
...     world[Service] is service
False
>>> with world.test.clone():
...     # By default, world will be frozen
...     @injectable
...     class MyService:
...         pass
Traceback (most recent call last):
...
FrozenCatalogError
>>> with world.test.clone(frozen=False):
...     # Now new dependencies can be declared
...     @injectable
...     class MyService:
...         pass
...     world[MyService]
<MyService object at ...>
>>> # They won't impact the real world though
... MyService in world
False
copy(*, frozen=True)#

Creates a test enviroment keeping a copy of all child catalogs, Provider`s, registered dependencies and even their values if any. Unscoped dependencies also keep their current values. If :py:func:.UnscopedCallback.update` was called and the dependency value wasn’t updated yet, the arguments passed to UnscopedCallback.update() will also be kept. By default, the catalog will be frozen. However, child catalogs will keep their previous state, staying unfrozen if they weren’t and frozen if not.

Warning

Be careful with this test environment, you’ll be modifying existing dependency values!

>>> from antidote import injectable, world
>>> @injectable
... class Service:
...     pass
>>> with world.test.copy():
...     # Existing dependencies are kept
...     Service in world
True
>>> service = world[Service]
>>> with world.test.copy():
...     # dependency values also are
...     world[Service] is service
True
>>> with world.test.copy():
...     # which implies that modification to singletons are propagated!
...     world[Service].hello = 'world'
>>> world[Service].hello
'world'
>>> with world.test.copy() as overrides:
...     # overrides won't though
...     overrides[Service] = Service()
...     world[Service] is service
False
>>> world[Service] is service
True
>>> with world.test.copy():
...     # By default, world will be frozen
...     @injectable
...     class MyService:
...         pass
Traceback (most recent call last):
...
FrozenCatalogError
>>> with world.test.copy(frozen=False):
...     # Now new dependencies can be declared
...     @injectable
...     class MyService:
...         pass
...     world[MyService]
<MyService object at ...>
>>> # They won't impact the real world though
... MyService in world
False
empty()#

Creates an empty test environment. This is mostly useful when testing a Provider.

>>> from antidote import world, injectable, antidote_lib_injectable
>>> @injectable
... class Service:
...     pass
>>> with world.test.empty():
...     Service in world
False
>>> with world.test.empty():
...     # Cannot use @injectable as no providers have been included.
...     @injectable
...     class MyService:
...         pass
Traceback (most recent call last):
...
MissingProviderError
>>> with world.test.empty():
...     world.include(antidote_lib_injectable)
...     @injectable
...     class MyService:
...         pass
...     world[MyService]
<MyService object at ...>
new(*, include=Default.sentinel)#

Creates a test environment with the beahvior as one created freshly with new_catalog(). The include argument behaves in the same way, an iterable of objects that will be included in the catalog with Catalog.include().

>>> from antidote import world, injectable
>>> @injectable
... class Service:
...     pass
>>> with world.test.new():
...     Service in world
False
>>> with world.test.new():
...     # Whether world was originally frozen or not, it won't be with new().
...     @injectable
...     class MyService:
...         pass
...     world[MyService]
<MyService object at ...>
>>> # They won't impact the real world though
... MyService in world
False
class CatalogOverrides#

See PublicCatalog.test().

of(catalog)#

Return the associated overrides for the specified catalog. Raises a KeyError if the catalog is not overridable (not a child) in this test environment.

>>> from antidote import world, new_catalog
>>> catalog = new_catalog(name='child')
>>> world.include(catalog)
>>> with world.test.clone() as overrides:
...     # overrides.of(catalog) also supports del, update() and factory()
...     overrides.of(catalog)['hello'] = 'child'
...     print(catalog['hello'])
...     # It's also possible to modify the private catalog
...     overrides.of(world.private)['hello'] = 'private'
...     print(world.private['hello'])
child
private
>>> 'hello' in catalog
False
class CatalogOverride#

See PublicCatalog.test() for its usage.

__delitem__(_CatalogOverride__dependency)#

Remove a dependency from the catalog

>>> from antidote import world, injectable
>>> @injectable
... class Service:
...     pass
>>> with world.test.new() as overrides:
...     del overrides[Service]
...     Service in world
False
__setitem__(_CatalogOverride__dependency, _CatalogOverride__value)#

Set a dependency to a singleton value.

>>> from antidote import world, injectable
>>> @injectable
... class Service:
...     pass
>>> with world.test.new() as overrides:
...     overrides['hello'] = 'world'
...     world['hello']
'world'
>>> with world.test.new() as overrides:
...     overrides[Service] = 'something'
...     world[Service]
'something'
factory(_CatalogOverride__dependency, *, singleton=False)#

Register a dependency with a factory function. By default, the lifetime of the dependency is None meaning the factory is executed on each access. The dependency can also be declared as a singleton.

>>> from antidote import world, injectable
>>> @injectable
... class Service:
...     pass
>>> with world.test.new() as overrides:
...     @overrides.factory(Service)
...     def build() -> str:
...         return "dummy"
...     world[Service]
'dummy'
>>> with world.test.new() as overrides:
...     @overrides.factory('random')
...     def build() -> object:
...         return object()
...     world['random'] is world['random']
False
>>> with world.test.new() as overrides:
...     @overrides.factory('sentinel', singleton=True)
...     def build() -> object:
...         return object()
...     world['sentinel'] is world['sentinel']
True
update(*args, **kwargs)#

Update the catalog with the key/value pairs, overwriting existing dependencies.

It accepts either another dictionary object or an iterable of key/value pairs (as tuples or other iterables of length two). If keyword arguments are specified, the dictionary is then updated with those key/value pairs.

>>> from antidote import world, injectable
>>> @injectable
... class Service:
...     pass
>>> with world.test.new() as overrides:
...     overrides.update({Service: None})
...     assert world[Service] is None
...     overrides.update(hello='world')
...     assert world['hello'] == 'world'
...     overrides.update([(42, 420)])
...     assert world[42] == 420

Scopes#

class ScopeGlobalVar(*, default=Default.sentinel, name=Default.sentinel, catalog=Default.sentinel)#

Declares a scope variable, a dependency with a value that can be updated at any moment. A similar API to ContextVar is exposed. Either:py:meth:~.ScopeGlobalVar.set` or :py:meth:~.ScopeGlobalVar.reset` can be used to update the value.

>>> from antidote import ScopeGlobalVar, world
>>> current_name = ScopeGlobalVar(default="Bob")
>>> world[current_name]
'Bob'
>>> current_name.set("Alice")
ScopeVarToken(old_value='Bob', ...)
>>> world[current_name]
'Alice'

It can be easily used as a context manager:

>>> from typing import Iterator
>>> from contextlib import contextmanager
>>> @contextmanager
... def name_of(name: str) -> Iterator[None]:
...     token = current_name.set(name)
...     try:
...         yield
...     finally:
...         current_name.reset(token)
>>> with name_of('John'):
...     world[current_name]
'John'

Scope variable have a special treatment as they impact the lifetime of their dependents.

  1. A singleton cannot depend on a scope variable. It wouldn’t take into account the updates.

>>> from antidote import injectable, inject
>>> @injectable
... class Dummy:
...     def __init__(self, name: str = inject[current_name]) -> None:
...         pass
>>> world[Dummy]
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
DependencyDefinitionError: Singletons cannot depend on any scope...

If yout need access to a scope variable within a singleton, it should be injected on a method instead:

>>> from antidote import injectable, inject
>>> @injectable
... class Dummy:
...     # Use the current name for a specific task
...     def process(self, name: str = inject[current_name]) -> int:
...         return len(name)
>>> world[Dummy].process()
5
  1. Whenever a scope variable is updated, all of its dependents with a SCOPED lifetime will be re-computed lazily.

>>> from antidote import lazy
>>> @lazy.value(lifetime='scoped')
... def length(name: str = inject[current_name]) -> int:
...     return len(name)
>>> world[length]
5
>>> world[length]  # returns the same, cached, object
5
>>> current_name.set("Unknown")
ScopeVarToken(old_value='Alice', ...)
>>> world[length]
7

Note

The ScopeGlobalVar creates a global variable and not a ContextVar. It’s the same for all threads and coroutines.

class ScopeVarToken(old_value, var)#
class Missing#

An enumeration.

Provider#

class Provider(*, catalog)#

Antidote distinguishes two aspects of dependency management:

  • The definition of the dependency and its value, which is the responsibility of a Provider.

  • The lifetime management of the dependency values, including their thread-safety, which is the responsibility of the Catalog.

All of Antidote’s dependencies, such as injectable() or lazy(), rely on a Provider underneath. So creating a new Provider allows one to define a new kind of dependencies. Before diving into the implementation, there are several expectations from the Catalog:

  • Providers MUST have a distinct set of dependencies. One provider cannot shadow another one. Use child catalogs for this.

  • Providers MUST NOT store any dependency values, they’re only expected to store how dependency are defined.

  • ONLY methods prefixed with unsafe are called in a thread-safe manner by the Catalog. For all others, you must ensure thread-safety yourself.

A Provider must implement at least two methods:

Here is a minimal example which provides the square of a number:

>>> from dataclasses import dataclass
>>> from antidote.core import Provider, ProvidedDependency, world, LifeTime, Dependency
>>> @dataclass(unsafe_hash=True)  # a dependency must be hashable
... class SquareOf(Dependency[int]):  # Dependency ensures proper typing with world & inject
...     number: int
>>> @world.include
... class SquareProvider(Provider):
...     def can_provide(self, dependency: object) -> bool:
...         return isinstance(dependency, SquareOf)
...
...     def unsafe_maybe_provide(self, dependency: object, out: ProvidedDependency) -> None:
...         # Checking whether we can provide the dependency
...         if isinstance(dependency, SquareOf):
...             out.set_value(dependency.number ** 2, lifetime=LifeTime.TRANSIENT)
>>> world[SquareOf(12)]
144

Other than those two methods, the catalog will also call the following which have a default implementation:

  • create() used when including the Provider in a catalog.

  • copy() used by copy() and clone() test environments.

  • maybe_debug() used by Catalog.debug().

The catalog only relies on create() and copy() for instantiation, so feel free to change __init__() however you wish.

abstract can_provide(dependency)#

Should return whether the dependency can be provided or not.

This method MUST be implemented in a thread-safe manner.

classmethod create(catalog)#

Used by the catalog to create an instance when using Catalog.include().

maybe_debug(dependency)#

Called by Catalog.debug() to generate a debug tree. It should return a DependencyDebug if the dependency can be provided or None otherwise. By default, it will only return a description specifying that this method is not implemented if the dependency can be provided.

This method MUST be implemented in a thread-safe manner. can_provide() is not called before using this method.

unsafe_copy()#

Used for the copy() and clone() test environments. It should return a deep copy of the provider, changes in the copy should not affect the original one.

abstract unsafe_maybe_provide(dependency, out)#

If the dependency can be provided, it should be provided through the specified ProvidedDependency. can_provide() is not called before using this method.

class ProvidedDependency#

Using by a Provider to return the value of a dependency. ProvidedDependency.set_value() can only be called once and will raise an error after.

set_value(value, *, lifetime, callback=None)#

Defines the value and the lifetime of a dependency. If a callback function is provided it will be used to generate the dependency value next time it’s needed. For a singleton, it’s silently ignored.

Warning

Beware that defining a callback for a transient dependency, will force Antidote to keep the dependency object inside its cache and as such hold a strong reference to it.

class ProviderCatalog#

Similar interface to ReadOnlyCatalog. However, it won’t use dependencyOf to unwrap dependencies and hence does not provide any typing. You should use the raw dependencies directly.

class LifeTime#

The lifetime of a dependency defines how long its value is kept by the Catalog:

  • transient: The value is never kept and re-computed at each request.

  • singleton: The value is computed at most once.

  • scoped: When depending on one or multiple ScopeGlobalVar, the value is re-computed if any of those change. As long as they do not, the value is cached.

class Dependency#

Protocol to be used to be used to add support for new dependencies on Catalog and inject.

A single method __antidote_dependency_hint__() must be defined. The return type hint is used to infer the type of the dependency value which will be provided. However, the returned object should be the dependency itself. This allows any object to wrap a dependency. The dependency can also be another Dependency, it will be unwrapped as many times as necessary.

As the type of the dependency is rarely the same as its dependency value, you should use typing.cast() to avoid static typing errors.

>>> from typing import Generic, TypeVar, cast
>>> from dataclasses import dataclass
>>> from antidote import world
>>> T = TypeVar('T')
>>> @dataclass
... class MyDependencyWrapper(Generic[T]):
...     wrapped: object
...     #                                         ⯆ Defines the type of the dependency value
...     def __antidote_dependency_hint__(self) -> T:
...         # actual dependency to be used by the catalog
...         return cast(T, self.wrapped)

Tip

If you only need to wrap a value and provide a type for the dependency value, consider simply using dependencyOf instead.

class dependencyOf(_dependencyOf__dependency, *, default=Default.sentinel)#

Used by both Catalog and inject to unwrap the actual dependency and define the default value to provide if any. Its main purpose is to be used typically when defining a custom Dependency or ParameterDependency for which a default value can be provided.

class DependencyDebug(*, description, lifetime, wired=tuple(), dependencies=tuple())#

Information that should be provided by a Provider when Catalog.debug() is called.

Parameters
  • description (str) – Concise description of the dependency.

  • lifetime (LifetimeType | None) – Scope of the dependency

  • wired (Callable[..., Any] | Sequence[Callable[..., Any]]) – All objects wired for this dependency. If it’s a sequence, all of those will be treated as child dependencies in the tree and their injected dependencies will appear underneath. If it’s a single callable, the callable itself won’t appear and all of its injections will appear as direct dependencies.

  • dependencies (Sequence[object]) – All direct dependencies.

class DebugInfoPrefix(*, prefix, dependency)#

Allows to add a prefix before the description of the dependencies.

Exceptions#

exception AntidoteError#

Base class of all errors of antidote.

exception CannotInferDependencyError#

Raised by Inject.me() when the dependency could not be inferred from the type hints.

exception DependencyDefinitionError#

Raised when a dependency was not correctly defined by a Provider.

exception DependencyNotFoundError(dependency, *, catalog)#

Raised when the dependency could not be found in the catalog.

exception DoubleInjectionError(func)#

Raised when injecting a function/method that already has been injected.

exception DuplicateDependencyError#

Raised when a dependency can already be provided.

exception DuplicateProviderError(*, catalog, provider_class)#

Raised when the provider was already included in the catalog

exception FrozenCatalogError(catalog)#

Raised by methods that cannot be used with a frozen catalog.

exception MissingProviderError(provider)#

Raised when the provider is not included in the catalog.

exception UndefinedScopeVarError(dependency)#

Raised when accessing the value of scope var for which it wasn’t defined yet.