Skip to content

Commit a94f570

Browse files
committed
Fix optional bodies
1 parent 6a86780 commit a94f570

File tree

30 files changed

+621
-120
lines changed

30 files changed

+621
-120
lines changed

.changeset/fix_optional_bodies.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
default: patch
3+
---
4+
5+
# Fix optional bodies
6+
7+
If a body is not required (the default), it will now:
8+
9+
1. Have `Unset` as part of its type annotation.
10+
2. Default to a value of `UNSET`
11+
3. Not be included in the request if it is `UNSET`
12+
13+
Thanks @orelmaliach for the report! Fixes #1354

end_to_end_tests/baseline_openapi_3.0.json

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,30 @@
104104
}
105105
}
106106
},
107+
"/bodies/optional": {
108+
"post": {
109+
"tags": [
110+
"bodies"
111+
],
112+
"description": "Test optional request body",
113+
"operationId": "optional-body",
114+
"requestBody": {
115+
"required": false,
116+
"content": {
117+
"application/json": {
118+
"schema": {
119+
"type": "object"
120+
}
121+
}
122+
}
123+
},
124+
"responses": {
125+
"200": {
126+
"description": "OK"
127+
}
128+
}
129+
}
130+
},
107131
"/tests/": {
108132
"get": {
109133
"tags": [
@@ -1813,6 +1837,47 @@
18131837
}
18141838
}
18151839
}
1840+
},
1841+
"/types/unions/duplicate-types": {
1842+
"post": {
1843+
"title": "duplicate union members",
1844+
"requestBody": {
1845+
"description": "The request body",
1846+
"content": {
1847+
"application/json": {
1848+
"schema": {
1849+
"oneOf": [
1850+
{
1851+
"$ref": "#/components/schemas/AModel"
1852+
},
1853+
{
1854+
"$ref": "#/components/schemas/AModel"
1855+
}
1856+
]
1857+
}
1858+
}
1859+
}
1860+
},
1861+
"responses": {
1862+
"200": {
1863+
"description": "Successful response",
1864+
"content": {
1865+
"application/json": {
1866+
"schema": {
1867+
"oneOf": [
1868+
{
1869+
"$ref": "#/components/schemas/AModel"
1870+
},
1871+
{
1872+
"$ref": "#/components/schemas/AModel"
1873+
}
1874+
]
1875+
}
1876+
}
1877+
}
1878+
}
1879+
}
1880+
}
18161881
}
18171882
},
18181883
"components": {

end_to_end_tests/baseline_openapi_3.1.yaml

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,30 @@ info:
100100
}
101101
}
102102
},
103+
"/bodies/optional": {
104+
"post": {
105+
"tags": [
106+
"bodies"
107+
],
108+
"description": "Test optional request body",
109+
"operationId": "optional-body",
110+
"requestBody": {
111+
"required": false,
112+
"content": {
113+
"application/json": {
114+
"schema": {
115+
"type": "object"
116+
}
117+
}
118+
}
119+
},
120+
"responses": {
121+
"200": {
122+
"description": "OK"
123+
}
124+
}
125+
}
126+
},
103127
"/tests/": {
104128
"get": {
105129
"tags": [
@@ -1779,6 +1803,39 @@ info:
17791803
}
17801804
}
17811805
}
1806+
},
1807+
"/types/unions/duplicate-types": {
1808+
"post": {
1809+
"title": "duplicate union members",
1810+
"requestBody": {
1811+
"description": "The request body",
1812+
"content": {
1813+
"application/json": {
1814+
"schema": {
1815+
"oneOf": [
1816+
{"$ref": "#/components/schemas/AModel"},
1817+
{"$ref": "#/components/schemas/AModel"}
1818+
]
1819+
}
1820+
}
1821+
}
1822+
},
1823+
"responses": {
1824+
"200": {
1825+
"description": "Successful response",
1826+
"content": {
1827+
"application/json": {
1828+
"schema": {
1829+
"oneOf": [
1830+
{"$ref": "#/components/schemas/AModel"},
1831+
{"$ref": "#/components/schemas/AModel"}
1832+
]
1833+
}
1834+
}
1835+
}
1836+
}
1837+
}
1838+
}
17821839
}
17831840
}
17841841
"components":

end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/bodies/__init__.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import types
44

5-
from . import json_like, post_bodies_multiple, refs
5+
from . import json_like, optional_body, post_bodies_multiple, refs
66

77

