Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@

Simple, typed dependency injection framework for Python.

It is production-ready and gives you the following:
Features:
- Simple async-first DI framework with IOC-container.
- Python 3.10+ support.
- Full coverage by types annotations (mypy in strict mode, pyrefly).
- Inbuilt FastAPI, FastStream and LiteStar compatibility.
- Built-in FastAPI, FastStream and Litestar integrations.
- Dependency context management with scopes.
- Overriding dependencies for tests.
- Injecting dependencies in functions and coroutines without wiring.
Expand Down
4 changes: 2 additions & 2 deletions docs/dev/contributing.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
# Contributing
`that-depends` is an opensource project, and we are opened to new contributors.
`that-depends` is an opensource project, and we are open to new contributors.

## Getting started
1. Make sure that you have [uv](https://docs.astral.sh/uv/) and [just](https://just.systems/) installed.
2. Clone project:
```
git@github.com:modern-python/that-depends.git
git clone git@github.com:modern-python/that-depends.git
cd that-depends
```
3. Install dependencies running `just install`
Expand Down
4 changes: 3 additions & 1 deletion docs/experimental/lazy.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,11 @@ You can use the lazy provider in exactly the same way as you would use the refer

```python
# first_container.py
import typing

from that_depends import BaseContainer, providers, ContextScopes

def my_creator() -> int:
def my_creator() -> typing.Iterator[int]:
yield 42

class FirstContainer(BaseContainer):
Expand Down
5 changes: 5 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ supports the following:

### Define a creator
```python
import logging

logger = logging.getLogger(__name__)


async def create_async_resource():
logger.debug("Async resource initiated")
try:
Expand Down
16 changes: 10 additions & 6 deletions docs/integrations/fastapi.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,9 @@ This will enable you to use dependency injection in your `FastAPI` endpoints:
If you wish to initialize the container context you can simply pass arguments to `create_fastapi_route_class()`:

```python
my_route_class = create_fastapi_route_class(Container, global_context={"key": "value"}, scope=ContextScopes.REQUEST)
from that_depends import ContextScopes

my_route_class = create_fastapi_route_class(MyContainer, global_context={"key": "value"}, scope=ContextScopes.REQUEST)
```

In the above example, all `ContextResources` in our container that are `REQUEST` scoped will be initialized
Expand Down Expand Up @@ -220,13 +222,14 @@ Then in your FastAPI app:
```python
# main.py
from fastapi import FastAPI, Depends
from that_depends.providers.context_resources import DIContextMiddleware
from that_depends.providers.context_resources import ContextScopes, DIContextMiddleware
from mycontainer import MyScopedContainer

app = FastAPI()
app.add_middleware(
DIContextMiddleware,
MyContainer,
MyScopedContainer,
scope=ContextScopes.REQUEST,
)

@app.get("/")
Expand Down Expand Up @@ -269,9 +272,9 @@ def test_read_db(client: TestClient):

1. **Global vs. Request Context**: Decide whether your container’s dependencies should be globally shared (e.g., singletons) or created anew per request (e.g., database or session).
2. **Combining with FastAPI’s `Depends`**: Generally, you can pass `Depends(MyContainer.some_provider)` to route handlers. Under the hood, that-depends will be invoked.
3. **Overriding**: You can override a provider in tests by calling `MyContainer.some_provider.override(...)` or using the context manager `with MyContainer.some_provider.override_context(...):`.
3. **Overriding**: You can override a provider in tests by calling `MyContainer.some_provider.override_sync(...)` or using the context manager `with MyContainer.some_provider.override_context_sync(...):`.
4. **Performance**: If you have expensive creation logic (like a DB engine that can be reused globally), prefer using a `Singleton` or `Object` provider. If you need ephemeral resources, use `ContextResource` with the `DIContextMiddleware`.
5. **Custom Context**: If you do not want to rely on the middleware, you can manually create a context in any async function by calling `async with container_context():`.
5. **Custom Context**: If you do not want to rely on the middleware, you can manually create a context in any async function by calling `async with container_context(MyContainer):`.
6. **Multiple Containers**: You can define multiple containers and connect them (e.g., `ContainerA.connect_containers(ContainerB)`), or add them all to the `DIContextMiddleware`. For advanced usage, see the that-depends documentation on “container connection.”


Expand All @@ -285,7 +288,7 @@ Sometimes you want to pass the `fastapi.Request` (or other request-scoped data)
# request_deps.py
from fastapi import Request
from typing import AsyncIterator
from that_depends import container_context, fetch_context_item
from that_depends import container_context

async def init_di_context(request: Request) -> AsyncIterator[None]:
# We store the request in a that_depends global context
Expand All @@ -299,6 +302,7 @@ Then in your `FastAPI` route:
# main.py (extended)
from fastapi import FastAPI, Request, Depends
from starlette.responses import JSONResponse
from that_depends import fetch_context_item

from request_deps import init_di_context
from mycontainer import MyContainer
Expand Down
25 changes: 20 additions & 5 deletions docs/integrations/faststream.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,22 +56,37 @@ broker = RabbitBroker(middlewares=[DIContextMiddleware(Container, scope=ContextS
Here is an example that includes life-cycle events:

```python
import datetime
import contextlib
import dataclasses
import datetime
import typing

from faststream import FastStream, Depends, Logger
from faststream.rabbit import RabbitBroker

from tests import container
from that_depends import BaseContainer, providers


async def create_async_resource() -> typing.AsyncIterator[datetime.datetime]:
yield datetime.datetime.now(tz=datetime.timezone.utc)


@dataclasses.dataclass(kw_only=True, slots=True)
class DependentFactory:
async_resource: datetime.datetime


class DIContainer(BaseContainer):
async_resource = providers.Resource(create_async_resource)
dependent_factory = providers.Factory(DependentFactory, async_resource=async_resource.cast)


@contextlib.asynccontextmanager
async def lifespan_manager() -> typing.AsyncIterator[None]:
try:
yield
finally:
await container.DIContainer.tear_down()
await DIContainer.tear_down()


broker = RabbitBroker()
Expand All @@ -82,8 +97,8 @@ app = FastStream(broker, lifespan=lifespan_manager)
async def read_root(
logger: Logger,
some_dependency: typing.Annotated[
container.DependentFactory,
Depends(container.DIContainer.dependent_factory)
DependentFactory,
Depends(DIContainer.dependent_factory)
],
) -> datetime.datetime:
startup_time = some_dependency.async_resource
Expand Down
20 changes: 14 additions & 6 deletions docs/integrations/litestar.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,23 @@
sibling DI framework.

```python
import typing
import fastapi
import contextlib
import typing

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
from that_depends import BaseContainer, providers


async def create_async_resource() -> typing.AsyncIterator[str]:
yield "async resource"


class DIContainer(BaseContainer):
async_resource = providers.Resource(create_async_resource)


@get("/")
Expand All @@ -23,16 +31,16 @@ async def index(injected: str) -> str:


@contextlib.asynccontextmanager
async def lifespan_manager(_: fastapi.FastAPI) -> typing.AsyncIterator[None]:
async def lifespan_manager(_: Litestar) -> typing.AsyncIterator[None]:
try:
yield
finally:
await container.DIContainer.tear_down()
await DIContainer.tear_down()


app = Litestar(
route_handlers=[index],
dependencies={"injected": Provide(container.DIContainer.async_resource)},
dependencies={"injected": Provide(DIContainer.async_resource)},
lifespan=[lifespan_manager],
)

Expand Down
4 changes: 2 additions & 2 deletions docs/introduction/generator-injection.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ def injected(val: float = Provide[Container.dependent_provider]) -> typing.Gener
yield val

# This will raise a `ContextProviderError`!
next(_injected())
next(injected())
```

1. Matches context scope of `sync_provider` provider, which is a dependency of the `dependent_provider` provider.
Expand All @@ -136,7 +136,7 @@ def injected(val: float = Provide[Container.dependent_provider]) -> typing.Gener

with container_context(scope=ContextScopes.REQUEST):
# This will resolve as expected
next(_injected())
next(injected())
```

Since no context initialization was needed, the generator will work as expected.
Expand Down
6 changes: 3 additions & 3 deletions docs/introduction/injection.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ def greet_user_direct(
return f"Greeting: {greeting}"
```

1. Notice that although `greeting` is a `str`, `mypy` and you IDE will not complain.
1. Notice that although `greeting` is a `str`, `mypy` and your IDE will not complain.

---

Expand Down Expand Up @@ -123,7 +123,7 @@ For more details regarding scopes and context management, see the [Context Resou

## Overriding Providers

In tests or specialized scenarios, you may want to override a provider’s value temporarily. You can do so with the container’s `override_providers()` method or the provider’s own `override_context()`:
In tests or specialized scenarios, you may want to override a provider’s value temporarily. You can do so with the container’s `override_providers_sync()` method or the provider’s own `override_context_sync()`:

```python
def test_greet_override():
Expand All @@ -135,7 +135,7 @@ def test_greet_override():

This is especially helpful for unit tests where you want to substitute real dependencies (e.g., database connections) with mocks or stubs.

For more details on overring providers, see the [Overriding Providers](../testing/provider-overriding.md) documentation.
For more details on overriding providers, see the [Overriding Providers](../testing/provider-overriding.md) documentation.

---

Expand Down
5 changes: 3 additions & 2 deletions docs/introduction/ioc-container.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ from that_depends import BaseContainer

class Container(BaseContainer):
# define your providers here
...
```

Then you can build your dependency graph within the container:
Expand All @@ -27,9 +28,9 @@ class Container(BaseContainer):
session = providers.Factory(create_db_session, config=config.db) # (1)!

user_repository = providers.Factory(
UserRepository,
UserRepository,
config.users,
session=session.cast, # (3)!
config.users
) # (2)!


Expand Down
16 changes: 13 additions & 3 deletions docs/introduction/multiple-containers.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,23 @@

You can use providers from other containers as following:
```python
from tests import container
import datetime
import typing

from that_depends import BaseContainer, providers


def create_sync_resource() -> typing.Iterator[datetime.datetime]:
yield datetime.datetime.now(tz=datetime.timezone.utc)


async def create_async_resource() -> typing.AsyncIterator[datetime.datetime]:
yield datetime.datetime.now(tz=datetime.timezone.utc)


class InnerContainer(BaseContainer):
sync_resource = providers.Resource(container.create_sync_resource)
async_resource = providers.Resource(container.create_async_resource)
sync_resource = providers.Resource(create_sync_resource)
async_resource = providers.Resource(create_async_resource)


class OuterContainer(BaseContainer):
Expand Down
14 changes: 8 additions & 6 deletions docs/introduction/scopes.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ Before continuing, make sure you're familiar with `ContextResource` providers by
## Quick Start

By default, `ContextResources` have the named scope `ANY`, meaning they will be re-initialized each time you enter a named scope.
A container that defines a `ContextResource` without an explicit scope must assign `default_scope` before it, otherwise defining the container raises `DefaultScopeNotDefinedError`.
Set `default_scope = ContextScopes.ANY` to keep the `ANY` behavior.
You can change the scope of a `ContextResource` in two ways:

### Setting the scope for providers
Expand All @@ -16,13 +18,13 @@ You can change the scope of a `ContextResource` in two ways:

~~~~python hl_lines="2"
class MyContainer(BaseContainer):
default_scope = ContextScope.APP
default_scope = ContextScopes.APP
p = providers.ContextResource(my_resource)
~~~~

2. By calling the `with_config()` method when creating a `ContextResource`. This also overrides the class default:
~~~~python
p = providers.ContextResource(my_resource).with_config(scope=ContextScope.APP)
p = providers.ContextResource(my_resource).with_config(scope=ContextScopes.APP)
~~~~

### Entering and exiting scopes
Expand Down Expand Up @@ -67,7 +69,7 @@ async with p.context_async():
Similarly, this will also not work:
```python
async with container_context(p, scope=ContextScopes.REQUEST):
# will raise and InvalidContextError since you are entering `REQUEST` scope
# will raise an InvalidContextError since you are entering `REQUEST` scope
...
```

Expand All @@ -78,7 +80,7 @@ await p.resolve() # will raise an exception

async with container_context(p, scope=ContextScopes.APP):
val_1 = await p.resolve() # will resolve
async with container_context(p, scope=ContextScopes.REQUEST):
async with container_context(scope=ContextScopes.REQUEST):
val_2 = await p.resolve() # will resolve
assert val_1 == val_2 # but value stays the same since context is the same
```
Expand Down Expand Up @@ -114,7 +116,7 @@ class Container(BaseContainer):
@Container.context(force=True)
@inject
async def injected(val = Provide[Container.p]):
return p
return val

await injected() # will resolve
```
Expand Down Expand Up @@ -188,7 +190,7 @@ injected()
2. Context for `Container.provider` is initialized and will exit when the function returns.
3. This assertion will pass since the context for this provider is still the same.

This implementation might seem complex at first glance, but it providers the following advantages:
This implementation might seem complex at first glance, but it provides the following advantages:

- Only context for `ContextResource` providers you need is initialized. This improves performance.
- It discourages explicit resolution via `.resolve()` or `.resolve_sync()` in the function body.
Expand Down
Loading
Loading