Skip to main content

Dependency injection.

Project description

https://img.shields.io/pypi/v/antidote.svg https://img.shields.io/pypi/l/antidote.svg https://img.shields.io/pypi/pyversions/antidote.svg https://github.com/Finistere/antidote/actions/workflows/main.yml/badge.svg?branch=master https://codecov.io/gh/Finistere/antidote/branch/master/graph/badge.svg https://readthedocs.org/projects/antidote/badge/?version=latest

Antidotes is a dependency injection micro-framework for Python 3.7+. It is built on the idea of ensuring best maintainability of your code while being as easy to use as possible. It also provides the fastest injection with @inject allowing you to use it virtually anywhere and fast full isolation of your tests.

Antidote provides the following features:

  • Ease of use
    • Injection anywhere you need through a decorator @inject

    • No **kwargs arguments hiding actual arguments and fully mypy typed, helping you and your IDE.

    • Documented, everything has tested examples.

    • No need for any custom setup, just use your injected function as usual. You just don’t have to specify injected arguments anymore. Allowing you to gradually migrate an existing project.

  • 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.

    • Scope support

    • Async injection

  • Maintainability
    • All dependencies can be tracked back to their declaration/implementation easily.

    • Mypy compatibility and usage of type hints as much as possible.

    • Overriding dependencies will raise an error outside of tests.

    • Dependencies can be frozen, which blocks any new declarations.

    • No double injection.

    • Everything is as explicit as possible, @inject does not inject anything implicitly.

    • Type checks when a type is explicitly defined with world.get and constants.

    • Thread-safe, cycle detection.

    • Immutable whenever possible.

  • Testability
    • @inject lets you override any injections by passing explicitly the arguments.

    • Fully isolate each test with world.test.clone. They will work on separate objects.

    • Override globally any dependency locally in a test.

    • When encountering issues you can retrieve the full dependency tree, nicely formatted, with world.debug.

  • Performance*
    • Fastest @inject with heavily tuned Cython.

    • As much as possible is done at import time.

    • Testing utilities are tuned to ensure that even with full isolation it stays fast.

    • Benchmarks: comparison, injection, test utilities

*with the compiled version, in Cython. Pre-built wheels for Linux. See further down for more details.

Comparison benchmark image

Installation

To install Antidote, simply run this command:

pip install antidote

Documentation

Beginner friendly tutorial, recipes, the reference and a FAQ can be found in the documentation.

Here are some links:

Issues / Questions

Feel free to open an issue on Github for questions or issues !

Hands-on quick start

Showcase of the most important features of Antidote with short and concise examples. Checkout the Getting started for a full beginner friendly tutorial.

Injection

from antidote import inject, injectable

@injectable
class Database:
    pass

@inject
def f(db: Database = inject.me()):
    return db

assert isinstance(f(), Database)  # works !

Simple, right ? And you can still use it like a normal function, typically when testing it:

f(Database())

@inject here used the marker inject.me() with the help of the type hint to determine the dependency. But it also supports the following ways to express the dependency wiring:

  • annotated type hints:
    from antidote import Inject
    
    @inject
    def f(db: Inject[Database]):
        pass
  • list (matching argument position):
    @inject([Database])
    def f(db):
        pass
  • mapping:
    @inject({'db': Database})
    def f(db):
        pass
  • optional dependencies:
    from typing import Optional
    
    class Dummy:
        pass
    
    # When the type_hint is optional and a marker like `inject.me()` is used, None will be
    # provided if the dependency does not exists.
    @inject
    def f(dummy: Optional[Dummy] = inject.me()):
        return dummy
    
    assert f() is None

You can also retrieve the dependency by hand with world.get:

from antidote import world

# Retrieve dependencies by hand, in tests typically
world.get(Database)
world.get[Database](Database)  # with type hint, enforced when possible

Injectable

Any class marked as @injectable can be provided by Antidote. It can be a singleton or not. Scopes and a factory method are also supported. Every method is injected by default, relying on annotated type hints and markers such as inject.me():

from antidote import injectable, inject

@injectable(singleton=False)
class QueryBuilder:
    # methods are also injected by default
    def __init__(self, db: Database = inject.me()):
        self._db = db

@inject
def load_data(builder: QueryBuilder = inject.me()):
    pass

load_data()  # yeah !

Constants

Constants can be provided lazily by Antidote:

from antidote import inject, const

class Config:
    DB_HOST = const('localhost')
    DB_PORT = const(5432)

@inject
def ping_db(db_host: str = Config.DB_HOST):
    pass

ping_db()  # nice !

