diff --git a/fastapi/dependencies/models.py b/fastapi/dependencies/models.py index 2a4d9a010..728af15b8 100644 --- a/fastapi/dependencies/models.py +++ b/fastapi/dependencies/models.py @@ -86,6 +86,12 @@ class Dependant: @cached_property def is_gen_callable(self) -> bool: + if self.call is not None: + if inspect.isgeneratorfunction(self.call): + return True + call_dunder = getattr(self.call, "__call__", None) # noqa: B004 + if inspect.isgeneratorfunction(call_dunder): + return True if inspect.isgeneratorfunction(self._unwrapped_call): return True dunder_call = getattr(self._unwrapped_call, "__call__", None) # noqa: B004 @@ -93,6 +99,12 @@ class Dependant: @cached_property def is_async_gen_callable(self) -> bool: + if self.call is not None: + if inspect.isasyncgenfunction(self.call): + return True + call_dunder = getattr(self.call, "__call__", None) # noqa: B004 + if inspect.isasyncgenfunction(call_dunder): + return True if inspect.isasyncgenfunction(self._unwrapped_call): return True dunder_call = getattr(self._unwrapped_call, "__call__", None) # noqa: B004 @@ -100,6 +112,14 @@ class Dependant: @cached_property def is_coroutine_callable(self) -> bool: + if self.call is not None: + if inspect.isroutine(self.call): + if iscoroutinefunction(self.call): + return True + else: + call_dunder = getattr(self.call, "__call__", None) # noqa: B004 + if iscoroutinefunction(call_dunder): + return True if inspect.isroutine(self._unwrapped_call): return iscoroutinefunction(self._unwrapped_call) if inspect.isclass(self._unwrapped_call): diff --git a/tests/test_async_wrapper_sync_function.py b/tests/test_async_wrapper_sync_function.py new file mode 100644 index 000000000..24dffcfe4 --- /dev/null +++ b/tests/test_async_wrapper_sync_function.py @@ -0,0 +1,65 @@ +"""Test for async wrapper around sync function. + +This tests the issue reported in #14442 where using functools.wraps on an async +wrapper around a sync function causes FastAPI to incorrectly treat the handler +as sync, resulting in: ValueError: [TypeError("'coroutine' object is not iterable"), ...] + +The issue is that Dependant.is_coroutine_callable uses inspect.unwrap() which follows +__wrapped__ back to the original sync function, instead of checking if the actual +registered handler is async. +""" + +from functools import wraps + +import pytest +from fastapi import Depends, FastAPI +from fastapi.testclient import TestClient + + +def async_wrap(func): + """A decorator that wraps a sync function with an async handler.""" + + @wraps(func) + async def wrapper(*args, **kwargs): + func(*args, **kwargs) + return "OK" + + return wrapper + + +app = FastAPI() + + +@app.get("/") +@async_wrap +def index(): + """A simple sync page function wrapped with async handler.""" + print("Hello!") + + +def sync_dependency(): + return "dep_value" + + +@app.get("/with-dependency") +@async_wrap +def with_dependency(value: str = Depends(sync_dependency)): + """A sync function with dependency, wrapped with async handler.""" + print(f"Got: {value}") + + +client = TestClient(app) + + +def test_async_wrapper_around_sync_function(): + """Test that async wrapper around sync function works correctly.""" + response = client.get("/") + assert response.status_code == 200, response.text + assert response.json() == "OK" + + +def test_async_wrapper_with_dependency(): + """Test that async wrapper with dependency works correctly.""" + response = client.get("/with-dependency") + assert response.status_code == 200, response.text + assert response.json() == "OK"