Skip to content

Commit c528b86

Browse files
author
Doug Borg
committed
feat: update service generator for 3.1 (multi-version types, body/response handling) with edge tests
1 parent c02c968 commit c528b86

3 files changed

Lines changed: 156 additions & 31 deletions

File tree

src/openapi_python_generator/language_converters/python/service_generator.py

Lines changed: 90 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import re
2+
from typing import Any
23
from typing import Dict
34
from typing import List
45
from typing import Literal
@@ -7,7 +8,29 @@
78
from typing import Union
89

910
import click
10-
from openapi_pydantic.v3.v3_0 import Reference, Schema, Operation, Parameter, RequestBody, Response, MediaType, PathItem
11+
from openapi_pydantic.v3 import (
12+
Reference,
13+
Schema,
14+
Operation,
15+
Parameter,
16+
RequestBody,
17+
Response,
18+
PathItem,
19+
)
20+
21+
# Import version-specific types for isinstance checks
22+
from openapi_pydantic.v3.v3_0 import (
23+
Reference as Reference30,
24+
Schema as Schema30,
25+
Response as Response30,
26+
MediaType as MediaType30,
27+
)
28+
from openapi_pydantic.v3.v3_1 import (
29+
Reference as Reference31,
30+
Schema as Schema31,
31+
Response as Response31,
32+
MediaType as MediaType31,
33+
)
1134

1235
from openapi_python_generator.language_converters.python import common
1336
from openapi_python_generator.language_converters.python.common import normalize_symbol
@@ -24,6 +47,44 @@
2447
from openapi_python_generator.models import TypeConversion
2548

2649

50+
# Helper functions for isinstance checks across OpenAPI versions
51+
def is_response_type(obj) -> bool:
52+
"""Check if object is a Response from any OpenAPI version"""
53+
return isinstance(obj, (Response30, Response31))
54+
55+
56+
def create_media_type_for_reference(reference_obj):
57+
"""Create a MediaType wrapper for a reference object, using the correct version"""
58+
# Check which version the reference object belongs to
59+
if isinstance(reference_obj, Reference30):
60+
return MediaType30(schema=reference_obj)
61+
elif isinstance(reference_obj, Reference31):
62+
return MediaType31(schema=reference_obj)
63+
else:
64+
# Fallback to v3.0 for generic Reference
65+
return MediaType30(schema=reference_obj)
66+
67+
68+
def is_media_type(obj) -> bool:
69+
"""Check if object is a MediaType from any OpenAPI version"""
70+
return isinstance(obj, (MediaType30, MediaType31))
71+
72+
73+
def is_reference_type(obj: Any) -> bool:
74+
"""Check if object is a Reference type across different versions."""
75+
return isinstance(obj, (Reference, Reference30, Reference31))
76+
77+
78+
def is_schema_type(obj: Any) -> bool:
79+
"""Check if object is a Schema type across different versions."""
80+
return isinstance(obj, (Schema, Schema30, Schema31))
81+
82+
83+
def is_schema_type(obj) -> bool:
84+
"""Check if object is a Schema from any OpenAPI version"""
85+
return isinstance(obj, (Schema30, Schema31))
86+
87+
2788
HTTP_OPERATIONS = ["get", "post", "put", "delete", "options", "head", "patch", "trace"]
2889

2990

@@ -45,9 +106,14 @@ def generate_body_param(operation: Operation) -> Union[str, None]:
45106
if media_type is None:
46107
return None # pragma: no cover
47108

48-
if isinstance(media_type.media_type_schema, Reference):
109+
if isinstance(
110+
media_type.media_type_schema, (Reference, Reference30, Reference31)
111+
):
49112
return "data.dict()"
50-
elif isinstance(media_type.media_type_schema, Schema):
113+
elif hasattr(media_type.media_type_schema, "ref"):
114+
# Handle Reference objects from different OpenAPI versions
115+
return "data.dict()"
116+
elif isinstance(media_type.media_type_schema, (Schema, Schema30, Schema31)):
51117
schema = media_type.media_type_schema
52118
if schema.type == "array":
53119
return "[i.dict() for i in data]"
@@ -109,11 +175,13 @@ def _generate_params_from_content(content: Union[Reference, Schema]):
109175
"application/json",
110176
"text/plain",
111177
"multipart/form-data",
178+
"application/octet-stream",
112179
]
113180

