Skip to content
This repository has been archived by the owner on Aug 10, 2024. It is now read-only.

Releases: Finistere/antidote

V2.0.0

31 Aug 21:27
Compare
Choose a tag to compare

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 by LifeTime and ScopeGlobalVar.
  • world.test environments API have been reworked. Creating one has a similar API and guarantees, but world.test.factory, world.test.singleton and all of world.test.override have been replaced by a better alternative. See TestContextBuilder.
  • Dependencies cannot be specified through inject({...}) and inject([...]) anymore.
  • QualifiedBy/qualified_by for interface/implementation now relies on equality instead of the id().
  • const API has been reworked. const() and cont.env() have API changes and const.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 and auto_provide parameters.
    • removed source parameter from @inject.me
  • Wiring

    • removed dependencies parameter.
    • renamed class_in_localns parameter to class_in_locals in .Wiring.wire().
  • @wire: removed dependencies parameter

  • renamed Get to dependencyOf. Usage of inject[]/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 renamed FrozenCatalogError.
    • world.test.new() now generates a test environment equivalent to a freshly created Catalog with new_catalog. It only impacts those using a custom Provider.
    • 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.
  • validate_injection() and validated_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 and scope parameters have been replaced by lifetime.

Interface

  • ImplementationsOf has been renamed to instanceOf.
  • PredicateConstraint protocol is now a callable instead of having an evaluate() 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 and scope parameters have been replaced by lifetime.
  • call() function was removed from lazy functions, use the __wrapped__ attribute instead.
  • In test contexts such as world.test.empty() and world.test.new(), previously defined lazy/const dependencies will not be available anymore.

Const

  • To specify a type for .Const.env use the convert() 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
  • 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 of PublicCatalog. 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 to ContextVar 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 multiple ScopeGlobalVar. When any of them changes, the value is re-computed otherwise it's cached.

    ScopeGlobalVar isn't a ContextVar though, it's a global variable. It's planned to add a ScopeContextVar.

    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, commonly self 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 now kwargs and fallback keywords to replace the old dependencies. kwargs takes priority over alternative injections styles and fallback is used in the same way as dependencies, 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...
Read more

v1.4.2

26 Jun 19:49
Compare
Choose a tag to compare

Bug fix

  • #56 Fix injection error for some union type hints such as str | List[str].

v1.4.1

01 Jun 16:11
Compare
Choose a tag to compare

Bug fix

  • Fix type error for implements.overriding().

v1.4.0

22 May 19:32
Compare
Choose a tag to compare

Deprecation

  • Constants is deprecated as not necessary anymore with the new const.
  • @factory is deprecated in favor of @lazy.

Features

  • @lazy has been added to replace @factory and the parameterized() methods of both Factory and Service.

    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
  • @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 legacy auto_cast from Constants.

Bug fix

  • Better behavior of inject and world.debug with function wrappers, having a __wrapped__ attribute.

V1.3.0

26 Apr 18:46
Compare
Choose a tag to compare

Deprecation

  • @service is deprecated in favor of @injectable which is a drop-in replacement.
  • @inject used to raise a RuntimeError when specifying ignore_type_hints=True and no injections were found. It now raises NoInjectionsFoundError
  • 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 with config Auto-detection is done through inspect 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 to Wiring and @wire

  • Added type_hints_locals and class_in_localns argument to Wiring.wire

Bug fix

  • Fix Optional detection in predicate constraints.

V1.2.0

19 Apr 15:43
Compare
Choose a tag to compare

Bug fix

  • Fix injection error when using the Klass | None notation instead of Optional[Klass] in Python 3.10.

Features

  • frozen keyword argument to world.test.clone() which allows one to control whether the cloned world is already frozen or not.
  • Both inject.get and world.get now strictly follow the same API.
  • interface() and implements() which provide a cleaner way to separate implementations from the public interface. Qualifiers are also supported out of the box. They can be added with qualified_by keyword and requested with either qualified_by or qualified_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 the QualifiedBy predicate which is part of the public API.

v1.1.1

25 Mar 22:25
Compare
Choose a tag to compare

Bug fix

  • Injected functions/methods with @inject did not behave correctly with inspect.isfunction, inspect.ismethod, inspect.iscoroutinefunction and inspect.iscoroutine.

v1.1.0

19 Mar 22:51
Compare
Choose a tag to compare

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 and ABCService 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's auto_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 and dependency @ 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 renamed Inject.

  • 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 and const have better type checking behavior, doing it only when the specified type is an actual instance of type. 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

v1.0.1

14 Nov 22:45
Compare
Choose a tag to compare
  • Update fastrlock dependency to >=0.7,<0.9 to support Python 3.10 for the compiled
    version.