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

View file

@ -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]):

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