88
class BodiesEndpoints:
@@ -26,3 +26,10 @@ def refs(cls) -> types.ModuleType:
2626
Test request body defined via ref
2727
"""
2828
return refs
29+
30+
@classmethod
31+
def optional_body(cls) -> types.ModuleType:
32+
"""
33+
Test optional request body
34+
"""
35+
return optional_body

end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/default/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
get_models_allof,
88
get_models_oneof_with_required_const,
99
post_common_parameters,
10+
post_types_unions_duplicate_types,
1011
reserved_parameters,
1112
)
1213

@@ -31,3 +32,7 @@ def get_models_allof(cls) -> types.ModuleType:
3132
@classmethod
3233
def get_models_oneof_with_required_const(cls) -> types.ModuleType:
3334
return get_models_oneof_with_required_const
35+
36+
@classmethod
37+
def post_types_unions_duplicate_types(cls) -> types.ModuleType:
38+
return post_types_unions_duplicate_types

end_to_end_tests/functional_tests/generated_code_execution/test_docstrings.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ def test_response_union_type(self, post_simple_thing_sync):
152152

153153
def test_request_body(self, post_simple_thing_sync):
154154
assert DocstringParser(post_simple_thing_sync).get_section("Args:") == [
155-
"body (Thing): The thing."
155+
"body (Thing | Unset): The thing."
156156
]
157157

158158
def test_params(self, get_attribute_by_index_sync):

end_to_end_tests/golden-record/my_test_api_client/api/bodies/json_like.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@
66
from ... import errors
77
from ...client import AuthenticatedClient, Client
88
from ...models.json_like_body import JsonLikeBody
9-
from ...types import Response
9+
from ...types import UNSET, Response, Unset
1010

1111

1212
def _get_kwargs(
1313
*,
14-
body: JsonLikeBody,
14+
body: JsonLikeBody | Unset = UNSET,
1515
) -> dict[str, Any]:
1616
headers: dict[str, Any] = {}
1717

@@ -20,7 +20,8 @@ def _get_kwargs(
2020
"url": "/bodies/json-like",
2121
}
2222

23-
_kwargs["json"] = body.to_dict()
23+
if not isinstance(body, Unset):
24+
_kwargs["json"] = body.to_dict()
2425

2526
headers["Content-Type"] = "application/vnd+json"
2627

@@ -50,12 +51,12 @@ def _build_response(*, client: AuthenticatedClient | Client, response: httpx.Res
5051
def sync_detailed(
5152
*,
5253
client: AuthenticatedClient | Client,
53-
body: JsonLikeBody,
54+
body: JsonLikeBody | Unset = UNSET,
5455
) -> Response[Any]:
5556
"""A content type that works like json but isn't application/json
5657
5758
Args:
58-
body (JsonLikeBody):
59+
body (JsonLikeBody | Unset):
5960
6061
Raises:
6162
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
@@ -79,12 +80,12 @@ def sync_detailed(
7980
async def asyncio_detailed(
8081
*,
8182
client: AuthenticatedClient | Client,
82-
body: JsonLikeBody,
83+
body: JsonLikeBody | Unset = UNSET,
8384
) -> Response[Any]:
8485
"""A content type that works like json but isn't application/json
8586
8687
Args:
87-
body (JsonLikeBody):
88+
body (JsonLikeBody | Unset):
8889
8990
Raises:
9091
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
from http import HTTPStatus
2+
from typing import Any
3+
4+
import httpx
5+
6+
from ... import errors
7+
from ...client import AuthenticatedClient, Client
8+
from ...models.optional_body_body import OptionalBodyBody
9+
from ...types import UNSET, Response, Unset
10+
11+
12+
def _get_kwargs(
13+
*,
14+
body: OptionalBodyBody | Unset = UNSET,
15+
) -> dict[str, Any]:
16+
headers: dict[str, Any] = {}
17+
18+
_kwargs: dict[str, Any] = {
19+
"method": "post",
20+
"url": "/bodies/optional",
21+
}
22+
23+
if not isinstance(body, Unset):
24+
_kwargs["json"] = body.to_dict()
25+
26+
headers["Content-Type"] = "application/json"
27+
28+
_kwargs["headers"] = headers
29+
return _kwargs
30+
31+
32+
def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Any | None:
33+
if response.status_code == 200:
34+
return None
35+
36+
if client.raise_on_unexpected_status:
37+
raise errors.UnexpectedStatus(response.status_code, response.content)
38+
else:
39+
return None
40+
41+
42+
def _build_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Response[Any]:
43+
return Response(
44+
status_code=HTTPStatus(response.status_code),
45+
content=response.content,
46+
headers=response.headers,
47+
parsed=_parse_response(client=client, response=response),
48+
)
49+
50+
51+
def sync_detailed(
52+
*,
53+
client: AuthenticatedClient | Client,
54+
body: OptionalBodyBody | Unset = UNSET,
55+
) -> Response[Any]:
56+
"""Test optional request body
57+
58+
Args:
59+
body (OptionalBodyBody | Unset):
60+
61+
Raises:
62+
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
63+
httpx.TimeoutException: If the request takes longer than Client.timeout.
64+
65+
Returns:
66+
Response[Any]
67+
"""
68+
69+
kwargs = _get_kwargs(
70+
body=body,
71+
)
72+
73+
response = client.get_httpx_client().request(
74+
**kwargs,
75+
)
76+
77+
return _build_response(client=client, response=response)
78+
79+
80+
async def asyncio_detailed(
81+
*,
82+
client: AuthenticatedClient | Client,
83+
body: OptionalBodyBody | Unset = UNSET,
84+
) -> Response[Any]:
85+
"""Test optional request body
86+
87+
Args:
88+
body (OptionalBodyBody | Unset):
89+
90+
Raises:
91+
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
92+
httpx.TimeoutException: If the request takes longer than Client.timeout.
93+
94+
Returns:
95+
Response[Any]
96+
"""
97+
98+
kwargs = _get_kwargs(
99+
body=body,
100+
)
101+
102+
response = await client.get_async_httpx_client().request(**kwargs)
103+
104+
return _build_response(client=client, response=response)

0 commit comments

Comments
 (0)