diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index 40dffba64b..6f6ff08d27 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -246,11 +246,32 @@ def get_typed_annotation(annotation: Any, globalns: dict[str, Any]) -> Any: if isinstance(annotation, str): annotation = ForwardRef(annotation) annotation = evaluate_forwardref(annotation, globalns, globalns) - if annotation is type(None): + if isinstance(annotation, ForwardRef): + if "Annotated[" in annotation.__forward_arg__: + annotation = _resolve_forward_ref_lenient(annotation, globalns) + elif annotation is type(None): return None return annotation +def _resolve_forward_ref_lenient( + fwd_ref: ForwardRef, + globalns: dict[str, Any], +) -> Any: + class _LenientNamespace(dict[str, Any]): + def __missing__(self, key: str) -> Any: + return Any + + localns = _LenientNamespace(globalns) + if sys.version_info >= (3, 13): + result = fwd_ref._evaluate( + globalns, localns, recursive_guard=frozenset(), type_params=() + ) + else: + result = fwd_ref._evaluate(globalns, localns, recursive_guard=frozenset()) + return result + + def get_typed_return_annotation(call: Callable[..., Any]) -> Any: signature = _get_signature(call) unwrapped = inspect.unwrap(call) diff --git a/pyproject.toml b/pyproject.toml index daa523ce28..f51dff8a84 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -229,6 +229,7 @@ addopts = [ strict_xfail = true filterwarnings = [ "error", + 'ignore:The private ForwardRef._evaluate method is deprecated:DeprecationWarning', ] timeout = "20" diff --git a/tests/test_annotated_forward_ref.py b/tests/test_annotated_forward_ref.py new file mode 100644 index 0000000000..ceb7636aa7 --- /dev/null +++ b/tests/test_annotated_forward_ref.py @@ -0,0 +1,66 @@ +from __future__ import annotations + +from typing import Annotated + +from fastapi import Depends, FastAPI +from inline_snapshot import snapshot + +# Simulate the real-world bug: Potato is defined AFTER the route decorator +# under `from __future__ import annotations`. + +app = FastAPI() + + +def get_potato(): + return Potato(color="red", size=10) + + +@app.get("/") +async def read_root(potato: Annotated[Potato, Depends(get_potato)]): + return {"color": potato.color, "size": potato.size} + + +from dataclasses import dataclass # noqa: E402 + + +@dataclass +class Potato: + color: str + size: int + + +def test_forward_ref_annotated_depends(): + from fastapi.testclient import TestClient + + client = TestClient(app) + resp = client.get("/") + assert resp.status_code == 200, resp.text + assert resp.json() == {"color": "red", "size": 10} + + +def test_forward_ref_annotated_depends_openapi(): + from fastapi.testclient import TestClient + + client = TestClient(app) + resp = client.get("/openapi.json") + assert resp.status_code == 200, resp.text + assert resp.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/": { + "get": { + "summary": "Read Root", + "operationId": "read_root__get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + } + }, + } + )