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, be it static methods, functions, etc.. By default, it will only rely on annotated type hints, but it supports a lot more!

    • 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
  • dictionary:
    @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, Constants, const

class Config(Constants):
    DB_HOST = const('localhost')

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

ping_db()  # nice !

This feature really shines when your constants aren’t hard-coded:

from typing import Optional
from antidote import inject, Constants, const

class Config(Constants):
    # Like world.get, a type hint can be provided and is enforced.
    DB_HOST = const[str]()
    DB_PORT = const[int]()
    DB_USER = const[str](default='postgres')  # default is used on LookupError

    # name of the constant and the arg given to const() if any.
    def provide_const(self, name: str, arg: Optional[object]):
        return os.environ[name]

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

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

check_connection()  # perfect !

Note that on the injection site, nothing changed!

Factory

Factories are used by Antidote to generate a dependency, typically a class from an external code:

from antidote import factory, inject

class User:
    pass

@factory(singleton=False)  # function is injected by default
def current_user(db: Database = inject.me()) -> User:
    return User()

# Consistency between the type hint and the factory result type hint is enforced.
@inject
def is_admin(user: User = inject.me(source=current_user)):
    pass

While it’s a bit verbose, you always know how the dependency is created. Obviously you can retrieve it from world:

from antidote import world

world.get(User, source=current_user)

Interface/Implementation

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

from typing import Protocol, TypeVar

from antidote import implements, inject, interface, world


class Event:
    ...


class InitializationEvent(Event):
    ...


E = TypeVar('E', bound=Event, contravariant=True)


@interface  # can be applied on protocols and "standard" classes
class EventSubscriber(Protocol[E]):
    def process(self, event: E) -> None:
        ...


# Ensures OnInitialization is really a EventSubscriber if possible
@implements(EventSubscriber).when(qualified_by=InitializationEvent)
class OnInitialization:
    def process(self, event: InitializationEvent) -> None:
        ...


@inject
def process_initialization(event: InitializationEvent,
                           # injects all subscribers qualified by InitializationEvent
                           subscribers: list[EventSubscriber[InitializationEvent]] \
                                   = inject.me(qualified_by=InitializationEvent)
                           ) -> None:
    for subscriber in subscribers:
        subscriber.process(event)


event = InitializationEvent()
process_initialization(event)
process_initialization(
    event,
    # Explicitly retrieving the subscribers
    subscribers=world.get[EventSubscriber].all(qualified_by=InitializationEvent)
)

Implementations can be can be retrieved in multiple ways:

# When you want to retrieve a single implementation matching your constraints
@inject
def f(subscriber: EventSubscriber[InitializationEvent] \
              = inject.me(qualified_by=InitializationEvent)
      ) -> EventSubscriber[InitializationEvent]:
    return subscriber


assert world.get[EventSubscriber].single(qualified_by=InitializationEvent) is f()

# When there's only one implementation
@inject
def f2(subscriber: EventSubscriber[InitializationEvent] = inject.me()
      ) -> EventSubscriber[InitializationEvent]:
    return subscriber


assert world.get(EventSubscriber) is f2()

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
└── Permanent implementation: MovieDB @ current_movie_db
    └──<∅> IMDBMovieDB
        └── ImdbAPI @ imdb_factory
            └── imdb_factory
                ├── 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.3.0.tar.gz (208.3 kB view details)

Uploaded Source

Built Distributions

antidote-1.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (2.2 MB view details)

Uploaded CPython 3.10 manylinux: glibc 2.17+ x86-64

antidote-1.3.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl (2.1 MB view details)

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

antidote-1.3.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl (2.1 MB view details)

Uploaded CPython 3.10 manylinux: glibc 2.17+ i686 manylinux: glibc 2.5+ i686

antidote-1.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (2.2 MB view details)

Uploaded CPython 3.9 manylinux: glibc 2.17+ x86-64

antidote-1.3.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl (2.1 MB view details)

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

antidote-1.3.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl (2.1 MB view details)

Uploaded CPython 3.9 manylinux: glibc 2.17+ i686 manylinux: glibc 2.5+ i686

antidote-1.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (2.2 MB view details)

Uploaded CPython 3.8 manylinux: glibc 2.17+ x86-64

antidote-1.3.0-cp38-cp38-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.8 manylinux: glibc 2.12+ x86-64 manylinux: glibc 2.5+ x86-64

antidote-1.3.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl (2.2 MB view details)

Uploaded CPython 3.8 manylinux: glibc 2.17+ i686 manylinux: glibc 2.5+ i686

antidote-1.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (2.0 MB view details)

Uploaded CPython 3.7m manylinux: glibc 2.17+ x86-64

antidote-1.3.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl (1.9 MB view details)

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

antidote-1.3.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl (1.9 MB view details)

Uploaded CPython 3.7m manylinux: glibc 2.17+ i686 manylinux: glibc 2.5+ i686

File details

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

File metadata

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

File hashes

Hashes for antidote-1.3.0.tar.gz
Algorithm Hash digest
SHA256 4423181a1af0f367208534ae7014d202f2efaa27134ce74dd3322104214c8905
MD5 71eb144a1c6046dea9c23be95e7a0f0d
BLAKE2b-256 8a4d26a543b7b1ac441293b2eec149312c413ee232994f793ddf1b492b408bb4

See more details on using hashes here.

Provenance

File details

