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:
argsandkwargs: 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
Nonecan 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_localswill 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 withinspectand frame manipulation by specifying'auto'. SpecifyingNonewill deactivate the use of locals. Whenignore_type_hintsisTrue, this features cannot be used. The default behavior depends on theconfigvalue ofauto_detect_type_hints_locals. IfTruethe default value is equivalent to specifying'auto', otherwise toNone.app_catalog (API.Experimental[ReadOnlyCatalog | None]) – Defines the
app_catalogto be used by the current injection and nested ones. If unspecified, the catalog used depends on the context. Usually it will beapp_catalogdefined 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 aCatalogorapp_catalog. The latter forcing the use of the currentapp_catalogcould 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
Optionalinject()won’t raiseDependencyNotFoundErrorbut will provideNoneinstead:>>> 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 –
PredicateConstraintto 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
injectfor methods to also inject the first argument, commonly namedself. More precisely, the dependency forselfis 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 classselfwill 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
Nonecan 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_localswill 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 withinspectand frame manipulation by specifying'auto'. SpecifyingNonewill deactivate the use of locals. Whenignore_type_hintsisTrue, this features cannot be used. The default behavior depends on theconfigvalue ofauto_detect_type_hints_locals. IfTruethe default value is equivalent to specifying'auto', otherwise toNone.app_catalog (API.Experimental[ReadOnlyCatalog | None]) – Defines the
app_catalogto be used by the current injection and nested ones. If unspecified, the catalog used depends on the context. Usually it will beapp_catalogdefined 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 aCatalogorapp_catalog. The latter forcing the use of the currentapp_catalogcould 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()andInjectwork 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=Nonedoes 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
ReadOnlyCatalogfor dependency access andCatalogfor 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
ReadOnlyCatalogfor 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
Providerclass will add a new provider to theCatalog. It behaves like a class decorator in this case.A
PublicCatalogwhich 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.injectprovides 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).injectprovides 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
Trueif the catalog is frozen, elseFalse. A frozen catalog cannot be changed anymore, all new dependency registrations will fail.
- raise_if_frozen()#
Raises an
FrozenCatalogErrorif 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
ReadOnlyCatalogor 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 (
withblock),worldwill 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 itsProviderand 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
CatalogOverridecan 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,
Providerand 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(). Theincludeargument 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
KeyErrorif 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
Nonemeaning 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
ContextVaris 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
SCOPEDlifetime 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
ScopeGlobalVarcreates 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 aProviderunderneath. So creating a newProviderallows 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
unsafeare called in a thread-safe manner by theCatalog. For all others, you must ensure thread-safety yourself.
A
Providermust 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 aDependencyDebugif the dependency can be provided orNoneotherwise. 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
Providerto 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 usedependencyOfto 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
Catalogandinject.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
dependencyOfinstead.
- class dependencyOf(_dependencyOf__dependency, *, default=Default.sentinel)#
Used by both
Catalogandinjectto unwrap the actual dependency and define the default value to provide if any. Its main purpose is to be used typically when defining a customDependencyorParameterDependencyfor which a default value can be provided.
- class DependencyDebug(*, description, lifetime, wired=tuple(), dependencies=tuple())#
Information that should be provided by a
ProviderwhenCatalog.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.