From 09f5941f0e18db2b28b40d35a5da7a94c23eb9ed Mon Sep 17 00:00:00 2001 From: =?utf8?q?Micka=C3=ABl=20Gu=C3=A9rin?= Date: Wed, 4 Feb 2026 14:49:44 +0100 Subject: [PATCH] =?utf8?q?=F0=9F=90=9B=20Fix=20TYPE=5FCHECKING=20annotatio?= =?utf8?q?ns=20for=20Python=203.14=20(PEP=20649)=20(#14789)?= MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit --- fastapi/dependencies/utils.py | 7 ++++- ...stringified_annotation_dependency_py314.py | 30 +++++++++++++++++++ .../test_dependencies/test_tutorial008.py | 10 +++++-- tests/utils.py | 4 +-- 4 files changed, 46 insertions(+), 5 deletions(-) create mode 100644 tests/test_stringified_annotation_dependency_py314.py diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index b647818c4b..fc5dfed85a 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -204,7 +204,12 @@ def _get_signature(call: Callable[..., Any]) -> inspect.Signature: except NameError: # Handle type annotations with if TYPE_CHECKING, not used by FastAPI # e.g. dependency return types - signature = inspect.signature(call) + if sys.version_info >= (3, 14): + from annotationlib import Format + + signature = inspect.signature(call, annotation_format=Format.FORWARDREF) + else: + signature = inspect.signature(call) else: signature = inspect.signature(call) return signature diff --git a/tests/test_stringified_annotation_dependency_py314.py b/tests/test_stringified_annotation_dependency_py314.py new file mode 100644 index 0000000000..da9b429fc0 --- /dev/null +++ b/tests/test_stringified_annotation_dependency_py314.py @@ -0,0 +1,30 @@ +from typing import TYPE_CHECKING, Annotated + +from fastapi import Depends, FastAPI +from fastapi.testclient import TestClient + +from .utils import needs_py314 + +if TYPE_CHECKING: # pragma: no cover + + class DummyUser: ... + + +@needs_py314 +def test_stringified_annotation(): + # python3.14: Use forward reference without "from __future__ import annotations" + async def get_current_user() -> DummyUser | None: + return None + + app = FastAPI() + + client = TestClient(app) + + @app.get("/") + async def get( + current_user: Annotated[DummyUser | None, Depends(get_current_user)], + ) -> str: + return "hello world" + + response = client.get("/") + assert response.status_code == 200 diff --git a/tests/test_tutorial/test_dependencies/test_tutorial008.py b/tests/test_tutorial/test_dependencies/test_tutorial008.py index 9d7377ebe4..5a2d226bff 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial008.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial008.py @@ -1,4 +1,5 @@ import importlib +import sys from types import ModuleType from typing import Annotated, Any from unittest.mock import Mock, patch @@ -12,8 +13,13 @@ from fastapi.testclient import TestClient name="module", params=[ "tutorial008_py39", - # Fails with `NameError: name 'DepA' is not defined` - pytest.param("tutorial008_an_py39", marks=pytest.mark.xfail), + pytest.param( + "tutorial008_an_py39", + marks=pytest.mark.xfail( + sys.version_info < (3, 14), + reason="Fails with `NameError: name 'DepA' is not defined`", + ), + ), ], ) def get_module(request: pytest.FixtureRequest): diff --git a/tests/utils.py b/tests/utils.py index efa0bfd52b..4cbfee79f5 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -6,8 +6,8 @@ needs_py39 = pytest.mark.skipif(sys.version_info < (3, 9), reason="requires pyth needs_py310 = pytest.mark.skipif( sys.version_info < (3, 10), reason="requires python3.10+" ) -needs_py_lt_314 = pytest.mark.skipif( - sys.version_info >= (3, 14), reason="requires python3.13-" +needs_py314 = pytest.mark.skipif( + sys.version_info < (3, 14), reason="requires python3.14+" ) -- 2.47.3