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:
args
andkwargs
: argument name to dependencies.Defaults (ex:
me()
) or PEP-593 Annotated type hints (ex:InjectMe
)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
, seemethod()
.- 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 nortype_hints_locals
will not be used at all. Defaults toFalse
.ignore_defaults (bool) – If
True
, default values such asinject.me()
are ignored. Defaults toFalse
.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 withinspect
and frame manipulation by specifying'auto'
. SpecifyingNone
will deactivate the use of locals. Whenignore_type_hints
isTrue
, this features cannot be used. The default behavior depends on theconfig
value ofauto_detect_type_hints_locals
. IfTrue
the default value is equivalent to specifying'auto'
, otherwise toNone
.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 beapp_catalog
defined by a upstreaminject
. If never never specified, it’s py:obj:.world. However, dependencies such asinjectable()
useInject.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 aCatalog
orapp_catalog
. The latter forcing the use of the currentapp_catalog
could otherwise be rewired byinjectable()
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 raiseDependencyNotFoundError
but will provideNone
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
*constraints –
PredicateConstraint
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 namedself
. More precisely, the dependency forself
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 classself
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 nortype_hints_locals
will not be used at all. Defaults toFalse
.ignore_defaults (bool) – If
True
, default values such asinject.me()
are ignored. Defaults toFalse
.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 withinspect
and frame manipulation by specifying'auto'
. SpecifyingNone
will deactivate the use of locals. Whenignore_type_hints
isTrue
, this features cannot be used. The default behavior depends on theconfig
value ofauto_detect_type_hints_locals
. IfTrue
the default value is equivalent to specifying'auto'
, otherwise toNone
.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 beapp_catalog
defined by a upstreaminject
. If never never specified, it’s py:obj:.world. However, dependencies such asinjectable()
useInject.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 aCatalog
orapp_catalog
. The latter forcing the use of the currentapp_catalog
could otherwise be rewired byinjectable()
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 howme()
andInject
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 thetype_hints_locals
. Specifyingtype_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 toinclude
.
- class CatalogId(name, test_context_ids)#
- class PublicCatalog#
Subclass of
ReadOnlyCatalog
for dependency access andCatalog
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 theCatalog
. 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 ...>
- class ReadOnlyCatalog#
- __contains__(_ReadOnlyCatalog__dependency)#
- __getitem__(_DependencyAccessor__dependency)#
Return the value for the dependency. Raises a
DependencyNotFoundError
(subclass ofKeyError
) 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 aDependencyNotFoundError
(subclass ofKeyError
).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, elseFalse
. 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_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 itsProvider
and child catalog. But those will be empty as if no dependency was ever registered.clone()
: All of theProvider
, 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 thanclone()
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 toUnscopedCallback.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()
. Theinclude
argument behaves in the same way, an iterable of objects that will be included in the catalog withCatalog.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.
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
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 aContextVar
. 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()
orlazy()
, rely on aProvider
underneath. So creating a newProvider
allows one to define a new kind of dependencies. Before diving into the implementation, there are several expectations from theCatalog
: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 theCatalog
. For all others, you must ensure thread-safety yourself.
A
Provider
must implement at least two methods:can_provide()
which returns whether the dependency can be provided or not.unsafe_maybe_provide()
which provides the dependency value if possible.can_provide()
is NOT called before using this method.
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:
copy()
used bycopy()
andclone()
test environments.maybe_debug()
used byCatalog.debug()
.
The catalog only relies on
create()
andcopy()
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 aDependencyDebug
if the dependency can be provided orNone
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()
andclone()
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 usedependencyOf
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 multipleScopeGlobalVar
, 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
andinject
.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 anotherDependency
, 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
andinject
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 customDependency
orParameterDependency
for which a default value can be provided.
- class DependencyDebug(*, description, lifetime, wired=tuple(), dependencies=tuple())#
Information that should be provided by a
Provider
whenCatalog.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.