con un clic
that-depends
// Guide for using the `that-depends` library, a Python dependency injection framework.
// Guide for using the `that-depends` library, a Python dependency injection framework.
| name | that-depends |
| description | Guide for using the `that-depends` library, a Python dependency injection framework. |
that-dependsthat-depends is a typed dependency-injection framework for Python. The core workflow is:
BaseContainer.@inject and Provide[...].ContextResource providers.Keep providers on BaseContainer subclasses instead of as standalone globals, especially if you need context features.
from that_depends import BaseContainer, providers
class Settings:
def __init__(self) -> None:
self.base_url = "https://api.example.com"
class ApiClient:
def __init__(self, base_url: str) -> None:
self.base_url = base_url
class UserService:
def __init__(self, client: ApiClient) -> None:
self.client = client
class Container(BaseContainer):
settings = providers.Singleton(Settings)
api_client = providers.Factory(ApiClient, base_url=settings.cast.base_url)
user_service = providers.Factory(UserService, client=api_client.cast)
Use @inject with Provide[Container.provider] defaults.
from that_depends import Provide, inject
@inject
def handle_user(service: UserService = Provide[Container.user_service]) -> UserService:
return service
This is the preferred style over explicit resolve() / resolve_sync() calls in application code.
| Provider | Use for |
|---|---|
Singleton / AsyncSingleton | One cached instance |
Factory / AsyncFactory | New value on each resolution |
Resource | Cached value with teardown |
ContextResource | Per-context / per-scope resource |
Sequence / Mapping | Aggregate multiple providers into read-only collection types |
Selector | Choose one provider from a key |
State | Pass runtime state through context |
Avoid calling resolve() and resolve_sync() inside normal application code. Inject dependencies into function parameters instead.
Reserve explicit resolution for bootstrapping, one-off scripts, REPL usage, or tests.
Avoid:
def create_handler() -> UserService:
return Container.user_service.resolve_sync()
Prefer:
@inject
def create_handler(service: UserService = Provide[Container.user_service]) -> UserService:
return service
Why this is better:
resolve() / resolve_sync() in injected function bodiesThis is especially important for ContextResource providers. The injection system initializes context for dependencies declared in Provide[...] defaults; explicit resolution inside the function body is discouraged and can fail for scoped resources.
Avoid:
import typing
from that_depends.providers.context_resources import ContextScopes
def open_session() -> typing.Iterator[str]:
yield "session"
class Container(BaseContainer):
default_scope = ContextScopes.INJECT
session = providers.ContextResource(open_session).with_config(scope=ContextScopes.INJECT)
@inject(scope=ContextScopes.INJECT)
def bad() -> str:
return Container.session.resolve_sync()
Prefer:
@inject(scope=ContextScopes.INJECT)
def good(session: str = Provide[Container.session]) -> str:
return session
provider.context() over container_context()If you only need to initialize context for one provider, prefer the provider decorator/context API. It is more local and easier to read.
Preferred for a single provider:
import typing
from that_depends import Provide, inject, providers, BaseContainer
def request_id_resource() -> typing.Iterator[str]:
yield "req-123"
class Container(BaseContainer):
request_id = providers.ContextResource(request_id_resource)
@Container.request_id.context
@inject
def endpoint(request_id: str = Provide[Container.request_id]) -> str:
return request_id
Use container_context() when you need one of these:
global_context;from that_depends import container_context
from that_depends.providers.context_resources import ContextScopes
async with container_context(
Container.request_id,
global_context={"trace_id": "abc-123"},
scope=ContextScopes.REQUEST,
):
...
If you need all ContextResource providers in one container, @Container.context is often cleaner than container_context(Container).
@Container.context
@inject
async def run_endpoint(request_id: str = Provide[Container.request_id]) -> str:
return request_id
Use Provide[Container.provider] directly in examples and normal application code:
@inject
def fn(service: UserService = Provide[Container.user_service]) -> UserService:
return service
Prefer provider or container override APIs over patching internals.
def test_handler_override() -> None:
fake_service = UserService(ApiClient("https://test.example.com"))
with Container.user_service.override_context_sync(fake_service):
assert create_handler() is fake_service
If you override an upstream cached dependency and need dependents to refresh, use tear_down_children=True.
Container.settings.override_sync(Settings(), tear_down_children=True)
Singleton and Resource values are cached. Tear them down between tests or at application shutdown.
import pytest_asyncio
from typing import AsyncIterator
@pytest_asyncio.fixture(autouse=True)
async def di_teardown() -> AsyncIterator[None]:
try:
yield
finally:
await Container.tear_down()
Generator injection is supported, but generator injection cannot initialize ContextResource contexts for you. Pre-initialize the context first if needed.Selector, Sequence, and Mapping help compose providers instead of manually wiring branches and aggregates in application code.State is useful for runtime values that should flow through provider resolution.When writing or reviewing code that uses that-depends, prefer these defaults:
BaseContainer.@inject plus Provide[Container.provider].resolve() / resolve_sync() in application code.provider.context() or Container.context() for context-managed dependencies.container_context() only when multiple providers/containers, global context, or explicit scope control are required.tear_down() in cleanup paths.For full package documentation, read the official documentation.