Details for the file antidote-1.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for antidote-1.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 f417c098e680a9df2da4953606b12d9374beba7e6660e4a62ff4b5d6780e0ec5
MD5 2818cc071704fcbbeab20e08408ab496
BLAKE2b-256 5fa9118a4b961c475e4a4e53273db13aed1d8c13f2274d1f9eadf53cc7f3a7bc

See more details on using hashes here.

Provenance

File details

Details for the file antidote-1.3.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.3.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl
Algorithm Hash digest
SHA256 db9c8412b0d55758a6054cdcb0910ec1d067ef456a2b760d2e3fff8b525a95df
MD5 2e7e3c3604ba91ed6af802639277d496
BLAKE2b-256 3b1e501ab296ef6a1c7cba3db7b8e7162e0c7a8623e85ad797d3a7889591b60a

See more details on using hashes here.

Provenance

File details

Details for the file antidote-1.3.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl.

File metadata

File hashes

Hashes for antidote-1.3.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl
Algorithm Hash digest
SHA256 bfbe048acfb6e6d271dc1a5c51bb3dbeccad03fb0091e5f82ad6befb234a246d
MD5 a50787276d96867634152dcad9a1a4c8
BLAKE2b-256 1f13b364ba7912ae6d6e19b7b464267315046987fc5b4e24a636d1ee4ba66412

See more details on using hashes here.

Provenance

File details

Details for the file antidote-1.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for antidote-1.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 0e356247edc53569114bc41d47dc0d62b90cdf2585977f93d23c8acbd30225dc
MD5 0113a0950b32069021e2c3f6afd7cdf0
BLAKE2b-256 d44c927e7f2b140e10aac0e5fb31f89d434b4293c7529e4db3abea7ef9bd1c26

See more details on using hashes here.

Provenance

File details

Details for the file antidote-1.3.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.3.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl
Algorithm Hash digest
SHA256 39d67e3d450d7fe86cdf8f2251c957d5d8f9d2d4c214e128e7ac37c799737af6
MD5 b1a46c6e8d03dfa39f17756e80b3ac2c
BLAKE2b-256 1d2aeb0635776790d79d787a793e1040f933ec05ef68a30d57073808a131be22

See more details on using hashes here.

Provenance

File details

Details for the file antidote-1.3.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl.

File metadata

File hashes

Hashes for antidote-1.3.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl
Algorithm Hash digest
SHA256 8a31c7d3cc938598b4fcdcfe537b2c799072c84de9f42e6bb547dbaa4898ee1f
MD5 652b2c4f005146c8b14c7337f0753e37
BLAKE2b-256 3321601eec6bc7bda0a7db6e53724540c4f3a1d3187a822f79e99ba32bf09422

See more details on using hashes here.

Provenance

File details

Details for the file antidote-1.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for antidote-1.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 bb704a233d9057ceedbb2e3483b840a588bba2c5065234c0bb9764826ec52825
MD5 f24f89364ddf9d4b698831574801e240
BLAKE2b-256 a8194b0523715bfab8cf5ec2975ccd2084e7710fb339af0260d8bbdedcc4bb8e

See more details on using hashes here.

Provenance

File details

Details for the file antidote-1.3.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.3.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl
Algorithm Hash digest
SHA256 fa0be79065147eeb44d969f46b2b1bfe819023893770b8ee003e78702b0b02d2
MD5 bb5a00b3e0f92a87c1ee3364abcf0cef
BLAKE2b-256 5eb9f40e35098fe813b5c69ab1e9b7a122ba3b6a1085cb58b0a5147cbd364766

See more details on using hashes here.

Provenance

File details

Details for the file antidote-1.3.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl.

File metadata

File hashes

Hashes for antidote-1.3.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl
Algorithm Hash digest
SHA256 7ad629eb6e89a1faa1e366996450736904828c16ca7eb245402fb250b569fa85
MD5 d247904ed857fa6206605debb678a292
BLAKE2b-256 9f50e4fa59d90a4286a1f5d72a2b5876c64f9aec4a84cce2ffbec3ff1ef555c3

See more details on using hashes here.

Provenance

File details

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

File metadata

File hashes

Hashes for antidote-1.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 a881776e38e68779553715743a7f5c08a2a4dbb06d05ecdef7c34d1c00bfc4d3
MD5 be46eac0ae8bcb99b198aa09380d0eb3
BLAKE2b-256 c11e66a23e46b9412323824b1876bdbf90d8049d9077f21107aecbd3e5dba18d

See more details on using hashes here.

Provenance

File details

Details for the file antidote-1.3.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.3.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl
Algorithm Hash digest
SHA256 3d476631ca3dc31612c802e7d994ce2f9a96deadb8154ac3d1ffe7a59c0d6427
MD5 10000ce238f9d66d5bc49586df9b993f
BLAKE2b-256 68e644cefc57696f19320f880ffdd0aa6f642f12a162c74c9f41a83d0a49c664

See more details on using hashes here.

Provenance

File details

Details for the file antidote-1.3.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl.

File metadata

File hashes

Hashes for antidote-1.3.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl
Algorithm Hash digest
SHA256 60d80390ad9571b3b49fbe143f304944c7cdb7c022ec209b06d47e7933dbd116
MD5 5de5a27ada20d3f604010d54a50793e1
BLAKE2b-256 d7ff8fdc714ea5a1928650b4a1baed24225a418c285625ad02583fae6c470d7a

See more details on using hashes here.

Provenance

Supported by

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