114181
if operation.requestBody is not None:
182+
# Check if this is a RequestBody (either v3.0 or v3.1) by checking for content attribute
115183
if (
116-
isinstance(operation.requestBody, RequestBody)
184+
hasattr(operation.requestBody, "content")
117185
and isinstance(operation.requestBody.content, dict)
118186
and any(
119187
[
@@ -129,8 +197,11 @@ def _generate_params_from_content(content: Union[Reference, Schema]):
129197
][0]
130198
content = operation.requestBody.content.get(get_keyword)
131199
if content is not None and (
132-
isinstance(content.media_type_schema, Schema)
133-
or isinstance(content.media_type_schema, Reference)
200+
hasattr(content, "media_type_schema")
201+
and (
202+
hasattr(content.media_type_schema, "type")
203+
or hasattr(content.media_type_schema, "ref")
204+
)
134205
):
135206
params += (
136207
f"{_generate_params_from_content(content.media_type_schema)}, "
@@ -199,20 +270,22 @@ def generate_return_type(operation: Operation) -> OpReturnType:
199270
return OpReturnType(type=None, status_code=200, complex_type=False)
200271

201272
chosen_response = good_responses[0][1]
273+
media_type_schema = None
202274

203-
if isinstance(chosen_response, Response) and chosen_response.content is not None:
204-
media_type_schema = chosen_response.content.get("application/json")
205-
elif isinstance(chosen_response, Reference):
206-
media_type_schema = MediaType(
207-
media_type_schema=chosen_response
208-
) # pragma: no cover
209-
else:
275+
if is_response_type(chosen_response):
276+
# It's a Response type, access content safely
277+
if hasattr(chosen_response, "content") and chosen_response.content is not None:
278+
media_type_schema = chosen_response.content.get("application/json")
279+
elif is_reference_type(chosen_response):
280+
media_type_schema = create_media_type_for_reference(chosen_response)
281+
282+
if media_type_schema is None:
210283
return OpReturnType(
211284
type=None, status_code=good_responses[0][0], complex_type=False
212285
)
213286

214-
if isinstance(media_type_schema, MediaType):
215-
if isinstance(media_type_schema.media_type_schema, Reference):
287+
if is_media_type(media_type_schema):
288+
if is_reference_type(media_type_schema.media_type_schema):
216289
type_conv = TypeConversion(
217290
original_type=media_type_schema.media_type_schema.ref,
218291
converted_type=media_type_schema.media_type_schema.ref.split("/")[-1],
@@ -223,7 +296,7 @@ def generate_return_type(operation: Operation) -> OpReturnType:
223296
status_code=good_responses[0][0],
224297
complex_type=True,
225298
)
226-
elif isinstance(media_type_schema.media_type_schema, Schema):
299+
elif is_schema_type(media_type_schema.media_type_schema):
227300
converted_result = type_converter(media_type_schema.media_type_schema, True)
228301
if "array" in converted_result.original_type and isinstance(
229302
converted_result.import_types, list
@@ -293,7 +366,7 @@ def generate_service_operation(
293366
)
294367

295368
so.content = jinja_env.get_template(library_config.template_name).render(
296-
**so.dict()
369+
**so.model_dump()
297370
)
298371

299372
if op.tags is not None and len(op.tags) > 0:

tests/test_service_generator.py

Lines changed: 38 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
import pytest
2-
from openapi_pydantic.v3.v3_0 import (
3-
Operation, Reference, RequestBody, MediaType, Schema, Parameter,
4-
DataType, Response, ParameterLocation
2+
from openapi_pydantic.v3 import (
3+
Operation,
4+
Reference,
5+
RequestBody,
6+
MediaType,
7+
Schema,
8+
Parameter,
9+
DataType,
10+
Response,
11+
ParameterLocation,
512
)
613

714
from openapi_python_generator.common import HTTPLibrary
@@ -20,10 +27,15 @@
2027
default_responses = {
2128
"200": Response(
2229
description="Default response",
23-
content={"application/json": MediaType(media_type_schema=Schema(type=DataType.OBJECT))}
30+
content={
31+
"application/json": MediaType(
32+
media_type_schema=Schema(type=DataType.OBJECT)
33+
)
34+
},
2435
)
2536
}
2637

38+
2739
@pytest.mark.parametrize(
2840
"test_openapi_operation, expected_result",
2941
[
@@ -38,14 +50,14 @@
3850
)
3951
)
4052
}
41-
)
53+
),
4254
),
4355
"data.dict()",
4456
),
4557
(
4658
Operation(
4759
responses=default_responses,
48-
requestBody=Reference(ref="#/components/schemas/TestModel")
60+
requestBody=Reference(ref="#/components/schemas/TestModel"),
4961
),
5062
"data.dict()",
5163
),
@@ -61,7 +73,7 @@
6173
)
6274
)
6375
}
64-
)
76+
),
6577
),
6678
"[i.dict() for i in data]",
6779
),
@@ -180,7 +192,7 @@ def test_generate_body_param(test_openapi_operation, expected_result):
180192
),
181193
),
182194
"test : TestModel, test2 : str, data : str, ",
183-
)
195+
),
184196
],
185197
)
186198
def test_generate_params(test_openapi_operation, expected_result):
@@ -195,15 +207,25 @@ def test_generate_params(test_openapi_operation, expected_result):
195207
"test_openapi_operation, operation_type, expected_result",
196208
[
197209
(Operation(responses=default_responses, operationId="test"), "get", "test"),
198-
(Operation(responses=default_responses, operationId="test-test"), "get", "test_test"),
210+
(
211+
Operation(responses=default_responses, operationId="test-test"),
212+
"get",
213+
"test_test",
214+
),
199215
(Operation(responses=default_responses, operationId="test"), "post", "test"),
200216
(Operation(responses=default_responses, operationId="test"), "GET", "test"),
201-
(Operation(responses=default_responses, operationId="test-test"), "GET", "test_test"),
217+
(
218+
Operation(responses=default_responses, operationId="test-test"),
219+
"GET",
220+
"test_test",
221+
),
202222
(Operation(responses=default_responses, operationId="test"), "POST", "test"),
203223
],
204224
)
205225
def test_generate_operation_id(test_openapi_operation, operation_type, expected_result):
206-
assert generate_operation_id(test_openapi_operation, operation_type) == expected_result
226+
assert (
227+
generate_operation_id(test_openapi_operation, operation_type) == expected_result
228+
)
207229

