From f1b0595fa6f785b08274a7753fd0367628d9a90e Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Sat, 17 Oct 2026 13:05:51 +0300 Subject: [PATCH] fix: accept handlers with **kwargs in inject --- modern_di_aiogram/main.py | 6 +++- tests/test_inject.py | 73 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+), 1 deletion(-) diff --git a/modern_di_aiogram/main.py b/modern_di_aiogram/main.py index 929f9a9..0dc8145 100644 --- a/modern_di_aiogram/main.py +++ b/modern_di_aiogram/main.py @@ -81,7 +81,9 @@ def inject(func: typing.Callable[..., T]) -> typing.Callable[..., T]: visible_params = [p for name, p in original_signature.parameters.items() if name not in di_params] container_param_injected = _CHILD_CONTAINER_KEY not in original_signature.parameters if container_param_injected: - visible_params.append( + has_var_keyword = bool(visible_params) and visible_params[-1].kind is inspect.Parameter.VAR_KEYWORD + visible_params.insert( + len(visible_params) - has_var_keyword, inspect.Parameter(_CHILD_CONTAINER_KEY, kind=inspect.Parameter.KEYWORD_ONLY, annotation=Container), ) @@ -89,6 +91,8 @@ def resolve(kwargs: dict[str, typing.Any]) -> dict[str, typing.Any]: container = _fetch_child_container(kwargs) if container_param_injected: del kwargs[_CHILD_CONTAINER_KEY] + for name in di_params: + kwargs.pop(name, None) return integrations.resolve_markers(container, di_params) wrapper: typing.Callable[..., typing.Any] diff --git a/tests/test_inject.py b/tests/test_inject.py index 8548c5d..e53588a 100644 --- a/tests/test_inject.py +++ b/tests/test_inject.py @@ -164,3 +164,76 @@ async def async_handler(app: typing.Annotated[SimpleCreator, FromDI(SimpleCreato assert not inspect.iscoroutinefunction(inject(sync_handler)) assert inspect.iscoroutinefunction(inject(async_handler)) + + +async def test_inject_handler_with_var_keyword_gets_dependency_and_aiogram_data( + dispatcher: Dispatcher, + bot: Bot, +) -> None: + seen: dict[str, typing.Any] = {} + + @dispatcher.message() + @inject + async def handler( + message: Message, # noqa: ARG001 + app_instance: typing.Annotated[SimpleCreator, FromDI(SimpleCreator)], + **kwargs: typing.Any, # noqa: ANN401 + ) -> None: + seen["app"] = app_instance.dep1 + seen["bot"] = kwargs.get("bot") is bot + seen["container_leaked"] = "modern_di_container" in kwargs + + await dispatcher.emit_startup() + await dispatcher.feed_update(bot, make_message_update()) + await dispatcher.emit_shutdown() + + assert seen == {"app": "original", "bot": True, "container_leaked": False} + + +def test_inject_keeps_var_keyword_last_in_wrapper_signature() -> None: + async def with_kwargs( + message: Message, + app: typing.Annotated[SimpleCreator, FromDI(SimpleCreator)], + **kwargs: typing.Any, # noqa: ANN401 + ) -> None: ... + + async def without_kwargs( + message: Message, + app: typing.Annotated[SimpleCreator, FromDI(SimpleCreator)], + ) -> None: ... + + def kinds(func: typing.Callable[..., typing.Any]) -> list[tuple[str, typing.Any]]: + return [(p.name, p.kind) for p in inspect.signature(inject(func)).parameters.values()] + + assert kinds(with_kwargs) == [ + ("message", inspect.Parameter.POSITIONAL_OR_KEYWORD), + ("modern_di_container", inspect.Parameter.KEYWORD_ONLY), + ("kwargs", inspect.Parameter.VAR_KEYWORD), + ] + assert kinds(without_kwargs) == [ + ("message", inspect.Parameter.POSITIONAL_OR_KEYWORD), + ("modern_di_container", inspect.Parameter.KEYWORD_ONLY), + ] + + +async def test_inject_fromdi_param_named_like_aiogram_data_wins_over_var_keyword( + dispatcher: Dispatcher, + bot: Bot, +) -> None: + seen: dict[str, typing.Any] = {} + + @dispatcher.message() + @inject + async def handler( + message: Message, # noqa: ARG001 + bot: typing.Annotated[SimpleCreator, FromDI(SimpleCreator)], + **kwargs: typing.Any, # noqa: ANN401 + ) -> None: + seen["bot"] = bot.dep1 + seen["has_event_chat"] = "event_chat" in kwargs + + await dispatcher.emit_startup() + await dispatcher.feed_update(bot, make_message_update()) + await dispatcher.emit_shutdown() + + assert seen == {"bot": "original", "has_event_chat": True}