2.0.0
Antidote core has been entirely reworked to be simpler and provide better static typing in addition of several features. The cython had to be dropped though for now by lack of time. It may eventually come back.
Breaking Changes
Important
- All previously deprecated changes have been removed.
- The previous
Scopeconcept has been replaced byLifeTimeandScopeGlobalVar. world.testenvironments API have been reworked. Creating one has a similar API and guarantees, butworld.test.factory,world.test.singletonand all ofworld.test.overridehave been replaced by a better alternative. SeeTestContextBuilder.- Dependencies cannot be specified through
inject({...})andinject([...])anymore. QualifiedBy/qualified_byfor interface/implementation now relies on equality instead of theid().constAPI has been reworked.const()andcont.env()have API changes andconst.providerhas been removed.- Thread-safety guarantees from Antidote are now simplified. It now only ensures lifetime consistency and some decorators such as
@injectable&@interfaceprovide some thread-safety guarantees. Providerhas been entirely reworked. It keeps the same name and purpose but has a different API and guarantees.
Core
-
@inject- removed
dependencies,strict_validationandauto_provideparameters. - removed
sourceparameter from@inject.me
- removed
-
Wiring- removed
dependenciesparameter. - renamed
class_in_localnsparameter toclass_in_localsin.Wiring.wire().
- removed
-
@wire: removeddependenciesparameter -
renamed
GettodependencyOf. Usage ofinject[]/inject.getis recommended instead for annotations. -
world- Providers are not dependencies anymore. Use :py
.Catalog.providers{.interpreted-text role="attr"}. - Providers do not check anymore that a dependency wasn't defined by another one before. They're expected to be independent.
- Exception during dependency retrieval are not wrapped in
DependencyInstantiationErroranymore FrozenWorldErrorhas been renamedFrozenCatalogError.world.test.new()now generates a test environment equivalent to a freshly created Catalog withnew_catalog. It only impacts those using a customProvider.- Removed dependency cycle detection and
DependencyCycleError. It wasn't perfectly accurate and it's not really worth it.world.debugdoes a better job at detecting and presenting those cycles.
- Providers are not dependencies anymore. Use :py
-
validate_injection()andvalidated_scope()functions have been removed. -
DependencyGetter,TypedDependencyGetterare not part of the API anymore.
Injectable
- The first argument
klassof@injectableis now positional-only. singletonandscopeparameters have been replaced bylifetime.
Interface
ImplementationsOfhas been renamed toinstanceOf.PredicateConstraintprotocol is now a callable instead of having anevaluate()method.- Classes wrapped by
implementsare now part of the private catalog by default, if you want them to be available, you'll need to apply@injectableexplicitly. @implements.overridingraises a :pyValueError{.interpreted-text role="exc"} instead of :pyRuntimeError{.interpreted-text role="exc"} if the implementation does not exist.- The default implementation is now only provided if no other implementations matched. It wasn't the case with
all()before. implements.by_defaulthas been renamed to@implements.as_defaultto be symmetrical with@interface.
Lazy
singletonandscopeparameters have been replaced bylifetime.call()function was removed from lazy functions, use the__wrapped__attribute instead.- In test contexts such as
world.test.empty()andworld.test.new(), previously defined lazy/const dependencies will not be available anymore.
Const
-
To specify a type for
.Const.envuse theconvert()argument. -
When defining static constant values such as
HOST = const('localhost'), it's NOT possible to:- define the type (
const[str]('localhost)) - define a default value
- not provide value at all anymore
- define the type (
-
const.providerhas been removed. Use@lazy.methodinstead. The only difference is that the const provider would return different objects even with the same arguments, while the lazy method won't.
Features
Core
-
AEP1: Instead of hack of module/functions
worldis now a proper instance ofPublicCatalog. Alternative catalogs can be created and included in one another. Dependencies can also now be private or public. The main goal is for now to expose a whole group of dependencies through a custom catalog.from antidote import new_catalog, inject, injectable, world # Includes by default all of Antidote catalog = new_catalog() # Only accessible from providers by default. @injectable(catalog=catalog.private) class PrivateDummy: ... @injectable(catalog=catalog) # if catalog is not specified, world is used. class Dummy: def __init__(self, private_dummy: PrivateDummy = inject.me()) -> None: self.private_dummy = private_dummy # Not directly accessible assert PrivateDummy not in catalog assert isinstance(catalog[Dummy], Dummy) # app_catalog is propagated downwards for all @inject that don't specify it. @inject(app_catalog=catalog) def f(dummy: Dummy = inject.me()) -> Dummy: return dummy assert f() is catalog[Dummy] # Not inside world yet assert Dummy not in world world.include(catalog) assert world[Dummy] is catalog[Dummy]
-
AEP2 (reworked): Antidote now defines a
ScopeGlobalVarwhich has a similar interface toContextVarand three kind of lifetimes to replace scopes:'singleton': instantiated only once'transient': instantiated on every request'scoped': used by dependencies depending on one or multipleScopeGlobalVar. When any of them changes, the value is re-computed otherwise it's cached.
ScopeGlobalVarisn't aContextVarthough, it's a global variable. It's planned to add aScopeContextVar.from antidote import inject, lazy, ScopeGlobalVar, world counter = ScopeGlobalVar(default=0) # Until update, the value stays the same. assert world[counter] == 0 assert world[counter] == 0 token = counter.set(1) assert world[counter] == 1 @lazy(lifetime='scoped') def dummy(count: int = inject[counter]) -> str: return f"Version {count}" # dummy will not be re-computed until counter changes. assert world[dummy()] == 'Version 1' assert world[dummy()] == 'Version 1' counter.reset(token) # same interface as ContextVar assert world[dummy()] == 'Version 0'
-
Catalogs, such as
worldand@inject, expose a dict-like read-only API. Typing has also been improved:from typing import Optional from antidote import const, inject, injectable, world class Conf: HOST = const('localhost') STATIC = 1 assert Conf.HOST in world assert Conf.STATIC not in world assert world[Conf.HOST] == 'localhost' assert world.get(Conf.HOST) == 'localhost' assert world.get(Conf.STATIC) is None assert world.get(Conf.STATIC, default=12) == 12 try: world[Conf.STATIC] except KeyError: pass @injectable class Dummy: pass assert isinstance(world[Dummy], Dummy) assert isinstance(world.get(Dummy), Dummy) @inject def f(host: str = inject[Conf.HOST]) -> str: return host @inject def g(host: Optional[int] = inject.get(Conf.STATIC)) -> Optional[int]: return host assert f() == 'localhost' assert g() is None
-
Testing has a simplified dict-like write-only API:
from antidote import world with world.test.new() as overrides: # add a singleton / override existing dependency overrides['hello'] = 'world' # add multiple singletons overrides.update({'second': object()}) # delete a dependency del overrides['x'] # add a factory @overrides.factory('greeting') def build() -> str: return "Hello!"
-
Added
@inject.methodwhich will inject the first argument, commonlyselfof a method with the dependency defined by the class. It won't inject when used as instance method though.from antidote import inject, injectable, world @injectable class Dummy: @inject.method def method(self) -> 'Dummy': return self assert Dummy.method() is world[Dummy] dummy = Dummy() assert dummy.method() is dummy
-
@injectnow supports wrapping function with*args. -
@injecthas nowkwargsandfallbackkeywords to replace the olddependencies.kwargstakes priority over alternative injections styles andfallbackis used in the same way asdependencies, after defaults and type hints.
Interface
-
@interfacenow supports function and@lazycalls. It also supports defining the interface as the default function with@interface.as_default:from antidote import interface, world, implements @interface def callback(x: int) -> int: ... @implements(callback) def callback_impl(x: int) -> int: return x * 2 assert world[callback] is callback_impl assert world[callback.single()] is callback_impl @interface.lazy.as_default def template(name: str) -> str: return f"Template {name!r}" assert world[template(name='test')] == "Template 'test'" @implements.lazy(template) def template_impl(name: str) -> str: return f"Alternative template {name!r}" assert world[template.all()(name='root')] == ["Alternative template 'root'"]
-
Better API for
Protocolstatic typing:from typing import Protocol from antidote import implements, instanceOf, interface, world @interface class Dummy(Protocol): ... @implements.protocol[Dummy]() class MyDummy: ... assert isinstance(world[instanceOf[Dummy]()], MyDummy) assert isinstance(world[instanceOf[Dummy]().single()], MyDummy)
-
QualifiedByrelies on equality instead of the id of the objects now. Limitations on the type of qualifiers has also been removed.from antidote import implements, interface @interface class Dummy: ... @implements(Dummy).when(qualified_by='a') class A(Dummy): ... @implements(Dummy).when(qualified_by='b') class B(Dummy): ...
-
implementshas awiringargument to prevent any wiring.
Lazy
-
@lazycan now wrap (static-)methods and define values/properties:from antidote import injectable, lazy, world @lazy.value def name() -> str: return "John" @injectable # required for lazy.property & lazy.method class Templates: @lazy.property def main(self) -> str: return "Lazy Main Template" @lazy.method def load(self, name: str) -> name: # has access to self return f"Lazy Method Template {name}" @staticmethod @lazy def static_load(name: str) -> str: return f"Lazy Static Template {name}" world[name] world[Templates.main] world[Templates.load(name='Alice')] world[Templates.static_load(name='Bob')]
-
@lazyhas now aninjectargument which can be used to prevent any injection.