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
18 changes: 10 additions & 8 deletions docs/integrations/celery.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ def run_report(report: typing.Annotated[Report, FromDI(Report)]) -> str:
return report.render()
```

`setup_di(app, container)` stores the container on `app.conf` and connects Celery's
`setup_di(app, container)` registers the container for the app and connects Celery's
worker signals that open and close it. `@inject` builds a `Scope.REQUEST` child container
for each task call and resolves `FromDI`-annotated parameters from it. It looks the
container up through Celery's `current_app` at call time, so it resolves against
Expand Down Expand Up @@ -105,12 +105,14 @@ Under prefork each child process gets its own APP-scoped instances. `worker_init
container and closing one with nothing cached are both no-ops, so the overlap is
harmless.

!!! warning "The prefork pool fails on macOS"
On macOS, billiard starts prefork children with the spawn method, which pickles
the Celery app. The container that `setup_di` stores on `app.conf` cannot be
pickled, so the worker stops at startup with
`TypeError: cannot pickle '_thread._local' object`. Run the worker with
`--pool=solo` or `--pool=threads` there.
!!! note "Prefork with the spawn start method"
Celery starts prefork children with fork on Linux and macOS. Spawn is used on Windows or
when you force it with `billiard.set_start_method("spawn", force=True)`, and Celery then
needs `FORKED_BY_MULTIPROCESSING=1` in the environment. Since modern-di-celery 4.0.1, a
spawn child gets the container that `setup_di` builds when the child re-imports your task
module, matched to the app by its `main` name, so give each app in a process its own name.
Earlier releases store the container on `app.conf`, which cannot be pickled, and the pool
fails at startup under spawn.

Code that calls tasks without a worker, such as a script or a test in eager mode,
fires none of these signals. Close the root yourself when it is done, for example
Expand Down Expand Up @@ -177,7 +179,7 @@ with container:

| Symbol | Description |
|---|---|
| `setup_di(app, container)` | Wire the APP-scope container into Celery: stores it on `app.conf` and opens/closes it on `worker_process_init`/`worker_process_shutdown` and `worker_init`/`worker_shutdown`. Returns the container. |
| `setup_di(app, container)` | Wire the APP-scope container into Celery: registers it for the app and opens/closes it on `worker_process_init`/`worker_process_shutdown` and `worker_init`/`worker_shutdown`. Returns the container. |
| `FromDI(provider_or_type)` | Marker for `Annotated[T, FromDI(...)]` in task signatures; accepts a provider instance or a plain type. |
| `@inject` | Decorator that builds a `Scope.REQUEST` child container per call, resolves `FromDI`-annotated parameters from it, and closes the child container with `close_sync()` afterwards. Raises `RuntimeError` naming `setup_di` when a task reaches it without `setup_di` called. A task with `FromDI` parameters that also declares `*args`/`**kwargs` raises `TypeError` at decoration. |
| `DITask` | `Task` subclass that applies `@inject` to a task's `run` method automatically; pass `task_cls=DITask` to `Celery(...)` or `base=DITask` to `@app.task(...)`. |
Expand Down
40 changes: 34 additions & 6 deletions docs/integrations/flask.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,8 @@
Flask has no dependency injection of its own, so `modern-di-flask` provides an `@inject` decorator
that fills the view parameters marked with `FromDI`. `setup_di` installs a
`before_request`/`teardown_appcontext` pair that gives each request a `Scope.REQUEST` child
container and closes it with `close_sync()` once the request finishes. Resolution is sync-only,
and views must be sync too: `@inject` on an `async def` view hands Flask a coroutine, and the
request fails with `TypeError`.
container and closes it with `close_sync()` once the request finishes. Views can be sync or
`async def`, and resolution is sync for both.

## Installation

Expand Down Expand Up @@ -123,6 +122,33 @@ else:
raise AssertionError("the late route was wrapped")
-->

### Async views

Since modern-di-flask 4.1.0, `@inject` and `auto_inject=True` also take `async def` views. Flask
runs them through asgiref, so install Flask's async extra (`flask[async]`). The markers are resolved
from the request's child container before the view is awaited:

