Finistere / antidote

Dependency injection for Python
MIT License
90 stars 9 forks source link
dependency dependency-injection injection python

######## Antidote ########

.. image:: https://img.shields.io/pypi/v/antidote.svg :target: https://pypi.python.org/pypi/antidote

.. image:: https://img.shields.io/pypi/l/antidote.svg :target: https://pypi.python.org/pypi/antidote

.. image:: https://img.shields.io/pypi/pyversions/antidote.svg :target: https://pypi.python.org/pypi/antidote

.. image:: https://github.com/Finistere/antidote/actions/workflows/main.yml/badge.svg?branch=master :target: https://github.com/Finistere/antidote/actions/workflows/main.yml

.. image:: https://codecov.io/gh/Finistere/antidote/branch/master/graph/badge.svg :target: https://codecov.io/gh/Finistere/antidote

.. image:: https://readthedocs.org/projects/antidote/badge/?version=latest :target: http://antidote.readthedocs.io/en/latest/?badge=latest

Antidote is a dependency injection micro-framework for Python 3.7+, featuring:

It is built on the idea of having a declarative, explicit and decentralized definition of dependencies at the type / function / variable definition. These definitions can be easily tracked down, including by static tooling and startup-time analysis.

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, and can be easily used in existing code and tested in isolation.


Installation


To install Antidote, simply run this command:

.. code-block:: bash

pip install antidote

Help & Issues


Feel free to open an issue <https://github.com/Finistere/antidote/issues> or a discussion <https://github.com/Finistere/antidote/discussions> on Github <https://github.com/Finistere/antidote>_ for questions, issues, proposals, etc. !


Documentation


Tutorial, reference and more can be found in the documentation <https://antidote.readthedocs.io/en/latest>_. Some quick links:


Overview


Accessing dependencies

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

.. code-block:: python

from antidote import injectable

@injectable
class Service:
    pass

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

By default, :code:@injectable defines a singleton. However, alternative lifetimes (how long the :code:world keeps value alive in its cache) can exist, such as :code:transient, where nothing is cached at all.

Dependencies can also be injected into a function/method with :code:@inject. For both kinds of callables, Mypy, Pyright and PyCharm will infer the correct types.

.. code-block:: python

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

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

.. code-block:: python

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, with all methods injected, by using :code:@wire. It is also possible to inject the first argument, commonly named :code:self, of a method with an instance of a class:

.. code-block:: python

@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 comes out-of-the-box with 4 kinds of dependencies:

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

Testing & Debugging

Injected functions can typically be tested by passing arguments explicitly but it's not always enough. Antidote provides a test context for full test isolation. The test context allows overriding any dependencies:

.. code-block:: python

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 :code:world.debug which returns a nicely-formatted tree to show what Antidote actually sees, without actually executing anything:

.. code-block:: text

šŸŸ‰ <lazy> f()
ā””ā”€ā”€ āˆ… Service
    ā””ā”€ā”€ Service.__init__
        ā””ā”€ā”€ šŸŸ‰ <const> Conf.HOST

 āˆ… = transient
 ā†» = bound
 šŸŸ‰ = singleton

Going Further

Check out the Guide <https://antidote.readthedocs.io/en/latest/guide/index.html> which goes more in depth or the Reference <https://antidote.readthedocs.io/en/latest/reference/index.html> 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.)