that-depends
that-depends copied to clipboard
DI-framework, inspired by python-dependency-injector, but without wiring. Python 3.12 is supported
"That Depends"
This package is dependency injection framework for Python, mostly inspired by python-dependency-injector
.
It is production-ready and gives you the following:
- Fully-async simple DI framework with IOC-container.
- Python 3.10-3.12 support.
- Full coverage by types annotations (mypy in strict mode).
- FastAPI and Litestar compatibility.
- Zero dependencies.
- Overriding dependencies for tests.
Main characteristics:
- Fully async -> means every dependency resolving is async, so you should construct with
await
keyword:
from tests.container import DIContainer
async def main():
some_dependency = await DIContainer.independent_factory()
- No wiring for injections in function arguments -> achieved by decision that only one instance of container is supported
from tests import container
from that_depends import Provide, inject
@inject
async def some_function(
independent_factory: container.IndependentFactory = Provide[container.DIContainer.independent_factory],
) -> None:
assert independent_factory.dep1
Quickstart
Install
pip install that-depends
Usage
DI-container with dependencies:
import dataclasses
import logging
import typing
from that_depends import BaseContainer, providers
logger = logging.getLogger(__name__)
def create_sync_resource() -> typing.Iterator[str]:
logger.debug("Resource initiated")
yield "sync resource"
logger.debug("Resource destructed")
async def create_async_resource() -> typing.AsyncIterator[str]:
logger.debug("Async resource initiated")
yield "async resource"
logger.debug("Async resource destructed")
@dataclasses.dataclass(kw_only=True, slots=True)
class IndependentFactory:
dep1: str
dep2: int
@dataclasses.dataclass(kw_only=True, slots=True)
class SyncDependentFactory:
independent_factory: IndependentFactory
sync_resource: str
@dataclasses.dataclass(kw_only=True, slots=True)
class AsyncDependentFactory:
independent_factory: IndependentFactory
async_resource: str
class DIContainer(BaseContainer):
sync_resource = providers.Resource(create_sync_resource)
async_resource = providers.AsyncResource(create_async_resource)
independent_factory = providers.Factory(IndependentFactory, dep1="text", dep2=123)
sync_dependent_factory = providers.Factory(
SyncDependentFactory,
independent_factory=independent_factory,
sync_resource=sync_resource,
)
async_dependent_factory = providers.Factory(
AsyncDependentFactory,
independent_factory=independent_factory,
async_resource=async_resource,
)
Usage with Fastapi
:
import contextlib
import typing
import fastapi
from starlette import status
from starlette.testclient import TestClient
from tests import container
@contextlib.asynccontextmanager
async def lifespan_manager(_: fastapi.FastAPI) -> typing.AsyncIterator[None]:
yield
await container.DIContainer.tear_down()
app = fastapi.FastAPI(lifespan=lifespan_manager)
@app.get("/")
async def read_root(
sync_dependency: typing.Annotated[
container.AsyncDependentFactory,
fastapi.Depends(container.DIContainer.async_dependent_factory),
],
) -> str:
return sync_dependency.async_resource
client = TestClient(app)
response = client.get("/")
assert response.status_code == status.HTTP_200_OK
assert response.json() == "async resource"
Usage with Litestar
:
import typing
import fastapi
import contextlib
from litestar import Litestar, get
from litestar.di import Provide
from litestar.status_codes import HTTP_200_OK
from litestar.testing import TestClient
from tests import container
@get("/")
async def index(injected: str) -> str:
return injected
@contextlib.asynccontextmanager
async def lifespan_manager(_: fastapi.FastAPI) -> typing.AsyncIterator[None]:
yield
await container.DIContainer.tear_down()
app = Litestar(
route_handlers=[index],
dependencies={"injected": Provide(container.DIContainer.async_resource)},
lifespan=[lifespan_manager],
)
def test_litestar_di() -> None:
with (TestClient(app=app) as client):
response = client.get("/")
assert response.status_code == HTTP_200_OK, response.text
assert response.text == "async resource"
Docs
Providers
Resource
- Resource initialized only once and have teardown logic.
- Generator function is required.
import typing
from that_depends import BaseContainer, providers
def create_sync_resource() -> typing.Iterator[str]:
# resource initialization
yield "sync resource"
# resource teardown
class DIContainer(BaseContainer):
sync_resource = providers.Resource(create_sync_resource)
AsyncResource
- Same as
Resource
but async generator function is required.
import typing
from that_depends import BaseContainer, providers
async def create_async_resource() -> typing.AsyncIterator[str]:
# resource initialization
yield "async resource"
# resource teardown
class DIContainer(BaseContainer):
async_resource = providers.AsyncResource(create_async_resource)
Singleton
- Initialized only once, but without teardown logic.
- Class or simple function is allowed.
import dataclasses
from that_depends import BaseContainer, providers
@dataclasses.dataclass(kw_only=True, slots=True)
class SingletonFactory:
dep1: bool
class DIContainer(BaseContainer):
singleton = providers.Singleton(SingletonFactory, dep1=True)
Factory
- Initialized on every call.
- Class or simple function is allowed.
import dataclasses
from that_depends import BaseContainer, providers
@dataclasses.dataclass(kw_only=True, slots=True)
class IndependentFactory:
dep1: str
dep2: int
class DIContainer(BaseContainer):
independent_factory = providers.Factory(IndependentFactory, dep1="text", dep2=123)
AsyncFactory
- Initialized on every call, as
Factory
. - Async function is required.
import datetime
from that_depends import BaseContainer, providers
async def async_factory() -> datetime.datetime:
return datetime.datetime.now(tz=datetime.timezone.utc)
class DIContainer(BaseContainer):
async_factory = providers.Factory(async_factory)
List
- List provider contains other providers.
- Resolves into list of dependencies.
import random
from that_depends import BaseContainer, providers
class DIContainer(BaseContainer):
random_number = providers.Factory(random.random)
numbers_sequence = providers.List(random_number, random_number)