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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
==
[![Test Coverage](https://codecov.io/gh/modern-python/that-depends/branch/main/graph/badge.svg)](https://codecov.io/gh/modern-python/that-depends)
[![MyPy Strict](https://img.shields.io/badge/mypy-strict-blue)](https://mypy.readthedocs.io/en/stable/getting_started.html#strict-mode-and-configuration)
[![pyrefly](https://img.shields.io/endpoint?url=https://pyrefly.org/badge.json)](https://github.com/facebook/pyrefly)
[![Supported versions](https://img.shields.io/pypi/pyversions/that-depends.svg)](https://pypi.python.org/pypi/that-depends)
[![PyPI Downloads](https://static.pepy.tech/badge/that-depends/month)](https://pepy.tech/projects/that-depends)
[![GitHub stars](https://img.shields.io/github/stars/modern-python/that-depends)](https://github.com/modern-python/that-depends/stargazers)
Expand Down
256 changes: 256 additions & 0 deletions that_depends/.agents/skills/that-depends/Skill.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
---
name: that-depends
description: Guide for using the `that-depends` library, a Python dependency injection framework.
---

# `that-depends`

`that-depends` is a typed dependency-injection framework for Python. The core workflow is:

1. Define a `BaseContainer`.
2. Register providers as class attributes.
3. Consume dependencies with `@inject` and `Provide[...]`.
4. Use context decorators for `ContextResource` providers.
5. Override providers in tests instead of patching call sites.

## Recommended usage

### Define providers on a container

Keep providers on `BaseContainer` subclasses instead of as standalone globals, especially if you need context features.

```python
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)
```

### Prefer the decorator API for application code

Use `@inject` with `Provide[Container.provider]` defaults.

```python
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 cheat sheet

| 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 |

## Best practices

### Prefer injection over explicit resolution

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:**

```python
def create_handler() -> UserService:
return Container.user_service.resolve_sync()
```

**Prefer:**

```python
@inject
def create_handler(service: UserService = Provide[Container.user_service]) -> UserService:
return service
```

Why this is better:

- it keeps call sites easy to override in tests;
- it works naturally with scoped/context-managed providers;
- callers can still pass explicit arguments without overriding providers.

### Do not call `resolve()` / `resolve_sync()` in injected function bodies

This 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:**

```python
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:**

```python
@inject(scope=ContextScopes.INJECT)
def good(session: str = Provide[Container.session]) -> str:
return session
```

### Prefer `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:**

```python
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:

- initialize context for multiple providers or containers at once;
- pass `global_context`;
- manually control a named scope.

```python
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)`.

```python
@Container.context
@inject
async def run_endpoint(request_id: str = Provide[Container.request_id]) -> str:
return request_id
```

### Prefer direct provider references

Use `Provide[Container.provider]` directly in examples and normal application code:

```python
@inject
def fn(service: UserService = Provide[Container.user_service]) -> UserService:
return service
```

### Use overrides in tests

Prefer provider or container override APIs over patching internals.

```python
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`.

```python
Container.settings.override_sync(Settings(), tear_down_children=True)
```

### Tear down cached providers in tests and shutdown hooks

`Singleton` and `Resource` values are cached. Tear them down between tests or at application shutdown.

```python
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()
```

## Notes on advanced features

- `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.
- For framework integrations, see FastAPI, FastStream, and Litestar support in the docs.

## Practical defaults

When writing or reviewing code that uses `that-depends`, prefer these defaults:

1. Put providers on a `BaseContainer`.
2. Use `@inject` plus `Provide[Container.provider]`.
3. Avoid `resolve()` / `resolve_sync()` in application code.
4. Use `provider.context()` or `Container.context()` for context-managed dependencies.
5. Reach for `container_context()` only when multiple providers/containers, global context, or explicit scope control are required.
6. Use override APIs in tests and `tear_down()` in cleanup paths.

## Detailed documentation

For full package documentation, read the official [documentation](https://that-depends.readthedocs.io/llms.txt).
Loading