Skip to content

Commit 05fa560

Browse files
authored
[python-nextgen] fix circular reference import (#15070)
* fix ciruclar reference import in python nextgen * update samples
1 parent 3ccd9be commit 05fa560

28 files changed

Lines changed: 1230 additions & 12 deletions

File tree

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

Lines changed: 143 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,11 @@ public class PythonNextgenClientCodegen extends AbstractPythonCodegen implements
6464

6565
private String testFolder;
6666

67+
// map of set (model imports)
68+
private HashMap<String, HashSet<String>> circularImports = new HashMap<>();
69+
// map of codegen models
70+
private HashMap<String, CodegenModel> codegenModelMap = new HashMap<>();
71+
6772
public PythonNextgenClientCodegen() {
6873
super();
6974

@@ -404,14 +409,17 @@ public String getTypeDeclaration(Schema p) {
404409
* @param typingImports typing imports
405410
* @param pydantic pydantic imports
406411
* @param datetimeImports datetime imports
412+
* @param modelImports model imports
413+
* @param classname class name
407414
* @return pydantic type
408415
*
409416
*/
410417
private String getPydanticType(CodegenParameter cp,
411418
Set<String> typingImports,
412419
Set<String> pydanticImports,
413420
Set<String> datetimeImports,
414-
Set<String> modelImports) {
421+
Set<String> modelImports,
422+
String classname) {
415423
if (cp == null) {
416424
// if codegen parameter (e.g. map/dict of undefined type) is null, default to string
417425
LOGGER.warn("Codegen property is null (e.g. map/dict of undefined type). Default to typing.Any.");
@@ -432,11 +440,12 @@ private String getPydanticType(CodegenParameter cp,
432440
}
433441
pydanticImports.add("conlist");
434442
return String.format(Locale.ROOT, "conlist(%s%s)",
435-
getPydanticType(cp.items, typingImports, pydanticImports, datetimeImports, modelImports),
443+
getPydanticType(cp.items, typingImports, pydanticImports, datetimeImports, modelImports, classname),
436444
constraints);
437445
} else if (cp.isMap) {
438446
typingImports.add("Dict");
439-
return String.format(Locale.ROOT, "Dict[str, %s]", getPydanticType(cp.items, typingImports, pydanticImports, datetimeImports, modelImports));
447+
return String.format(Locale.ROOT, "Dict[str, %s]",
448+
getPydanticType(cp.items, typingImports, pydanticImports, datetimeImports, modelImports, classname));
440449
} else if (cp.isString || cp.isBinary || cp.isByteArray) {
441450
if (cp.hasValidation) {
442451
List<String> fieldCustomization = new ArrayList<>();
@@ -612,7 +621,7 @@ private String getPydanticType(CodegenParameter cp,
612621
CodegenMediaType cmt = contents.get(key);
613622
// TODO process the first one only at the moment
614623
if (cmt != null)
615-
return getPydanticType(cmt.getSchema(), typingImports, pydanticImports, datetimeImports, modelImports);
624+
return getPydanticType(cmt.getSchema(), typingImports, pydanticImports, datetimeImports, modelImports, classname);
616625
}
617626
throw new RuntimeException("Error! Failed to process getPydanticType when getting the content: " + cp);
618627
} else {
@@ -627,14 +636,17 @@ private String getPydanticType(CodegenParameter cp,
627636
* @param typingImports typing imports
628637
* @param pydantic pydantic imports
629638
* @param datetimeImports datetime imports
639+
* @param modelImports model imports
640+
* @param classname class name
630641
* @return pydantic type
631642
*
632643
*/
633644
private String getPydanticType(CodegenProperty cp,
634645
Set<String> typingImports,
635646
Set<String> pydanticImports,
636647
Set<String> datetimeImports,
637-
Set<String> modelImports) {
648+
Set<String> modelImports,
649+
String classname) {
638650
if (cp == null) {
639651
// if codegen property (e.g. map/dict of undefined type) is null, default to string
640652
LOGGER.warn("Codegen property is null (e.g. map/dict of undefined type). Default to typing.Any.");
@@ -674,11 +686,11 @@ private String getPydanticType(CodegenProperty cp,
674686
pydanticImports.add("conlist");
675687
typingImports.add("List"); // for return type
676688
return String.format(Locale.ROOT, "conlist(%s%s)",
677-
getPydanticType(cp.items, typingImports, pydanticImports, datetimeImports, modelImports),
689+
getPydanticType(cp.items, typingImports, pydanticImports, datetimeImports, modelImports, classname),
678690
constraints);
679691
} else if (cp.isMap) {
680692
typingImports.add("Dict");
681-
return String.format(Locale.ROOT, "Dict[str, %s]", getPydanticType(cp.items, typingImports, pydanticImports, datetimeImports, modelImports));
693+
return String.format(Locale.ROOT, "Dict[str, %s]", getPydanticType(cp.items, typingImports, pydanticImports, datetimeImports, modelImports, classname));
682694
} else if (cp.isString) {
683695
if (cp.hasValidation) {
684696
List<String> fieldCustomization = new ArrayList<>();
@@ -846,10 +858,24 @@ private String getPydanticType(CodegenProperty cp,
846858
typingImports.add("Any");
847859
return "Dict[str, Any]";
848860
} else if (!cp.isPrimitiveType || cp.isModel) { // model
849-
if (!cp.isCircularReference) {
850-
// skip import if it's a circular reference
861+
// skip import if it's a circular reference
862+
if (classname == null) {
863+
// for parameter model, import directly
851864
hasModelsToImport = true;
852865
modelImports.add(cp.dataType);
866+
} else {
867+
if (circularImports.containsKey(cp.dataType)) {
868+
if (circularImports.get(cp.dataType).contains(classname)) {
869+
// cp.dataType import map of set contains this model (classname), don't import
870+
LOGGER.debug("Skipped importing {} in {} due to circular import.", cp.dataType, classname);
871+
} else {
872+
// not circular import, so ok to import it
873+
hasModelsToImport = true;
874+
modelImports.add(cp.dataType);
875+
}
876+
} else {
877+
LOGGER.error("Failed to look up {} from the imports (map of set) of models.", cp.dataType);
878+
}
853879
}
854880
return cp.dataType;
855881
} else {
@@ -871,7 +897,7 @@ public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List<Mo
871897

872898
List<CodegenParameter> params = operation.allParams;
873899
for (CodegenParameter param : params) {
874-
String typing = getPydanticType(param, typingImports, pydanticImports, datetimeImports, modelImports);
900+
String typing = getPydanticType(param, typingImports, pydanticImports, datetimeImports, modelImports, null);
875901
List<String> fields = new ArrayList<>();
876902
String firstField = "";
877903

@@ -923,7 +949,7 @@ public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List<Mo
923949
// update typing import for operation return type
924950
if (!StringUtils.isEmpty(operation.returnType)) {
925951
String typing = getPydanticType(operation.returnProperty, typingImports,
926-
new TreeSet<>() /* skip pydantic import for return type */, datetimeImports, modelImports);
952+
new TreeSet<>() /* skip pydantic import for return type */, datetimeImports, modelImports, null);
927953
}
928954

929955
}
@@ -983,13 +1009,118 @@ public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List<Mo
9831009
@Override
9841010
public Map<String, ModelsMap> postProcessAllModels(Map<String, ModelsMap> objs) {
9851011
final Map<String, ModelsMap> processed = super.postProcessAllModels(objs);
1012+
1013+
for (Map.Entry<String, ModelsMap> entry : objs.entrySet()) {
1014+
// create hash map of codegen model
1015+
CodegenModel cm = ModelUtils.getModelByName(entry.getKey(), objs);
1016+
codegenModelMap.put(cm.classname, ModelUtils.getModelByName(entry.getKey(), objs));
1017+
}
1018+
1019+
// create circular import
1020+
for (String m : codegenModelMap.keySet()) {
1021+
createImportMapOfSet(m, codegenModelMap);
1022+
}
1023+
9861024
for (Map.Entry<String, ModelsMap> entry : processed.entrySet()) {
9871025
entry.setValue(postProcessModelsMap(entry.getValue()));
9881026
}
9891027

9901028
return processed;
9911029
}
9921030

1031+
/**
1032+
* Update circularImports with the model name (key) and its imports gathered recursively
1033+
*
1034+
* @param modelName model name
1035+
* @param codegenModelMap a map of CodegenModel
1036+
*/
1037+
void createImportMapOfSet(String modelName, Map<String, CodegenModel> codegenModelMap) {
1038+
HashSet<String> imports = new HashSet<>();
1039+
circularImports.put(modelName, imports);
1040+
1041+
CodegenModel cm = codegenModelMap.get(modelName);
1042+
1043+
if (cm == null) {
1044+
LOGGER.warn("Failed to lookup model in createImportMapOfSet: " + modelName);
1045+
return;
1046+
}
1047+
1048+
List<CodegenProperty> codegenProperties = null;
1049+
if (cm.oneOf != null && !cm.oneOf.isEmpty()) { // oneOf
1050+
codegenProperties = cm.getComposedSchemas().getOneOf();
1051+
} else if (cm.anyOf != null && !cm.anyOf.isEmpty()) { // anyOF
1052+
codegenProperties = cm.getComposedSchemas().getAnyOf();
1053+
} else { // typical model
1054+
codegenProperties = cm.vars;
1055+
}
1056+
1057+
for (CodegenProperty cp : codegenProperties) {
1058+
String modelNameFromDataType = getModelNameFromDataType(cp);
1059+
if (modelNameFromDataType != null) { // model
1060+
imports.add(modelNameFromDataType); // update import
1061+
// go through properties or sub-schemas of the model recursively to identify more (model) import if any
1062+
updateImportsFromCodegenModel(modelNameFromDataType, codegenModelMap.get(modelNameFromDataType), imports);
1063+
}
1064+
}
1065+
}
1066+
1067+
/**
1068+
* Update set of imports from codegen model recursivly
1069+
*
1070+
* @param modelName model name
1071+
* @param cm codegen model
1072+
* @param imports set of imports
1073+
*/
1074+
public void updateImportsFromCodegenModel(String modelName, CodegenModel cm, Set<String> imports) {
1075+
if (cm == null) {
1076+
LOGGER.warn("Failed to lookup model in createImportMapOfSet " + modelName);
1077+
return;
1078+
}
1079+
1080+
List<CodegenProperty> codegenProperties = null;
1081+
if (cm.oneOf != null && !cm.oneOf.isEmpty()) { // oneOfValidationError
1082+
codegenProperties = cm.getComposedSchemas().getOneOf();
1083+
} else if (cm.anyOf != null && !cm.anyOf.isEmpty()) { // anyOF
1084+
codegenProperties = cm.getComposedSchemas().getAnyOf();
1085+
} else { // typical model
1086+
codegenProperties = cm.vars;
1087+
}
1088+
1089+
for (CodegenProperty cp : codegenProperties) {
1090+
String modelNameFromDataType = getModelNameFromDataType(cp);
1091+
if (modelNameFromDataType != null) { // model
1092+
if (modelName.equals(modelNameFromDataType)) { // self referencing
1093+
continue;
1094+
} else if (imports.contains(modelNameFromDataType)) { // circular import
1095+
continue;
1096+
} else {
1097+
imports.add(modelNameFromDataType); // update import
1098+
// go through properties of the model recursively to identify more (model) import if any
1099+
updateImportsFromCodegenModel(modelNameFromDataType, codegenModelMap.get(modelNameFromDataType), imports);
1100+
}
1101+
}
1102+
}
1103+
}
1104+
1105+
/**
1106+
* Returns the model name (if any) from data type of codegen property.
1107+
* Returns null if it's not a model.
1108+
*
1109+
* @param cp Codegen property
1110+
* @return model name
1111+
*/
1112+
private String getModelNameFromDataType(CodegenProperty cp) {
1113+
if (cp.isArray) {
1114+
return getModelNameFromDataType(cp.items);
1115+
} else if (cp.isMap) {
1116+
return getModelNameFromDataType(cp.items);
1117+
} else if (!cp.isPrimitiveType || cp.isModel) {
1118+
return cp.dataType;
1119+
} else {
1120+
return null;
1121+
}
1122+
}
1123+
9931124
private ModelsMap postProcessModelsMap(ModelsMap objs) {
9941125
// process enum in models
9951126
objs = postProcessModelsEnum(objs);
@@ -1044,7 +1175,7 @@ private ModelsMap postProcessModelsMap(ModelsMap objs) {
10441175

10451176
//loop through properties/schemas to set up typing, pydantic
10461177
for (CodegenProperty cp : codegenProperties) {
1047-
String typing = getPydanticType(cp, typingImports, pydanticImports, datetimeImports, modelImports);
1178+
String typing = getPydanticType(cp, typingImports, pydanticImports, datetimeImports, modelImports, model.classname);
10481179
List<String> fields = new ArrayList<>();
10491180
String firstField = "";
10501181

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2126,3 +2126,24 @@ components:
21262126
properties:
21272127
optionalDict:
21282128
$ref: "#/components/schemas/DictWithAdditionalProperties"
2129+
Circular-Reference-Model:
2130+
type: object
2131+
properties:
2132+
size:
2133+
type: integer
2134+
nested:
2135+
$ref: '#/components/schemas/FirstRef'
2136+
FirstRef:
2137+
type: object
2138+
properties:
2139+
category:
2140+
type: string
2141+
self_ref:
2142+
$ref: '#/components/schemas/SecondRef'
2143+
SecondRef:
2144+
type: object
2145+
properties:
2146+
category:
2147+
type: string
2148+
circular_ref:
2149+
$ref: '#/components/schemas/Circular-Reference-Model'

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ docs/Capitalization.md
1818
docs/Cat.md
1919
docs/CatAllOf.md
2020
docs/Category.md
21+
docs/CircularReferenceModel.md
2122
docs/ClassModel.md
2223
docs/Client.md
2324
docs/Color.md
@@ -34,6 +35,7 @@ docs/FakeApi.md
3435
docs/FakeClassnameTags123Api.md
3536
docs/File.md
3637
docs/FileSchemaTestClass.md
38+
docs/FirstRef.md
3739
docs/Foo.md
3840
docs/FooGetDefaultResponse.md
3941
docs/FormatTest.md
@@ -61,6 +63,7 @@ docs/Pet.md
6163
docs/PetApi.md
6264
docs/Pig.md
6365
docs/ReadOnlyFirst.md
66+
docs/SecondRef.md
6467
docs/SelfReferenceModel.md
6568
docs/SingleRefType.md
6669
docs/SpecialCharacterEnum.md
@@ -99,6 +102,7 @@ petstore_api/models/capitalization.py
99102
petstore_api/models/cat.py
100103
petstore_api/models/cat_all_of.py
101104
petstore_api/models/category.py
105+
petstore_api/models/circular_reference_model.py
102106
petstore_api/models/class_model.py
103107
petstore_api/models/client.py
104108
petstore_api/models/color.py
@@ -112,6 +116,7 @@ petstore_api/models/enum_class.py
112116
petstore_api/models/enum_test.py
113117
petstore_api/models/file.py
114118
petstore_api/models/file_schema_test_class.py
119+
petstore_api/models/first_ref.py
115120
petstore_api/models/foo.py
116121
petstore_api/models/foo_get_default_response.py
117122
petstore_api/models/format_test.py
@@ -138,6 +143,7 @@ petstore_api/models/parent_with_optional_dict.py
138143
petstore_api/models/pet.py
139144
petstore_api/models/pig.py
140145
petstore_api/models/read_only_first.py
146+
petstore_api/models/second_ref.py
141147
petstore_api/models/self_reference_model.py
142148
petstore_api/models/single_ref_type.py
143149
petstore_api/models/special_character_enum.py

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ Class | Method | HTTP request | Description
146146
- [Cat](docs/Cat.md)
147147
- [CatAllOf](docs/CatAllOf.md)
148148
- [Category](docs/Category.md)
149+
- [CircularReferenceModel](docs/CircularReferenceModel.md)
149150
- [ClassModel](docs/ClassModel.md)
150151
- [Client](docs/Client.md)
151152
- [Color](docs/Color.md)
@@ -159,6 +160,7 @@ Class | Method | HTTP request | Description
159160
- [EnumTest](docs/EnumTest.md)
160161
- [File](docs/File.md)
161162
- [FileSchemaTestClass](docs/FileSchemaTestClass.md)
163+
- [FirstRef](docs/FirstRef.md)
162164
- [Foo](docs/Foo.md)
163165
- [FooGetDefaultResponse](docs/FooGetDefaultResponse.md)
164166
- [FormatTest](docs/FormatTest.md)
@@ -185,6 +187,7 @@ Class | Method | HTTP request | Description
185187
- [Pet](docs/Pet.md)
186188
- [Pig](docs/Pig.md)
187189
- [ReadOnlyFirst](docs/ReadOnlyFirst.md)
190+
- [SecondRef](docs/SecondRef.md)
188191
- [SelfReferenceModel](docs/SelfReferenceModel.md)
189192
- [SingleRefType](docs/SingleRefType.md)
190193
- [SpecialCharacterEnum](docs/SpecialCharacterEnum.md)
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# CircularReferenceModel
2+
3+
4+
## Properties
5+
Name | Type | Description | Notes
6+
------------ | ------------- | ------------- | -------------
7+
**size** | **int** | | [optional]
8+
**nested** | [**FirstRef**](FirstRef.md) | | [optional]
9+
10+
## Example
11+
12+
```python
13+
from petstore_api.models.circular_reference_model import CircularReferenceModel
14+
15+
# TODO update the JSON string below
16+
json = "{}"
17+
# create an instance of CircularReferenceModel from a JSON string
18+
circular_reference_model_instance = CircularReferenceModel.from_json(json)
19+
# print the JSON string representation of the object
20+
print CircularReferenceModel.to_json()
21+
22+
# convert the object into a dict
23+
circular_reference_model_dict = circular_reference_model_instance.to_dict()
24+
# create an instance of CircularReferenceModel from a dict
25+
circular_reference_model_form_dict = circular_reference_model.from_dict(circular_reference_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+

0 commit comments

Comments
 (0)