From 80006ededb7077791df5dd5ccec05112060daff9 Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Tue, 15 Sep 2026 20:02:07 +0300 Subject: [PATCH 1/2] fix: raise a clear RuntimeError when inject runs without setup_di --- modern_di_arq/main.py | 14 +++++++++++++- tests/test_jobs.py | 7 +++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/modern_di_arq/main.py b/modern_di_arq/main.py index 691bfc7..5f0ad0d 100644 --- a/modern_di_arq/main.py +++ b/modern_di_arq/main.py @@ -30,6 +30,18 @@ def _set_setting(worker_settings: typing.Any, name: str, value: typing.Any) -> N setattr(worker_settings, name, value) +def _fetch_child_container(ctx: dict[str, typing.Any]) -> Container: + try: + return typing.cast(Container, ctx[_CHILD_CONTAINER_KEY]) + except KeyError: + msg = ( + "No modern-di container found for this job. " + "Call setup_di(worker_settings, container) so jobs pass through the modern-di hooks " + "before using @inject." + ) + raise RuntimeError(msg) from None + + _Hook = typing.Callable[[dict[str, typing.Any]], typing.Awaitable[None]] @@ -163,7 +175,7 @@ def inject(func: typing.Callable[..., typing.Awaitable[T]]) -> typing.Callable[. @functools.wraps(func) async def wrapper(*args: typing.Any, **kwargs: typing.Any) -> T: # noqa: ANN401 ctx = typing.cast("dict[str, typing.Any]", args[0]) - child = typing.cast(Container, ctx[_CHILD_CONTAINER_KEY]) + child = _fetch_child_container(ctx) # Reference-count opens so nested AND concurrent (@inject fan-out via gather) # share one open child and close it exactly once, when the LAST @inject body # exits. The check/open/increment run without an await, so asyncio cannot diff --git a/tests/test_jobs.py b/tests/test_jobs.py index 9754226..f1a5121 100644 --- a/tests/test_jobs.py +++ b/tests/test_jobs.py @@ -298,3 +298,10 @@ async def bad_task( with pytest.raises(TypeError): inject(bad_task) + + +async def test_inject_without_setup_di_names_the_fix() -> None: + ctx: dict[str, typing.Any] = {} + + with pytest.raises(RuntimeError, match=r"setup_di\(worker_settings, container\)"): + await resolves_app_and_request(ctx, 7) From b8f1ace99a6711a806db4e927dfc8bbb905ad396 Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Tue, 15 Sep 2026 20:02:44 +0300 Subject: [PATCH 2/2] docs: note the RuntimeError inject raises without setup_di --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1261fba..b6417c2 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,7 @@ Run the worker as usual (`arq mymodule.WorkerSettings`) and enqueue jobs with on |---|---| | `setup_di(worker_settings, container)` | Seeds the root container into arq's `ctx` and wires root + per-job lifecycle onto `on_startup`/`on_shutdown`/`on_job_start`/`on_job_end`. Accepts a `WorkerSettings` class/object or a settings `dict`; composes with existing hooks; returns the container. Raises `TypeError` if called twice on the same `worker_settings` | | `FromDI(dependency)` | Inert marker for `Annotated[T, FromDI(...)]` in task signatures; accepts a provider instance or a type | -| `inject(task)` | Decorator that resolves `FromDI`-annotated parameters from the per-job `Scope.REQUEST` child. Order-insensitive; passthrough for tasks with no `FromDI`; raises `TypeError` at decoration if the task also declares `*args`/`**kwargs` | +| `inject(task)` | Decorator that resolves `FromDI`-annotated parameters from the per-job `Scope.REQUEST` child. Order-insensitive; passthrough for tasks with no `FromDI`; raises `TypeError` at decoration if the task also declares `*args`/`**kwargs`. Raises `RuntimeError` naming `setup_di` when a job reaches it without the modern-di hooks installed | | `fetch_di_container(ctx)` | Returns the root container from an arq `ctx` dict | ## 📦 [PyPI](https://pypi.org/project/modern-di-arq)