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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
14 changes: 13 additions & 1 deletion modern_di_arq/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]]


Expand Down Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions tests/test_jobs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Loading