Constants really shines when they aren’t hard-coded:

from typing import Optional

class Config:
    # Retrieve constant from environment variables
    DB_HOST = const.env()  # using the constant name
    # or with an explicit name, default value, and forced conversion to int
    # (and other selected types)
    DB_PORT = const.env[int]("DATABASE_PORT", default=5432)

import os
os.environ['DB_HOST'] = 'localhost'
os.environ['DATABASE_PORT'] = '5432'

@inject
def check_connection(db_host: str = Config.DB_HOST,
                     db_port: int = Config.DB_PORT):
    pass

check_connection()  # perfect !

Note that on the injection site, nothing changed! And obviously you can create your own logic:

from antidote import const

@const.provider
def static(name: str, arg: Optional[str]) -> str:
    return arg

class Config:
    DB_HOST = static.const('localhost')

Stateful configuration is also possible:

from antidote import injectable, const

@injectable
class Config:
    def __init__(self):
        self.data = {'host': 'localhost'}

    @const.provider
    def get(self, name: str, arg: Optional[str]) -> str:
        return self.data[arg]

    DB_HOST = get.const('host')

Lazy

Lazy functions can be used in multiple cases:

  • to load external classes:

from antidote import lazy, inject

class Redis:
    pass

@lazy  # singleton by default
def load_redis() -> Redis:
    return Redis()

@inject
def task(redis = load_redis()):
    ...
  • as a factory:

from antidote import lazy, inject

class User:
    pass

@lazy(singleton=False)
def current_user(db: Database = inject.me()) -> User:
    return User()

@inject
def is_admin(user: User = current_user()):
    pass
  • or even to parameterize dependencies:

from dataclasses import dataclass
from antidote import lazy, inject

@dataclass
class Template:
    path: str

@lazy
def load_template(path: str) -> Template:
    return Template(path=path)

@inject
def registration(template: Template = load_template('registration.html')):
    pass

Interface/Implementation

Antidote also works with interfaces which can have one or multiple implementations which can be overridden:

from antidote import implements, inject, interface, world


@interface
class Task:
    pass


@implements(Task).by_default
class Default(Task):
    pass


@implements(Task)
class Custom(Task):
    pass


world.get(Task)


@inject
def f(task: Task = inject.me()) -> Task:
    return task

Implementations support qualifiers out of the box:

import enum


class Hook(enum.Enum):
    START = enum.auto()
    STOP = enum.auto()


@interface
class Task:
    pass


@implements(Task).when(qualified_by=Hook.START)
class StartX(Task):
    pass


@implements(Task).when(qualified_by=Hook.STOP)
class StopX(Task):
    pass


assert world.get[Task].single(qualified_by=Hook.START) == world.get(StartX)
assert world.get[Task].all(qualified_by=Hook.START) == [world.get(StartX)]


@inject
def get_single_task(task: Task = inject.me(qualified_by=Hook.START)) -> Task:
    return task


@inject
def get_all_task(tasks: list[Task] = inject.me(qualified_by=Hook.START)) -> list[Task]:
    return tasks

Testing and Debugging

inject always allows you to pass your own argument to override the injection:

from antidote import injectable, inject

@injectable
class Database:
    pass

@inject
def f(db: Database = inject.me()):
    pass

f()
f(Database())  # test with specific arguments in unit tests

You can also fully isolate your tests from each other and override any dependency within that context:

from antidote import world

# Clone current world to isolate it from the rest
with world.test.clone():
    x = object()
    # Override the Database
    world.test.override.singleton(Database, x)
    f()  # will have `x` injected for the Database

    @world.test.override.factory(Database)
    def override_database():
        class DatabaseMock:
            pass

        return DatabaseMock()

    f()  # will have `DatabaseMock()` injected for the Database

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:

def function_with_complex_dependencies():
    pass