```python
async_app = Flask(__name__)


@async_app.route("/async-report")
@inject
async def get_async_report(report: typing.Annotated[Report, FromDI(Report)]) -> dict[str, str]:
return report.as_dict()


setup_di(async_app, Container(groups=[AppGroup]))
```

<!-- invisible-code-block: python
async_app.testing = True
assert async_app.test_client().get("/async-report").json == {"service": "catalog"}
-->

Teardown is still sync (`close_sync()`), so a REQUEST-scoped provider with an async finalizer is
not supported; see [Scopes and lifecycle](#scopes-and-lifecycle).

## Scopes and lifecycle

Flask has no websocket concept, so the integration only ever opens one child scope; see
Expand All @@ -136,8 +162,7 @@ and it or any other failing finalizer reaches Flask as a `FinalizerError` from t
request then fails with a 500 under the Werkzeug server, and the test client raises the error.

`setup_di` does not close the root container, because Flask has no application-shutdown hook to
run it from. Close it yourself where your process shuts down, for example with `atexit`. That
close is a `close_sync()` too, so APP-scoped finalizers must be sync as well:
run it from. Close it yourself where your process shuts down, for example with `atexit`:

```python
import atexit
Expand All @@ -147,6 +172,9 @@ from modern_di_flask import fetch_di_container
atexit.register(fetch_di_container(app).close_sync)
```

`close_sync()` cannot run an async finalizer. If an APP-scoped provider has one, close the root
with `asyncio.run(fetch_di_container(app).close_async())` instead.

## Framework context objects

The integration makes `flask.Request` available to your factories. See
Expand Down Expand Up @@ -207,6 +235,6 @@ assert request_app.test_client().get("/describe").json == [{"method": "GET", "ur
|---|---|
| `setup_di(app, container, *, auto_inject=False)` | Stores the container on `app.extensions`, registers the request context provider, installs the `before_request`/`teardown_appcontext` pair that builds and closes a per-request `Scope.REQUEST` child container, and, if `auto_inject=True`, wraps every currently registered view with `inject`; returns the container. It does not close the root container. |
| `FromDI(dependency)` | Marker (used with `@inject`) that resolves a provider or type from the per-request child container. |
| `inject` | Decorator for a sync view function; resolves its `FromDI`-annotated parameters. Raises `RuntimeError` naming `setup_di` when a request reaches it without `setup_di` called. |
| `inject` | Decorator for a sync or `async def` view; returns a wrapper of the same kind and resolves its `FromDI`-annotated parameters. Raises `RuntimeError` naming `setup_di` when a request reaches it without `setup_di` called. |
| `fetch_di_container(app)` | Returns the root `Container` stored on `app.extensions`. |
| `flask_request_provider` | `ContextProvider` for `flask.Request` (REQUEST scope), auto-registered by type. |
6 changes: 4 additions & 2 deletions docs/integrations/litestar.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,10 @@ If an attribute name appears in more than one group, or matches a dependency the
last one wins.

!!! warning "Don't name a provider `di_container`"
An autowired provider named `di_container` replaces the plugin's own container dependency,
which every `FromDI` depends on. Litestar then fails to build the app with `RecursionError`.
The name `di_container` is reserved for the plugin's own dependency, which every `FromDI`
depends on. Since modern-di-litestar 4.0.1, an autowired group that declares a provider under
that name raises `litestar.exceptions.ImproperlyConfiguredException` when the app is created.
Earlier releases replace the plugin's dependency, and Litestar never finishes building the app.

With `autowired_groups` set, `FromDI` on a route can still take a type. Pass a provider instance
only for providers outside `autowired_groups`: Litestar rejects one provider registered under two
Expand Down
2 changes: 1 addition & 1 deletion justfile
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ test-docs *args:
uv run --no-sync pytest docs {{ args }}

# Packages the docs import that no modern-di-<page> package depends on.
docs_integration_extras := "faststream[kafka,nats] aiogram-dialog sqlalchemy[asyncio] asyncpg redis pydantic-ai-slim[openai]"
docs_integration_extras := "faststream[kafka,nats] flask[async] aiogram-dialog sqlalchemy[asyncio] asyncpg redis pydantic-ai-slim[openai]"

# Run every page under docs/, the framework pages under docs/integrations/ included, with the latest
# modern-di-<page> release of every integration and the extras above installed and modern-di pinned to
Expand Down
Loading