Skip to main content

Antidote

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

Antidote is a dependency injection micro-framework for Python 3.7+.

It is built on the idea of having a declarative, explicit and decentralized definitions of dependencies at the type / function / variable definition which can be easily tracked down.

Features are built with a strong focus on maintainability, simplicity and ease of use in mind. Everything is statically typed (mypy & pyright), documented with tested examples, can be easily used in existing code and tested in isolation.

Installation

To install Antidote, simply run this command:

pip install antidote

Help & Issues

Feel free to open an issue or a discussion on Github for questions, issues, proposals, etc. !

Documentation

Tutorial, reference and more can be found in the documentation. Some quick links:

Overview

Accessing dependencies

Antidote works with a Catalog which is a sort of collection of dependencies. Multiple ones can co-exist, but world is used by default. The most common form of a dependency is an instance of a given class

from antidote import injectable

@injectable
class Service:
    pass

world[Service]  # retrieve the instance
world.get(Service, default='something')  # similar to a dict

By default, @injectable defines a singleton but alternative lifetimes (how long the world keeps value alive in its cache) exists such as transient where nothing is cached at all. Dependencies can also be injected into a function/method with @inject. With both, Mypy, Pyright and PyCharm will infer the correct types.

from antidote import inject

@inject  #                      ⯆ Infers the dependency from the type hint
def f(service: Service = inject.me()) -> Service:
    return service

f()  # service injected
f(Service())  # useful for testing: no injection, argument is used

@inject supports a variety of ways to bind arguments to their dependencies if any. This binding is always explicit. for example:

from antidote import InjectMe

# recommended with inject.me() for best static-typing experience
@inject
def f2(service = inject[Service]):
    ...

@inject(kwargs={'service': Service})
def f3(service):
    ...

@inject
def f4(service: InjectMe[Service]):
    ...

Classes can also be fully wired, all methods injected, easily with @wire. It is also possible to inject the first argument, commonly named self, of a method with an instance of a class:

@injectable
class Dummy:
    @inject.method
    def method(self) -> 'Dummy':
        return self

# behaves like a class method
assert Dummy.method() is world[Dummy]

# useful for testing: when accessed trough an instance, no injection
dummy = Dummy()
assert dummy.method() is dummy

Defining dependencies

Antidote provides out of the box 4 kinds of dependencies:

  • @injectable classes for which an instance is provided.

    from antidote import injectable
    
    #           ⯆ optional: would just call Service() otherwise.
    @injectable(factory_method='load')
    class Service:
        @classmethod
        def load(cls) -> 'Service':
            return cls()
    
    world[Service]
  • const for defining simple constants.

    from antidote import const
    
    # Used as namespace
    class Conf:
        TMP_DIR = const('/tmp')
    
        # From environment variables, lazily retrieved
        LOCATION = const.env("PWD")
        USER = const.env()  # uses the name of the variable
        PORT = const.env(convert=int)  # convert the environment variable to a given type
        UNKNOWN = const.env(default='unknown')
    
    world[Conf.TMP_DIR]
    
    @inject
    def f(tmp_dir: str = inject[Conf.TMP_DIR]):
        ...
  • @lazy function calls (taking into account arguments) used for (stateful-)factories, parameterized dependencies, complex constants, etc.

    from dataclasses import dataclass
    
    from antidote import lazy
    
    @dataclass
    class Template:
        name: str
    
    # the wrapped template function is only executed when accessed through world/@inject
    @lazy
    def template(name: str) -> Template:
        return Template(name=name)
    
    # By default a singleton, so it always returns the same instance of Template
    world[template(name="main")]
    
    @inject
    def f(main_template: Template = inject[template(name="main")]):
        ...

    @lazy will automatically apply @inject and can also be a value, property or even a method similarly to @inject.method.

  • @interface for a function, class or even @lazy function call for which one or multiple implementations can be provided.

    from antidote import interface, implements
    
    @interface
    class Task:
        pass
    
    @implements(Task)
    class CustomTask(Task):
        pass
    
    world[Task]  # instance of CustomTask

    The interface does not need to be a class. It can also be a Protocol, a function or a @lazy function call!

    @interface
    def callback(event: str) -> bool:
        ...
    
    @implements(callback)
    def on_event(event: str) -> bool:
        # do stuff
        return True
    
    # returns the on_event function
    assert world[callback] is on_event

    @implements will enforce as much as possible that the interface is correctly implemented. Multiple implementations can also be retrieved. Conditions, filters on metadata and weighting implementation are all supported to allow full customization of which implementation should be retrieved in which use case.