world.debug(function_with_complex_dependencies)
# would output something like this:
"""
function_with_complex_dependencies
└──<∅> IMDBMovieDB
    └── ImdbAPI
        └── load_imdb
            ├── Config.IMDB_API_KEY
            ├── Config.IMDB_PORT
            └── Config.IMDB_HOST

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 !

Compiled

The compiled implementation is roughly 10x faster than the Python one and strictly follows the same API than the pure Python implementation. Pre-compiled wheels are available only for Linux currently. You can check whether you’re using the compiled version or not with:

from antidote import is_compiled

f"Is Antidote compiled ? {is_compiled()}"

You can force the compilation of antidote yourself when installing:

ANTIDOTE_COMPILED=true pip install antidote

On the contrary, you can force the pure Python version with:

pip install --no-binary antidote

How to Contribute

  1. Check for open issues or open a fresh issue to start a discussion around a feature or a bug.

  2. 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.

  3. Start making your changes to the master branch.

  4. Writes tests which shows that your code is working as intended. (This also means 100% coverage.)

  5. 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:

  • public classes/functions have not docstrings documenting their behavior with examples.

  • tests do not cover all of code changes (100% coverage) in the pure python.

If you face issues with the Cython part of Antidote, I may implement it myself.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

antidote-1.4.0.tar.gz (225.1 kB view details)

Uploaded Source

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

antidote-1.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl (1.8 MB view details)

Uploaded CPython 3.10manylinux: glibc 2.17+ x86-64manylinux: glibc 2.24+ x86-64

antidote-1.4.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl (2.2 MB view details)

Uploaded CPython 3.10manylinux: glibc 2.12+ x86-64manylinux: glibc 2.5+ x86-64

antidote-1.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl (1.7 MB view details)

Uploaded CPython 3.10manylinux: glibc 2.24+ i686manylinux: glibc 2.5+ i686

antidote-1.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl (1.8 MB view details)

Uploaded CPython 3.9manylinux: glibc 2.17+ x86-64manylinux: glibc 2.24+ x86-64

antidote-1.4.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl (2.2 MB view details)

Uploaded CPython 3.9manylinux: glibc 2.12+ x86-64manylinux: glibc 2.5+ x86-64

antidote-1.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl (1.7 MB view details)

Uploaded CPython 3.9manylinux: glibc 2.24+ i686manylinux: glibc 2.5+ i686

antidote-1.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl (2.0 MB view details)

Uploaded CPython 3.8manylinux: glibc 2.17+ x86-64manylinux: glibc 2.24+ x86-64

antidote-1.4.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl (2.4 MB view details)

Uploaded CPython 3.8manylinux: glibc 2.12+ x86-64manylinux: glibc 2.5+ x86-64

antidote-1.4.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl (1.9 MB view details)

Uploaded CPython 3.8manylinux: glibc 2.24+ i686manylinux: glibc 2.5+ i686

antidote-1.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl (1.7 MB view details)

Uploaded CPython 3.7mmanylinux: glibc 2.17+ x86-64manylinux: glibc 2.24+ x86-64

antidote-1.4.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl (2.0 MB view details)

Uploaded CPython 3.7mmanylinux: glibc 2.12+ x86-64manylinux: glibc 2.5+ x86-64

antidote-1.4.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl (1.6 MB view details)

Uploaded CPython 3.7mmanylinux: glibc 2.24+ i686manylinux: glibc 2.5+ i686

File details

Details for the file antidote-1.4.0.tar.gz.

File metadata

  • Download URL: antidote-1.4.0.tar.gz
  • Upload date:
  • Size: 225.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/4.0.0 CPython/3.10.4

File hashes

Hashes for antidote-1.4.0.tar.gz
Algorithm Hash digest
SHA256 f443c1af560fb0e899831491a041eea0dbcbf6ba7e5799a9edf09b0785acc1d5
MD5 89999c90fc9d70827172fce4624ff0ae
BLAKE2b-256 c6f100ebf66da3757a7011e3a52f9c7b30dfc86daf14a2e9f77d0072eb26cbc9

See more details on using hashes here.

File details

Details for the file antidote-1.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl.

File metadata

File hashes

Hashes for antidote-1.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl
Algorithm Hash digest
SHA256 4e4534ee814f9f46e9bc5172d7ad1eb93cd83cb6685e90b8292206dee3d5802a
MD5 7e8f30c27a76849437ff93d968dfbe35
BLAKE2b-256 d4d838a91e61efa904b1a4df61d89792bd7dfc355d4ca4b74f81506533fb7c9f

See more details on using hashes here.

File details

Details for the file antidote-1.4.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl.

File metadata

File hashes

Hashes for antidote-1.4.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl
Algorithm Hash digest
SHA256 1a2465a0541c78a066f954eadec70f059127bfac99ca9976ad6761e5c66ca14a
MD5 084942fae4d58753fa2ae0ee33dd28ee
BLAKE2b-256 65c32bf4d7b785bec3bbd35497d92b29d210527f2857c05ff767f6b7ae0c3b82

See more details on using hashes here.

File details

Details for the file antidote-1.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl.

File metadata

File hashes

Hashes for antidote-1.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl
Algorithm Hash digest
SHA256 18bc36328b72ff55cbabaeaa23d780ad5d2a5b3a9f5a4a9fc61fa9f7b986d92d
MD5 2b7003f4418bc961ba7ef170f2884be8
BLAKE2b-256 bba686cf58f0dad089e3c6847731948c0a7b20c717781fa6bf2558be3ad5910b

See more details on using hashes here.

File details

Details for the file antidote-1.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl.

File metadata

File hashes

Hashes for antidote-1.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl
Algorithm Hash digest
SHA256 01c76ba45d8df52c92f19ced2dc5b744452e8ecd6c47a6006274c9a505c47943
MD5 7c42f895d5a11a47bbe301e451d532fa
BLAKE2b-256 e596bd3e948117991d8764d9485d29263e2c093a1757ede057e941d76833fa12

See more details on using hashes here.

File details

Details for the file antidote-1.4.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl.

File metadata

File hashes

Hashes for antidote-1.4.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl
Algorithm Hash digest
SHA256 8bba8c21fd92c6e925561aa3abd20ea32273f42e4b0676c6c09193c1e8333834
MD5 9d1eada783b4d2c87776fff0212f4952
BLAKE2b-256 4b90b6f7b63aaf7baf8ba8e03378a4dfc19254d97db400f62fe3b69394bd634a

See more details on using hashes here.

File details

Details for the file antidote-1.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl.

File metadata

File hashes

Hashes for antidote-1.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl
Algorithm Hash digest
SHA256 f4cb82b3f0fb0bcd837967a7a41dcc14b061288c247746f4a084cf12977c4d2a
MD5 11b2062606f6f72463f44725d1da58c1
BLAKE2b-256 3674c9f6d4f90ba8c0589b7f1249b09350e1f89d335604c62c47b8e2b3adf8d6

See more details on using hashes here.

File details

Details for the file antidote-1.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl.

File metadata

File hashes

Hashes for antidote-1.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl
Algorithm Hash digest
SHA256 d5ea0fffa610d80052ec2508ea0d5312a359af3bc8abaf756470378b95e40d16
MD5 62f1d027222992a42af115785827ddd8
BLAKE2b-256 5b0496ec601a7593d4ce84439d4d74d0c39d91e2f88a9f7a6074b046c0757cbd

See more details on using hashes here.

File details

Details for the file antidote-1.4.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl.

File metadata

File hashes

Hashes for antidote-1.4.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl
Algorithm Hash digest
SHA256 689194f0812090c3e4580865079d81ec2627ef56fd8b0d57912eeb46a8189ee5
MD5 7745d156722a4ad5dd327b6b1a2321ef
BLAKE2b-256 ab2ebcc7e37ad35d681148eb97f6b78deced794ce8b2bc2397a4d5e68662affb

See more details on using hashes here.

File details

Details for the file antidote-1.4.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl.

File metadata

File hashes

Hashes for antidote-1.4.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl
Algorithm Hash digest
SHA256 5843d7b18f6ab79c1c9a6d14cf0ceafc5cee854ab31a117a8f2b6d9008c728aa
MD5 f346b57010ca41241e15668973d4f209
BLAKE2b-256 044a595198dea5f2520a6d0ac357ecbc6912d7c8b2c7600725887f717976a720

See more details on using hashes here.

File details

Details for the file antidote-1.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl.

File metadata

File hashes

Hashes for antidote-1.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl
Algorithm Hash digest
SHA256 fc6a01c00890b8aa39d9bfcf907015be3409d19adcc526ecc6073e3137537bd6
MD5 5ab28905ce6d423e9858c24d745f7b0b
BLAKE2b-256 d166d2f903299644e56bafb9d4fe0385d64bb8bc68cbde43b7dcfa5d7fb36f0d

See more details on using hashes here.

File details

Details for the file antidote-1.4.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl.

File metadata

File hashes

Hashes for antidote-1.4.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl
Algorithm Hash digest
SHA256 4bf9f2f39c3095ec01c2d6b9dd78b95ff78966d8266958ee048b6104e5024656
MD5 80c377bff50750e9b5ef664f0463e1a2
BLAKE2b-256 e49db5ed861d49957593f62039f3d111c58b8eaabb83e8cf1df21e7d2ed79643

See more details on using hashes here.

File details

Details for the file antidote-1.4.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl.

File metadata

File hashes

Hashes for antidote-1.4.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl
Algorithm Hash digest
SHA256 87863b85f12ac33ffa4f61bb7f1101791cf34e511bdfcef9916587a03d71b2fb
MD5 9967468bc8cab8b970c017619b4699ec
BLAKE2b-256 4b1e04fc536c2f1f900848a832e400f442e93e59f69d548b7183adfa713d8f19

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page