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 withProvide
from antidote import Service, inject, Provide class Database(Service): pass @inject def f(db: Provide[Database]) -> Database: return db
>>> f() <Database ...>
A
Factory
,factory()
andimplementation()
can be retrieved withFrom
: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 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'
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'
-
>>> 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