Skip to content

Commit b40e4d8

Browse files
Added enumUnknownDefaultCase property to python generator
1 parent 44a3be1 commit b40e4d8

File tree

3 files changed

+133
-0
lines changed

3 files changed

+133
-0
lines changed

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ public PythonClientCodegen() {
153153
cliOptions.add(new CliOption(CodegenConstants.USE_ONEOF_DISCRIMINATOR_LOOKUP, CodegenConstants.USE_ONEOF_DISCRIMINATOR_LOOKUP_DESC).defaultValue("false"));
154154
cliOptions.add(new CliOption(POETRY1_FALLBACK, "Fallback to formatting pyproject.toml to Poetry 1.x format."));
155155
cliOptions.add(new CliOption(LAZY_IMPORTS, "Enable lazy imports.").defaultValue(Boolean.FALSE.toString()));
156+
cliOptions.add(new CliOption(CodegenConstants.ENUM_UNKNOWN_DEFAULT_CASE, CodegenConstants.ENUM_UNKNOWN_DEFAULT_CASE_DESC).defaultValue("false"));
156157

157158
supportedLibraries.put("urllib3", "urllib3-based client");
158159
supportedLibraries.put("asyncio", "asyncio-based client");
@@ -271,6 +272,10 @@ public void processOpts() {
271272
additionalProperties.put(LAZY_IMPORTS, Boolean.valueOf(additionalProperties.get(LAZY_IMPORTS).toString()));
272273
}
273274

275+
if (additionalProperties.containsKey(CodegenConstants.ENUM_UNKNOWN_DEFAULT_CASE)) {
276+
setEnumUnknownDefaultCase(Boolean.parseBoolean(additionalProperties.get(CodegenConstants.ENUM_UNKNOWN_DEFAULT_CASE).toString()));
277+
}
278+
274279
String modelPath = packagePath() + File.separatorChar + modelPackage.replace('.', File.separatorChar);
275280
String apiPath = packagePath() + File.separatorChar + apiPackage.replace('.', File.separatorChar);
276281

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,13 @@ class {{classname}}({{vendorExtensions.x-py-enum-type}}, Enum):
2424
def from_json(cls, json_str: str) -> Self:
2525
"""Create an instance of {{classname}} from a JSON string"""
2626
return cls(json.loads(json_str))
27+
{{#enumUnknownDefaultCase}}
28+
29+
@classmethod
30+
def _missing_(cls, value):
31+
"""Handle unknown enum values"""
32+
return cls.UNKNOWN_DEFAULT_OPEN_API
33+
{{/enumUnknownDefaultCase}}
2734

2835
{{#defaultValue}}
2936

modules/openapi-generator/src/test/java/org/openapitools/codegen/python/PythonClientCodegenTest.java

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
import io.swagger.v3.parser.util.SchemaTypeUtil;
2727
import org.openapitools.codegen.*;
2828
import org.openapitools.codegen.config.CodegenConfigurator;
29+
import org.openapitools.codegen.model.ModelMap;
30+
import org.openapitools.codegen.model.ModelsMap;
2931
import org.openapitools.codegen.languages.PythonClientCodegen;
3032
import org.openapitools.codegen.languages.features.CXFServerFeatures;
3133
import org.testng.Assert;
@@ -38,6 +40,8 @@
3840
import java.nio.file.Paths;
3941
import java.util.ArrayList;
4042
import java.util.Arrays;
43+
import java.util.Collections;
44+
import java.util.HashMap;
4145
import java.util.List;
4246
import java.util.Map;
4347
import java.util.function.Function;
@@ -47,6 +51,7 @@
4751
import static org.junit.jupiter.api.Assertions.assertNull;
4852
import static org.openapitools.codegen.TestUtils.assertFileContains;
4953
import static org.openapitools.codegen.TestUtils.assertFileExists;
54+
import static org.openapitools.codegen.TestUtils.assertFileNotContains;
5055

5156
public class PythonClientCodegenTest {
5257

@@ -685,4 +690,120 @@ public void testNonPoetry1LicenseFormat() throws IOException {
685690
// Verify it does NOT use the legacy string format
686691
TestUtils.assertFileNotContains(pyprojectPath, "license = \"BSD-3-Clause\"");
687692
}
693+
694+
@Test(description = "test enumUnknownDefaultCase option")
695+
public void testEnumUnknownDefaultCaseOption() {
696+
final PythonClientCodegen codegen = new PythonClientCodegen();
697+
698+
// Test default value is false
699+
codegen.processOpts();
700+
Assert.assertEquals(codegen.getEnumUnknownDefaultCase(), Boolean.FALSE);
701+
702+
// Test setting via additionalProperties
703+
codegen.additionalProperties().put(CodegenConstants.ENUM_UNKNOWN_DEFAULT_CASE, "true");
704+
codegen.processOpts();
705+
Assert.assertEquals(codegen.getEnumUnknownDefaultCase(), Boolean.TRUE);
706+
}
707+
708+
@Test(description = "test enum model generation with enumUnknownDefaultCase")
709+
public void testEnumModelWithUnknownDefaultCase() {
710+
final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_0/enum_unknown_default_case.yaml");
711+
final PythonClientCodegen codegen = new PythonClientCodegen();
712+
713+
// Enable enumUnknownDefaultCase
714+
codegen.additionalProperties().put(CodegenConstants.ENUM_UNKNOWN_DEFAULT_CASE, "true");
715+
codegen.setOpenAPI(openAPI);
716+
codegen.processOpts();
717+
718+
// Verify that enumUnknownDefaultCase is set
719+
Assert.assertEquals(codegen.getEnumUnknownDefaultCase(), Boolean.TRUE);
720+
721+
// Process all models to trigger enum processing
722+
Map<String, Schema> schemas = openAPI.getComponents().getSchemas();
723+
Map<String, ModelsMap> allModels = new HashMap<>();
724+
for (String modelName : schemas.keySet()) {
725+
Schema schema = schemas.get(modelName);
726+
CodegenModel cm = codegen.fromModel(modelName, schema);
727+
ModelsMap modelsMap = new ModelsMap();
728+
modelsMap.setModels(Collections.singletonList(new ModelMap(Collections.singletonMap("model", cm))));
729+
allModels.put(modelName, modelsMap);
730+
}
731+
732+
// Post-process to add enumVars
733+
allModels = codegen.postProcessAllModels(allModels);
734+
735+
// Get the ColorEnum model
736+
CodegenModel colorEnum = null;
737+
for (Map.Entry<String, ModelsMap> entry : allModels.entrySet()) {
738+
if ("ColorEnum".equals(entry.getKey())) {
739+
colorEnum = entry.getValue().getModels().get(0).getModel();
740+
break;
741+
}
742+
}
743+
744+
Assert.assertNotNull(colorEnum);
745+
Assert.assertNotNull(colorEnum.allowableValues);
746+
747+
List<Map<String, Object>> enumVars = (List<Map<String, Object>>) colorEnum.allowableValues.get("enumVars");
748+
Assert.assertNotNull(enumVars);
749+
750+
// Check that we have the expected enum values including UNKNOWN_DEFAULT_OPEN_API
751+
Assert.assertTrue(enumVars.stream().anyMatch(var -> "'RED'".equals(var.get("value"))));
752+
Assert.assertTrue(enumVars.stream().anyMatch(var -> "'GREEN'".equals(var.get("value"))));
753+
Assert.assertTrue(enumVars.stream().anyMatch(var -> "'BLUE'".equals(var.get("value"))));
754+
Assert.assertTrue(enumVars.stream().anyMatch(var -> "'YELLOW'".equals(var.get("value"))));
755+
Assert.assertTrue(enumVars.stream().anyMatch(var -> "'unknown_default_open_api'".equals(var.get("value"))));
756+
}
757+
758+
@Test(description = "test enum generation with enumUnknownDefaultCase enabled")
759+
public void testEnumGenerationWithUnknownDefaultCase() throws IOException {
760+
File output = Files.createTempDirectory("test").toFile().getCanonicalFile();
761+
output.deleteOnExit();
762+
String outputPath = output.getAbsolutePath().replace('\\', '/');
763+
764+
final CodegenConfigurator configurator = new CodegenConfigurator()
765+
.setGeneratorName("python")
766+
.setInputSpec("src/test/resources/3_0/enum_unknown_default_case.yaml")
767+
.setOutputDir(outputPath)
768+
.addAdditionalProperty(CodegenConstants.ENUM_UNKNOWN_DEFAULT_CASE, "true");
769+
770+
DefaultGenerator generator = new DefaultGenerator();
771+
List<File> files = generator.opts(configurator.toClientOptInput()).generate();
772+
files.forEach(File::deleteOnExit);
773+
774+
Path enumFile = Paths.get(outputPath, "openapi_client", "models", "color_enum.py");
775+
776+
// Check that UNKNOWN_DEFAULT_OPEN_API is added (with single quotes as Python generates)
777+
TestUtils.assertFileContains(enumFile, "UNKNOWN_DEFAULT_OPEN_API = 'unknown_default_open_api'");
778+
779+
// Check that _missing_ method is added
780+
TestUtils.assertFileContains(enumFile, "@classmethod");
781+
TestUtils.assertFileContains(enumFile, "def _missing_(cls, value):");
782+
TestUtils.assertFileContains(enumFile, "return cls.UNKNOWN_DEFAULT_OPEN_API");
783+
}
784+
785+
@Test(description = "test enum generation with enumUnknownDefaultCase disabled")
786+
public void testEnumGenerationWithoutUnknownDefaultCase() throws IOException {
787+
File output = Files.createTempDirectory("test").toFile().getCanonicalFile();
788+
output.deleteOnExit();
789+
String outputPath = output.getAbsolutePath().replace('\\', '/');
790+
791+
final CodegenConfigurator configurator = new CodegenConfigurator()
792+
.setGeneratorName("python")
793+
.setInputSpec("src/test/resources/3_0/enum_unknown_default_case.yaml")
794+
.setOutputDir(outputPath)
795+
.addAdditionalProperty(CodegenConstants.ENUM_UNKNOWN_DEFAULT_CASE, "false");
796+
797+
DefaultGenerator generator = new DefaultGenerator();
798+
List<File> files = generator.opts(configurator.toClientOptInput()).generate();
799+
files.forEach(File::deleteOnExit);
800+
801+
Path enumFile = Paths.get(outputPath, "openapi_client", "models", "color_enum.py");
802+
803+
// Check that UNKNOWN_DEFAULT_OPEN_API is NOT added
804+
TestUtils.assertFileNotContains(enumFile, "UNKNOWN_DEFAULT_OPEN_API");
805+
806+
// Check that _missing_ method is NOT added
807+
TestUtils.assertFileNotContains(enumFile, "def _missing_(cls, value):");
808+
}
688809
}

0 commit comments

Comments
 (0)