Skip to content

Commit 4d83cfe

Browse files
[Python] Fix python template for list and dicts of dicts (#23112)
* Fix python template for list and dicts of dicts * Add test cases for dict of dict and list of dict * Add generated samples
1 parent 054af0e commit 4d83cfe

56 files changed

Lines changed: 1355 additions & 15 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

modules/openapi-generator/src/main/resources/python/model_generic.mustache

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,21 @@ class {{classname}}({{#parent}}{{{.}}}{{/parent}}{{^parent}}BaseModel{{/parent}}
182182
_dict['{{{baseName}}}'] = _items
183183
{{/items.items.isPrimitiveType}}
184184
{{/items.isArray}}
185+
{{#items.isMap}}
186+
{{^items.items.isPrimitiveType}}
187+
# override the default output from pydantic by calling `to_dict()` of each item in {{{name}}} (list of dict)
188+
_items = []
189+
if self.{{{name}}}:
190+
for _item_{{{name}}} in self.{{{name}}}:
191+
if _item_{{{name}}}:
192+
_items.append(
193+
{_inner_key: _inner_value.to_dict() for _inner_key, _inner_value in _item_{{{name}}}.items()}
194+
)
195+
_dict['{{{baseName}}}'] = _items
196+
{{/items.items.isPrimitiveType}}
197+
{{/items.isMap}}
185198
{{^items.isArray}}
199+
{{^items.isMap}}
186200
{{^items.isPrimitiveType}}
187201
{{^items.isEnumOrRef}}
188202
# override the default output from pydantic by calling `to_dict()` of each item in {{{name}}} (list)
@@ -194,6 +208,7 @@ class {{classname}}({{#parent}}{{{.}}}{{/parent}}{{^parent}}BaseModel{{/parent}}
194208
_dict['{{{baseName}}}'] = _items
195209
{{/items.isEnumOrRef}}
196210
{{/items.isPrimitiveType}}
211+
{{/items.isMap}}
197212
{{/items.isArray}}
198213
{{/isArray}}
199214
{{#isMap}}
@@ -210,6 +225,20 @@ class {{classname}}({{#parent}}{{{.}}}{{/parent}}{{^parent}}BaseModel{{/parent}}
210225
_dict['{{{baseName}}}'] = _field_dict_of_array
211226
{{/items.items.isPrimitiveType}}
212227
{{/items.isArray}}
228+
{{#items.isMap}}
229+
{{^items.items.isPrimitiveType}}
230+
# override the default output from pydantic by calling `to_dict()` of each value in {{{name}}} (dict of dict)
231+
_field_dict_of_dict = {}
232+
if self.{{{name}}}:
233+
for _key_{{{name}}}, _value_{{{name}}} in self.{{{name}}}.items():
234+
if _value_{{{name}}} is not None:
235+
_field_dict_of_dict[_key_{{{name}}}] = {
236+
_key: _value.to_dict() for _key, _value in _value_{{{name}}}.items()
237+
}
238+
_dict['{{{baseName}}}'] = _field_dict_of_dict
239+
{{/items.items.isPrimitiveType}}
240+
{{/items.isMap}}
241+
{{^items.isMap}}
213242
{{^items.isArray}}
214243
{{^items.isPrimitiveType}}
215244
{{^items.isEnumOrRef}}
@@ -223,6 +252,7 @@ class {{classname}}({{#parent}}{{{.}}}{{/parent}}{{^parent}}BaseModel{{/parent}}
223252
{{/items.isEnumOrRef}}
224253
{{/items.isPrimitiveType}}
225254
{{/items.isArray}}
255+
{{/items.isMap}}
226256
{{/isMap}}
227257
{{/isContainer}}
228258
{{^isContainer}}
@@ -293,6 +323,7 @@ class {{classname}}({{#parent}}{{{.}}}{{/parent}}{{^parent}}BaseModel{{/parent}}
293323
{{#allVars}}
294324
{{#isContainer}}
295325
{{#isArray}}
326+
{{#items.isContainer}}
296327
{{#items.isArray}}
297328
{{#items.items.isPrimitiveType}}
298329
"{{{baseName}}}": obj.get("{{{baseName}}}"){{^-last}},{{/-last}}
@@ -304,7 +335,19 @@ class {{classname}}({{#parent}}{{{.}}}{{/parent}}{{^parent}}BaseModel{{/parent}}
304335
] if obj.get("{{{baseName}}}") is not None else None{{^-last}},{{/-last}}
305336
{{/items.items.isPrimitiveType}}
306337
{{/items.isArray}}
307-
{{^items.isArray}}
338+
{{#items.isMap}}
339+
{{#items.items.isPrimitiveType}}
340+
"{{{baseName}}}": obj.get("{{{baseName}}}"){{^-last}},{{/-last}}
341+
{{/items.items.isPrimitiveType}}
342+
{{^items.items.isPrimitiveType}}
343+
"{{{baseName}}}": [
344+
{_inner_key: {{{items.items.dataType}}}.from_dict(_inner_value) for _inner_key, _inner_value in _item.items()}
345+
for _item in obj["{{{baseName}}}"]
346+
] if obj.get("{{{baseName}}}") is not None else None{{^-last}},{{/-last}}
347+
{{/items.items.isPrimitiveType}}
348+
{{/items.isMap}}
349+
{{/items.isContainer}}
350+
{{^items.isContainer}}
308351
{{^items.isPrimitiveType}}
309352
{{#items.isEnumOrRef}}
310353
"{{{baseName}}}": obj.get("{{{baseName}}}"){{^-last}},{{/-last}}
@@ -316,7 +359,7 @@ class {{classname}}({{#parent}}{{{.}}}{{/parent}}{{^parent}}BaseModel{{/parent}}
316359
{{#items.isPrimitiveType}}
317360
"{{{baseName}}}": obj.get("{{{baseName}}}"){{^-last}},{{/-last}}
318361
{{/items.isPrimitiveType}}
319-
{{/items.isArray}}
362+
{{/items.isContainer}}
320363
{{/isArray}}
321364
{{#isMap}}
322365
{{^items.isPrimitiveType}}
@@ -331,7 +374,7 @@ class {{classname}}({{#parent}}{{{.}}}{{/parent}}{{^parent}}BaseModel{{/parent}}
331374
if _v is not None
332375
else None
333376
)
334-
for _k, _v in obj.get("{{{baseName}}}").items()
377+
for _k, _v in obj["{{{baseName}}}"].items()
335378
)
336379
if obj.get("{{{baseName}}}") is not None
337380
else None{{^-last}},{{/-last}}

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2137,6 +2137,12 @@ components:
21372137
type: object
21382138
additionalProperties:
21392139
type: string
2140+
map_of_map_non_primitive_property:
2141+
type: object
2142+
additionalProperties:
2143+
type: object
2144+
additionalProperties:
2145+
$ref: '#/components/schemas/Pet'
21402146
MixedPropertiesAndAdditionalPropertiesClass:
21412147
type: object
21422148
properties:
@@ -2705,6 +2711,15 @@ components:
27052711
type: array
27062712
items:
27072713
$ref: "#/components/schemas/Tag"
2714+
ArrayOfMapModel:
2715+
type: object
2716+
properties:
2717+
array_of_map_property:
2718+
type: array
2719+
items:
2720+
type: object
2721+
additionalProperties:
2722+
$ref: "#/components/schemas/Tag"
27082723
ArrayOfArrayOfModel:
27092724
type: object
27102725
properties:

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ docs/AnyOfColor.md
1515
docs/AnyOfPig.md
1616
docs/ArrayOfArrayOfModel.md
1717
docs/ArrayOfArrayOfNumberOnly.md
18+
docs/ArrayOfMapModel.md
1819
docs/ArrayOfNumberOnly.md
1920
docs/ArrayTest.md
2021
docs/BaseDiscriminator.md
@@ -151,6 +152,7 @@ petstore_api/models/any_of_color.py
151152
petstore_api/models/any_of_pig.py
152153
petstore_api/models/array_of_array_of_model.py
153154
petstore_api/models/array_of_array_of_number_only.py
155+
petstore_api/models/array_of_map_model.py
154156
petstore_api/models/array_of_number_only.py
155157
petstore_api/models/array_test.py
156158
petstore_api/models/base_discriminator.py

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ Class | Method | HTTP request | Description
162162
- [AnyOfPig](docs/AnyOfPig.md)
163163
- [ArrayOfArrayOfModel](docs/ArrayOfArrayOfModel.md)
164164
- [ArrayOfArrayOfNumberOnly](docs/ArrayOfArrayOfNumberOnly.md)
165+
- [ArrayOfMapModel](docs/ArrayOfMapModel.md)
165166
- [ArrayOfNumberOnly](docs/ArrayOfNumberOnly.md)
166167
- [ArrayTest](docs/ArrayTest.md)
167168
- [BaseDiscriminator](docs/BaseDiscriminator.md)

samples/openapi3/client/petstore/python-aiohttp/docs/AdditionalPropertiesClass.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ Name | Type | Description | Notes
77
------------ | ------------- | ------------- | -------------
88
**map_property** | **Dict[str, str]** | | [optional]
99
**map_of_map_property** | **Dict[str, Dict[str, str]]** | | [optional]
10+
**map_of_map_non_primitive_property** | **Dict[str, Dict[str, Pet]]** | | [optional]
1011

1112
## Example
1213

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# ArrayOfMapModel
2+
3+
4+
## Properties
5+
6+
Name | Type | Description | Notes
7+
------------ | ------------- | ------------- | -------------
8+
**array_of_map_property** | **List[Dict[str, Tag]]** | | [optional]
9+
10+
## Example
11+
12+
```python
13+
from petstore_api.models.array_of_map_model import ArrayOfMapModel
14+
15+
# TODO update the JSON string below
16+
json = "{}"
17+
# create an instance of ArrayOfMapModel from a JSON string
18+
array_of_map_model_instance = ArrayOfMapModel.from_json(json)
19+
# print the JSON string representation of the object
20+
print(ArrayOfMapModel.to_json())
21+
22+
# convert the object into a dict
23+
array_of_map_model_dict = array_of_map_model_instance.to_dict()
24+
# create an instance of ArrayOfMapModel from a dict
25+
array_of_map_model_from_dict = ArrayOfMapModel.from_dict(array_of_map_model_dict)
26+
```
27+
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
28+
29+

samples/openapi3/client/petstore/python-aiohttp/petstore_api/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
"AnyOfPig",
4848
"ArrayOfArrayOfModel",
4949
"ArrayOfArrayOfNumberOnly",
50+
"ArrayOfMapModel",
5051
"ArrayOfNumberOnly",
5152
"ArrayTest",
5253
"BaseDiscriminator",
@@ -185,6 +186,7 @@
185186
from petstore_api.models.any_of_pig import AnyOfPig as AnyOfPig
186187
from petstore_api.models.array_of_array_of_model import ArrayOfArrayOfModel as ArrayOfArrayOfModel
187188
from petstore_api.models.array_of_array_of_number_only import ArrayOfArrayOfNumberOnly as ArrayOfArrayOfNumberOnly
189+
from petstore_api.models.array_of_map_model import ArrayOfMapModel as ArrayOfMapModel
188190
from petstore_api.models.array_of_number_only import ArrayOfNumberOnly as ArrayOfNumberOnly
189191
from petstore_api.models.array_test import ArrayTest as ArrayTest
190192
from petstore_api.models.base_discriminator import BaseDiscriminator as BaseDiscriminator

samples/openapi3/client/petstore/python-aiohttp/petstore_api/models/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
from petstore_api.models.any_of_pig import AnyOfPig
2525
from petstore_api.models.array_of_array_of_model import ArrayOfArrayOfModel
2626
from petstore_api.models.array_of_array_of_number_only import ArrayOfArrayOfNumberOnly
27+
from petstore_api.models.array_of_map_model import ArrayOfMapModel
2728
from petstore_api.models.array_of_number_only import ArrayOfNumberOnly
2829
from petstore_api.models.array_test import ArrayTest
2930
from petstore_api.models.base_discriminator import BaseDiscriminator

samples/openapi3/client/petstore/python-aiohttp/petstore_api/models/additional_properties_class.py

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
from pydantic import BaseModel, ConfigDict, StrictStr
2121
from typing import Any, ClassVar, Dict, List, Optional
22+
from petstore_api.models.pet import Pet
2223
from typing import Optional, Set
2324
from typing_extensions import Self
2425

@@ -28,7 +29,8 @@ class AdditionalPropertiesClass(BaseModel):
2829
""" # noqa: E501
2930
map_property: Optional[Dict[str, StrictStr]] = None
3031
map_of_map_property: Optional[Dict[str, Dict[str, StrictStr]]] = None
31-
__properties: ClassVar[List[str]] = ["map_property", "map_of_map_property"]
32+
map_of_map_non_primitive_property: Optional[Dict[str, Dict[str, Pet]]] = None
33+
__properties: ClassVar[List[str]] = ["map_property", "map_of_map_property", "map_of_map_non_primitive_property"]
3234

3335
model_config = ConfigDict(
3436
validate_by_name=True,
@@ -70,6 +72,15 @@ def to_dict(self) -> Dict[str, Any]:
7072
exclude=excluded_fields,
7173
exclude_none=True,
7274
)
75+
# override the default output from pydantic by calling `to_dict()` of each value in map_of_map_non_primitive_property (dict of dict)
76+
_field_dict_of_dict = {}
77+
if self.map_of_map_non_primitive_property:
78+
for _key_map_of_map_non_primitive_property, _value_map_of_map_non_primitive_property in self.map_of_map_non_primitive_property.items():
79+
if _value_map_of_map_non_primitive_property is not None:
80+
_field_dict_of_dict[_key_map_of_map_non_primitive_property] = {
81+
_key: _value.to_dict() for _key, _value in _value_map_of_map_non_primitive_property.items()
82+
}
83+
_dict['map_of_map_non_primitive_property'] = _field_dict_of_dict
7384
return _dict
7485

7586
@classmethod
@@ -83,7 +94,19 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]:
8394

8495
_obj = cls.model_validate({
8596
"map_property": obj.get("map_property"),
86-
"map_of_map_property": obj.get("map_of_map_property")
97+
"map_of_map_property": obj.get("map_of_map_property"),
98+
"map_of_map_non_primitive_property": dict(
99+
(_k, dict(
100+
(_ik, Pet.from_dict(_iv))
101+
for _ik, _iv in _v.items()
102+
)
103+
if _v is not None
104+
else None
105+
)
106+
for _k, _v in obj["map_of_map_non_primitive_property"].items()
107+
)
108+
if obj.get("map_of_map_non_primitive_property") is not None
109+
else None
87110
})
88111
return _obj
89112

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
# coding: utf-8
2+
3+
"""
4+
OpenAPI Petstore
5+
6+
This spec is mainly for testing Petstore server and contains fake endpoints, models. Please do not use this for any other purpose. Special characters: \" \\
7+
8+
The version of the OpenAPI document: 1.0.0
9+
Generated by OpenAPI Generator (https://openapi-generator.tech)
10+
11+
Do not edit the class manually.
12+
""" # noqa: E501
13+
14+
15+
from __future__ import annotations
16+
import pprint
17+
import re # noqa: F401
18+
import json
19+
20+
from pydantic import BaseModel, ConfigDict
21+
from typing import Any, ClassVar, Dict, List, Optional
22+
from petstore_api.models.tag import Tag
23+
from typing import Optional, Set
24+
from typing_extensions import Self
25+
26+
class ArrayOfMapModel(BaseModel):
27+
"""
28+
ArrayOfMapModel
29+
""" # noqa: E501
30+
array_of_map_property: Optional[List[Dict[str, Tag]]] = None
31+
__properties: ClassVar[List[str]] = ["array_of_map_property"]
32+
33+
model_config = ConfigDict(
34+
validate_by_name=True,
35+
validate_by_alias=True,
36+
validate_assignment=True,
37+
protected_namespaces=(),
38+
)
39+
40+
41+
def to_str(self) -> str:
42+
"""Returns the string representation of the model using alias"""
43+
return pprint.pformat(self.model_dump(by_alias=True))
44+
45+
def to_json(self) -> str:
46+
"""Returns the JSON representation of the model using alias"""
47+
# TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead
48+
return json.dumps(self.to_dict())
49+
50+
@classmethod
51+
def from_json(cls, json_str: str) -> Optional[Self]:
52+
"""Create an instance of ArrayOfMapModel from a JSON string"""
53+
return cls.from_dict(json.loads(json_str))
54+
55+
def to_dict(self) -> Dict[str, Any]:
56+
"""Return the dictionary representation of the model using alias.
57+
58+
This has the following differences from calling pydantic's
59+
`self.model_dump(by_alias=True)`:
60+
61+
* `None` is only added to the output dict for nullable fields that
62+
were set at model initialization. Other fields with value `None`
63+
are ignored.
64+
"""
65+
excluded_fields: Set[str] = set([
66+
])
67+
68+
_dict = self.model_dump(
69+
by_alias=True,
70+
exclude=excluded_fields,
71+
exclude_none=True,
72+
)
73+
# override the default output from pydantic by calling `to_dict()` of each item in array_of_map_property (list of dict)
74+
_items = []
75+
if self.array_of_map_property:
76+
for _item_array_of_map_property in self.array_of_map_property:
77+
if _item_array_of_map_property:
78+
_items.append(
79+
{_inner_key: _inner_value.to_dict() for _inner_key, _inner_value in _item_array_of_map_property.items()}
80+
)
81+
_dict['array_of_map_property'] = _items
82+
return _dict
83+
84+
@classmethod
85+
def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]:
86+
"""Create an instance of ArrayOfMapModel from a dict"""
87+
if obj is None:
88+
return None
89+
90+
if not isinstance(obj, dict):
91+
return cls.model_validate(obj)
92+
93+
_obj = cls.model_validate({
94+
"array_of_map_property": [
95+
{_inner_key: Tag.from_dict(_inner_value) for _inner_key, _inner_value in _item.items()}
96+
for _item in obj["array_of_map_property"]
97+
] if obj.get("array_of_map_property") is not None else None
98+
})
99+
return _obj
100+
101+

0 commit comments

Comments
 (0)