From 73c411e1b92a1659fef76655562e6d2f28be064e Mon Sep 17 00:00:00 2001 From: Matthew Martin Date: Tue, 2 Dec 2025 07:34:19 -0600 Subject: [PATCH 1/4] =?UTF-8?q?=E2=9C=A8=20Handle=20wrapped=20dependencies?= =?UTF-8?q?=20(#9555)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> Co-authored-by: Yurii Motov Co-authored-by: Sebastián Ramírez --- fastapi/dependencies/models.py | 22 +++++---- tests/test_dependency_wrapped.py | 77 ++++++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+), 8 deletions(-) create mode 100644 tests/test_dependency_wrapped.py 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/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 From 13a98c99889cdeec97632bd287127f3bbcd94a9f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 2 Dec 2025 13:34:45 +0000 Subject: [PATCH 2/4] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 74c7eb9ea..5319af0eb 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Features + +* ✨ Handle wrapped dependencies. PR [#9555](https://github.com/fastapi/fastapi/pull/9555) by [@phy1729](https://github.com/phy1729). + ## 0.123.4 ### Fixes From 247ef32e790ef296d8febc3fbc639849ff24b1a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 2 Dec 2025 05:43:31 -0800 Subject: [PATCH 3/4] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor=20internals,?= =?UTF-8?q?=20update=20`is=5Fcoroutine`=20check=20to=20reuse=20internal=20?= =?UTF-8?q?supported=20variants=20(unwrap,=20check=20class)=20(#14434)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastapi/routing.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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) ) From f636513390096512dbfc49ce8edf93e9d17eac78 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 2 Dec 2025 13:43:52 +0000 Subject: [PATCH 4/4] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 5319af0eb..f540ac78e 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -11,6 +11,10 @@ hide: * ✨ 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