Skip to content

Commit 1bd3ce7

Browse files
authored
[python-nextgen] Better oneOf, anyOf support (#14743)
* better oneof, anyof support * improve anyof support * fix deprecation warning * fix anyof, add tests * add nullable support, add test
1 parent 0891b60 commit 1bd3ce7

File tree

34 files changed

+1185
-46
lines changed

34 files changed

+1185
-46
lines changed

modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PythonNextgenClientCodegen.java

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -403,8 +403,23 @@ private String getPydanticType(CodegenParameter cp,
403403
}
404404

405405
if (cp.isArray) {
406-
typingImports.add("List");
407-
return String.format(Locale.ROOT, "List[%s]", getPydanticType(cp.items, typingImports, pydanticImports, datetimeImports, modelImports));
406+
if (cp.maxItems != null || cp.minItems != null) {
407+
String maxOrMinItems = "";
408+
if (cp.maxItems != null) {
409+
maxOrMinItems += String.format(Locale.ROOT, ", max_items=%d", cp.maxItems);
410+
}
411+
if (cp.minItems != null) {
412+
maxOrMinItems += String.format(Locale.ROOT, ", min_items=%d", cp.minItems);
413+
}
414+
pydanticImports.add("conlist");
415+
return String.format(Locale.ROOT, "conlist(%s%s)",
416+
getPydanticType(cp.items, typingImports, pydanticImports, datetimeImports, modelImports),
417+
maxOrMinItems);
418+
419+
} else {
420+
typingImports.add("List");
421+
return String.format(Locale.ROOT, "List[%s]", getPydanticType(cp.items, typingImports, pydanticImports, datetimeImports, modelImports));
422+
}
408423
} else if (cp.isMap) {
409424
typingImports.add("Dict");
410425
return String.format(Locale.ROOT, "Dict[str, %s]", getPydanticType(cp.items, typingImports, pydanticImports, datetimeImports, modelImports));
@@ -638,8 +653,22 @@ private String getPydanticType(CodegenProperty cp,
638653
return String.format(Locale.ROOT, "%sEnum", cp.nameInCamelCase);
639654
} else*/
640655
if (cp.isArray) {
641-
typingImports.add("List");
642-
return String.format(Locale.ROOT, "List[%s]", getPydanticType(cp.items, typingImports, pydanticImports, datetimeImports, modelImports));
656+
if (cp.maxItems != null || cp.minItems != null) {
657+
String maxOrMinItems = "";
658+
if (cp.maxItems != null) {
659+
maxOrMinItems += String.format(Locale.ROOT, ", max_items=%d", cp.maxItems);
660+
}
661+
if (cp.minItems != null) {
662+
maxOrMinItems += String.format(Locale.ROOT, ", min_items=%d", cp.minItems);
663+
}
664+
pydanticImports.add("conlist");
665+
return String.format(Locale.ROOT, "conlist(%s%s)",
666+
getPydanticType(cp.items, typingImports, pydanticImports, datetimeImports, modelImports),
667+
maxOrMinItems);
668+
} else {
669+
typingImports.add("List");
670+
return String.format(Locale.ROOT, "List[%s]", getPydanticType(cp.items, typingImports, pydanticImports, datetimeImports, modelImports));
671+
}
643672
} else if (cp.isMap) {
644673
typingImports.add("Dict");
645674
return String.format(Locale.ROOT, "Dict[str, %s]", getPydanticType(cp.items, typingImports, pydanticImports, datetimeImports, modelImports));
@@ -1077,9 +1106,9 @@ private ModelsMap postProcessModelsMap(ModelsMap objs) {
10771106

10781107
// setup x-py-name for each oneOf/anyOf schema
10791108
if (!model.oneOf.isEmpty()) { // oneOf
1080-
cp.vendorExtensions.put("x-py-name", String.format(Locale.ROOT, "__oneof_schema_%d", property_count++));
1109+
cp.vendorExtensions.put("x-py-name", String.format(Locale.ROOT, "oneof_schema_%d_validator", property_count++));
10811110
} else if (!model.anyOf.isEmpty()) { // anyOf
1082-
cp.vendorExtensions.put("x-py-name", String.format(Locale.ROOT, "__anyof_schema_%d", property_count++));
1111+
cp.vendorExtensions.put("x-py-name", String.format(Locale.ROOT, "anyof_schema_%d_validator", property_count++));
10831112
}
10841113
}
10851114

modules/openapi-generator/src/main/resources/python-nextgen/model_anyof.mustache

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22
from inspect import getfullargspec
3+
import json
34
import pprint
45
import re # noqa: F401
56
{{#vendorExtensions.x-py-datetime-imports}}{{#-first}}from datetime import{{/-first}} {{{.}}}{{^-last}},{{/-last}}{{/vendorExtensions.x-py-datetime-imports}}
@@ -39,14 +40,38 @@ class {{classname}}({{#parent}}{{{.}}}{{/parent}}{{^parent}}BaseModel{{/parent}}
3940

4041
@validator('actual_instance')
4142
def actual_instance_must_validate_anyof(cls, v):
43+
{{#isNullable}}
44+
if v is None:
45+
return v
46+
47+
{{/isNullable}}
48+
instance = cls()
4249
error_messages = []
4350
{{#composedSchemas.anyOf}}
4451
# validate data type: {{{dataType}}}
45-
if type(v) is not {{^isPrimitiveType}}{{/isPrimitiveType}}{{{dataType}}}:
52+
{{#isContainer}}
53+
try:
54+
instance.{{vendorExtensions.x-py-name}} = v
55+
return v
56+
except ValidationError as e:
57+
error_messages.append(str(e))
58+
{{/isContainer}}
59+
{{^isContainer}}
60+
{{#isPrimitiveType}}
61+
try:
62+
instance.{{vendorExtensions.x-py-name}} = v
63+
return v
64+
except ValidationError as e:
65+
error_messages.append(str(e))
66+
{{/isPrimitiveType}}
67+
{{^isPrimitiveType}}
68+
if type(v) is not {{{dataType}}}:
4669
error_messages.append(f"Error! Input type `{type(v)}` is not `{{{dataType}}}`")
4770
else:
4871
return v
4972

73+
{{/isPrimitiveType}}
74+
{{/isContainer}}
5075
{{/composedSchemas.anyOf}}
5176
if error_messages:
5277
# no match
@@ -58,8 +83,36 @@ class {{classname}}({{#parent}}{{{.}}}{{/parent}}{{^parent}}BaseModel{{/parent}}
5883
def from_json(cls, json_str: str) -> {{{classname}}}:
5984
"""Returns the object represented by the json string"""
6085
instance = cls()
86+
{{#isNullable}}
87+
if json_str is None:
88+
return instance
89+
90+
{{/isNullable}}
6191
error_messages = []
6292
{{#composedSchemas.anyOf}}
93+
{{#isContainer}}
94+
# deserialize data into {{{dataType}}}
95+
try:
96+
# validation
97+
instance.{{vendorExtensions.x-py-name}} = json.loads(json_str)
98+
# assign value to actual_instance
99+
instance.actual_instance = instance.{{vendorExtensions.x-py-name}}
100+
return instance
101+
except ValidationError as e:
102+
error_messages.append(str(e))
103+
{{/isContainer}}
104+
{{^isContainer}}
105+
{{#isPrimitiveType}}
106+
# deserialize data into {{{dataType}}}
107+
try:
108+
# validation
109+
instance.{{vendorExtensions.x-py-name}} = json.loads(json_str)
110+
# assign value to actual_instance
111+
instance.actual_instance = instance.{{vendorExtensions.x-py-name}}
112+
return instance
113+
except ValidationError as e:
114+
error_messages.append(str(e))
115+
{{/isPrimitiveType}}
63116
{{^isPrimitiveType}}
64117
# {{vendorExtensions.x-py-name}}: {{{vendorExtensions.x-py-typing}}}
65118
try:
@@ -68,6 +121,7 @@ class {{classname}}({{#parent}}{{{.}}}{{/parent}}{{^parent}}BaseModel{{/parent}}
68121
except ValidationError as e:
69122
error_messages.append(str(e))
70123
{{/isPrimitiveType}}
124+
{{/isContainer}}
71125
{{/composedSchemas.anyOf}}
72126

73127
if error_messages:

modules/openapi-generator/src/main/resources/python-nextgen/model_oneof.mustache

Lines changed: 55 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from __future__ import annotations
22
from inspect import getfullargspec
3-
import pprint
43
import json
4+
import pprint
55
import re # noqa: F401
66
{{#vendorExtensions.x-py-datetime-imports}}{{#-first}}from datetime import{{/-first}} {{{.}}}{{^-last}},{{/-last}}{{/vendorExtensions.x-py-datetime-imports}}
77
{{#vendorExtensions.x-py-typing-imports}}{{#-first}}from typing import{{/-first}} {{{.}}}{{^-last}},{{/-last}}{{/vendorExtensions.x-py-typing-imports}}
@@ -40,15 +40,39 @@ class {{classname}}({{#parent}}{{{.}}}{{/parent}}{{^parent}}BaseModel{{/parent}}
4040

4141
@validator('actual_instance')
4242
def actual_instance_must_validate_oneof(cls, v):
43+
{{#isNullable}}
44+
if v is None:
45+
return v
46+
47+
{{/isNullable}}
48+
instance = cls()
4349
error_messages = []
4450
match = 0
4551
{{#composedSchemas.oneOf}}
4652
# validate data type: {{{dataType}}}
47-
if type(v) is not {{^isPrimitiveType}}{{/isPrimitiveType}}{{{dataType}}}:
53+
{{#isContainer}}
54+
try:
55+
instance.{{vendorExtensions.x-py-name}} = v
56+
match += 1
57+
except ValidationError as e:
58+
error_messages.append(str(e))
59+
{{/isContainer}}
60+
{{^isContainer}}
61+
{{#isPrimitiveType}}
62+
try:
63+
instance.{{vendorExtensions.x-py-name}} = v
64+
match += 1
65+
except ValidationError as e:
66+
error_messages.append(str(e))
67+
{{/isPrimitiveType}}
68+
{{^isPrimitiveType}}
69+
if type(v) is not {{{dataType}}}:
4870
error_messages.append(f"Error! Input type `{type(v)}` is not `{{{dataType}}}`")
4971
else:
5072
match += 1
5173

74+
{{/isPrimitiveType}}
75+
{{/isContainer}}
5276
{{/composedSchemas.oneOf}}
5377
if match > 1:
5478
# more than 1 match
@@ -67,6 +91,11 @@ class {{classname}}({{#parent}}{{{.}}}{{/parent}}{{^parent}}BaseModel{{/parent}}
6791
def from_json(cls, json_str: str) -> {{{classname}}}:
6892
"""Returns the object represented by the json string"""
6993
instance = cls()
94+
{{#isNullable}}
95+
if json_str is None:
96+
return instance
97+
98+
{{/isNullable}}
7099
error_messages = []
71100
match = 0
72101

@@ -89,6 +118,29 @@ class {{classname}}({{#parent}}{{{.}}}{{/parent}}{{^parent}}BaseModel{{/parent}}
89118
{{/discriminator}}
90119
{{/useOneOfDiscriminatorLookup}}
91120
{{#composedSchemas.oneOf}}
121+
{{#isContainer}}
122+
# deserialize data into {{{dataType}}}
123+
try:
124+
# validation
125+
instance.{{vendorExtensions.x-py-name}} = json.loads(json_str)
126+
# assign value to actual_instance
127+
instance.actual_instance = instance.{{vendorExtensions.x-py-name}}
128+
match += 1
129+
except ValidationError as e:
130+
error_messages.append(str(e))
131+
{{/isContainer}}
132+
{{^isContainer}}
133+
{{#isPrimitiveType}}
134+
# deserialize data into {{{dataType}}}
135+
try:
136+
# validation
137+
instance.{{vendorExtensions.x-py-name}} = json.loads(json_str)
138+
# assign value to actual_instance
139+
instance.actual_instance = instance.{{vendorExtensions.x-py-name}}
140+
match += 1
141+
except ValidationError as e:
142+
error_messages.append(str(e))
143+
{{/isPrimitiveType}}
92144
{{^isPrimitiveType}}
93145
# deserialize data into {{{dataType}}}
94146
try:
@@ -97,6 +149,7 @@ class {{classname}}({{#parent}}{{{.}}}{{/parent}}{{^parent}}BaseModel{{/parent}}
97149
except ValidationError as e:
98150
error_messages.append(str(e))
99151
{{/isPrimitiveType}}
152+
{{/isContainer}}
100153
{{/composedSchemas.oneOf}}
101154

102155
if match > 1:
@@ -125,7 +178,3 @@ class {{classname}}({{#parent}}{{{.}}}{{/parent}}{{^parent}}BaseModel{{/parent}}
125178
def to_str(self) -> str:
126179
"""Returns the string representation of the actual instance"""
127180
return pprint.pformat(self.dict())
128-
129-
130-
131-

modules/openapi-generator/src/main/resources/python-nextgen/rest.mustache

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,11 @@ class RESTResponse(io.IOBase):
2929

3030
def getheaders(self):
3131
"""Returns a dictionary of the response headers."""
32-
return self.urllib3_response.getheaders()
32+
return self.urllib3_response.headers
3333

3434
def getheader(self, name, default=None):
3535
"""Returns a given response header."""
36-
return self.urllib3_response.getheader(name, default)
36+
return self.urllib3_response.headers.get(name, default)
3737

3838

3939
class RESTClientObject(object):

modules/openapi-generator/src/test/resources/3_0/python/petstore-with-fake-endpoints-models-for-testing.yaml

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2035,3 +2035,41 @@ components:
20352035
type: string
20362036
self_ref:
20372037
$ref: '#/components/schemas/Self-Reference-Model'
2038+
RgbColor:
2039+
description: RGB three element array with values 0-255.
2040+
type: array
2041+
items:
2042+
type: integer
2043+
minimum: 0
2044+
maximum: 255
2045+
minItems: 3
2046+
maxItems: 3
2047+
RgbaColor:
2048+
description: RGBA four element array with values 0-255.
2049+
type: array
2050+
items:
2051+
type: integer
2052+
minimum: 0
2053+
maximum: 255
2054+
minItems: 4
2055+
maxItems: 4
2056+
HexColor:
2057+
description: 'Hex color string, such as #00FF00.'
2058+
type: string
2059+
pattern: ^#(?:[0-9a-fA-F]{3}){1,2}$
2060+
minLength: 7
2061+
maxLength: 7
2062+
Color:
2063+
nullable: true
2064+
description: RGB array, RGBA array, or hex string.
2065+
oneOf:
2066+
- $ref: '#/components/schemas/RgbColor'
2067+
- $ref: '#/components/schemas/RgbaColor'
2068+
- $ref: '#/components/schemas/HexColor'
2069+
#- type: "null"
2070+
AnyOfColor:
2071+
description: Any of RGB array, RGBA array, or hex string.
2072+
anyOf:
2073+
- $ref: '#/components/schemas/RgbColor'
2074+
- $ref: '#/components/schemas/RgbaColor'
2075+
- $ref: '#/components/schemas/HexColor'

samples/client/echo_api/python-nextgen/openapi_client/rest.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,11 @@ def __init__(self, resp):
3838

3939
def getheaders(self):
4040
"""Returns a dictionary of the response headers."""
41-
return self.urllib3_response.getheaders()
41+
return self.urllib3_response.headers
4242

4343
def getheader(self, name, default=None):
4444
"""Returns a given response header."""
45-
return self.urllib3_response.getheader(name, default)
45+
return self.urllib3_response.headers.get(name, default)
4646

4747

4848
class RESTClientObject(object):

samples/openapi3/client/petstore/python-nextgen-aiohttp/.openapi-generator/FILES

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ docs/AdditionalPropertiesClass.md
77
docs/AllOfWithSingleRef.md
88
docs/Animal.md
99
docs/AnotherFakeApi.md
10+
docs/AnyOfColor.md
1011
docs/AnyOfPig.md
1112
docs/ApiResponse.md
1213
docs/ArrayOfArrayOfNumberOnly.md
@@ -19,6 +20,7 @@ docs/CatAllOf.md
1920
docs/Category.md
2021
docs/ClassModel.md
2122
docs/Client.md
23+
docs/Color.md
2224
docs/DanishPig.md
2325
docs/DefaultApi.md
2426
docs/DeprecatedObject.md
@@ -83,6 +85,7 @@ petstore_api/models/__init__.py
8385
petstore_api/models/additional_properties_class.py
8486
petstore_api/models/all_of_with_single_ref.py
8587
petstore_api/models/animal.py
88+
petstore_api/models/any_of_color.py
8689
petstore_api/models/any_of_pig.py
8790
petstore_api/models/api_response.py
8891
petstore_api/models/array_of_array_of_number_only.py
@@ -95,6 +98,7 @@ petstore_api/models/cat_all_of.py
9598
petstore_api/models/category.py
9699
petstore_api/models/class_model.py
97100
petstore_api/models/client.py
101+
petstore_api/models/color.py
98102
petstore_api/models/danish_pig.py
99103
petstore_api/models/deprecated_object.py
100104
petstore_api/models/dog.py

samples/openapi3/client/petstore/python-nextgen-aiohttp/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ Class | Method | HTTP request | Description
130130
- [AdditionalPropertiesClass](docs/AdditionalPropertiesClass.md)
131131
- [AllOfWithSingleRef](docs/AllOfWithSingleRef.md)
132132
- [Animal](docs/Animal.md)
133+
- [AnyOfColor](docs/AnyOfColor.md)
133134
- [AnyOfPig](docs/AnyOfPig.md)
134135
- [ApiResponse](docs/ApiResponse.md)
135136
- [ArrayOfArrayOfNumberOnly](docs/ArrayOfArrayOfNumberOnly.md)
@@ -142,6 +143,7 @@ Class | Method | HTTP request | Description
142143
- [Category](docs/Category.md)
143144
- [ClassModel](docs/ClassModel.md)
144145
- [Client](docs/Client.md)
146+
- [Color](docs/Color.md)
145147
- [DanishPig](docs/DanishPig.md)
146148
- [DeprecatedObject](docs/DeprecatedObject.md)
147149
- [Dog](docs/Dog.md)

0 commit comments

Comments
 (0)