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 @@ -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` |
Expand Down
6 changes: 3 additions & 3 deletions modern_di_aiogram/dialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__ = [
Expand 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(
Expand Down
18 changes: 15 additions & 3 deletions modern_di_aiogram/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)

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