From 2a03352e35e19413afc9747b2752a6f82daf167d Mon Sep 17 00:00:00 2001 From: Dylan Merigaud Date: Wed, 7 Oct 2026 01:57:06 +0200 Subject: [PATCH 1/3] Fix is_simple_callable with unresolvable annotations on Python 3.14 inspect.signature() evaluates annotations on Python 3.14 (PEP 649), so a method annotated with a name imported only under TYPE_CHECKING raised NameError when used as a serializer field source. Ask for FORWARDREF annotations instead, as only parameter kinds and defaults are needed. Refs #9959 --- rest_framework/fields.py | 12 +++++++++++- tests/test_fields.py | 10 ++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 9b922ed918..eca75cf960 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -6,6 +6,7 @@ import inspect import math import re +import sys import uuid import warnings from collections.abc import Mapping @@ -74,7 +75,16 @@ def is_simple_callable(obj): if not (inspect.isfunction(obj) or inspect.ismethod(obj) or isinstance(obj, functools.partial)): return False - sig = inspect.signature(obj) + signature_kwargs = {} + if sys.version_info >= (3, 14): + # Annotations are evaluated lazily since Python 3.14 (PEP 649), and + # inspect.signature() evaluates them, raising NameError for names only + # imported under TYPE_CHECKING. Only parameter kinds and defaults are + # needed here. See https://github.com/encode/django-rest-framework/discussions/9959 + import annotationlib + signature_kwargs['annotation_format'] = annotationlib.Format.FORWARDREF + + sig = inspect.signature(obj, **signature_kwargs) params = sig.parameters.values() return all( param.kind == param.VAR_POSITIONAL or diff --git a/tests/test_fields.py b/tests/test_fields.py index 4cbbc030e3..914b7cc094 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -2,6 +2,7 @@ import math import os import re +import sys import uuid import warnings from decimal import ROUND_DOWN, ROUND_UP, Decimal @@ -121,6 +122,15 @@ def test_type_annotation(self): assert is_simple_callable(valid) + @pytest.mark.skipif(sys.version_info < (3, 14), reason="Annotations are evaluated lazily from Python 3.14") + def test_unresolvable_type_annotation(self): + # A name only imported under `if TYPE_CHECKING:` is undefined at runtime. + class Foo: + def valid(self) -> UndefinedType: # noqa: F821 + pass + + assert is_simple_callable(Foo().valid) + # Tests for field keyword arguments and core functionality. # --------------------------------------------------------- From e370940ff46c4ed94ec51dae1c2b29b44ba07f43 Mon Sep 17 00:00:00 2001 From: Dylan Merigaud Date: Sat, 10 Oct 2026 11:43:43 +0200 Subject: [PATCH 2/3] Build is_simple_callable signature kwargs once at import The Python version cannot change while the process runs, so the check and the dict move to a module level SIGNATURE_KWARGS constant. --- rest_framework/fields.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index eca75cf960..6561b3d63d 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -59,6 +59,17 @@ class BuiltinSignatureError(Exception): pass +if sys.version_info >= (3, 14): + # Annotations are evaluated lazily since Python 3.14 (PEP 649), and + # inspect.signature() evaluates them, raising NameError for names only + # imported under TYPE_CHECKING. Only parameter kinds and defaults are + # needed in is_simple_callable(). See https://github.com/encode/django-rest-framework/discussions/9959 + import annotationlib + SIGNATURE_KWARGS = {'annotation_format': annotationlib.Format.FORWARDREF} +else: + SIGNATURE_KWARGS = {} + + def is_simple_callable(obj): """ True if the object is a callable that takes no arguments. @@ -75,16 +86,7 @@ def is_simple_callable(obj): if not (inspect.isfunction(obj) or inspect.ismethod(obj) or isinstance(obj, functools.partial)): return False - signature_kwargs = {} - if sys.version_info >= (3, 14): - # Annotations are evaluated lazily since Python 3.14 (PEP 649), and - # inspect.signature() evaluates them, raising NameError for names only - # imported under TYPE_CHECKING. Only parameter kinds and defaults are - # needed here. See https://github.com/encode/django-rest-framework/discussions/9959 - import annotationlib - signature_kwargs['annotation_format'] = annotationlib.Format.FORWARDREF - - sig = inspect.signature(obj, **signature_kwargs) + sig = inspect.signature(obj, **SIGNATURE_KWARGS) params = sig.parameters.values() return all( param.kind == param.VAR_POSITIONAL or From d82ec4749e4b57dd8257b0751cdb4c1f1ebf8b4e Mon Sep 17 00:00:00 2001 From: Dylan Merigaud Date: Sat, 10 Oct 2026 11:43:48 +0200 Subject: [PATCH 3/3] Test unresolvable parameter annotations in is_simple_callable Cover a parameter annotated with an undefined name, both with a default (simple callable) and without one (not a simple callable). --- tests/test_fields.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/test_fields.py b/tests/test_fields.py index 914b7cc094..f52dd8e514 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -129,7 +129,15 @@ class Foo: def valid(self) -> UndefinedType: # noqa: F821 pass + def valid(param: UndefinedType = None): # noqa: F821 + pass + + def invalid(param: UndefinedType): # noqa: F821 + pass + assert is_simple_callable(Foo().valid) + assert is_simple_callable(valid) + assert not is_simple_callable(invalid) # Tests for field keyword arguments and core functionality.