Skip to main content

Async-first dependency injection library for Python

Project description

aiodine

python pypi travis black codecov license

aiodine provides async-first dependency injection in the style of Pytest fixtures for Python 3.6+.

Installation

pip install aiodine

Usage

Note: this section is under construction.

aiodine revolves around two concepts: providers and consumers.

Providers are:

  • Explicit: once a provider is defined, a consumer can use it by declaring it as a function parameter.
  • Modular: a provider can itself use other provider.
  • Flexible: providers are reusable within the scope of a function or a whole session, and support a variety of syntaxes (asynchronous or synchronous, function or generator) to make provisioning resources fun again.

Providers

Providers make a resource available to consumers within a certain scope. They are created by decorating a provider function with @aiodine.provider.

Here's a "hello world" provider:

import aiodine

@aiodine.provider
async def hello():
    return "Hello, aiodine!"

Tip: synchronous provider functions are also supported!

Providers are available in two scopes:

  • function: the provider's value is re-computed everytime it is consumed.
  • session: the provider's value is computed only once (the first time it is consumed) and is reused in subsequent calls.

By default, providers are function-scoped.

Consumers

Once a provider has been declared, it can be used by consumers. A consumer is built by decoratinga consumer function with @aiodine.consumer. A consumer can declare a provider as one of its parameters and aiodine will inject it at runtime.

Here's an example consumer:

@aiodine.consumer
async def show_friendly_message(hello):
    print(hello)

Tip: synchronous consumer functions are also supported!

All aiodine consumers are asynchronous, so you'll need to run them in an asynchronous context:

from asyncio import run

async def main():
    await show_friendly_message()

run(main())  # "Hello, aiodine!"

Of course, a consumer can declare non-provider parameters too. These are then regular parameters and will have to be passed when calling the consumer.

@aiodine.consumer
async def show_friendly_message(hello, repeat=1):
    for _ in range(repeat):
        print(hello)

async def main():
    await show_friendly_message(repeat=10)

Providers consuming other providers

Providers are modular in the sense that they can themselves consume other providers.

For this to work however, providers need to be frozen first. This ensures that the dependency graph can be correctly resolved regardless of the declaration order.

import aiodine

@aiodine.provider
async def email():
    return "user@example.net"

@aiodine.provider
async def send_email(email):
    print(f"Sending email to {email}…")

aiodine.freeze()  # <- Ensures that `send_email` has resolved `email`.

It is safe to call .freeze() multiple times.

A context manager syntax is also available:

import aiodine

with aiodine.exit_freeze():
    @aiodine.provider
    async def email():
        return "user@example.net"

    @aiodine.provider
    async def send_email(email):
        print(f"Sending email to {email}…")

Generator providers

Generator providers can be used to perform cleanup (finalization) operations after a provider has gone out of scope.

import os
import aiodine

@aiodine.provider
async def complex_resource():
    print("setting up complex resource…")
    try:
        yield "complex"
    finally:
        print("cleaning up complex resource…")

Tip: synchronous generator providers are also supported.

Note: session-scoped generator providers will only be cleaned up if using them in the context of a session. See Sessions for details.

Lazy async providers

When the provider function is asynchronous, its return value is awaited before being injected into the consumer. In other words, async providers are eager by default.

You can mark a provider as lazy in order to defer awaiting the provided value to the consumer. This is useful when the provider needs to be conditionally evaluated.

from asyncio import sleep
import aiodine

@aiodine.provider(lazy=True)
async def expensive_computation():
    await sleep(10)
    return 42

@aiodine.consumer
async def compute(expensive_computation, cache=None):
    if cache:
        return cache
    return await expensive_computation

Factory providers

Instead of returning a scalar value, factory providers return a function. Factory providers are useful to implement reusable providers that accept a variety of inputs.

This is a design pattern more than anything else. In fact, there's no extra code in aiodine to support this feature.

The following example defines a factory provider for a (simulated) database query:

import aiodine

@aiodine.provider(scope="session")
async def notes():
    # Some hard-coded sticky notes.
    return [
        {"id": 1, "text": "Groceries"},
        {"id": 2, "text": "Make potatoe smash"},
    ]

