Skip to content

Commit a589674

Browse files
author
Joakim Nordling
committed
Improve handling of signatures for route handlers
If a handler function had a modified function signature (like a header was removed from it), the generated OpenAPI spec could still end up having the parameter included due to how the handler was copied and the annotation for the request model was added. This changes how the handler function is copied and how the annotation for the request model is added so it works correctly in the case when the signature of the handler function has been modified.
1 parent 817888d commit a589674

3 files changed

Lines changed: 65 additions & 7 deletions

File tree

openapi_to_fastapi/tests/test_router.py

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
from typing import Any, Dict
1+
import inspect
2+
from typing import Any, Dict, Optional
23

34
import pydantic
45
import pytest
@@ -596,3 +597,42 @@ def make_strict_request(json: Dict[str, Any]) -> Any:
596597
assert resp.status_code == expected_strict_code, resp.json()
597598
if resp.status_code != 200:
598599
assert json_snapshot == resp.json()
600+
601+
602+
def test_modified_handler_signatures(app, client, specs_root):
603+
604+
spec_router = SpecRouter(specs_root / "definitions")
605+
606+
def handler_1(request, x_my_header: Optional[str] = Header(None)):
607+
return {}
608+
609+
def handler_2(request, x_my_header: Optional[str] = Header(None)):
610+
return {}
611+
612+
# Remove the header from handler_2
613+
sig = inspect.signature(handler_2)
614+
params = sig.parameters
615+
filtered_params = [
616+
param for param_name, param in params.items() if param_name != "x_my_header"
617+
]
618+
handler_2.__signature__ = sig.replace(parameters=filtered_params)
619+
620+
# Add handlers to router (non-decorator syntax)
621+
spec_router.post("/TestValidation_v0.1")(handler_1)
622+
spec_router.post("/TestValidation_v0.2")(handler_2)
623+
624+
router = spec_router.to_fastapi_router()
625+
app.include_router(router)
626+
openapi_spec = app.openapi()
627+
628+
route_spec_1 = openapi_spec["paths"]["/TestValidation_v0.1"]
629+
route_spec_2 = openapi_spec["paths"]["/TestValidation_v0.2"]
630+
631+
parameters_1 = route_spec_1["post"].get("parameters", {})
632+
parameters_2 = route_spec_2["post"].get("parameters", {})
633+
634+
headers_1 = {p.get("name") for p in parameters_1 if p.get("in") == "header"}
635+
headers_2 = {p.get("name") for p in parameters_2 if p.get("in") == "header"}
636+
637+
assert "x-my-header" in headers_1
638+
assert "x-my-header" not in headers_2

openapi_to_fastapi/utils.py

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ def copy_function(fn) -> Callable:
2222
)
2323
g.__kwdefaults__ = deepcopy(fn.__kwdefaults__)
2424
g.__annotations__ = deepcopy(fn.__annotations__)
25+
26+
# Signature is immutable, no need to copy/deepcopy
27+
# Mypy doesn't know about __signature__: https://github.com/python/mypy/issues/12472
28+
g.__signature__ = inspect.signature(fn) # type: ignore[attr-defined]
29+
2530
return g
2631

2732

@@ -32,9 +37,22 @@ def add_annotation_to_first_argument(fn: FunctionType, model: Type[pydantic.Base
3237
:param fn: Function to patch
3338
:param model: Type to add to the first argument
3439
"""
35-
fn_spec = inspect.getfullargspec(fn)
36-
if not len(fn_spec.args):
40+
41+
sig = inspect.signature(fn)
42+
params = sig.parameters
43+
if not params:
3744
raise ValueError(f"Function {fn.__name__} has no arguments")
38-
untyped_args = [a for a in fn_spec.args if a not in fn.__annotations__]
39-
if untyped_args:
40-
fn.__annotations__[untyped_args[0]] = model
45+
46+
updated = False
47+
updated_params = []
48+
for param_name, param in params.items():
49+
if not updated and param.annotation is inspect.Parameter.empty:
50+
updated_params.append(param.replace(annotation=model))
51+
updated = True
52+
else:
53+
updated_params.append(param)
54+
55+
# Mypy doesn't know about __signature__: https://github.com/python/mypy/issues/12472
56+
fn.__signature__ = sig.replace( # type: ignore[attr-defined]
57+
parameters=updated_params
58+
)

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "openapi-to-fastapi"
3-
version = "0.20.0"
3+
version = "0.21.0"
44
description = "Create FastAPI routes from OpenAPI spec"
55
authors = ["IOXIO Ltd"]
66
license = "BSD-3-Clause"

0 commit comments

Comments
 (0)