Fix getting callback params on py3.14+ (#1741)

* Add test to reproduce `TypeError: unsupported callable` on `python >=3.14`

* Fix getting callback params on py3.14+

Add 1741.bugfix.rst

* Code optimization
This commit is contained in:
Andrew 2026-01-01 23:42:40 +02:00 committed by GitHub
parent 79ee135331
commit b27ca9a45d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 63 additions and 5 deletions

3
CHANGES/1741.bugfix.rst Normal file
View file

@ -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.

View file

@ -1,5 +1,6 @@
import asyncio import asyncio
import inspect import inspect
import sys
import warnings import warnings
from collections.abc import Callable from collections.abc import Callable
from dataclasses import dataclass, field from dataclasses import dataclass, field
@ -16,6 +17,12 @@ from aiogram.utils.warnings import Recommendation
CallbackType = Callable[..., Any] CallbackType = Callable[..., Any]
_ACCEPTED_PARAM_KINDS = {
inspect.Parameter.POSITIONAL_ONLY,
inspect.Parameter.POSITIONAL_OR_KEYWORD,
inspect.Parameter.KEYWORD_ONLY,
}
@dataclass @dataclass
class CallableObject: class CallableObject:
@ -27,9 +34,30 @@ class CallableObject:
def __post_init__(self) -> None: def __post_init__(self) -> None:
callback = inspect.unwrap(self.callback) callback = inspect.unwrap(self.callback)
self.awaitable = inspect.isawaitable(callback) or inspect.iscoroutinefunction(callback) self.awaitable = inspect.isawaitable(callback) or inspect.iscoroutinefunction(callback)
spec = inspect.getfullargspec(callback)
self.params = {*spec.args, *spec.kwonlyargs} kwargs: dict[str, Any] = {}
self.varkw = spec.varkw is not None 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]: def _prepare_kwargs(self, kwargs: dict[str, Any]) -> dict[str, Any]:
if self.varkw: if self.varkw:

View file

@ -57,8 +57,8 @@ class TestCallableObject:
pytest.param(callback1, {"foo", "bar", "baz"}), pytest.param(callback1, {"foo", "bar", "baz"}),
pytest.param(callback2, {"foo", "bar", "baz"}), pytest.param(callback2, {"foo", "bar", "baz"}),
pytest.param(callback3, {"foo"}), pytest.param(callback3, {"foo"}),
pytest.param(TestFilter(), {"self", "foo", "bar", "baz"}), pytest.param(TestFilter(), {"foo", "bar", "baz"}),
pytest.param(SyncCallable(), {"self", "foo", "bar", "baz"}), pytest.param(SyncCallable(), {"foo", "bar", "baz"}),
], ],
) )
def test_init_args_spec(self, callback: Callable, args: Set[str]): def test_init_args_spec(self, callback: Callable, args: Set[str]):

View file

@ -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