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.