Each of those have several knobs to adapt them to your needs which are presented in the documentation.

Testing & Debugging

Injected functions can typically be tested by passing arguments explicitly but it’s not always enough. Antidote provides test context which full isolate themselves and allow overriding any dependencies:

original = world[Service]
with world.test.clone() as overrides:
    # dependency value is different, but it's still a singleton Service instance
    assert world[Service] is not original

    # override examples
    overrides[Service] = 'x'
    assert world[Service] == 'x'

    del overrides[Service]
    assert world.get(Service) is None

    @overrides.factory(Service)
    def build_service() -> object:
        return 'z'


    # Test context can be nested and it wouldn't impact the current test context
    with world.test.clone() as nested_overrides:
        ...

# Outside the test context, nothing changed.
assert world[Service] is original

Antidote also provides introspection capabilities with world.debug which returns a nicely formatted tree to show what Antidote actually sees without executing anything like the following:

🟉 <lazy> f()
└── ∅ Service
    └── Service.__init__
        └── 🟉 <const> Conf.HOST

 ∅ = transient
 ↻ = bound
 🟉 = singleton

Going Further

  • Scopes are supported. Defining a ScopeGlobalVar and using it as dependency will force any dependents to be updated whenever it changes (a request for example).

  • Multiple catalogs can be used which allow you to expose only a subset of your API (dependencies) to your consumer within a catalog.

  • You can easily define your kind of dependencies with proper typing from both world and inject. @injectable, @lazy, inject.me() etc.. all rely on Antidote’s core (Provider, Dependency, etc.) which is part of public API.

Check out the Guide which goes more in depth or the Reference for specific features.

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. 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 ! You’re also more than welcome to open a discussion or an issue on any topic!

But, no code changes will be merged if they do not pass mypy, pyright, don’t have 100% test coverage or documentation with tested examples if relevant

Metadata

Release files for antidote 2.0.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for antidote 2.0.0
File Size Uploaded Provenance Actions
antidote-2.0.0.tar.gz 186.6 kB Without provenance Details for antidote-2.0.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for antidote 2.0.0
File Interpreter ABI Platform Actions
antidote-2.0.0-py3-none-any.whl Python 3 none any Details for antidote-2.0.0-py3-none-any.whl

Total release size: 283.9 kB

Release files / antidote-2.0.0.tar.gz

Download URL antidote-2.0.0.tar.gz
Size 186.6 kB
Tags Source
SHA-256 checksum
How to use checksums
e2f5c31851f577b380ffa6212ee681d2ad0fa263eed4f2fa1adde0163dc6e950
BLAKE2b-256 checksum
How to use checksums
c1535f44fe4d4df87866ae32957c1a80394cf82cf9e20918a2958c25988d0031
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/4.0.1 CPython/3.10.4

Release files / antidote-2.0.0-py3-none-any.whl

Download URL antidote-2.0.0-py3-none-any.whl
Size 97.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
437ce4a812ef26550ca1cf4037ffafa65f5a3a7f2dde27f5f5aad743b9463bae
BLAKE2b-256 checksum
How to use checksums
a4ae41795ba24a12485608122877ce36fbde77b14b3e298e21712a15c5496c4f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/4.0.1 CPython/3.10.4

Security for antidote 2.0.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

2.0.0 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page