From 972e75a4fa19b284aa927cba0029fc736e7e54c0 Mon Sep 17 00:00:00 2001 From: Lucas Wiman Date: Fri, 24 Jun 2022 10:16:08 -0700 Subject: [PATCH 1/6] Add failing test for #5065. --- tests/forward_reference_type.py | 9 +++++++ .../test_wrapped_method_forward_reference.py | 24 +++++++++++++++++++ 2 files changed, 33 insertions(+) create mode 100644 tests/forward_reference_type.py create mode 100644 tests/test_wrapped_method_forward_reference.py diff --git a/tests/forward_reference_type.py b/tests/forward_reference_type.py new file mode 100644 index 000000000..02be56bec --- /dev/null +++ b/tests/forward_reference_type.py @@ -0,0 +1,9 @@ +from pydantic import BaseModel + + +def forwardref_method(input: "ForwardRef") -> "ForwardRef": + return ForwardRef() + + +class ForwardRef(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..05681604d --- /dev/null +++ b/tests/test_wrapped_method_forward_reference.py @@ -0,0 +1,24 @@ +import functools + +from fastapi import FastAPI + +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, then + the types are still processed correctly, including dereferencing of forward + references. + """ + app = FastAPI() + app.get("/endpoint")(passthrough(forwardref_method)) From 6341bc38a202c3220bc6d3d7df67da33c6a02114 Mon Sep 17 00:00:00 2001 From: Lucas Wiman Date: Fri, 24 Jun 2022 10:31:41 -0700 Subject: [PATCH 2/6] Traverse __wrapped__ attribute to get to original namespace. --- fastapi/dependencies/utils.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index f397e333c..08ea969a4 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -245,7 +245,13 @@ def is_scalar_sequence_field(field: ModelField) -> bool: def get_typed_signature(call: Callable[..., Any]) -> inspect.Signature: signature = inspect.signature(call) - globalns = getattr(call, "__globals__", {}) + nsobj = call + while hasattr(nsobj, "__wrapped__"): + # The __wrapped__ attribute is set by decorators, e.g. functools.wraps. + # This while loop allows rereferencing forward references on decorated + # methods. + nsobj = nsobj.__wrapped__ + globalns = getattr(nsobj, "__globals__", {}) typed_params = [ inspect.Parameter( name=param.name, From cd17a8ceaf82d64029116dc0ff60905883926a16 Mon Sep 17 00:00:00 2001 From: Lucas Wiman Date: Fri, 24 Jun 2022 10:35:02 -0700 Subject: [PATCH 3/6] Verify that __wrapped__ is traversed more than one level. --- tests/test_wrapped_method_forward_reference.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_wrapped_method_forward_reference.py b/tests/test_wrapped_method_forward_reference.py index 05681604d..2b4d25b81 100644 --- a/tests/test_wrapped_method_forward_reference.py +++ b/tests/test_wrapped_method_forward_reference.py @@ -22,3 +22,4 @@ def test_wrapped_method_type_inference(): """ app = FastAPI() app.get("/endpoint")(passthrough(forwardref_method)) + app.get("/endpoint2")(passthrough(passthrough(forwardref_method))) From c1691f5d94f3f7a97bcd4bec34c7522db105c25d Mon Sep 17 00:00:00 2001 From: Lucas Wiman Date: Fri, 24 Jun 2022 11:06:08 -0700 Subject: [PATCH 4/6] mypy complains about this code since Callable does not have a .__wrapped__ attribute. It is guarded by a hasattr check, so it is safe to ignore this error. --- fastapi/dependencies/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index 08ea969a4..948f5800d 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -250,7 +250,7 @@ def get_typed_signature(call: Callable[..., Any]) -> inspect.Signature: # The __wrapped__ attribute is set by decorators, e.g. functools.wraps. # This while loop allows rereferencing forward references on decorated # methods. - nsobj = nsobj.__wrapped__ + nsobj = nsobj.__wrapped__ # type: ignore globalns = getattr(nsobj, "__globals__", {}) typed_params = [ inspect.Parameter( From c3591b32b950066a29c903c9204110df7b30a275 Mon Sep 17 00:00:00 2001 From: Lucas Wiman Date: Fri, 24 Jun 2022 12:00:01 -0700 Subject: [PATCH 5/6] Make tests more end-to-end to keep code coverage high. --- tests/forward_reference_type.py | 2 +- tests/test_wrapped_method_forward_reference.py | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/tests/forward_reference_type.py b/tests/forward_reference_type.py index 02be56bec..b9cb9c22c 100644 --- a/tests/forward_reference_type.py +++ b/tests/forward_reference_type.py @@ -2,7 +2,7 @@ from pydantic import BaseModel def forwardref_method(input: "ForwardRef") -> "ForwardRef": - return ForwardRef() + return ForwardRef(x=input.x + 1) class ForwardRef(BaseModel): diff --git a/tests/test_wrapped_method_forward_reference.py b/tests/test_wrapped_method_forward_reference.py index 2b4d25b81..f01fe1474 100644 --- a/tests/test_wrapped_method_forward_reference.py +++ b/tests/test_wrapped_method_forward_reference.py @@ -1,6 +1,7 @@ import functools from fastapi import FastAPI +from fastapi.testclient import TestClient from .forward_reference_type import forwardref_method @@ -21,5 +22,10 @@ def test_wrapped_method_type_inference(): references. """ app = FastAPI() - app.get("/endpoint")(passthrough(forwardref_method)) - app.get("/endpoint2")(passthrough(passthrough(forwardref_method))) + 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} From f412d36f62dafae61ea588ec0d1e29a4c16cd97f Mon Sep 17 00:00:00 2001 From: "[object Object]" Date: Sun, 26 Jun 2022 11:06:50 -0700 Subject: [PATCH 6/6] Update fastapi/dependencies/utils.py Co-authored-by: Yurii Karabas <1998uriyyo@gmail.com> --- fastapi/dependencies/utils.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index 948f5800d..19ae9ad40 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -245,12 +245,7 @@ def is_scalar_sequence_field(field: ModelField) -> bool: def get_typed_signature(call: Callable[..., Any]) -> inspect.Signature: signature = inspect.signature(call) - nsobj = call - while hasattr(nsobj, "__wrapped__"): - # The __wrapped__ attribute is set by decorators, e.g. functools.wraps. - # This while loop allows rereferencing forward references on decorated - # methods. - nsobj = nsobj.__wrapped__ # type: ignore + nsobj = inspect.unwrap(call) globalns = getattr(nsobj, "__globals__", {}) typed_params = [ inspect.Parameter(