Skip to content

Usage

Providers

Providers are the backbone of AnyDI. A provider is a function or a class that returns an instance of a specific type. Once a provider is registered with Container, it can be used to resolve dependencies throughout the application.

Registering Providers

To register a provider, you can use the register method of the Container instance. The method takes three arguments: the type of the object to be provided, the provider function or class, and an scope.

from anydi import Container

container = Container()


def message() -> str:
    return "Hello, message!"


container.register(str, message, scope="singleton")

assert container.resolve(str) == "Hello, world!"

Alternatively, you can use the @provider decorator to register a provider function. The decorator takes care of registering the provider with Container.

from anydi import Container

container = Container()


@container.provider(scope="singleton")
def message() -> str:
    return "Hello, message!"


assert container.resolve(str) == "Hello, world!"

Annotated Providers

Sometimes, it's useful to register multiple providers for the same type. For example, you might want to register a provider for a string that returns a different message depending on the name of the provider. This can be achieved by using the Annotated type hint with the string argument:

from typing import Annotated

from anydi import Container

container = Container()


@container.provider(scope="singleton")
def message1() -> Annotated[str, "message1"]:
    return "Message1"


@container.provider(scope="singleton")
def message2() -> Annotated[str, "message2"]:
    return "Message2"


assert container.resolve(Annotated[str, "message1"]) == "Message1"
assert container.resolve(Annotated[str, "message2"]) == "Message2"

In this code example, we define two providers, message1 and message2, each returning a different message. The Annotated type hint with string argument allows you to specify which provider to retrieve based on the name provided within the annotation.

Unregistering Providers

To unregister a provider, you can use the unregister method of the Container instance. The method takes interface of the dependency to be unregistered.

from anydi import Container

container = Container()


@container.provider(scope="singleton")
def message() -> str:
    return "Hello, message!"


assert container.is_registered(str)

container.unregister(str)

assert not container.is_registered(str)

Resolved Providers

To check if a registered provider has a resolved instance, you can use the is_resolved method of the Container instance. This method takes the interface of the dependency to be checked.

from anydi import Container

container = Container()


@container.provider(scope="singleton")
def message() -> str:
    return "Hello, message!"


# Check if an instance is resolved
assert not container.is_resolved(str)

assert container.resolve(str) == "Hello, world!"

assert container.is_resolved(str)

container.release(str)

assert not container.is_resolved(str)

To release a provider instance, you can use the release method of the Container instance. This method takes the interface of the dependency to be reset. Alternatively, you can reset all instances with the reset method.

from anydi import Container

container = Container()
container.register(str, lambda: "Hello, world!", scope="singleton")
container.register(int, lambda: 100, scope="singleton")

container.resolve(str)
container.resolve(int)

assert container.is_resolved(str)
assert container.is_resolved(int)

container.reset()

assert not container.is_resolved(str)
assert not container.is_resolved(int)

Note

This pattern can be used while writing unit tests to ensure that each test case has a clean dependency graph.

Strict Mode

AnyDI operates in non-strict mode by default, meaning it doesn't require explicit registration for every type. It can dynamically resolve or auto-register dependencies, simplifying setups where manual registration for each type is impractical.

Consider a scenario with class dependencies:

from dataclasses import dataclass

class Database:
    def connect(self) -> None:
        print("connect")
    def disconnect(self) -> None:
        print("disconnect")

@dataclass
class Repository:
    db: Database

@dataclass
class Service:
    repo: Repository

You can instantiate these classes without manually registering each one:

from typing import Iterator

from anydi import Container

container = Container()  # Non-strict mode

@container.provider(scope="singleton")
def db() -> Iterator[Database]:
    db = Database()
    db.connect()
    yield db
    db.disconnect()

# Retrieving an instance of Service
_ = container.resolve(Service)

assert container.is_resolved(Service)
assert container.is_resolved(Repository)
assert container.is_resolved(Database)

Enabling Strict Mode

For strict checking, enable strict mode by setting strict=True when creating the Container. In strict mode, all types must be explicitly registered or have a definable provider before instantiation.

container = Container(strict=True)

# Raises LookupError if `Service` or dependencies aren't registered.
_ = container.resolve(Service)

Scopes

AnyDI supports three different scopes for providers:

  • transient
  • singleton
  • request

transient scope

Providers with transient scope create a new instance of the object each time it's requested. You can set the scope when registering a provider.

import random

from anydi import Container

container = Container()


@container.provider(scope="transient")
def message() -> str:
    return random.choice(["hello", "hola", "ciao"])


print(container.resolve(str))  # will print random message

singleton scope

Providers with singleton scope create a single instance of the object and return it every time it's requested.

from anydi import Container


class Service:
    def __init__(self, name: str) -> None:
        self.name = name


container = Container()


@container.provider(scope="singleton")
def service() -> Service:
    return Service(name="demo")


assert container.resolve(Service) == container.resolve(Service)

request scope

Providers with request scope create an instance of the object for each request. The instance is only available within the context of the request.

from anydi import Container


class Request:
    def __init__(self, path: str) -> None:
        self.path = path


container = Container()


@container.provider(scope="request")
def request_provider() -> Request:
    return Request(path="/")


with container.request_context():
    assert container.resolve(Request).path == "/"

container.resolve(Request)  # this will raise LookupError

or using asynchronous request context:

from anydi import Container

container = Container()


@container.provider(scope="request")
def request_provider() -> Request:
    return Request(path="/")


async def main() -> None:
    async with container.arequest_context():
        assert (await container.aresolve(Request).path) == "/"

Resource Providers

Resource providers are special types of providers that need to be started and stopped. AnyDI supports synchronous and asynchronous resource providers.

Synchronous Resources

Here is an example of a synchronous resource provider that manages the lifecycle of a Resource object:

from typing import Iterator

from anydi import Container


class Resource:
    def __init__(self, name: str) -> None:
        self.name = name

    def start(self) -> None:
        print("start resource")

    def close(self) -> None:
        print("close resource")


container = Container()


@container.provider(scope="singleton")
def resource_provider() -> t.Iterator[Resource]:
    resource = Resource(name="demo")
    resource.start()
    yield resource
    resource.close()


container.start()  # start resources

assert container.resolve(Resource).name == "demo"

container.close()  # close resources

In this example, the resource_provider function returns an iterator that yields a single Resource object. The .start method is called when the resource is created, and the .close method is called when the resource is released.

Asynchronous Resources

Here is an example of an asynchronous resource provider that manages the lifecycle of an asynchronous Resource object:

import asyncio
from typing import AsyncIterator

from anydi import Container


class Resource:
    def __init__(self, name: str) -> None:
        self.name = name

    async def start(self) -> None:
        print("start resource")

    async def close(self) -> None:
        print("close resource")


container = Container()


@container.provider(scope="singleton")
async def resource_provider() -> t.AsyncIterator[Resource]:
    resource = Resource(name="demo")
    await resource.start()
    yield resource
    await resource.close()


async def main() -> None:
    await container.astart()  # start resources

    assert (await container.resolve(Resource)).name == "demo"

    await container.aclose()  # close resources


asyncio.run(main())

In this example, the resource_provider function returns an asynchronous iterator that yields a single Resource object. The .astart method is called asynchronously when the resource is created, and the .aclose method is called asynchronously when the resource is released.

Resource events

Sometimes, it can be useful to split the process of initializing and managing the lifecycle of an instance into separate providers.

from typing import Iterator

from anydi import Container


class Client:
    def __init__(self) -> None:
        self.started = False
        self.closed = False

    def start(self) -> None:
        self.started = True

    def close(self) -> None:
        self.closed = True


container = Container()


@container.provider(scope="singleton")
def client_provider() -> Client:
    return Client()


@container.provider(scope="singleton")
def client_lifespan(client: Client) -> t.Iterator[None]:
    client.start()
    yield
    client.close()


client = container.resolve(Client)

assert not client.started
assert not client.closed

container.start()

assert client.started
assert not client.closed

container.close()

assert client.started
assert client.closed

Note

This pattern can be used for both synchronous and asynchronous resources.

Overriding Providers

Sometimes it's necessary to override a provider with a different implementation. To do this, you can register the provider with the override=True property set.

For example, suppose you have registered a singleton provider for a string:

from anydi import Container

container = Container()


@container.provider(scope="singleton")
def hello_message() -> str:
    return "Hello, world!"


@container.provider(scope="singleton", override=True)
def goodbye_message() -> str:
    return "Goodbye!"


assert container.resolve(str) == "Goodbye!"

Note that if you try to register the provider without passing the override parameter as True, it will raise an error:

@container.provider(scope="singleton")  # will raise an error
def goodbye_message() -> str:
    return "Good-bye!"

Injecting Dependencies

In order to use the dependencies that have been provided to the Container, they need to be injected into the functions or classes that require them. This can be done by using the @container.inject decorator.

Here's an example of how to use the @container.inject decorator:

from anydi import Container, dep


class Service:
    def __init__(self, name: str) -> None:
        self.name = name


container = Container()


@container.provider(scope="singleton")
def service() -> Service:
    return Service(name="demo")


@container.injectable
def handler(service: Service = dep()) -> None:
    print(f"Hello, from service `{service.name}`")

Note that the service argument in the handler function has been given a default value of dep() mark. This is done so that AnyDI knows which dependency to inject when the handler function is called.

