Dependency injection.
Project description
Antidotes is a dependency injection micro-framework for Python 3.6+. It is designed on two core ideas:
Keep dependency declaration close to the actual code. Dependency injection is about removing the responsibility of building dependencies from their clients. It does not imply that dependency management should be done in a separate file.
Help you create navigable and maintainable code by: working with Mypy, preventing ambiguous dependency declaration (duplicates/overrides/conflicts typically), make it easy to track back where and how a dependency is declared, be as type-safe as possible.
It provides the following features:
- Ease of use
injection anywhere you need through a decorator @inject, be it static methods, functions, etc.. By default, it will only rely on type hints (classes), but it supports a lot more!
no **kwargs arguments hiding actual arguments and fully mypy typed, helping you and your IDE.
documented, see https://antidote.readthedocs.io/en/stable. If you don’t find what you need, open an issue ;)
thread-safe, cycle detection.
- Flexibility
Most common dependencies out of the box: services, configuration, factories, interface/implementation.
All of those are implemented on top of the core implementation. If Antidote doesn’t provide what you need, there’s a good chance you can implement it yourself quickly.
scope support
async injection
- Maintainability
The different kinds of dependencies are designed to be easy to track back. Finding where and how a dependency is defined is easy.
Overriding dependencies (duplicates) and injecting twice will raise an exception.
Dependencies can be frozen, which blocks any new definitions.
You can specify type hints for dependencies that dynamically retrieved (world.get, world.lazy, const)
- Testability
@inject lets you override any injections by passing explicitly the arguments.
Change dependencies locally within a context manager.
When encountering issues you can retrieve the full dependency tree, nicely formatted with world.debug.
Override locally in a test any dependencies.
- Performance
Antidote has two implementations: the pure Python one which is the reference and the compiled one (cython) which is heavily tuned for fast injection. The compiled version is the fastest dependency injection library. See injection benchmark
Installation
To install Antidote, simply run this command:
pip install antidote
Documentation
Documentation can be found at https://antidote.readthedocs.io/en/stable.
Hands-on quick start
Short and concise example of some of the most important features of Antidote. The docuemYou can find a very beginner friendly tutorial
How does injection looks like ? Here is a simple example:
from antidote import inject, Service, Constants, const, world, Provide, Get
from typing import Annotated
# from typing_extensions import Annotated # Python < 3.9
class Conf(Constants):
DB_HOST = const[str]('localhost:6789')
DB_HOST_WITHOUT_TYPE_HINT = const('localhost:6789')
class Database(Service): # Defined as a Service, so injectable.
def __init__(self, host: Annotated[str, Get(Conf.DB_HOST)]):
self._host = host # <=> Conf().get('host')
@inject # Nothing is injected implicitly.
def f(db: Provide[Database] = None):
# Defaulting to None allows for MyPy compatibility but isn't required to work.
assert db is not None
pass
f() # works !
f(Database('localhost:6789')) # but you can still use the function normally
# You can also retrieve dependencies by hand
world.get(Conf.DB_HOST)
world.get[str](Conf.DB_HOST) # with type hint
# if the dependency is the type itself, you may omit it:
world.get[Database]()
Or without annotated type hints (PEP-593):
class Database(Service):
@inject({'host': Conf.DB_HOST})
def __init__(self, host: str):
self._host = host
@inject([Database])
def f(db: Database = None):
assert db is not None
pass
# Or with auto_provide=True, all class type hints will be treated as dependencies.
# you can also explicitly say which classes with `auto_provide=[Database]`.
@inject(auto_provide=True)
def f(db: Database = None):
assert db is not None
pass
Want more ? Here is an over-engineered example to showcase a lot more features:
"""
Simple example where a MovieDB interface is defined which can be used
to retrieve the best movies. In our case the implementation uses IMDB
to dot it.
"""
from antidote import (Constants, factory, inject, world, const, Service,
implementation, Get, From)
from typing import Annotated
# from typing_extensions import Annotated # Python < 3.9
class MovieDB:
""" Interface """
class ImdbAPI:
""" Class from an external library. """
def __init__(self, *args, **kwargs):
pass
class Conf(Constants):
IMDB_HOST = const[str]('imdb.host')
# Constants will by default automatically enforce the cast to int,
# float and str. Can be removed or extended to support Enums.
IMDB_PORT = const[int]('imdb.port')
# But specifying a type is not required at all, it's mostly to help Mypy.
IMDB_API_KEY = const('imdb.api_key')
def __init__(self):
"""
Load configuration from somewhere. You can change how you configure your
application later, it won't impact the whole application.
"""
self._raw_conf = {
'imdb': {
'host': 'dummy_host',
'api_key': 'dummy_api_key',
'port': '80'
}
}
def get_const(self, name: str, arg: str):
from functools import reduce
# self.get('a.b') <=> self._raw_conf['a']['b']
return reduce(dict.get, arg.split('.'), self._raw_conf) # type: ignore
# Provides ImdbAPI, as defined by the return type annotation.
@factory
def imdb_factory(host: Annotated[str, Get(Conf.IMDB_HOST)],
port: Annotated[int, Get(Conf.IMDB_PORT)],
api_key: Annotated[str, Get(Conf.IMDB_API_KEY)]
) -> ImdbAPI:
# Here host = Conf().get('imdb.host')
return ImdbAPI(host=host, port=port, api_key=api_key)
@implementation(MovieDB)
def current_movie_db():
return IMDBMovieDB # dependency to be provided for MovieDB
class IMDBMovieDB(MovieDB, Service):
# New instance each time
__antidote__ = Service.Conf(singleton=False)
def __init__(self, imdb_api: Annotated[ImdbAPI, From(imdb_factory)]):
self._imdb_api = imdb_api
@inject
def f(movie_db: Annotated[MovieDB, From(current_movie_db)] = None):
assert movie_db is not None # for Mypy
pass
f()
Or without annotated type hints:
@factory
@inject([Conf.IMDB_HOST, Conf.IMDB_PORT, Conf.IMDB_API_KEY])
def imdb_factory(host: str, port: int, api_key: str) -> ImdbAPI:
return ImdbAPI(host=host, port=port, api_key=api_key)
class IMDBMovieDB(MovieDB, Service):
__antidote__ = Service.Conf(singleton=False)
@inject({'imdb_api': ImdbAPI @ imdb_factory})
def __init__(self, imdb_api: ImdbAPI):
self._imdb_api = imdb_api
@inject([MovieDB @ current_movie_db])
def f(movie_db: MovieDB = None):
assert movie_db is not None
pass
We’ve seen that you can override any parameter:
conf = Conf()
f(IMDBMovieDB(imdb_factory(
# The class attributes will retrieve the actual value when called on a instance.
# Hence this is equivalent to conf.get('imdb.host'), making your tests easier.
host=conf.IMDB_HOST,
port=conf.IMDB_PORT,
api_key=conf.IMDB_API_KEY, # <=> conf.get('imdb.api_key')
)))
But if you only to change one part in a complex dependency graph, you can override them locally with:
# When testing you can also override locally some dependencies:
with world.test.clone(keep_singletons=True):
world.test.override.singleton(Conf.IMDB_HOST, 'other host')
f()
If you ever need to debug your dependency injections, Antidote also provides a tool to have a quick summary of what is actually going on:
world.debug(f)
# will output:
"""
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
"""
Hooked ? Check out the documentation ! There are still features not presented here !
Comparison
Disclaimer: This comparison is mostly based on the documentation of the most popular libraries I know of and is obviously somewhat biased. :)
In short, how does Antidote compare to other libraries ?
Everything is explicit: Some libraries using an
@inject
-like decorator, such as injector, lagom or python_inject will try to instantiate a class when used as a type hint if the argument is missing. Antidote will only inject dependencies that you have defined as such. Also by default Antidote will not inject anything unless explicitly told so, even for class type hints. This makes it a lot easier to understand what will be injected or not when looking at unknown code.Rich ecosystem: With the exception of dependency_injector, I don’t know of any library providing as much flexibility regarding dependency management. Most of them will only inject class, support simple factories and singletons. With Antidote you can also express configuration, interfaces, lazy methods or functions.
Maintainability: Again with the exception of dependency_injector, dependency injection libraries may hide things from you. Typically when defining a factory for class, it’ll be defined somewhere and you won’t have any way of knowing easily where to find the factory without knowing the application. Dependencies defined out of the box with Antidote ensure that you can always track back a dependency to its definition with nothing more than a “go to definition” from your IDE.
Performance: Antidote’s
@inject
is heavily tuned for performance in the compiled version (Cython). Not other library goes as far. Now whether it’s really necessary for a dependency injection library is debatable. But this allows you to use@inject
virtually anywhere easily.
The main difference with dependency_injector is the philosophy of the library. With dependency_injector declaration of
dependencies (to the container
) and their implementation are in two separate files:
# my_service.py
# Dependency Injector
class MyService:
pass
# services.py
# Dependency Injector
from dependency_injector import containers, providers
class Container(containers.DeclarativeContainer):
my_service = providers.Singleton(MyService)
This implies that you have one more file to maintain. And with a lot of dependency management you start managing either one big container or multiple ones.
However this one big advantage compared to most other dependency injection libraries: it’s easy to understand how dependencies are wired together, making it a lot more maintainable than most libraries. It is especially when declaring factories. With dependency_injector you would do something like that:
# services.py
# Dependency Injector
class Container(containers.DeclarativeContainer):
my_service = providers.Factory(my_factory)
While most other libraries you have no easy way to know how MyService
is created by the dependency injection
framework:
# services.py
# Injector
@provider
def my_factory() -> MyService:
pass
@inject
def f(s: MyService):
pass
# Lagom
container[MyService] = my_factory
@magic_bind_to_container(container)
def f(s: MyService):
pass
# Python Inject
def config(binder):
binder.bind(MyService, my_factory)
inject.configure(config)
@inject.autoparams()
def f(s: MyService):
pass
But with Antidote you always can track back to the definition of a dependency:
from antidote import factory, inject, From
@factory
def my_factory() -> MyService:
pass
@inject(dict(my_service=MyService @ my_factory))
def f(my_service: MyService):
pass
# Or with annotated type hints
@inject
def f(my_service: Annotated[MyService, From(my_factory)]):
pass
So Antidote provides the same level of maintainability of dependency_injector, well even more IMHO, while being simpler to use and to integrate into you existing projects.
Cython
The cython implementation is roughly 10x faster than the Python one and strictly follows the same API than the pure Python implementation. This implies that you cannot depend on it in your own Cython code if any. It may be moved to another language.
If you encounter any inconsistencies, please open an issue ! You can avoid the Cython version from PyPI with the following:
pip install --no-binary antidote
Beware that PyPy is only tested with the pure Python version, not the Cython one.
Issues / Feature Requests / Questions
Feel free to open an issue on Github for questions, requests or issues ! ;)
How to Contribute
Check for open issues or open a fresh issue to start a discussion around a feature or a bug.
Fork the repo on GitHub. Run the tests to confirm they all pass on your machine. If you cannot find why it fails, open an issue.
Start making your changes to the master branch.
Writes tests which shows that your code is working as intended. (This also means 100% coverage.)
Send a pull request.
Be sure to merge the latest from “upstream” before making a pull request!
If you have any issue during development or just want some feedback, don’t hesitate to open a pull request and ask for help !
Pull requests will not be accepted if:
classes and non trivial functions have not docstrings documenting their behavior.
tests do not cover all of code changes (100% coverage) in the pure python.
If you face issues with the Cython part of Antidote just send the pull request, I can adapt the Cython part myself.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distributions
Hashes for antidote-0.11.0-cp39-cp39-manylinux2014_x86_64.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | e99849871226db99892e95608e3f618b9548c9ee2d6fcada2277ac224a70116f |
|
MD5 | 790677c1bbb544149946ca76e4dc93a9 |
|
BLAKE2b-256 | 43d469d753d9691cc7bfda280f77649a9cbfffd6d2a42ccc47f36edad654f629 |
Hashes for antidote-0.11.0-cp39-cp39-manylinux2014_i686.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | edca089c8726cad55aa312847fe8df7b0ad114351ccdb99eea4042c293d27b7d |
|
MD5 | 529aeee47d5eeb964277741596543a24 |
|
BLAKE2b-256 | 27774d5c1901dc0d5795c2eb7f0c0de07e7d23b504b4ac7cc46e344f1b5e2d58 |
Hashes for antidote-0.11.0-cp39-cp39-manylinux2010_x86_64.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | 555b0954033238560a5640e880ce38d904a189f03831e5a2eda507e4e2ce58e4 |
|
MD5 | da7632d123f27c2af8e8dfade468f094 |
|
BLAKE2b-256 | 073d7d6e516c08741213f691cf78f8c8744c77d0e82bc36c653405acd07ad105 |
Hashes for antidote-0.11.0-cp39-cp39-manylinux1_x86_64.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | fa774343209805ae79ddc1ed0bd6749901f91b8782817b926fcb2caa4d1828a1 |
|
MD5 | 7abf1cc6d8b34fb5487833ff57dc02e7 |
|
BLAKE2b-256 | 839d8bc3d5cf3d7753fb5ef80f30c9e87a2c5cd28211b762b478cc5f0ad5e8fe |
Hashes for antidote-0.11.0-cp39-cp39-manylinux1_i686.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | 1b7036df6a359f638ee92d4883b80c981e0919590f56e34692e8b57150b5be83 |
|
MD5 | 930774b694271f65783da6b6a51a48dc |
|
BLAKE2b-256 | 35f39689ef1eadc8c042f085229cd1966eb20ea537d9aebd25e7c77294a61a80 |
Hashes for antidote-0.11.0-cp38-cp38-manylinux2014_x86_64.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | 2dcfa29e9c4c293879520633638ff3dffeb8c965b0e28221eceea8dddf9806cc |
|
MD5 | e955163db27b08f57c878c1647bb9889 |
|
BLAKE2b-256 | 6a76d6e3cdd96c46fd23a673f537751c2d596f5e335367a8b8feddbf9c71cb14 |
Hashes for antidote-0.11.0-cp38-cp38-manylinux2014_i686.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | 72e7b0b9c71b22fc21ffe8f292bac6966b536858c26d5057c62826deb4010bb5 |
|
MD5 | bbbd8320e4867cc77e049cec5c8f0db9 |
|
BLAKE2b-256 | 01d5d3befe97903219d5ef0629c8969acf23c3224247adfaf22288db6d066a5c |
Hashes for antidote-0.11.0-cp38-cp38-manylinux2010_x86_64.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | 4cd233fd60837937a3f452c20c9e464c1e198ab50e87ae47ab605c4b4191aee7 |
|
MD5 | 1b9f0f7774047fdef5a93e7b31b4164b |
|
BLAKE2b-256 | 07db5da6ff2ac802cbfc948be79965cdedabee2c8b90b4ce8bf368cb252592e9 |
Hashes for antidote-0.11.0-cp38-cp38-manylinux1_x86_64.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | 759a3ce47f54454fdca69ad36f045a543656dd284c80e669a5cebb6179c0eafd |
|
MD5 | 0c1caa4a6db83cd395a157105b90e2d0 |
|
BLAKE2b-256 | 6566f3507e750c314225c6a91d58e2759cfd9e367bacd9d3cc676fd900bc3e90 |
Hashes for antidote-0.11.0-cp38-cp38-manylinux1_i686.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | 3bcad1cfcedfd974a9f98fda048a05be124e464f1d79174edb79e7b8c61649f8 |
|
MD5 | 0ab2bb5ebb5adf5e1d3ceb9c268e2241 |
|
BLAKE2b-256 | 21de9a49d29aabd68d483bc25039f52e5de3fbe4ed9ffc048b620ec8968a2175 |
Hashes for antidote-0.11.0-cp37-cp37m-manylinux2014_x86_64.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | 9a8dfb3eb5adc94fe850424e671e18ca492680c6e9054b431acdac2bd8f97342 |
|
MD5 | d2f37c31e77e52d6819c74efe2d104ce |
|
BLAKE2b-256 | abe1c7e0b1cc0a6c90a40d241e78658005132652ee0d05a7fb392a2e0695fc1d |
Hashes for antidote-0.11.0-cp37-cp37m-manylinux2014_i686.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | 7bab357bbe5a6e38855662ae5fde616d014794cf53ebe961c5ea8898e3ef8c41 |
|
MD5 | 5e0b416d6ad64fbcb5dcb79df9949979 |
|
BLAKE2b-256 | 43352a39e5d59a14351c2faefa122d08da7cac857b5e07e4b0d356c11e1bb902 |
Hashes for antidote-0.11.0-cp37-cp37m-manylinux2010_x86_64.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | 2662326bfc77bece4964199442784402e481b1d2c599c77b65d6a43e7fb87432 |
|
MD5 | eb750b0b45dbc509803790e68ac3ac9e |
|
BLAKE2b-256 | b87835f4b15f1c9173ff083554e7dc634feda3c326f3e769a6997d6e8a466bb1 |
Hashes for antidote-0.11.0-cp37-cp37m-manylinux1_x86_64.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | 04cb9cd6ef3db003c5eb8617698376eba900cba353ce5aff23779da4bff9b8cb |
|
MD5 | 64973dc1eef719e84cb2f28d4108a049 |
|
BLAKE2b-256 | 11836755a4f2c0cab60bca4c31d26511e54d4119e18d903a5e3fe8ed799f35e7 |
Hashes for antidote-0.11.0-cp37-cp37m-manylinux1_i686.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | aa2948feaed0bc90cb6d2f4f1aba64a98f7351169ba362678d36b85a993a3e77 |
|
MD5 | 9532f1fe09bd64da47a0c91ea8a2393f |
|
BLAKE2b-256 | a56e3cf89a23a164b56e43f59511c585dfbd4ed7160dfbcce652820a14c4e17e |
Hashes for antidote-0.11.0-cp36-cp36m-manylinux2014_x86_64.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | 1a2730c268eb327b6d36f5477dd559bbfe82451d2a1d156359868a33685ff4d8 |
|
MD5 | 6909261df047510b05bd77f27c28a067 |
|
BLAKE2b-256 | 011a487e0b52f46d6ff67cc02f90f188ff4362155f49341a27c677c3c6f60b5c |
Hashes for antidote-0.11.0-cp36-cp36m-manylinux2014_i686.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | ce0bdeed480b1980e81df3d951dca6b497535f7c9fbf2505d1cd5389ae3b3424 |
|
MD5 | c8961d9bdad29fe8f62b4efc95a32c3b |
|
BLAKE2b-256 | b2d07cce20d0936fe6eb088f87edea450fabdfd2c9116dd3b412da2ba49196c6 |
Hashes for antidote-0.11.0-cp36-cp36m-manylinux2010_x86_64.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | a0d4b281eb8e78e346c960954b95925ef30dfd6ecbbecf495a7f784f1d7e8b0f |
|
MD5 | 450f4a9cb92bf2947f29b2de9c419c69 |
|
BLAKE2b-256 | 2f5461cda0d986994894ba0bacb8e4dc2d569b05dd3366d7cb7f35510c9b812f |
Hashes for antidote-0.11.0-cp36-cp36m-manylinux1_x86_64.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | 6d081479cd66c25161aa8ba820cf63d55e614e1914581f656aafde2d8c84bfbc |
|
MD5 | 1a2559261e93d52c5ef9401243a10c16 |
|
BLAKE2b-256 | 4a3abd1ee3a221b2b49746e5430243c118a6da2a81fa88ad9fb580bb7a53438d |
Hashes for antidote-0.11.0-cp36-cp36m-manylinux1_i686.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | 2156cc4fe6b8f3aefbcf03c70dee07197d3749b61b70a1484bff17e281fcac0f |
|
MD5 | d7f341de7df6a9411d661144ae73effb |
|
BLAKE2b-256 | c8e3b03245e18ca702cf923ff90af4149a2e7a200d9c2b2af9919d34a60c640e |