diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 74c7eb9ea..f540ac78e 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,14 @@ hide: ## Latest Changes +### Features + +* ✨ Handle wrapped dependencies. PR [#9555](https://github.com/fastapi/fastapi/pull/9555) by [@phy1729](https://github.com/phy1729). + +### Refactors + +* ♻️ Refactor internals, update `is_coroutine` check to reuse internal supported variants (unwrap, check class). PR [#14434](https://github.com/fastapi/fastapi/pull/14434) by [@tiangolo](https://github.com/tiangolo). + ## 0.123.4 ### Fixes diff --git a/fastapi/dependencies/models.py b/fastapi/dependencies/models.py index fbb666a7d..13486dd18 100644 --- a/fastapi/dependencies/models.py +++ b/fastapi/dependencies/models.py @@ -75,27 +75,33 @@ class Dependant: return True return False + @cached_property + def _unwrapped_call(self) -> Any: + if self.call is None: + return self.call # pragma: no cover + return inspect.unwrap(self.call) + @cached_property def is_gen_callable(self) -> bool: - if inspect.isgeneratorfunction(self.call): + if inspect.isgeneratorfunction(self._unwrapped_call): return True - dunder_call = getattr(self.call, "__call__", None) # noqa: B004 + dunder_call = getattr(self._unwrapped_call, "__call__", None) # noqa: B004 return inspect.isgeneratorfunction(dunder_call) @cached_property def is_async_gen_callable(self) -> bool: - if inspect.isasyncgenfunction(self.call): + if inspect.isasyncgenfunction(self._unwrapped_call): return True - dunder_call = getattr(self.call, "__call__", None) # noqa: B004 + dunder_call = getattr(self._unwrapped_call, "__call__", None) # noqa: B004 return inspect.isasyncgenfunction(dunder_call) @cached_property def is_coroutine_callable(self) -> bool: - if inspect.isroutine(self.call): - return iscoroutinefunction(self.call) - if inspect.isclass(self.call): + if inspect.isroutine(self._unwrapped_call): + return iscoroutinefunction(self._unwrapped_call) + if inspect.isclass(self._unwrapped_call): return False - dunder_call = getattr(self.call, "__call__", None) # noqa: B004 + dunder_call = getattr(self._unwrapped_call, "__call__", None) # noqa: B004 return iscoroutinefunction(dunder_call) @cached_property diff --git a/fastapi/routing.py b/fastapi/routing.py index a8e12eb60..94e8b0722 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -80,9 +80,9 @@ from starlette.websockets import WebSocket from typing_extensions import Annotated, deprecated if sys.version_info >= (3, 13): # pragma: no cover - from inspect import iscoroutinefunction + pass else: # pragma: no cover - from asyncio import iscoroutinefunction + pass # Copy of starlette.routing.request_response modified to include the @@ -308,7 +308,7 @@ def get_request_handler( embed_body_fields: bool = False, ) -> Callable[[Request], Coroutine[Any, Any, Response]]: assert dependant.call is not None, "dependant.call must be a function" - is_coroutine = iscoroutinefunction(dependant.call) + is_coroutine = dependant.is_coroutine_callable is_body_form = body_field and isinstance( body_field.field_info, (params.Form, temp_pydantic_v1_params.Form) ) diff --git a/tests/test_dependency_wrapped.py b/tests/test_dependency_wrapped.py new file mode 100644 index 000000000..f581ccba4 --- /dev/null +++ b/tests/test_dependency_wrapped.py @@ -0,0 +1,77 @@ +from functools import wraps +from typing import AsyncGenerator, Generator + +import pytest +from fastapi import Depends, FastAPI +from fastapi.testclient import TestClient + + +def noop_wrap(func): + @wraps(func) + def wrapper(*args, **kwargs): + return func(*args, **kwargs) + + return wrapper + + +app = FastAPI() + + +@noop_wrap +def wrapped_dependency() -> bool: + return True + + +@noop_wrap +def wrapped_gen_dependency() -> Generator[bool, None, None]: + yield True + + +@noop_wrap +async def async_wrapped_dependency() -> bool: + return True + + +@noop_wrap +async def async_wrapped_gen_dependency() -> AsyncGenerator[bool, None]: + yield True + + +@app.get("/wrapped-dependency/") +async def get_wrapped_dependency(value: bool = Depends(wrapped_dependency)): + return value + + +@app.get("/wrapped-gen-dependency/") +async def get_wrapped_gen_dependency(value: bool = Depends(wrapped_gen_dependency)): + return value + + +@app.get("/async-wrapped-dependency/") +async def get_async_wrapped_dependency(value: bool = Depends(async_wrapped_dependency)): + return value + + +@app.get("/async-wrapped-gen-dependency/") +async def get_async_wrapped_gen_dependency( + value: bool = Depends(async_wrapped_gen_dependency), +): + return value + + +client = TestClient(app) + + +@pytest.mark.parametrize( + "route", + [ + "/wrapped-dependency", + "/wrapped-gen-dependency", + "/async-wrapped-dependency", + "/async-wrapped-gen-dependency", + ], +) +def test_class_dependency(route): + response = client.get(route) + assert response.status_code == 200, response.text + assert response.json() is True