From c2c3ea7e14f31de801170463499e1f06a21e9e0f Mon Sep 17 00:00:00 2001 From: Rigel Bezerra de Melo Date: Mon, 9 Feb 2026 18:52:42 +0000 Subject: [PATCH] prediction gpt 14448 --- fastapi/dependencies/models.py | 87 +++++++++++++++++++++++++++------- 1 file changed, 70 insertions(+), 17 deletions(-) diff --git a/fastapi/dependencies/models.py b/fastapi/dependencies/models.py index 2a4d9a01027be..d753c3f46888d 100644 --- a/fastapi/dependencies/models.py +++ b/fastapi/dependencies/models.py @@ -15,6 +15,35 @@ from asyncio import iscoroutinefunction +def _unwrap_partial(obj: Any) -> Any: + while isinstance(obj, partial): + obj = obj.func + return obj + + +def _iter_wrapped_callables(obj: Any): + """ + Yield the callable and any objects in its ``__wrapped__`` chain. + + This intentionally does *not* use ``inspect.unwrap()``, because that would only + return the innermost wrapped object, and FastAPI needs to understand whether + *any* layer is async/generator based (e.g. an async wrapper created with + ``functools.wraps`` around a sync function). + """ + seen: set[int] = set() + while True: + obj = _unwrap_partial(obj) + obj_id = id(obj) + if obj_id in seen: + return + seen.add(obj_id) + yield obj + wrapped = getattr(obj, "__wrapped__", None) + if wrapped is None: + return + obj = wrapped + + @dataclass class SecurityRequirement: security_scheme: SecurityBase @@ -79,33 +108,57 @@ def _uses_scopes(self) -> bool: def _unwrapped_call(self) -> Any: if self.call is None: return self.call # pragma: no cover - unwrapped = inspect.unwrap(self.call) - if isinstance(unwrapped, partial): - unwrapped = unwrapped.func - return unwrapped + # NOTE: We intentionally do not unwrap the ``__wrapped__`` chain (as created + # by ``functools.wraps``). Execution mode (sync vs async) must be detected + # based on the actual callable that will run (the outer wrapper), and in + # some cases also based on any wrapped functions (e.g. a sync wrapper + # returning a coroutine from an async wrapped function). + return _unwrap_partial(self.call) @cached_property def is_gen_callable(self) -> bool: - if inspect.isgeneratorfunction(self._unwrapped_call): - return True - dunder_call = getattr(self._unwrapped_call, "__call__", None) # noqa: B004 - return inspect.isgeneratorfunction(dunder_call) + # If there's any coroutine layer, it must be treated as async instead of a + # generator dependency. + if self.is_coroutine_callable: + return False + call = self._unwrapped_call + if inspect.isroutine(call): + return any(inspect.isgeneratorfunction(c) for c in _iter_wrapped_callables(call)) + dunder_call = getattr(call, "__call__", None) # noqa: B004 + if dunder_call is None: + return False + dunder_call = _unwrap_partial(dunder_call) + return any(inspect.isgeneratorfunction(c) for c in _iter_wrapped_callables(dunder_call)) @cached_property def is_async_gen_callable(self) -> bool: - if inspect.isasyncgenfunction(self._unwrapped_call): - return True - dunder_call = getattr(self._unwrapped_call, "__call__", None) # noqa: B004 - return inspect.isasyncgenfunction(dunder_call) + # If there's any coroutine layer, it must be treated as async instead of an + # async-generator dependency. + if self.is_coroutine_callable: + return False + call = self._unwrapped_call + if inspect.isroutine(call): + return any(inspect.isasyncgenfunction(c) for c in _iter_wrapped_callables(call)) + dunder_call = getattr(call, "__call__", None) # noqa: B004 + if dunder_call is None: + return False + dunder_call = _unwrap_partial(dunder_call) + return any(inspect.isasyncgenfunction(c) for c in _iter_wrapped_callables(dunder_call)) @cached_property def is_coroutine_callable(self) -> bool: - if inspect.isroutine(self._unwrapped_call): - return iscoroutinefunction(self._unwrapped_call) - if inspect.isclass(self._unwrapped_call): + # If either the original callable OR any wrapper in a ``__wrapped__`` chain + # is async, we should treat this callable as async. + call = self._unwrapped_call + if inspect.isroutine(call): + return any(iscoroutinefunction(c) for c in _iter_wrapped_callables(call)) + if inspect.isclass(call): + return False + dunder_call = getattr(call, "__call__", None) # noqa: B004 + if dunder_call is None: return False - dunder_call = getattr(self._unwrapped_call, "__call__", None) # noqa: B004 - return iscoroutinefunction(dunder_call) + dunder_call = _unwrap_partial(dunder_call) + return any(iscoroutinefunction(c) for c in _iter_wrapped_callables(dunder_call)) @cached_property def computed_scope(self) -> Union[str, None]: