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 a scope.
from anydi import Container
container = Container()
def message() -> str:
return "Hello, World!"
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, World!"
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, World!"
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, World!"
# 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.
Auto-Registration
AnyDI doesn't require explicit registration for every type. It can dynamically resolve and auto-register dependencies,
simplifying setups where manual registration for each type is impractical.
Consider a scenario with class dependencies:
class Database:
def connect(self) -> None:
print("connect")
def disconnect(self) -> None:
print("disconnect")
class Repository:
def __init__(self, db: Database) -> None:
self.db = db
class Service:
def __init__(self, repo: Repository) -> None:
self.repo = repo
You can instantiate these classes without manually registering each one:
from typing import Iterator
from anydi import Container
container = Container()
@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)
Automatic Resource Management
When your class dependencies implement the context manager protocol by defining the __enter__/__aenter__ and __exit__/__aexit__ methods, these resources are automatically managed by the container for singleton and request scoped providers.
from anydi import Container, singleton
@singleton
class Connection:
def __init__(self) -> None:
self.connected = False
self.disconnected = False
def __enter__(self) -> None:
self.connected = True
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
self.connected = False
self.disconnected = True
container = Container()
connection = container.resolve(Connection)
assert container.is_resolved(Connection)
assert connection.connected
container.close()
assert connection.disconnected
Scopes
AnyDI supports three different scopes for providers:
transientsingletonrequest
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) == "/"
request scoped instances
In AnyDI, you can create request-scoped instances to manage dependencies that should be instantiated per request.
This is particularly useful when handling dependencies with request-specific data that need to be isolated across different requests.
To create a request context, you use the request_context (or arequest_context for async) method on the container.
This context is then used to resolve dependencies scoped to the current request.
from typing import Annotated
from anydi import Container
container = Container()
@container.provider(scope="request")
def request_param(request: Request) -> Annotated[str, "request.param"]:
return request.param
with container.request_context() as ctx:
ctx.set(Request, Request(param="param1"))
assert container.resolve(Annotated[str, "request.param"]) == "param1"
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() -> 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.aresolve(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 auto, 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")
@container.inject
def handler(service: Service = auto) -> None:
print(f"Hello, from service `{service.name}`")
Note that the service argument in the handler function has been given a default value of auto 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 auto, 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")
def handler(service: Service = auto) -> 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 auto, injectable
from app.services import Service
@injectable
def my_handler(service: Service = auto) -> 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, use TestContainer and call the .override(interface=..., instance=...) 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 ensures 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 dataclasses import dataclass
from unittest import mock
from anydi import auto
from anydi.testing import TestContainer
@dataclass(kw_only=True)
class Item:
name: str
class Repository:
def __init__(self) -> None:
self.items: list[Item] = []
def all(self) -> list[Item]:
return self.items
class Service:
def __init__(self, repo: Repository) -> None:
self.repo = repo
def get_items(self) -> list[Item]:
return self.repo.all()
container = TestContainer()
@container.inject
def get_items(service: Service = auto) -> list[Item]:
return service.get_items()
def test_handler() -> None:
repo_mock = mock.Mock(spec=Repository)
repo_mock.all.return_value = [Item(name="mock1"), Item(name="mock2")]
with container.override(Repository, repo_mock):
assert get_items() == [Item(name="mock1"), Item(name="mock2")]
To create a TestContainer from the original container for testing, use the .from_container() method:
from anydi import Container
from anydi.testing import TestContainer
def init_container(testing: bool = False) -> Container:
container = Container()
if testing:
return TestContainer.from_container(container)
return container
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.
import pytest
from anydi.testing import TestContainer
@pytest.fixture(scope="session")
def container() -> TestContainer:
return TestContainer()
@pytest.mark.inject
def test_service_get_items(container: TestContainer, service: Service) -> None:
repo_mock = mock.Mock(spec=Repository)
repo_mock.all.return_value = [Item(name="mock1"), Item(name="mock2")]
with container.override(Repository, repo_mock):
assert service.get_items() == [Item(name="mock1"), Item(name="mock2")]
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.
Using .create method you can create a new instance with overridden dependencies for testing:
def test_handler() -> None:
repo_mock = mock.Mock(spec=Repository)
repo_mock.all.return_value = [Item(name="mock1"), Item(name="mock2")]
service = container.create(Service, repo=repo_mock)
assert service.get_items() == [Item(name="mock1"), Item(name="mock2")]
Conclusion
Check examples which shows how to use AnyDI in real-life application.