Once the dependencies have been injected, the function can be called as usual, like so:

handler()

You can also call the callable object with injected dependencies using the run method of the Container instance:

from anydi import Container, dep


class Service:
    def __init__(self, name: str) -> None:
        self.name = name


container = Container()


@container.provider(scope="singleton")
def service() -> Service:
    return Service(name="demo")


def handler(service: Service = dep()) -> None:
    print(f"Hello, from service `{service.name}`")


container.run(handler)

In this case, the run method will automatically inject the dependencies and call the handler function. Using @container.inject is not necessary in this case.

Scanning Injections

AnyDI provides a simple way to inject dependencies by scanning Python modules or packages. For example, your application might have the following structure:

/app
  api/
    handlers.py
  main.py
  services.py

services.py defines a service class:

class Service:
    def __init__(self, name: str) -> None:
        self.name = name

handlers.py uses the Service class:

from anydi import dep, injectable

from app.services import Service


@injectable
def my_handler(service: Service = dep()) -> None:
    print(f"Hello, from service `{service.name}`")

main.py starts the DI container and scans the app handlers.py module:

from anydi import Container

from app.services import Service

container = Container()


@container.provider(scope="singleton")
def service() -> Service:
    return Service(name="demo")


container.scan(["app.handlers"])
container.start()

# application context

container.close()

The scan method takes a list of directory paths as an argument and recursively searches those directories for Python modules containing @inject-decorated functions or classes.

Scanning Injections by tags

You can also scan for providers or injectables in specific tags. To do so, you need to use the tags argument when registering providers or injectables. For example:

from anydi import Container

container = Container()
container.scan(["app.handlers"], tags=["tag1"])

This will scan for @injectable annotated target only with defined tags within the app.handlers module.

Modules

AnyDI provides a way to organize your code and configure dependencies for the dependency injection container. A module is a class that extends the Module base class and contains the configuration for the container.

Here's an example how to create and register simple module:

from anydi import Container, Module, provider


class Repository:
    pass


class Service:
    def __init__(self, repo: Repository) -> None:
        self.repo = repo


class AppModule(Module):
    def configure(self, container: Container) -> None:
        container.register(Repository, lambda: Repository(), scope="singleton")

    @provider(scope="singleton")
    def service(self, repo: Repository) -> Service:
        return Service(repo=repo)


container = Container(modules=[AppModule()])

# or
# container.register_module(AppModule())

assert container.is_registered(Service)
assert container.is_registered(Repository)

With AnyDI's Modules, you can keep your code organized and easily manage your dependencies.

Testing

To use AnyDI with your testing framework, you can use the override context manager to temporarily replace a dependency with an overridden instance during testing. This allows you to isolate the code being tested from its dependencies. The with container.override() context manager is used to ensure that the overridden instance is used only within the context of the with block. Once the block is exited, the original dependency is restored.

from unittest import mock

from anydi import Container, dep


class Service:
    def __init__(self, name: str) -> None:
        self.name = name

    def say_hello(self) -> str:
        return f"Hello, from `{self.name}` service!"


container = Container()


@container.provider(scope="singleton")
def service() -> Service:
    return Service(name="demo")


@container.inject
def hello_handler(service: Service = dep()) -> str:
    return service.say_hello()


def test_hello_handler() -> None:
    service_mock = mock.Mock(spec=Service)
    service_mock.say_hello.return_value = "Hello, from service mock!"

    with container.override(Service, service_mock):
        assert hello_handler() == "Hello, from service mock!"

Pytest Plugin

AnyDI offers a pytest plugin that simplifies the testing process. You can annotate a test function with the @pytest.mark.inject decorator to automatically inject dependencies into the test function, or you can set the global configuration value anydi_inject_all to True to inject dependencies into all test functions automatically.

Additionally, you need to define a container fixture to provide a Container instance for the test function, or use the anydi_setup_container fixture.

from typing import Annotated
import pytest

from anydi import Container


@pytest.fixture(scope="session")
def container() -> Container:
    container = Container()  # or pass your application container
    container.register(
        Annotated[str, "message"],
        lambda: "Hello, world!",
        scope="singleton",
    )
    return container


@pytest.mark.inject
def test_hello(message: Annotated[str, "message"]) -> None:
    assert message == "Hello, world!"

The message argument is injected into the test function thanks to the @pytest.mark.inject decorator.

PS! Pytest fixtures will always have higher priority than the @pytest.mark.inject decorator. This means that if both a pytest fixture and the @pytest.mark.inject decorator attempt to provide a value for the same name, the value from the pytest fixture will be used.

Conclusion

Check examples which shows how to use AnyDI in real-life application.