diff --git a/docs/integrations/celery.md b/docs/integrations/celery.md index e2378123..de862a6f 100644 --- a/docs/integrations/celery.md +++ b/docs/integrations/celery.md @@ -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 @@ -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 @@ -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(...)`. | diff --git a/docs/integrations/flask.md b/docs/integrations/flask.md index b8b6ee94..dc2c108e 100644 --- a/docs/integrations/flask.md +++ b/docs/integrations/flask.md @@ -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 @@ -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])) +``` + + + +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 @@ -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 @@ -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 @@ -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. | diff --git a/docs/integrations/litestar.md b/docs/integrations/litestar.md index 12595d09..169dbd4d 100644 --- a/docs/integrations/litestar.md +++ b/docs/integrations/litestar.md @@ -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 diff --git a/justfile b/justfile index 145d58be..6c97f5ba 100644 --- a/justfile +++ b/justfile @@ -44,7 +44,7 @@ test-docs *args: uv run --no-sync pytest docs {{ args }} # Packages the docs import that no modern-di- 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- release of every integration and the extras above installed and modern-di pinned to