From 6dd31495a1189536df95eb34a9b91852d18e2aaf Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Sat, 25 Apr 2026 14:25:36 +0300 Subject: [PATCH] improve action scope API and docs - Remove global _container; store container in app context_settings so each Typer app carries its own container - Make build_command_container private (_build_command_container) - Inject modern_di.Container directly in test_action_scope, using Container as context manager for ACTION child - Update README: use context manager to close container, add action scope example --- README.md | 29 ++++++++++++++++++++++++++--- modern_di_typer/__init__.py | 2 -- modern_di_typer/main.py | 4 ++-- tests/test_commands.py | 8 ++++---- 4 files changed, 32 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 6212967..a248220 100644 --- a/README.md +++ b/README.md @@ -45,8 +45,32 @@ def my_command( if __name__ == "__main__": - app() - container.close_sync() + with container: + app() +``` + +## Action scope + +To resolve `Scope.ACTION` dependencies, inject `modern_di.Container` (the REQUEST-scoped container created by `@inject`) and build a child: + +```python +import modern_di +from modern_di import Scope, providers, Group +from modern_di_typer import FromDI, inject + + +class Dependencies(Group): + job = providers.Factory(scope=Scope.ACTION, creator=MyJob, bound_type=None) + + +@app.command() +@inject +def my_command( + container: typing.Annotated[modern_di.Container, FromDI(modern_di.Container)], +) -> None: + with container.build_child_container() as action_container: + job = action_container.resolve_provider(Dependencies.job) + job.run() ``` ## API @@ -54,5 +78,4 @@ if __name__ == "__main__": - `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 - `FromDI(provider)` — marker used in `Annotated[T, FromDI(...)]`; accepts a provider instance or a type -- `build_command_container(ctx)` — context manager yielding a `Scope.REQUEST` child container; use inside `@inject` commands for `Scope.ACTION` work - `fetch_di_container(ctx)` — returns the app-scoped container from `ctx.obj` diff --git a/modern_di_typer/__init__.py b/modern_di_typer/__init__.py index c703719..40cb90b 100644 --- a/modern_di_typer/__init__.py +++ b/modern_di_typer/__init__.py @@ -1,6 +1,5 @@ from modern_di_typer.main import ( FromDI, - build_command_container, fetch_di_container, inject, setup_di, @@ -9,7 +8,6 @@ __all__ = [ "FromDI", - "build_command_container", "fetch_di_container", "inject", "setup_di", diff --git a/modern_di_typer/main.py b/modern_di_typer/main.py index a4a1841..a4b09f5 100644 --- a/modern_di_typer/main.py +++ b/modern_di_typer/main.py @@ -35,7 +35,7 @@ def fetch_di_container(ctx: typer.Context) -> Container: @contextlib.contextmanager -def build_command_container(ctx: typer.Context) -> typing.Iterator[Container]: +def _build_command_container(ctx: typer.Context) -> typing.Iterator[Container]: container = fetch_di_container(ctx).build_child_container(scope=Scope.REQUEST) try: yield container @@ -108,7 +108,7 @@ def wrapper(*args: typing.Any, **kwargs: typing.Any) -> T: # noqa: ANN401 if not di_params: return func(**arguments) - with build_command_container(ctx) as cmd_container: + with _build_command_container(ctx) as cmd_container: arguments.update(_resolve_di_params(cmd_container, di_params)) return func(**arguments) diff --git a/tests/test_commands.py b/tests/test_commands.py index 964fbd5..fc46aa5 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -1,10 +1,11 @@ import typing +import modern_di import typer from typer.testing import CliRunner import modern_di_typer -from modern_di_typer import FromDI, build_command_container, inject +from modern_di_typer import FromDI, inject from tests.dependencies import Dependencies, DependentCreator, SimpleCreator @@ -47,9 +48,8 @@ def test_action_scope(app: typer.Typer) -> None: @app.command() @inject - def cmd(ctx: typer.Context) -> None: - with build_command_container(ctx) as cmd_container: - action_container = cmd_container.build_child_container() + def cmd(container: typing.Annotated[modern_di.Container, FromDI(modern_di.Container)]) -> None: + with container.build_child_container() as action_container: instance = action_container.resolve_provider(Dependencies.action_factory) assert isinstance(instance, DependentCreator)