Skip to content

Commit 4c512e1

Browse files
committed
feat: support inline form_body, not only refs
Additionally, refactor the form_body handling to be analog to multipart_body and json_body.
1 parent 7955faa commit 4c512e1

File tree

5 files changed

+85
-42
lines changed

5 files changed

+85
-42
lines changed

openapi_python_client/parser/openapi.py

Lines changed: 39 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -118,20 +118,33 @@ class Endpoint:
118118
header_parameters: Dict[str, Property] = field(default_factory=dict)
119119
cookie_parameters: Dict[str, Property] = field(default_factory=dict)
120120
responses: List[Response] = field(default_factory=list)
121-
form_body_class: Optional[Class] = None
121+
form_body: Optional[Property] = None
122122
json_body: Optional[Property] = None
123123
multipart_body: Optional[Property] = None
124124
errors: List[ParseError] = field(default_factory=list)
125125
used_python_identifiers: Set[PythonIdentifier] = field(default_factory=set)
126126

127127
@staticmethod
128-
def parse_request_form_body(*, body: oai.RequestBody, config: Config) -> Optional[Class]:
129-
"""Return form_body_reference"""
128+
def parse_request_form_body(
129+
*, body: oai.RequestBody, schemas: Schemas, parent_name: str, config: Config
130+
) -> Tuple[Union[Property, PropertyError, None], Schemas]:
131+
"""Return form_body and updated schemas"""
130132
body_content = body.content
131133
form_body = body_content.get("application/x-www-form-urlencoded")
132-
if form_body is not None and isinstance(form_body.media_type_schema, oai.Reference):
133-
return Class.from_string(string=form_body.media_type_schema.ref, config=config)
134-
return None
134+
if form_body is not None and form_body.media_type_schema is not None:
135+
prop, schemas = property_from_data(
136+
name="data",
137+
required=True,
138+
data=form_body.media_type_schema,
139+
schemas=schemas,
140+
parent_name=parent_name,
141+
config=config,
142+
)
143+
if isinstance(prop, ModelProperty):
144+
prop = attr.evolve(prop, is_form_body=True)
145+
schemas = attr.evolve(schemas, classes_by_name={**schemas.classes_by_name, prop.class_info.name: prop})
146+
return prop, schemas
147+
return None, schemas
135148