@aiodine.provider
async def get_note(notes):
    async def _get_note(pk: int) -> list:
        try:
            # TODO: fetch from a database instead?
            return next(note for note in notes if note["id"] == pk)
        except StopIteration:
            raise ValueError(f"Note with ID {pk} does not exist.")

    return _get_note

Example usage in a consumer:

@aiodine.consumer
async def show_note(pk: int, get_note):
    print(await get_note(pk))

Tip: you can combine factory providers with generator providers to cleanup any resources the factory needs to use. Here's an example that provides temporary files and removes them on cleanup:

import os
import aiodine

@aiodine.provider(scope="session")
def tmpfile():
    files = set()

    async def _create_tmpfile(path: str):
        with open(path, "w") as tmp:
            files.add(path)
            return tmp

    yield _create_tmpfile

    for path in files:
        os.remove(path)

Using providers without declaring them as parameters

Sometimes, a consumer needs to use a provider but doesn't care about the value it returns. In these situations, you can use the @useprovider decorator and skip declaring it as a parameter.

Tip: the @useprovider decorator accepts a variable number of providers, which can be given by name or by reference.

import os
import aiodine

@aiodine.provider
def cache():
    os.makedirs("cache", exist_ok=True)

@aiodine.provider
def debug_log_file():
    with open("debug.log", "w"):
        pass
    yield
    os.remove("debug.log")

@aiodine.consumer
@aiodine.useprovider("cache", debug_log_file)
async def build_index():
    ...

Auto-used providers

Auto-used providers are automatically activated (within their configured scope) without having to declare them as a parameter in the consumer.

This can typically spare you from decorating all your consumers with an @useprovider.

For example, the auto-used provider below would result in printing the current date and time to the console every time a consumer is called.

import datetime
import aiodine

@aiodine.provider(autouse=True)
async def logdatetime():
    print(datetime.now())

Sessions

A session is the context in which session providers live.

More specifically, session providers (resp. generator session providers) are instanciated (resp. setup) when entering a session, and destroyed (resp. cleaned up) when exiting the session.

To enter a session, use:

await aiodine.enter_session()

To exit it:

await aiodine.exit_session()

An async context manager syntax is also available:

async with aiodine.session():
    ...

FAQ

Why "aiodine"?

aiodine contains "aio" as in asyncio, and "di" as in Dependency Injection. The last two letters end up making aiodine pronounce like iodine, the chemical element.

Changelog

See CHANGELOG.md.

License

MIT

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

aiodine-1.0.0.tar.gz (14.6 kB view details)

Uploaded Source

Built Distribution

aiodine-1.0.0-py3-none-any.whl (17.7 kB view details)

Uploaded Python 3

File details

Details for the file aiodine-1.0.0.tar.gz.

File metadata

  • Download URL: aiodine-1.0.0.tar.gz
  • Upload date:
  • Size: 14.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/1.13.0 pkginfo/1.5.0.1 requests/2.21.0 setuptools/40.8.0 requests-toolbelt/0.9.1 tqdm/4.31.1 CPython/3.6.3

File hashes

Hashes for aiodine-1.0.0.tar.gz
Algorithm Hash digest
SHA256 3f019e243cb410307b820bf404bcaf477409ed1c1de80fc6704f8de4867154a7
MD5 eb22d42cf7fabbb10c6b3c635afa9566
BLAKE2b-256 4ee3e9b58c0241598a1ea3f1a345301a4c10237baf15df36a80c111d43526b88

See more details on using hashes here.

File details

Details for the file aiodine-1.0.0-py3-none-any.whl.

File metadata

  • Download URL: aiodine-1.0.0-py3-none-any.whl
  • Upload date:
  • Size: 17.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/1.13.0 pkginfo/1.5.0.1 requests/2.21.0 setuptools/40.8.0 requests-toolbelt/0.9.1 tqdm/4.31.1 CPython/3.6.3

File hashes

Hashes for aiodine-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 4d3ef6d7f7fc54c6930a750c27ed3316b3ba112564c4414a4100961a36d76f96
MD5 0357fa2c2e72243afbe9cb11c601982f
BLAKE2b-256 ac87c87af51ff17930d3552aba5fa9d86005657127c8feb31854ed05253e08bb

See more details on using hashes here.

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