How to

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 Inject

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

    from antidote import factory, inject, From, Get
    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
    
    @inject
    def g(db: Annotated[Database, Get(Database, source=current_db)]) -> Database:
        return db
    
    >>> f()
    <Database ...>
    >>> g()
    <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'
    

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 inject, service

@service
class Database:
    pass

@inject
def f(db: Database = inject.me()) -> 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

@service
class MyService:
    pass

@inject
def f(s: MyService = inject.me()):
    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