Releases: Finistere/antidote
V2.0.0
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
Scope
concept has been replaced byLifeTime
andScopeGlobalVar
. world.test
environments API have been reworked. Creating one has a similar API and guarantees, butworld.test.factory
,world.test.singleton
and all ofworld.test.override
have been replaced by a better alternative. SeeTestContextBuilder
.- Dependencies cannot be specified through
inject({...})
andinject([...])
anymore. QualifiedBy
/qualified_by
for interface/implementation now relies on equality instead of theid()
.const
API has been reworked.const()
andcont.env()
have API changes andconst.provider
has been removed.- Thread-safety guarantees from Antidote are now simplified. It now only ensures lifetime consistency and some decorators such as
@injectable
&@interface
provide some thread-safety guarantees. Provider
has been entirely reworked. It keeps the same name and purpose but has a different API and guarantees.
Core
-
@inject
- removed
dependencies
,strict_validation
andauto_provide
parameters. - removed
source
parameter from@inject.me
- removed
-
Wiring
- removed
dependencies
parameter. - renamed
class_in_localns
parameter toclass_in_locals
in.Wiring.wire()
.
- removed
-
@wire
: removeddependencies
parameter -
renamed
Get
todependencyOf
. Usage ofinject[]
/inject.get
is 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
DependencyInstantiationError
anymore FrozenWorldError
has 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.debug
does 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
,TypedDependencyGetter
are not part of the API anymore.
Injectable
- The first argument
klass
of@injectable
is now positional-only. singleton
andscope
parameters have been replaced bylifetime
.
Interface
ImplementationsOf
has been renamed toinstanceOf
.PredicateConstraint
protocol is now a callable instead of having anevaluate()
method.- Classes wrapped by
implements
are now part of the private catalog by default, if you want them to be available, you'll need to apply@injectable
explicitly. @implements.overriding
raises 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_default
has been renamed to@implements.as_default
to be symmetrical with@interface
.
Lazy
singleton
andscope
parameters 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.env
use 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.provider
has been removed. Use@lazy.method
instead. 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
world
is 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
ScopeGlobalVar
which has a similar interface toContextVar
and 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.
ScopeGlobalVar
isn't aContextVar
though, 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
world
and@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.method
which will inject the first argument, commonlyself
of 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
-
@inject
now supports wrapping function with*args
. -
@inject
has nowkwargs
andfallback
keywords to replace the olddependencies
.kwargs
takes priority over alternative injections styles andfallback
is used in the same way asdependencies
, after defaults and type hints.
Interface
-
@interface
now supports function and@lazy
calls. 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...
v1.4.2
v1.4.1
v1.4.0
Deprecation
Constants
is deprecated as not necessary anymore with the newconst
.@factory
is deprecated in favor of@lazy
.
Features
-
@lazy
has been added to replace@factory
and theparameterized()
methods of bothFactory
andService
.from antidote import lazy, inject class Redis: pass @lazy # singleton by default def load_redis() -> Redis: return Redis() @inject def task(redis = load_redis()): ...
-
const
has been entirely reworked for better typing and ease of use:- it doesn't require
Constants
anymore. - environment variables are supported out of the box with
const.env
. - custom logic for retrieval can be defined with
@const.provider
.
Here's a rough overview:
from typing import Optional from antidote import const, injectable class Conf: THREADS = const(12) # static const PORT = const.env[int]() # converted to int automatically HOST = const.env("HOSTNAME") # define environment variable name explicitly, @injectable class Conf2: # stateful factory. It can also be stateless outside of Conf2. @const.provider def get(self, name: str, arg: Optional[str]) -> str: return arg or name DUMMY = get.const() NUMBER = get.const[int]("90") # value will be 90
- it doesn't require
-
@implements.overriding
overrides an existing implementation, and will be used in exactly the same conditions as the overridden one: default or not, predicates... -
@implements.by_default
defines a default implementation for an interface outside the weight system.
Experimental
const.converter
provides a similar to feature to the legacyauto_cast
fromConstants
.
Bug fix
- Better behavior of
inject
andworld.debug
with function wrappers, having a__wrapped__
attribute.
V1.3.0
Deprecation
@service
is deprecated in favor of@injectable
which is a drop-in replacement.@inject
used to raise aRuntimeError
when specifyingignore_type_hints=True
and no injections were found. It now raisesNoInjectionsFoundError
Wiring.wire
used to return the wired class, it won't be the case anymore.
Features
-
Add local type hint support with
type_hints_locals
argument for@inject
,@injectable
,@implements
and@wire
The default behavior can be configured globally withconfig
Auto-detection is done throughinspect
and frame manipulation. It's mostly helpful inside tests.from __future__ import annotations from antidote import config, inject, injectable, world def function() -> None: @injectable class Dummy: pass @inject(type_hints_locals='auto') def f(dummy: Dummy = inject.me()) -> Dummy: return dummy assert f() is world.get(Dummy) function() config.auto_detect_type_hints_locals = True def function2() -> None: @injectable class Dummy: pass @inject def f(dummy: Dummy = inject.me()) -> Dummy: return dummy assert f() is world.get(Dummy) function2()
-
Add
factory_method
to@injectable
(previous@service
)from __future__ import annotations from antidote import injectable @injectable(factory_method='build') class Dummy: @classmethod def build(cls) -> Dummy: return cls()
-
Added
ignore_type_hints
argument toWiring
and@wire
-
Added
type_hints_locals
andclass_in_localns
argument toWiring.wire
Bug fix
- Fix
Optional
detection in predicate constraints.
V1.2.0
Bug fix
- Fix injection error when using the
Klass | None
notation instead ofOptional[Klass]
in Python 3.10.
Features
frozen
keyword argument toworld.test.clone()
which allows one to control whether the cloned world is already frozen or not.- Both
inject.get
andworld.get
now strictly follow the same API. interface()
andimplements()
which provide a cleaner way to separate implementations from the public interface. Qualifiers are also supported out of the box. They can be added withqualified_by
keyword and requested with eitherqualified_by
orqualified_by_one_of
.
from antidote import implements, inject, interface, world, QualifiedBy
V1 = object()
V2 = object()
@interface
class Service:
pass
@implements(Service).when(qualified_by=V1)
class ServiceImpl(Service):
pass
@implements(Service).when(QualifiedBy(V2))
class ServiceImplV2(Service):
pass
world.get[Service].single(qualified_by=V1)
world.get[Service].all()
@inject
def f(service: Service = inject.me(QualifiedBy(V2))) -> Service:
return service
@inject
def f(services: list[Service] = inject.me(qualified_by=[V1, V2])) -> list[Service]:
return services
Experimental
Predicate
API is experimental allows you to define your custom logic for selecting the right implementation for a given interface. Qualifiers are implemented with theQualifiedBy
predicate which is part of the public API.
v1.1.1
v1.1.0
Breaking static typing change
- A function decorated with
@factory
will not have the@
operator anymore from a static typing perspective. It's unfortunately not possible with the addition of the class support for the decorator.
Deprecation
-
Service
andABCService
are deprecated in favor of@service
. -
Passing a function to the argument
dependencies
of@inject
is deprecated. If you want to customize how Antidote injects dependencies, just wrap@inject
instead. -
@inject
'sauto_provide
argument is deprecated. If you rely on this behavior, wrap@inject
. -
world.lazy
is deprecated. It never brought a lot of value, one can easily write it oneself. -
dependency @ factory
anddependency @ implementation
are replaced by the more explicit notation:world.get(dependency, source=factory) @inject(dependencies={'db': Get(dependency, source=factory)}) def (db): ...
-
Annotation
Provide
has been renamedInject
. -
world.get
will not support extracting annotated dependencies anymore. -
Omitting the dependency when a type is specified in
world.get
is deprecated.world.get
provides now better type information.from antidote import world, service @service class Dummy: pass # this will expose the correct type: world.get(Dummy) # so this is deprecated world.get[Dummy]() # you can still specify the type explicitly world.get[Dummy](Dummy)
Change
- Both
world.get
andconst
have better type checking behavior, doing it only when the specified type is an actual instance oftype
. For protocols, type check will only be done with those decorated with@typing.runtime_checkable
. - Dropped Python 3.6 support.
Features
-
Add
ignore_type_hints
to@inject
to support cases when type hints cannot be evaluated, typically in circular imports. -
Adding Markers for
@inject
used as default arguments to declare injections:from antidote import const, Constants, factory, inject, service class Config(Constants): HOST = const[str]("host") @service class Dummy: value: str @factory def dummy_factory() -> Dummy: return Dummy() # inject type hint @inject def f(dummy: Dummy = inject.me()) -> Dummy: return dummy # inject type hint with factory @inject def f2(dummy: Dummy = inject.me(source=dummy_factory)) -> Dummy: return dummy # inject constants @inject def f3(host: str = Config.HOST) -> str: return host # inject a dependency explicitly @inject def f4(x=inject.get(Dummy)) -> Dummy: return x # inject a dependency with a factory explicitly @inject def f5(x=inject.get(Dummy, source=dummy_factory)) -> Dummy: return x