diff --git a/README.md b/README.md index c91f9cc..83052fc 100644 --- a/README.md +++ b/README.md @@ -94,7 +94,7 @@ def my_command(ctx: typer.Context) -> None: - `setup_di(app, container)` — register the container with a Typer app - `inject` — decorator that resolves `FromDI`-annotated parameters before the command runs; also exposes `typer.Context` with `ctx.obj["di_container"]` for manual use. Raises `RuntimeError` naming `setup_di` when a command reaches it without `setup_di` called - `FromDI(provider)` — marker used in `Annotated[T, FromDI(...)]`; accepts a provider instance or a type -- `action_scope(ctx)` — context manager yielding a fresh `Scope.ACTION` container (a child of the command container); open one per action +- `action_scope(ctx)` — context manager yielding a fresh `Scope.ACTION` container (a child of the command container); open one per action. Raises `RuntimeError` naming `@inject` when called from a command without it - `fetch_di_container(ctx)` — returns the app-scoped container from `ctx.obj` ## Used by diff --git a/modern_di_typer/main.py b/modern_di_typer/main.py index 25a77b5..bb2f980 100644 --- a/modern_di_typer/main.py +++ b/modern_di_typer/main.py @@ -43,7 +43,14 @@ def _build_command_container(ctx: typer.Context) -> typing.Iterator[Container]: @contextlib.contextmanager def action_scope(ctx: typer.Context) -> typing.Iterator[Container]: - request_container: Container = ctx.meta[_COMMAND_CONTAINER_KEY] + try: + request_container: Container = ctx.meta[_COMMAND_CONTAINER_KEY] + except KeyError: + msg = ( + "No modern-di command container found for this command. " + "Decorate the command with @inject so action_scope has a per-command container to nest under." + ) + raise RuntimeError(msg) from None with request_container.build_child_container() as action_container: yield action_container diff --git a/tests/test_commands.py b/tests/test_commands.py index c23dd0f..6941586 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -284,3 +284,15 @@ def cmd(ctx: typer.Context) -> None: with pytest.raises(RuntimeError, match=r"setup_di\(app, container\)"): runner.invoke(app, catch_exceptions=False) + + +def test_action_scope_without_inject_raises_clear_error(app: typer.Typer) -> None: + runner = CliRunner() + + @app.command() + def cmd(ctx: typer.Context) -> None: + with action_scope(ctx): + ... + + with pytest.raises(RuntimeError, match="@inject"): + runner.invoke(app, catch_exceptions=False)