Skip to content

Commit 6a62cb1

Browse files
committed
fix(open-api): Add support for the usage of tuple returning type
1 parent 4b2a239 commit 6a62cb1

File tree

3 files changed

+60
-4
lines changed

3 files changed

+60
-4
lines changed

.gitignore

+4-1
Original file line numberDiff line numberDiff line change
@@ -314,4 +314,7 @@ examples/**/sam/.aws-sam
314314

315315
cdk.out
316316
# NOTE: different accounts will be used for E2E thus creating unnecessary git clutter
317-
cdk.context.json
317+
cdk.context.json
318+
319+
# vim
320+
*.swp

aws_lambda_powertools/event_handler/openapi/params.py

+20-3
Original file line numberDiff line numberDiff line change
@@ -932,7 +932,8 @@ def analyze_param(
932932
ModelField | None
933933
The type annotation and the Pydantic field representing the parameter
934934
"""
935-
field_info, type_annotation = get_field_info_and_type_annotation(annotation, value, is_path_param)
935+
field_info, type_annotation = \
936+
get_field_info_and_type_annotation(annotation, value, is_path_param, is_response_param)
936937

937938
# If the value is a FieldInfo, we use it as the FieldInfo for the parameter
938939
if isinstance(value, FieldInfo):
@@ -962,7 +963,9 @@ def analyze_param(
962963
return field
963964

964965

965-
def get_field_info_and_type_annotation(annotation, value, is_path_param: bool) -> tuple[FieldInfo | None, Any]:
966+
def get_field_info_and_type_annotation(
967+
annotation, value, is_path_param: bool, is_response_param: bool
968+
) -> tuple[FieldInfo | None, Any]:
966969
"""
967970
Get the FieldInfo and type annotation from an annotation and value.
968971
"""
@@ -976,19 +979,33 @@ def get_field_info_and_type_annotation(annotation, value, is_path_param: bool) -
976979
# If the annotation is a Response type, we recursively call this function with the inner type
977980
elif get_origin(annotation) is Response:
978981
field_info, type_annotation = get_field_info_response_type(annotation, value)
982+
# If the response param is a tuple with two elements, we use the first element as the type annotation,
983+
# just like we did in the APIGateway._to_response
984+
elif is_response_param and get_origin(annotation) is tuple and len(get_args(annotation)) == 2:
985+
field_info, type_annotation = get_field_info_tuple_type(annotation, value)
979986
# If the annotation is not an Annotated type, we use it as the type annotation
980987
else:
981988
type_annotation = annotation
982989

983990
return field_info, type_annotation
984991

985992

993+
def get_field_info_tuple_type(annotation, value) -> tuple[FieldInfo | None, Any]:
994+
(inner_type, _) = get_args(annotation)
995+
996+
# If the inner type is an Annotated type, we need to extract the type annotation and the FieldInfo
997+
if get_origin(inner_type) is Annotated:
998+
return get_field_info_annotated_type(inner_type, value, False)
999+
1000+
return None, inner_type
1001+
1002+
9861003
def get_field_info_response_type(annotation, value) -> tuple[FieldInfo | None, Any]:
9871004
# Example: get_args(Response[inner_type]) == (inner_type,) # noqa: ERA001
9881005
(inner_type,) = get_args(annotation)
9891006

9901007
# Recursively resolve the inner type
991-
return get_field_info_and_type_annotation(inner_type, value, False)
1008+
return get_field_info_and_type_annotation(inner_type, value, False, True)
9921009

9931010

9941011
def get_field_info_annotated_type(annotation, value, is_path_param: bool) -> tuple[FieldInfo | None, Any]:

tests/functional/event_handler/_pydantic/test_openapi_params.py

+36
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,42 @@ def handler() -> Response[Annotated[str, Body(title="Response title")]]:
172172
assert response.schema_.type == "string"
173173

174174

175+
def test_openapi_with_tuple_returns():
176+
app = APIGatewayRestResolver()
177+
178+
@app.get("/")
179+
def handler() -> tuple[str, int]:
180+
return "Hello, world", 200
181+
182+
schema = app.get_openapi_schema()
183+
assert len(schema.paths.keys()) == 1
184+
185+
get = schema.paths["/"].get
186+
assert get.parameters is None
187+
188+
response = get.responses[200].content[JSON_CONTENT_TYPE]
189+
assert response.schema_.title == "Return"
190+
assert response.schema_.type == "string"
191+
192+
193+
def test_openapi_with_tuple_annotated_returns():
194+
app = APIGatewayRestResolver()
195+
196+
@app.get("/")
197+
def handler() -> tuple[Annotated[str, Body(title="Response title")], int]:
198+
return "Hello, world", 200
199+
200+
schema = app.get_openapi_schema()
201+
assert len(schema.paths.keys()) == 1
202+
203+
get = schema.paths["/"].get
204+
assert get.parameters is None
205+
206+
response = get.responses[200].content[JSON_CONTENT_TYPE]
207+
assert response.schema_.title == "Response title"
208+
assert response.schema_.type == "string"
209+
210+
175211
def test_openapi_with_omitted_param():
176212
app = APIGatewayRestResolver()
177213

0 commit comments

Comments
 (0)