From 1c1e584abd5cb98920bf22966f7a8b79d3ec64be Mon Sep 17 00:00:00 2001 From: "[object Object]" Date: Tue, 2 Dec 2025 09:23:14 -0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Add=20support=20for=20wrapped=20fun?= =?UTF-8?q?ctions=20(e.g.=20`@functools.wraps()`)=20used=20with=20forward?= =?UTF-8?q?=20references=20(#5077)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Yurii Karabas <1998uriyyo@gmail.com> Co-authored-by: Sebastián Ramírez --- fastapi/dependencies/utils.py | 6 ++-- tests/forward_reference_type.py | 9 ++++++ .../test_wrapped_method_forward_reference.py | 31 +++++++++++++++++++ 3 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 tests/forward_reference_type.py create mode 100644 tests/test_wrapped_method_forward_reference.py diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index ef3f56417..1a493a9fd 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -192,7 +192,8 @@ def get_flat_params(dependant: Dependant) -> List[ModelField]: def get_typed_signature(call: Callable[..., Any]) -> inspect.Signature: signature = inspect.signature(call) - globalns = getattr(call, "__globals__", {}) + unwrapped = inspect.unwrap(call) + globalns = getattr(unwrapped, "__globals__", {}) typed_params = [ inspect.Parameter( name=param.name, @@ -217,12 +218,13 @@ def get_typed_annotation(annotation: Any, globalns: Dict[str, Any]) -> Any: def get_typed_return_annotation(call: Callable[..., Any]) -> Any: signature = inspect.signature(call) + unwrapped = inspect.unwrap(call) annotation = signature.return_annotation if annotation is inspect.Signature.empty: return None - globalns = getattr(call, "__globals__", {}) + globalns = getattr(unwrapped, "__globals__", {}) return get_typed_annotation(annotation, globalns) diff --git a/tests/forward_reference_type.py b/tests/forward_reference_type.py new file mode 100644 index 000000000..52a0d4a70 --- /dev/null +++ b/tests/forward_reference_type.py @@ -0,0 +1,9 @@ +from pydantic import BaseModel + + +def forwardref_method(input: "ForwardRefModel") -> "ForwardRefModel": + return ForwardRefModel(x=input.x + 1) + + +class ForwardRefModel(BaseModel): + x: int = 0 diff --git a/tests/test_wrapped_method_forward_reference.py b/tests/test_wrapped_method_forward_reference.py new file mode 100644 index 000000000..7403f6002 --- /dev/null +++ b/tests/test_wrapped_method_forward_reference.py @@ -0,0 +1,31 @@ +import functools + +from fastapi import FastAPI +from fastapi.testclient import TestClient + +from .forward_reference_type import forwardref_method + + +def passthrough(f): + @functools.wraps(f) + def method(*args, **kwargs): + return f(*args, **kwargs) + + return method + + +def test_wrapped_method_type_inference(): + """ + Regression test ensuring that when a method imported from another module + is decorated with something that sets the __wrapped__ attribute (functools.wraps), + then the types are still processed correctly, including dereferencing of forward + references. + """ + app = FastAPI() + client = TestClient(app) + app.post("/endpoint")(passthrough(forwardref_method)) + app.post("/endpoint2")(passthrough(passthrough(forwardref_method))) + with client: + response = client.post("/endpoint", json={"input": {"x": 0}}) + response2 = client.post("/endpoint2", json={"input": {"x": 0}}) + assert response.json() == response2.json() == {"x": 1}