From 63998b27edb096794f5d6abc412bd27100c70e77 Mon Sep 17 00:00:00 2001 From: armorbreak001 Date: Wed, 22 Apr 2026 22:59:51 +0800 Subject: [PATCH] fix(deps): resolve Annotated forward refs with __future__ annotations When from __future__ import annotations is active, an annotation like Annotated[Potato, Depends(get_potato)] stays as a ForwardRef because the lenient evaluator cannot resolve *Potato* (defined later in the file). This meant the Depends() marker was never discovered and the parameter was incorrectly treated as a body field instead of a dependency. Use eval with a safe-globals dict that turns undefined names into ForwardRef objects, allowing the Annotated wrapper to be unpacked so that FastAPI-specific markers (Depends / Field / Body) are found. Fixes #13056 --- fastapi/dependencies/utils.py | 35 ++++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index 6b14dac8dc..94f76ece7a 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -302,6 +302,8 @@ def get_dependant( own_oauth_scopes=own_oauth_scopes, parent_oauth_scopes=parent_oauth_scopes, ) + unwrapped_call = inspect.unwrap(call) + globalns = getattr(unwrapped_call, "__globals__", {}) current_scopes = (parent_oauth_scopes or []) + (own_oauth_scopes or []) path_param_names = get_path_param_names(path) endpoint_signature = get_typed_signature(call) @@ -313,6 +315,7 @@ def get_dependant( annotation=param.annotation, value=param.default, is_path_param=is_path_param, + globalns=globalns, ) if param_details.depends is not None: assert param_details.depends.dependency @@ -396,6 +399,7 @@ def analyze_param( annotation: Any, value: Any, is_path_param: bool, + globalns: dict[str, Any] | None = None, ) -> ParamDetails: field_info = None depends = None @@ -407,10 +411,39 @@ def analyze_param( if annotation is not inspect.Signature.empty: use_annotation = annotation type_annotation = annotation + # When ``from __future__ import annotations`` is active, the annotation + # may still be a ForwardRef (e.g. ``Annotated[Potato, Depends(...)]`` + # where *Potato* hasn't been defined yet). The lenient evaluator leaves + # it as a ForwardRef because it cannot resolve *Potato*, but we still + # need to unpack the Annotated wrapper so that FastAPI-specific markers + # (Depends / Field / Body) are discovered. We use ``eval`` with a + # dict that turns undefined names into ForwardRef objects, which gives + # us the same partial-evaluation behaviour that the normal code-path + # expects. + if isinstance(use_annotation, ForwardRef): + if globalns is not None: + try: + _safe_ns: dict[str, Any] = dict(globalns) + _missing = ForwardRef # noqa: F811 + class _SafeDict(dict): + def __missing__(self, key): # type: ignore[override] + return _missing(key) + use_annotation = eval( + use_annotation.__forward_arg__, {"__builtins__": {}}, _SafeDict(_safe_ns) + ) + if use_annotation is not inspect.Signature.empty: + type_annotation = use_annotation + except Exception: + pass # Extract Annotated info if get_origin(use_annotation) is Annotated: - annotated_args = get_args(annotation) + annotated_args = get_args(use_annotation) type_annotation = annotated_args[0] + # Resolve forward references in the inner type (e.g. when using + # ``from __future__ import annotations`` with Annotated[..., Depends()]) + if isinstance(type_annotation, (str, ForwardRef)): + if globalns is not None: + type_annotation = get_typed_annotation(type_annotation, globalns) fastapi_annotations = [ arg for arg in annotated_args[1:]