Skip to content

Commit a130a3b

Browse files
Merge pull request #13 from MarcoMuellner/11-circular-import-in-generated-gitea-api-client
11 circular import in generated gitea api client
2 parents 6a102dc + eaf49db commit a130a3b

File tree

9 files changed

+174
-43
lines changed

9 files changed

+174
-43
lines changed

poetry.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

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-python-generator"
3-
version = "0.4.0"
3+
version = "0.4.1"
44
description = "Openapi Python Generator"
55
authors = ["Marco Müllner <[email protected]>"]
66
license = "MIT"

src/openapi_python_generator/language_converters/python/model_generator.py

Lines changed: 42 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,11 @@
2121
from openapi_python_generator.models import TypeConversion
2222

2323

24-
def type_converter(schema: Schema, required: bool = False) -> TypeConversion:
24+
def type_converter(schema: Schema, required: bool = False, model_name: Optional[str] = None,) -> TypeConversion:
2525
"""
2626
Converts an OpenAPI type to a Python type.
2727
:param schema: Schema containing the type to be converted
28+
:param model_name: Name of the original model on which the type is defined
2829
:param required: Flag indicating if the type is required by the class
2930
:return: The converted type
3031
"""
@@ -45,14 +46,23 @@ def type_converter(schema: Schema, required: bool = False) -> TypeConversion:
4546
conversions.append(type_converter(sub_schema, True))
4647
else:
4748
import_type = sub_schema.ref.split("/")[-1]
48-
import_types = [f"from .{import_type} import {import_type}"]
49-
conversions.append(
50-
TypeConversion(
51-
original_type=sub_schema.ref,
52-
converted_type=import_type,
53-
import_types=import_types,
49+
if import_type == model_name:
50+
conversions.append(
51+
TypeConversion(
52+
original_type=sub_schema.ref,
53+
converted_type='"' + model_name + '"',
54+
import_types=None,
55+
)
56+
)
57+
else:
58+
import_types = [f"from .{import_type} import {import_type}"]
59+
conversions.append(
60+
TypeConversion(
61+
original_type=sub_schema.ref,
62+
converted_type=import_type,
63+
import_types=import_types,
64+
)
5465
)
55-
)
5666

5767
original_type = (
5868
"tuple<" + ",".join([i.original_type for i in conversions]) + ">"
@@ -121,10 +131,10 @@ def type_converter(schema: Schema, required: bool = False) -> TypeConversion:
121131
elif schema.type == "array":
122132
retVal = pre_type + "List["
123133
if isinstance(schema.items, Reference):
124-
import_type = schema.items.ref.split("/")[-1]
125-
import_types = [f"from .{import_type} import {import_type}"]
126-
original_type = "array<" + schema.items.ref.split("/")[-1] + ">"
127-
retVal += schema.items.ref.split("/")[-1]
134+
converted_reference = _generate_property_from_reference(model_name, "", schema.items, schema, required)
135+
import_types = converted_reference.type.import_types
136+
original_type = "array<" + converted_reference.type.original_type + ">"
137+
retVal += converted_reference.type.converted_type
128138
elif isinstance(schema.items, Schema):
129139
original_type = "array<" + str(schema.items.type) + ">"
130140
retVal += type_converter(schema.items, True).converted_type
@@ -148,11 +158,12 @@ def type_converter(schema: Schema, required: bool = False) -> TypeConversion:
148158

149159

150160
def _generate_property_from_schema(
151-
name: str, schema: Schema, parent_schema: Optional[Schema] = None
161+
model_name : str, name: str, schema: Schema, parent_schema: Optional[Schema] = None
152162
) -> Property:
153163
"""
154164
Generates a property from a schema. It takes the type of the schema and converts it to a python type, and then
155165
creates the according property.
166+
:param model_name: Name of the model this property belongs to
156167
:param name: Name of the schema
157168
:param schema: schema to be converted
158169
:param parent_schema: Component this belongs to
@@ -165,35 +176,43 @@ def _generate_property_from_schema(
165176
)
166177
return Property(
167178
name=name,
168-
type=type_converter(schema, required),
179+
type=type_converter(schema, required, model_name),
169180
required=required,
170181
default=None if required else "None",
171182
)
172183

173184

174185
def _generate_property_from_reference(
175-
name: str, reference: Reference, parent_schema: Optional[Schema] = None
186+
model_name: str, name: str, reference: Reference, parent_schema: Optional[Schema] = None, force_required: bool = False
176187
) -> Property:
177188
"""
178189
Generates a property from a reference. It takes the name of the reference as the type, and then
179190
returns a property type
180191
:param name: Name of the schema
181192
:param reference: reference to be converted
182193
:param parent_schema: Component this belongs to
194+
:param force_required: Force the property to be required
183195
:return: Property and model to be imported by the file
184196
"""
185197
required = (
186198
parent_schema is not None
187199
and parent_schema.required is not None
188200
and name in parent_schema.required
189-
)
201+
) or force_required
190202
import_model = reference.ref.split("/")[-1]
191203

192-
type_conv = TypeConversion(
193-
original_type=reference.ref,
194-
converted_type=import_model if required else "Optional[" + import_model + "]",
195-
import_types=[f"from .{import_model} import {import_model}"],
196-
)
204+
if import_model == model_name:
205+
type_conv = TypeConversion(
206+
original_type=reference.ref,
207+
converted_type=import_model if required else 'Optional["' + import_model + '"]',
208+
import_types=None
209+
)
210+
else:
211+
type_conv = TypeConversion(
212+
original_type=reference.ref,
213+
converted_type=import_model if required else "Optional[" + import_model + "]",
214+
import_types=[f"from .{import_model} import {import_model}"],
215+
)
197216
return Property(
198217
name=name,
199218
type=type_conv,
@@ -250,11 +269,11 @@ def generate_models(components: Components) -> List[Model]:
250269
for prop_name, property in property_iterator:
251270
if isinstance(property, Reference):
252271
conv_property = _generate_property_from_reference(
253-
prop_name, property, schema_or_reference
272+
name, prop_name, property, schema_or_reference
254273
)
255274
else:
256275
conv_property = _generate_property_from_schema(
257-
prop_name, property, schema_or_reference
276+
name, prop_name, property, schema_or_reference
258277
)
259278
properties.append(conv_property)
260279

src/openapi_python_generator/language_converters/python/templates/models.jinja2

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,5 @@ class {{ schema_name }}(BaseModel):
1818
"""
1919
{% for property in properties %}
2020

21-
{{ property.name | replace("@","")}} : {{ property.type.converted_type }} = Field(alias="{{ property.name }}" {% if not property.required %}, default = {{ property.default }} {% endif %})
21+
{{ property.name | replace("@","")}} : {{ property.type.converted_type | safe }} = Field(alias="{{ property.name }}" {% if not property.required %}, default = {{ property.default }} {% endif %})
2222
{% endfor %}

tests/regression/test_issue_11.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import pytest
2+
from click.testing import CliRunner
3+
4+
from openapi_python_generator.__main__ import main
5+
from openapi_python_generator.common import HTTPLibrary
6+
from tests.conftest import test_data_folder
7+
from tests.conftest import test_result_path
8+
9+
10+
@pytest.fixture
11+
def runner() -> CliRunner:
12+
"""Fixture for invoking command-line interfaces."""
13+
return CliRunner()
14+
15+
@pytest.mark.parametrize(
16+
"library",
17+
[HTTPLibrary.httpx, HTTPLibrary.requests, HTTPLibrary.aiohttp],
18+
)
19+
def test_issue_11(runner: CliRunner, model_data_with_cleanup, library) -> None:
20+
"""
21+
https://github.com/MarcoMuellner/openapi-python-generator/issues/7
22+
"""
23+
result = runner.invoke(
24+
main,
25+
[
26+
str(test_data_folder / "openapi_gitea_converted.json"),
27+
str(test_result_path),
28+
"--library",
29+
library.value,
30+
],
31+
)
32+
assert result.exit_code == 0
33+

tests/test_data/gitea_issue_11.json

Lines changed: 1 addition & 0 deletions
Large diffs are not rendered by default.

tests/test_generated_code.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import json
22
import os
3+
import traceback
34
from datetime import datetime
45

56
import orjson
@@ -205,9 +206,14 @@ def test_generate_code(model_data_with_cleanup, library, use_orjson):
205206
exec(exec_code_base, globals(), _locals)
206207
assert root_route.called
207208

208-
exec_code_base = f"from .test_result import *\nresp_result = get_users_users_get()"
209+
exec_code_base = f"try:\n\tfrom .test_result import *\n\tresp_result = get_users_users_get()\nexcept Exception as e:\n\tprint(e)\n\traise e"
209210

210-
exec(exec_code_base, globals(), _locals)
211+
try:
212+
exec(exec_code_base, globals(), _locals)
213+
except Exception as e:
214+
print(e)
215+
print(traceback.format_exc())
216+
raise e
211217

212218
exec(
213219
"from .test_result.services.general_service import *\nassert isinstance(resp_result, list)",

tests/test_model_generator.py

Lines changed: 46 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@
5959
(
6060
Schema(type="array", items=Reference(ref="#/components/schemas/test_name")),
6161
TypeConversion(
62-
original_type="array<test_name>",
62+
original_type="array<#/components/schemas/test_name>",
6363
converted_type="List[test_name]",
6464
import_types=["from .test_name import test_name"],
6565
),
@@ -72,10 +72,19 @@
7272
)
7373
def test_type_converter_simple(test_openapi_types, expected_python_types):
7474
assert type_converter(test_openapi_types, True) == expected_python_types
75-
assert (
76-
type_converter(test_openapi_types, False).converted_type
77-
== "Optional[" + expected_python_types.converted_type + "]"
78-
)
75+
76+
if test_openapi_types.type == "array" and isinstance(test_openapi_types.items, Reference):
77+
expected_type = expected_python_types.converted_type.split("[")[-1].split("]")[0]
78+
79+
assert (
80+
type_converter(test_openapi_types, False).converted_type
81+
== "Optional[List[Optional[" + expected_type + "]]]"
82+
)
83+
else:
84+
assert (
85+
type_converter(test_openapi_types, False).converted_type
86+
== "Optional[" + expected_python_types.converted_type + "]"
87+
)
7988

8089

8190
@pytest.mark.parametrize(
@@ -120,7 +129,7 @@ def test_type_converter_simple(test_openapi_types, expected_python_types):
120129
(
121130
Schema(type="array", items=Reference(ref="#/components/schemas/test_name")),
122131
TypeConversion(
123-
original_type="array<test_name>",
132+
original_type="array<#/components/schemas/test_name>",
124133
converted_type="List[test_name]",
125134
import_types=["from .test_name import test_name"],
126135
),
@@ -135,11 +144,19 @@ def test_type_converter_simple_orjson(test_openapi_types, expected_python_types)
135144
orjson_usage = common.get_use_orjson()
136145
common.set_use_orjson(True)
137146
assert type_converter(test_openapi_types, True) == expected_python_types
138-
assert (
139-
type_converter(test_openapi_types, False).converted_type
140-
== "Optional[" + expected_python_types.converted_type + "]"
141-
)
142-
common.set_use_orjson(orjson_usage)
147+
if test_openapi_types.type == "array" and isinstance(test_openapi_types.items, Reference):
148+
expected_type = expected_python_types.converted_type.split("[")[-1].split("]")[0]
149+
150+
assert (
151+
type_converter(test_openapi_types, False).converted_type
152+
== "Optional[List[Optional[" + expected_type + "]]]"
153+
)
154+
else:
155+
assert (
156+
type_converter(test_openapi_types, False).converted_type
157+
== "Optional[" + expected_python_types.converted_type + "]"
158+
)
159+
common.set_use_orjson(orjson_usage)
143160

144161

145162
def test_type_converter_all_of_reference():
@@ -211,9 +228,10 @@ def test_type_converter_exceptions():
211228

212229

213230
@pytest.mark.parametrize(
214-
"test_name, test_schema, test_parent_schema, expected_property",
231+
"test_model_name, test_name, test_schema, test_parent_schema, expected_property",
215232
[
216233
(
234+
"SomeModel",
217235
"test_name",
218236
Schema(type="string"),
219237
Schema(type="object"),
@@ -227,6 +245,7 @@ def test_type_converter_exceptions():
227245
),
228246
),
229247
(
248+
"SomeModel",
230249
"test_name",
231250
Schema(type="string"),
232251
Schema(type="object", required=["test_name"]),
@@ -237,13 +256,25 @@ def test_type_converter_exceptions():
237256
imported_type=["test_name"],
238257
),
239258
),
259+
(
260+
"SomeModel",
261+
"SomeModel",
262+
Schema(allOf=[Reference(ref="#/components/schemas/SomeModel")]),
263+
Schema(type="object", required=["SomeModel"]),
264+
Property(
265+
name='SomeModel',
266+
type=TypeConversion(original_type='tuple<#/components/schemas/SomeModel>', converted_type='"SomeModel"',import_types = []),
267+
required=True,
268+
imported_type=[],
269+
),
270+
),
240271
],
241272
)
242273
def test_type_converter_property(
243-
test_name, test_schema, test_parent_schema, expected_property
274+
test_model_name, test_name, test_schema, test_parent_schema, expected_property
244275
):
245276
assert (
246-
_generate_property_from_schema(test_name, test_schema, test_parent_schema)
277+
_generate_property_from_schema(test_model_name,test_name, test_schema, test_parent_schema)
247278
== expected_property
248279
)
249280

@@ -289,7 +320,7 @@ def test_type_converter_property_reference(
289320
test_name, test_reference, parent_schema, expected_property
290321
):
291322
assert (
292-
_generate_property_from_reference(test_name, test_reference, parent_schema)
323+
_generate_property_from_reference("",test_name, test_reference, parent_schema)
293324
== expected_property
294325
)
295326

tests/test_service_generator.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,47 @@ def test_generate_query_params(test_openapi_operation, expected_result):
370370
complex_type=True,
371371
),
372372
),
373+
(
374+
Operation(
375+
responses={
376+
"200": Response(
377+
description="Successful Response",
378+
content={"application/json": MediaType(media_type_schema=Reference(ref="#/components/schemas/User"))}
379+
)
380+
}
381+
),
382+
OpReturnType(
383+
type=TypeConversion(
384+
original_type="#/components/schemas/User",
385+
converted_type="User",
386+
import_types=["User"],
387+
),
388+
status_code="200",
389+
complex_type=True,
390+
)
391+
392+
),
393+
(
394+
Operation(
395+
responses={
396+
"200": Response(
397+
description="Successful Response",
398+
content={"application/json": MediaType(media_type_schema=Schema(type='array',items=Reference(ref="#/components/schemas/User")))}
399+
)
400+
}
401+
),
402+
OpReturnType(
403+
type=TypeConversion(
404+
original_type="array<#/components/schemas/User>",
405+
converted_type="List[User]",
406+
import_types=['from .User import User'],
407+
),
408+
status_code="200",
409+
complex_type=True,
410+
list_type="User"
411+
)
412+
413+
),
373414
(
374415
Operation(
375416
responses={

0 commit comments

Comments
 (0)