136149
@staticmethod
137150
def parse_multipart_body(
@@ -186,7 +199,20 @@ def _add_body(
186199
if data.requestBody is None or isinstance(data.requestBody, oai.Reference):
187200
return endpoint, schemas
188201

189-
endpoint.form_body_class = Endpoint.parse_request_form_body(body=data.requestBody, config=config)
202+
form_body, schemas = Endpoint.parse_request_form_body(
203+
body=data.requestBody, schemas=schemas, parent_name=endpoint.name, config=config
204+
)
205+
206+
if isinstance(form_body, ParseError):
207+
return (
208+
ParseError(
209+
header=f"Cannot parse form body of endpoint {endpoint.name}",
210+
detail=form_body.detail,
211+
data=form_body.data,
212+
),
213+
schemas,
214+
)
215+
190216
json_body, schemas = Endpoint.parse_request_json_body(
191217
body=data.requestBody, schemas=schemas, parent_name=endpoint.name, config=config
192218
)
@@ -213,8 +239,9 @@ def _add_body(
213239
schemas,
214240
)
215241

216-
if endpoint.form_body_class:
217-
endpoint.relative_imports.add(import_string_from_class(endpoint.form_body_class, prefix="...models"))
242+
if form_body is not None:
243+
endpoint.form_body = form_body
244+
endpoint.relative_imports.update(endpoint.form_body.get_imports(prefix="..."))
218245
if multipart_body is not None:
219246
endpoint.multipart_body = multipart_body
220247
endpoint.relative_imports.update(endpoint.multipart_body.get_imports(prefix="..."))
@@ -285,9 +312,9 @@ def add_parameters(
285312
config: User-provided config for overrides within parameters.
286313
287314
Returns:
288-
`(result, schemas)` where `result` is either an updated Endpoint containing the parameters or a ParseError
289-
describing what went wrong. `schemas` is an updated version of the `schemas` input, adding any new enums
290-
or classes.
315+
`(result, schemas, parameters)` where `result` is either an updated Endpoint containing the parameters or a
316+
ParseError describing what went wrong. `schemas` is an updated version of the `schemas` input, adding any
317+
new enums or classes. `parameters` is an updated version of the `parameters` input, adding new parameters.
291318
292319
See Also:
293320
- https://swagger.io/docs/specification/describing-parameters/

openapi_python_client/parser/properties/model_property.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ class ModelProperty(Property):
2727
template: ClassVar[str] = "model_property.py.jinja"
2828
json_is_dict: ClassVar[bool] = True
2929
is_multipart_body: bool = False
30+
is_form_body: bool = False
3031

3132
def get_base_type_string(self) -> str:
3233
return self.class_info.name

openapi_python_client/templates/endpoint_macros.py.jinja

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,8 @@ client: AuthenticatedClient,
9090
client: Client,
9191
{% endif %}
9292
{# Form data if any #}
93-
{% if endpoint.form_body_class %}
94-
form_data: {{ endpoint.form_body_class.name }},
93+
{% if endpoint.form_body %}
94+
form_data: {{ endpoint.form_body.get_type_string() }},
9595
{% endif %}
9696
{# Multipart data if any #}
9797
{% if endpoint.multipart_body %}
@@ -120,7 +120,7 @@ json_body: {{ endpoint.json_body.get_type_string() }},
120120
{{ parameter.python_name }}={{ parameter.python_name }},
121121
{% endfor %}
122122
client=client,
123-
{% if endpoint.form_body_class %}
123+
{% if endpoint.form_body %}
124124
form_data=form_data,
125125
{% endif %}
126126
{% if endpoint.multipart_body %}

openapi_python_client/templates/endpoint_module.py.jinja

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ def _get_kwargs(
4444
"headers": headers,
4545
"cookies": cookies,
4646
"timeout": client.get_timeout(),
47-
{% if endpoint.form_body_class %}
47+
{% if endpoint.form_body %}
4848
"data": form_data.to_dict(),
4949
{% elif endpoint.multipart_body %}
5050
"files": {{ "multipart_" + endpoint.multipart_body.python_name }},

tests/test_parser/test_openapi.py

Lines changed: 41 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -134,34 +134,42 @@ def make_endpoint(self):
134134
relative_imports={"import_3"},
135135
)
136136

137-
def test_parse_request_form_body(self, mocker):
138-
ref = mocker.MagicMock()
137+
def test_parse_request_form_body(self, mocker, model_property_factory):
138+
from openapi_python_client.parser.properties import Class
139+
140+
schema = oai.Reference.construct(ref=mocker.MagicMock())
139141
body = oai.RequestBody.construct(
140-
content={
141-
"application/x-www-form-urlencoded": oai.MediaType.construct(
142-
media_type_schema=oai.Reference.construct(ref=ref)
143-
)
144-
}
142+
content={"application/x-www-form-urlencoded": oai.MediaType.construct(media_type_schema=schema)}
143+
)
144+
class_info = Class(name="class_name", module_name="module_name")
145+
prop_before = model_property_factory(class_info=class_info, is_form_body=False)
146+
schemas_before = Schemas()
147+
property_from_data = mocker.patch(
148+
f"{MODULE_NAME}.property_from_data", return_value=(prop_before, schemas_before)
145149
)
146-
from_string = mocker.patch(f"{MODULE_NAME}.Class.from_string")
147150
config = mocker.MagicMock()
148151

149152
from openapi_python_client.parser.openapi import Endpoint
150153

151-
result = Endpoint.parse_request_form_body(body=body, config=config)
154+
result = Endpoint.parse_request_form_body(body=body, schemas=schemas_before, parent_name="name", config=config)
152155

153-
from_string.assert_called_once_with(string=ref, config=config)
154-
assert result == from_string.return_value
156+
property_from_data.assert_called_once_with(
157+
name="data", required=True, data=schema, schemas=schemas_before, parent_name="name", config=config
158+
)
159+
prop_after = model_property_factory(class_info=class_info, is_form_body=True)
160+
schemas_after = Schemas(classes_by_name={class_info.name: prop_after})
161+
assert result == (prop_after, schemas_after)
155162

156163
def test_parse_request_form_body_no_data(self):
157164
body = oai.RequestBody.construct(content={})
158165
config = MagicMock()
166+
schemas = MagicMock()
159167

160168
from openapi_python_client.parser.openapi import Endpoint
161169

162-
result = Endpoint.parse_request_form_body(body=body, config=config)
170+
result = Endpoint.parse_request_form_body(body=body, schemas=schemas, parent_name="name", config=config)
163171

164-
assert result is None
172+
assert result == (None, schemas)
165173

166174
def test_parse_multipart_body(self, mocker, model_property_factory):
167175
from openapi_python_client.parser.openapi import Endpoint, Schemas
@@ -279,13 +287,13 @@ def test_add_body_no_data(self, mocker):
279287
def test_add_body_bad_json_data(self, mocker):
280288
from openapi_python_client.parser.openapi import Endpoint, Schemas
281289

282-
mocker.patch.object(Endpoint, "parse_request_form_body")
290+
schemas = Schemas()
291+
mocker.patch.object(Endpoint, "parse_request_form_body", return_value=(None, schemas))
283292
parse_error = ParseError(data=mocker.MagicMock(), detail=mocker.MagicMock())
284293
other_schemas = mocker.MagicMock()
285294
mocker.patch.object(Endpoint, "parse_request_json_body", return_value=(parse_error, other_schemas))
286295
endpoint = self.make_endpoint()
287296
request_body = mocker.MagicMock()
288-
schemas = Schemas()
289297

290298
result = Endpoint._add_body(
291299
endpoint=endpoint,
@@ -306,14 +314,14 @@ def test_add_body_bad_json_data(self, mocker):
306314
def test_add_body_bad_multipart_data(self, mocker):
307315
from openapi_python_client.parser.openapi import Endpoint, Schemas
308316

309-
mocker.patch.object(Endpoint, "parse_request_form_body")
317+
schemas = Schemas()
318+
mocker.patch.object(Endpoint, "parse_request_form_body", return_value=(None, schemas))
310319
mocker.patch.object(Endpoint, "parse_request_json_body", return_value=(mocker.MagicMock(), mocker.MagicMock()))
311320
parse_error = ParseError(data=mocker.MagicMock(), detail=mocker.MagicMock())
312321
other_schemas = mocker.MagicMock()
313322
mocker.patch.object(Endpoint, "parse_multipart_body", return_value=(parse_error, other_schemas))
314323
endpoint = self.make_endpoint()
315324
request_body = mocker.MagicMock()
316-
schemas = Schemas()
317325

318326
result = Endpoint._add_body(
319327
endpoint=endpoint,
@@ -332,13 +340,19 @@ def test_add_body_bad_multipart_data(self, mocker):
332340
)
333341

334342
def test_add_body_happy(self, mocker):
335-
from openapi_python_client.parser.openapi import Class, Endpoint
343+
from openapi_python_client.parser.openapi import Endpoint
336344
from openapi_python_client.parser.properties import Property
337345

338346
request_body = mocker.MagicMock()
339347
config = mocker.MagicMock()
340-
form_body_class = Class(name="A", module_name="a")
341-
parse_request_form_body = mocker.patch.object(Endpoint, "parse_request_form_body", return_value=form_body_class)
348+
349+
form_body = mocker.MagicMock(autospec=Property)
350+
form_body_imports = mocker.MagicMock()
351+
form_body.get_imports.return_value = {form_body_imports}
352+
form_schemas = mocker.MagicMock()
353+
parse_request_form_body = mocker.patch.object(
354+
Endpoint, "parse_request_form_body", return_value=(form_body, form_schemas)
355+
)
342356

343357
multipart_body = mocker.MagicMock(autospec=Property)
344358
multipart_body_imports = mocker.MagicMock()
@@ -355,7 +369,6 @@ def test_add_body_happy(self, mocker):
355369
parse_request_json_body = mocker.patch.object(
356370
Endpoint, "parse_request_json_body", return_value=(json_body, json_schemas)
357371
)
358-
import_string_from_class = mocker.patch(f"{MODULE_NAME}.import_string_from_class", return_value="import_1")
359372

360373
endpoint = self.make_endpoint()
361374
initial_schemas = mocker.MagicMock()
@@ -368,19 +381,21 @@ def test_add_body_happy(self, mocker):
368381
)
369382

370383
assert response_schemas == multipart_schemas
371-
parse_request_form_body.assert_called_once_with(body=request_body, config=config)
372-
parse_request_json_body.assert_called_once_with(
384+
parse_request_form_body.assert_called_once_with(
373385
body=request_body, schemas=initial_schemas, parent_name="name", config=config
374386
)
387+
parse_request_json_body.assert_called_once_with(
388+
body=request_body, schemas=form_schemas, parent_name="name", config=config
389+
)
375390
parse_multipart_body.assert_called_once_with(
376391
body=request_body, schemas=json_schemas, parent_name="name", config=config
377392
)
378-
import_string_from_class.assert_called_once_with(form_body_class, prefix="...models")
393+
form_body.get_imports.assert_called_once_with(prefix="...")
379394
json_body.get_imports.assert_called_once_with(prefix="...")
380395
multipart_body.get_imports.assert_called_once_with(prefix="...")
381-
assert endpoint.relative_imports == {"import_1", "import_3", json_body_imports, multipart_body_imports}
396+
assert endpoint.relative_imports == {"import_3", form_body_imports, json_body_imports, multipart_body_imports}
382397
assert endpoint.json_body == json_body
383-
assert endpoint.form_body_class == form_body_class
398+
assert endpoint.form_body == form_body
384399
assert endpoint.multipart_body == multipart_body
385400

386401
def test__add_responses_status_code_error(self, mocker):

0 commit comments

Comments
 (0)