208230

209231
@pytest.mark.parametrize(
@@ -254,7 +276,7 @@ def test_generate_operation_id(test_openapi_operation, operation_type, expected_
254276
param_schema=Schema(type=DataType.STRING),
255277
required=True,
256278
),
257-
]
279+
],
258280
),
259281
["'test' : test", "'test2' : test2"],
260282
),
@@ -359,6 +381,8 @@ def test_generate_services(model_data):
359381
for i in result:
360382
compile(i.content, "<string>", "exec")
361383

362-
result = generate_services(model_data.paths, library_config_dict[HTTPLibrary.requests])
384+
result = generate_services(
385+
model_data.paths, library_config_dict[HTTPLibrary.requests]
386+
)
363387
for i in result:
364-
compile(i.content, "<string>", "exec")
388+
compile(i.content, "<string>", "exec")
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
from openapi_pydantic.v3 import Response, MediaType, Schema, DataType, Operation
2+
from openapi_python_generator.language_converters.python import service_generator
3+
from openapi_python_generator.models import OpReturnType
4+
5+
6+
def test_is_schema_type_helper():
7+
# Ensure the helper function body executes
8+
assert service_generator.is_schema_type(Schema(type=DataType.STRING)) is True
9+
10+
11+
def test_generate_return_type_no_json_content():
12+
# Response with only text/plain should yield type None branch
13+
op = Operation(
14+
responses={
15+
"200": Response(
16+
description="",
17+
content={
18+
"text/plain": MediaType(
19+
media_type_schema=Schema(type=DataType.STRING)
20+
)
21+
},
22+
)
23+
}
24+
)
25+
rt = service_generator.generate_return_type(op)
26+
assert isinstance(rt, OpReturnType)
27+
assert rt.type is None
28+
assert rt.complex_type is False

0 commit comments

Comments
 (0)