From 76245f1101b5dbc200cdfb5e6d1c6d9d61e8cd83 Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Tue, 15 Sep 2026 19:44:54 +0300 Subject: [PATCH] fix: raise a clear RuntimeError when inject runs without setup_di Closes #13 --- README.md | 2 +- modern_di_aiogram/dialog.py | 6 +++--- modern_di_aiogram/main.py | 18 +++++++++++++++--- tests/test_dialog.py | 20 ++++++++++++++++++++ tests/test_inject.py | 15 +++++++++++++++ 5 files changed, 54 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 14e8902..572aada 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,7 @@ Pass `auto_inject=True` to `setup_di` to wrap every handler already registered o |---|---| | `setup_di(dispatcher, container, *, auto_inject=False)` | Stores the container on the dispatcher, registers the update/event providers, wires `dispatcher.startup`/`dispatcher.shutdown` to open/close it, and installs the per-update middleware. With `auto_inject=True`, also wraps every handler already registered at startup | | `FromDI(dependency)` | Inert marker (used with `@inject`) that resolves a provider or type from the per-update child container | -| `inject(handler)` | Decorator for an aiogram handler; resolves its `FromDI`-annotated parameters. Not needed when `setup_di(..., auto_inject=True)` is used | +| `inject(handler)` | Decorator for an aiogram handler; resolves its `FromDI`-annotated parameters. Not needed when `setup_di(..., auto_inject=True)` is used. Raises `RuntimeError` naming `setup_di` when an update reaches it without the middleware installed | | `fetch_di_container(dispatcher)` | Returns the root `Container` stored on the dispatcher | | `aiogram_update_provider` | `ContextProvider` for the current `aiogram.types.Update` (`REQUEST` scope) | | `aiogram_event_provider` | `ContextProvider` for the current `aiogram.types.TelegramObject` (`REQUEST` scope) — the concrete event unwrapped from the `Update` | diff --git a/modern_di_aiogram/dialog.py b/modern_di_aiogram/dialog.py index c7be50c..86c4898 100644 --- a/modern_di_aiogram/dialog.py +++ b/modern_di_aiogram/dialog.py @@ -13,7 +13,7 @@ from modern_di import Container, integrations -from modern_di_aiogram.main import _CHILD_CONTAINER_KEY, FromDI +from modern_di_aiogram.main import FromDI, _fetch_child_container __all__ = [ @@ -27,10 +27,10 @@ def _container_from_call(args: tuple[typing.Any, ...], kwargs: dict[str, typing.Any]) -> Container: if not args: # getter: aiogram-dialog calls it as getter(**manager.middleware_data) - return typing.cast(Container, kwargs[_CHILD_CONTAINER_KEY]) + return _fetch_child_container(kwargs) # callbacks carry a DialogManager positionally: (data, manager) or (event, widget, manager[, item]) manager = args[-1] if len(args) == _ON_DIALOG_EVENT_ARGS else args[2] - return typing.cast(Container, manager.middleware_data[_CHILD_CONTAINER_KEY]) + return _fetch_child_container(manager.middleware_data) def inject( diff --git a/modern_di_aiogram/main.py b/modern_di_aiogram/main.py index 9e1681a..1a0b11b 100644 --- a/modern_di_aiogram/main.py +++ b/modern_di_aiogram/main.py @@ -20,6 +20,18 @@ _CHILD_CONTAINER_KEY = "modern_di_container" +def _fetch_child_container(data: typing.Mapping[str, typing.Any]) -> Container: + try: + return typing.cast(Container, data[_CHILD_CONTAINER_KEY]) + except KeyError: + msg = ( + "No modern-di container found for this update. " + "Call setup_di(dispatcher, container) so updates pass through the modern-di middleware " + "before using @inject." + ) + raise RuntimeError(msg) from None + + class _DiMiddleware(BaseMiddleware): def __init__(self, container: Container) -> None: self.container = container @@ -74,9 +86,9 @@ def inject(func: typing.Callable[..., typing.Awaitable[T]]) -> typing.Callable[. ) async def wrapper(*args: typing.Any, **kwargs: typing.Any) -> T: # noqa: ANN401 - container: Container = ( - kwargs.pop(_CHILD_CONTAINER_KEY) if container_param_injected else kwargs[_CHILD_CONTAINER_KEY] - ) + container = _fetch_child_container(kwargs) + if container_param_injected: + del kwargs[_CHILD_CONTAINER_KEY] resolved = integrations.resolve_markers(container, di_params) return await func(*args, **kwargs, **resolved) diff --git a/tests/test_dialog.py b/tests/test_dialog.py index 96ce070..3cd26fb 100644 --- a/tests/test_dialog.py +++ b/tests/test_dialog.py @@ -178,3 +178,23 @@ async def _start(_message: Message, dialog_manager: DialogManager) -> None: await client.dp.emit_shutdown() assert teardowns == ["closed"] + + +def _dialog_without_setup_di(dialog: Dialog) -> BotClient: + dispatcher = Dispatcher() + dispatcher.message.register(_start_command, CommandStart()) + dispatcher.include_router(dialog) + setup_dialogs(dispatcher, message_manager=MockMessageManager()) + return BotClient(dispatcher) + + +async def test_getter_without_setup_di_names_the_fix() -> None: + client = _dialog_without_setup_di(Dialog(Window(Format("{name}"), state=MainSG.window, getter=main_getter))) + with pytest.raises(RuntimeError, match=r"setup_di\(dispatcher, container\)"): + await client.send("/start") + + +async def test_callback_without_setup_di_names_the_fix() -> None: + client = _dialog_without_setup_di(Dialog(Window(Const("x"), state=MainSG.window), on_start=on_start)) + with pytest.raises(RuntimeError, match=r"setup_di\(dispatcher, container\)"): + await client.send("/start") diff --git a/tests/test_inject.py b/tests/test_inject.py index 2819633..4187ee8 100644 --- a/tests/test_inject.py +++ b/tests/test_inject.py @@ -1,6 +1,7 @@ import contextlib import typing +import pytest from aiogram import Bot, Dispatcher from aiogram.types import Message from modern_di import Container, Group, Scope, providers @@ -98,3 +99,17 @@ async def handler( await dispatcher.emit_shutdown() assert teardowns == ["closed"] # per-update child closed (finalizer ran) on the error path + + +async def test_inject_without_setup_di_names_the_fix(bot: Bot) -> None: + dispatcher = Dispatcher() + + @dispatcher.message() + @inject + async def handler( + message: Message, + _app: typing.Annotated[SimpleCreator, FromDI(SimpleCreator)], + ) -> None: ... + + with pytest.raises(RuntimeError, match=r"setup_di\(dispatcher, container\)"): + await dispatcher.feed_update(bot, make_message_update())