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 withInject
from antidote import service, inject, Inject @service class Database: pass @inject def f(db: Inject[Database]) -> Database: return db
>>> f() <Database ...>
A
factory()
andimplementation()
can be retrieved withFrom
orGet
: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 withGet
. 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'
-
>>> 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