diff --git a/CHANGES/1741.bugfix.rst b/CHANGES/1741.bugfix.rst new file mode 100644 index 00000000..5ca46ee5 --- /dev/null +++ b/CHANGES/1741.bugfix.rst @@ -0,0 +1,3 @@ +`inspect.getfullargspec(callback)` can't process callback if it's arguments have "ForwardRef" annotations in Py3.14+ + +This PR replaces the old way with `inspect.signature(callback)` and add `annotation_format = annotationlib.Format.FORWARDREF` argument to it if runtime python version >=3.14. diff --git a/aiogram/dispatcher/event/handler.py b/aiogram/dispatcher/event/handler.py index 7aeaab96..204a548d 100644 --- a/aiogram/dispatcher/event/handler.py +++ b/aiogram/dispatcher/event/handler.py @@ -1,5 +1,6 @@ import asyncio import inspect +import sys import warnings from collections.abc import Callable from dataclasses import dataclass, field @@ -16,6 +17,12 @@ from aiogram.utils.warnings import Recommendation CallbackType = Callable[..., Any] +_ACCEPTED_PARAM_KINDS = { + inspect.Parameter.POSITIONAL_ONLY, + inspect.Parameter.POSITIONAL_OR_KEYWORD, + inspect.Parameter.KEYWORD_ONLY, +} + @dataclass class CallableObject: @@ -27,9 +34,30 @@ class CallableObject: def __post_init__(self) -> None: callback = inspect.unwrap(self.callback) self.awaitable = inspect.isawaitable(callback) or inspect.iscoroutinefunction(callback) - spec = inspect.getfullargspec(callback) - self.params = {*spec.args, *spec.kwonlyargs} - self.varkw = spec.varkw is not None + + kwargs: dict[str, Any] = {} + if sys.version_info >= (3, 14): + import annotationlib + + kwargs["annotation_format"] = annotationlib.Format.FORWARDREF + + try: + signature = inspect.signature(callback, **kwargs) + except (ValueError, TypeError): # pragma: no cover + self.params = set() + self.varkw = False + return + + params: set[str] = set() + varkw: bool = False + + for p in signature.parameters.values(): + if p.kind in _ACCEPTED_PARAM_KINDS: + params.add(p.name) + elif p.kind == inspect.Parameter.VAR_KEYWORD: + varkw = True + self.params = params + self.varkw = varkw def _prepare_kwargs(self, kwargs: dict[str, Any]) -> dict[str, Any]: if self.varkw: diff --git a/tests/test_dispatcher/test_event/test_handler.py b/tests/test_dispatcher/test_event/test_handler.py index 1f8be4af..7105a058 100644 --- a/tests/test_dispatcher/test_event/test_handler.py +++ b/tests/test_dispatcher/test_event/test_handler.py @@ -57,8 +57,8 @@ class TestCallableObject: pytest.param(callback1, {"foo", "bar", "baz"}), pytest.param(callback2, {"foo", "bar", "baz"}), pytest.param(callback3, {"foo"}), - pytest.param(TestFilter(), {"self", "foo", "bar", "baz"}), - pytest.param(SyncCallable(), {"self", "foo", "bar", "baz"}), + pytest.param(TestFilter(), {"foo", "bar", "baz"}), + pytest.param(SyncCallable(), {"foo", "bar", "baz"}), ], ) def test_init_args_spec(self, callback: Callable, args: Set[str]): diff --git a/tests/test_issues/test_1741_forward_ref_in_callbacks.py b/tests/test_issues/test_1741_forward_ref_in_callbacks.py new file mode 100644 index 00000000..cf3fdde8 --- /dev/null +++ b/tests/test_issues/test_1741_forward_ref_in_callbacks.py @@ -0,0 +1,27 @@ +from sys import version_info +from typing import TYPE_CHECKING + +import pytest + +from aiogram.dispatcher.event.handler import HandlerObject + + +@pytest.mark.skipif( + version_info < (3, 14), reason="Requires Python >=3.14 for TypeError on unresolved ForwardRef" +) +def test_forward_ref_in_callback(): + if TYPE_CHECKING: + from aiogram.types import Message + + def my_handler(message: Message): + pass + + HandlerObject(callback=my_handler) + + +def test_forward_ref_in_callback_with_str_annotation(): + def my_handler(message: "Message"): + pass + + handler = HandlerObject(callback=my_handler) + assert "message" in handler.params