diff --git a/CONTEXT.md b/CONTEXT.md index 8fac0e9..14233e7 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -26,7 +26,8 @@ updates in flight finish first, then the other shutdown handlers run, then it is The single `Scope.REQUEST` child the middleware builds for one incoming `Update`, seeded with that `Update` and its concrete event. Every handler, dialog getter and dialog callback that the update reaches shares this one container, and it is closed when the middleware returns, on the error path -included. +included. An error handler runs after that close, so the same middleware on `dispatcher.errors` +builds it a second child, seeded with the failing `Update` and its event. _Avoid_: request container. The unit of work is an update, not an HTTP request, the same reason `modern-di` rejects "request" for `Connection`. diff --git a/modern_di_aiogram/main.py b/modern_di_aiogram/main.py index 3704e31..7438189 100644 --- a/modern_di_aiogram/main.py +++ b/modern_di_aiogram/main.py @@ -8,7 +8,7 @@ import aiogram from aiogram import BaseMiddleware, Dispatcher from aiogram.dispatcher.event.handler import HandlerObject -from aiogram.types import TelegramObject, Update +from aiogram.types import ErrorEvent, TelegramObject, Update from aiogram.types.update import UpdateTypeLookupError from modern_di import Container, Scope, integrations, providers @@ -50,9 +50,10 @@ async def __call__( event: TelegramObject, data: dict[str, typing.Any], ) -> typing.Any: # noqa: ANN401 - context: dict[type[typing.Any], typing.Any] = {Update: event} + update = event.update if isinstance(event, ErrorEvent) else event + context: dict[type[typing.Any], typing.Any] = {Update: update} with contextlib.suppress(UpdateTypeLookupError): - context[TelegramObject] = typing.cast("Update", event).event + context[TelegramObject] = typing.cast("Update", update).event self.in_flight += 1 try: async with self.container.build_child_container(scope=Scope.REQUEST, context=context) as child_container: @@ -116,6 +117,7 @@ def open_root() -> None: dispatcher.startup.register(open_root) dispatcher.update.outer_middleware(middleware) + dispatcher.errors.outer_middleware(middleware) if auto_inject: dispatcher.startup.register(functools.partial(_inject_router, dispatcher)) return container diff --git a/tests/test_inject.py b/tests/test_inject.py index 566ff11..e88cd26 100644 --- a/tests/test_inject.py +++ b/tests/test_inject.py @@ -6,7 +6,7 @@ import pytest from aiogram import Bot, Dispatcher from aiogram.dispatcher.event.bases import UNHANDLED -from aiogram.types import Message, Update +from aiogram.types import ErrorEvent, Message, Update from modern_di import Container, Group, Scope, providers from modern_di_aiogram import FromDI, inject, setup_di @@ -104,6 +104,32 @@ async def handler( assert teardowns == ["closed"] # per-update child closed (finalizer ran) on the error path +async def test_inject_resolves_in_an_error_handler(dispatcher: Dispatcher, bot: Bot) -> None: + seen: dict[str, typing.Any] = {} + + @dispatcher.message() + async def handler(message: Message) -> None: # noqa: ARG001 + msg = "boom" + raise ValueError(msg) + + @dispatcher.errors() + @inject + async def on_error( + event: ErrorEvent, + request_instance: typing.Annotated[DependentCreator, FromDI(Dependencies.request_factory)], + event_type: typing.Annotated[str, FromDI(Dependencies.event_type)], + ) -> None: + seen["exception"] = str(event.exception) + seen["request_ok"] = isinstance(request_instance, DependentCreator) + seen["event_type"] = event_type + + await dispatcher.emit_startup() + await dispatcher.feed_update(bot, make_message_update()) + await dispatcher.emit_shutdown() + + assert seen == {"exception": "boom", "request_ok": True, "event_type": "Message"} + + async def test_inject_without_setup_di_names_the_fix(bot: Bot) -> None: dispatcher = Dispatcher()