How to

Be compatible with Mypy

Antidote passes the strict Mypy check and exposes its type information (PEP 561). Unfortunately static typing for decorators is limited to simple cases, hence Antidote @inject will just return the same signature from Mypys point of view. The best way, currently that I know of, is to define arguments as optional as shown below:

from antidote import inject, Service, Provide

class MyService(Service):
    pass

@inject
def f(my_service: Provide[MyService] = None) -> MyService:
    # We never expect it to be None, but it Mypy will now
    # understand that my_service may not be provided.
    assert my_service is not None
    return my_service


s: MyService = f()

# You can also overload the function, if you want a more accurate type definition:
from typing import overload

@overload
def g(my_service: MyService) -> MyService: ...

@overload
def g() -> MyService: ...

@inject
def g(my_service: Provide[MyService] = None) -> MyService:
    assert my_service is not None
    return my_service


s2: MyService = g()

Note that any of this is only necessary if you’re calling _explicitly_ the function, if only instantiate MyService through Antidote for example, you won’t need this for its __init__() function typically. You could also use a Protocol to define a different signature, but it’s more complex.

Use annotated type hints

Antidote supports a variety of annotated type hints which can be used to specify any existing dependency:

  • A Service can be retrieved with Provide

    from antidote import Service, inject, Provide
    
    class Database(Service):
        pass
    
    @inject
    def f(db: Provide[Database]) -> Database:
        return db
    
    >>> f()
    <Database ...>
    
  • A Factory, factory() and implementation() can be retrieved with From:

    from antidote import factory, inject, From
    from typing import Annotated
    # from typing_extensions import Annotated # Python < 3.9
    
    class Database:
        pass
    
    @factory
    def current_db() -> Database:
        return Database()
    
    @inject
    def f(db: Annotated[Database, From(current_db)]) -> Database:
        return db
    
    >>> f()
    <Database ...>
    
  • A constant from Constants can be retrieved with Get. Actually any dependency can be retrieved with it:

    from antidote import Constants, const, inject, Get
    from typing import Annotated
    # from typing_extensions import Annotated # Python < 3.9
    
    class Config(Constants):
        HOST = const('localhost')
    
    @inject
    def f(host: Annotated[str, Get(Config.HOST)]) -> str:
        return host
    
    >>> f()
    'localhost'
    

There is also FromArg which allows you to use information on the argument itself to decide what should be injected. The same can be done without annotated type hints with the arguments dependencies of inject().

Note

As annotated type hints can quickly become a bit tedious, using type aliases can help:

>>> CurrentDatabase = Annotated[Database, From(current_db)]
>>> @inject
... def f(db: CurrentDatabase) -> Database:
...     return db
>>> f()
<Database ...>

Test in isolation

Testing injected function or class can easily be done by simply specifying manually the arguments:

from antidote import Service, inject, Provide

class Database(Service):
    pass

@inject
def f(db: Provide[Database]) -> Database:
    return db
>>> f()
<Database ...>
>>> class TestDatabase:
...     pass
>>> f(TestDatabase())
<TestDatabase ...>

This works well for unit tests, but less for integration or functional tests. So Antidote can isolate your tests with world.test.clone(). Inside you’ll have access to any existing dependency, but their value will be different.

>>> from antidote import world
>>> real_db = world.get[Database]()
>>> with world.test.clone():
...     world.get[Database]() is real_db
False

You can also override them easily with:

  • world.test.override.singleton()

    >>> with world.test.clone():
    ...     world.test.override.singleton(Database, "fake database")
    ...     world.get(Database)
    'fake database'
    
  • world.test.override.factory()

    >>> with world.test.clone():
    ...     @world.test.override.factory()
    ...     def local_db() -> Database:
    ...         return "fake database"
    ...     # Or
    ...     @world.test.override.factory(Database)
    ...     def local_db():
    ...         return "fake database"
    ...
    ...     world.get(Database)
    'fake database'
    

You can override as many times as you want:

>>> with world.test.clone():
...     world.test.override.singleton(Database, "fake database 1 ")
...     @world.test.override.factory(Database)
...     def local_db():
...         return "fake database 2"
...
...     world.test.override.singleton(Database, "fake database 3")
...     world.get(Database)
'fake database 3'

Note

world.test.clone() will freeze() the cloned world, meaning no new dependencies can be defined.

All of the above should be what you need 99% of the time.

There is also a “joker” override world.test.override.provider() which allows more complex overrides. But I do NOT recommend its usage unless your absolutely have to. It can conflict with other overrides and will not appear in world.debug().

Debug dependency issues

If you encounter dependency issues or cycles, you can take a look at the whole dependency tree with world.debug():

from antidote import world, Service, inject, Provide

class MyService(Service):
    pass

@inject
def f(s: Provide[MyService]):
    pass

print(world.debug(f))

It will output:

f
└── MyService

Singletons have no scope markers.
<∅> = no scope (new instance each time)
<name> = custom scope

Note

If you’re not using scopes, you only need to remember that <∅> is equivalent to singleton=False.

Now wit the more complex example presented in the home page of Antidote we have:

f
└── Permanent implementation: MovieDB @ current_movie_db
    └──<∅> IMDBMovieDB
        └── ImdbAPI @ imdb_factory
            └── imdb_factory
                ├── Const: Conf.IMDB_API_KEY
                │   └── Conf
                │       └── Singleton: 'conf_path' -> '/etc/app.conf'
                ├── Const: Conf.IMDB_PORT
                │   └── Conf
                │       └── Singleton: 'conf_path' -> '/etc/app.conf'
                └── Const: Conf.IMDB_HOST
                    └── Conf
                        └── Singleton: 'conf_path' -> '/etc/app.conf'

Singletons have no scope markers.
<∅> = no scope (new instance each time)
<name> = custom scope

If you ever encounter a cyclic dependency, it will be present with a:

/!\\ Cyclic dependency: X

Ambiguous dependencies, which cannot be identified uniquely through their name, such as tags, will have their id added to help differentiate them:

from antidote import LazyCall

def current_status():
    pass

STATUS = LazyCall(current_status)

print(world.debug(STATUS))

will output the following

Lazy: current_status()  #...

Singletons have no scope markers.
<∅> = no scope (new instance each time)
<name> = custom scope
Lazy: current_status()  #0P2QAw

Singletons have no scope markers.
<∅> = no scope (new instance each time)
<name> = custom scope