diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractPythonCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractPythonCodegen.java index 3e9ab4d0ff8b..a0ae81960dfb 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractPythonCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractPythonCodegen.java @@ -216,6 +216,13 @@ public String toDefaultValue(Schema p) { if (p.getDefault() != null) { String defaultValue = String.valueOf(p.getDefault()); if (defaultValue != null) { + // String enums (type: string + enum) must be quoted: templates embed defaultValue + // directly into Query()/Header()/Cookie() calls (e.g. Query('B', ...)). Without + // quotes, values like uploadTime or desc become bare identifiers and cause NameError. + // See https://github.com/OpenAPITools/openapi-generator/issues/23774 + if (ModelUtils.isStringSchema(p)) { + return formatPythonStringLiteral(defaultValue); + } return defaultValue; } } @@ -223,13 +230,7 @@ public String toDefaultValue(Schema p) { if (p.getDefault() != null) { String defaultValue = String.valueOf(p.getDefault()); if (defaultValue != null) { - defaultValue = defaultValue.replace("\\", "\\\\") - .replace("'", "\\'"); - if (Pattern.compile("\r\n|\r|\n").matcher(defaultValue).find()) { - return "'''" + defaultValue + "'''"; - } else { - return "'" + defaultValue + "'"; - } + return formatPythonStringLiteral(defaultValue); } } } else if (ModelUtils.isArraySchema(p)) { @@ -242,6 +243,55 @@ public String toDefaultValue(Schema p) { return null; } + /** + * Format a string as a Python string literal (single-quoted). + * Shared by plain string and string-enum default value paths in {@link #toDefaultValue(Schema)}. + */ + protected String formatPythonStringLiteral(String defaultValue) { + defaultValue = defaultValue.replace("\\", "\\\\") + .replace("'", "\\'"); + if (Pattern.compile("\r\n|\r|\n").matcher(defaultValue).find()) { + return "'''" + defaultValue + "'''"; + } + return "'" + defaultValue + "'"; + } + + /** + * If {@code value} is already a Python string literal, return the unescaped inner content. + * Used when reconciling quoted defaults from {@link #toDefaultValue(Schema)} with enum-var + * matching in {@link #getEnumDefaultValue(String, String)}. + */ + protected String unwrapPythonStringLiteral(String value) { + if (value == null) { + return null; + } + if (value.length() >= 6 && value.startsWith("'''") && value.endsWith("'''")) { + return value.substring(3, value.length() - 3); + } + if (value.length() >= 2 && value.startsWith("'") && value.endsWith("'")) { + return value.substring(1, value.length() - 1) + .replace("\\'", "'") + .replace("\\\\", "\\"); + } + return null; + } + + /** + * Normalize enum default values before matching against enum member literals. + * {@link #toDefaultValue(Schema)} now quotes string-enum defaults (e.g. {@code 'B'}), but + * {@code updateCodegenPropertyEnum} compares against unquoted enum values via + * {@code toEnumValue}. Unwrap first so model properties still resolve to EnumClass.MEMBER. + */ + @Override + protected String getEnumDefaultValue(String defaultValue, String dataType) { + if (isDataTypeString(dataType)) { + String unquoted = unwrapPythonStringLiteral(defaultValue); + if (unquoted != null) { + defaultValue = unquoted; + } + } + return super.getEnumDefaultValue(defaultValue, dataType); + } @Override public String toVarName(String name) { diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/python/PythonClientCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/python/PythonClientCodegenTest.java index 6f9a00780e6d..d2c217a66833 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/python/PythonClientCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/python/PythonClientCodegenTest.java @@ -150,6 +150,26 @@ public void testBackslashDefault() { Assert.assertEquals("'\\\\'", defaultValue); } + @Test(description = "test string enum default is quoted") + public void testStringEnumDefaultIsQuoted() { + final PythonClientCodegen codegen = new PythonClientCodegen(); + StringSchema schema = new StringSchema(); + schema.setEnum(Arrays.asList("uploadTime", "duration", "fileSize")); + schema.setDefault("uploadTime"); + String defaultValue = codegen.toDefaultValue(schema); + Assert.assertEquals("'uploadTime'", defaultValue); + } + + @Test(description = "test string enum default with special chars is quoted") + public void testStringEnumDefaultFalseIsQuoted() { + final PythonClientCodegen codegen = new PythonClientCodegen(); + StringSchema schema = new StringSchema(); + schema.setEnum(Arrays.asList("true", "false")); + schema.setDefault("false"); + String defaultValue = codegen.toDefaultValue(schema); + Assert.assertEquals("'false'", defaultValue); + } + @Test(description = "convert a python model with dots") public void modelTest() { final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_0/v1beta3.yaml"); diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/python/PythonFastapiCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/python/PythonFastapiCodegenTest.java index 931d6cd41ce2..83fede778c66 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/python/PythonFastapiCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/python/PythonFastapiCodegenTest.java @@ -72,6 +72,36 @@ public void testNoAdditionalSlashesInQueryRegex() throws IOException { "r\"^[a-zA-Z0-9]+[a-zA-Z0-9\\.\\-_]*[a-zA-Z0-9]+$\""); } + @Test + public void testStringEnumQueryParameterDefaultsAreQuoted() throws IOException { + // Regression for https://github.com/OpenAPITools/openapi-generator/issues/23774 + // Repro spec: src/test/resources/3_0/parameter-test-spec.yaml (query_default_enum, etc.) + File output = Files.createTempDirectory("test").toFile(); + output.deleteOnExit(); + + final CodegenConfigurator configurator = new CodegenConfigurator() + .setGeneratorName("python-fastapi") + .setOutputDir(output.getAbsolutePath().replace("\\", "/")) + .setInputSpec("src/test/resources/3_0/parameter-test-spec.yaml"); + + DefaultGenerator generator = new DefaultGenerator(); + List files = generator.opts(configurator.toClientOptInput()).generate(); + files.forEach(File::deleteOnExit); + + final String apiFile = output + "/src/openapi_server/apis/default_api.py"; + TestUtils.assertFileContains(Paths.get(apiFile), + "= Query('available', description=\"query default\", alias=\"query_default\"),"); + TestUtils.assertFileContains(Paths.get(apiFile), + "= Query('B', description=\"query default enum\", alias=\"query_default_enum\"),"); + TestUtils.assertFileContains(Paths.get(apiFile), + "= Header('B', description=\"header default enum\"),"); + TestUtils.assertFileContains(Paths.get(apiFile), + "= Cookie('B', description=\"cookie default enum\"),"); + TestUtils.assertFileNotContains(Paths.get(apiFile), "= Query(B, description=\"query default enum\""); + TestUtils.assertFileNotContains(Paths.get(apiFile), "= Header(B, description=\"header default enum\""); + TestUtils.assertFileNotContains(Paths.get(apiFile), "= Cookie(B, description=\"cookie default enum\""); + } + @Test public void testRegexPatternCheckedForArrayItems() throws IOException { File output = Files.createTempDirectory("test").toFile(); diff --git a/samples/openapi3/client/petstore/python-aiohttp/docs/FakeApi.md b/samples/openapi3/client/petstore/python-aiohttp/docs/FakeApi.md index 8f79394afc28..2c434569b655 100644 --- a/samples/openapi3/client/petstore/python-aiohttp/docs/FakeApi.md +++ b/samples/openapi3/client/petstore/python-aiohttp/docs/FakeApi.md @@ -131,7 +131,7 @@ configuration = petstore_api.Configuration( async with petstore_api.ApiClient(configuration) as api_client: # Create an instance of the API class api_instance = petstore_api.FakeApi(api_client) - enum_ref = -efg # EnumClass | enum reference (optional) (default to -efg) + enum_ref = '-efg' # EnumClass | enum reference (optional) (default to '-efg') try: # test enum reference query parameter @@ -147,7 +147,7 @@ async with petstore_api.ApiClient(configuration) as api_client: Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- - **enum_ref** | [**EnumClass**](.md)| enum reference | [optional] [default to -efg] + **enum_ref** | [**EnumClass**](.md)| enum reference | [optional] [default to '-efg'] ### Return type diff --git a/samples/openapi3/client/petstore/python-httpx-sync/docs/FakeApi.md b/samples/openapi3/client/petstore/python-httpx-sync/docs/FakeApi.md index 5114a50b06fd..4ef59dc7aa36 100644 --- a/samples/openapi3/client/petstore/python-httpx-sync/docs/FakeApi.md +++ b/samples/openapi3/client/petstore/python-httpx-sync/docs/FakeApi.md @@ -135,7 +135,7 @@ configuration = petstore_api.Configuration( async with petstore_api.ApiClient(configuration) as api_client: # Create an instance of the API class api_instance = petstore_api.FakeApi(api_client) - enum_ref = -efg # EnumClass | enum reference (optional) (default to -efg) + enum_ref = '-efg' # EnumClass | enum reference (optional) (default to '-efg') try: # test enum reference query parameter @@ -151,7 +151,7 @@ async with petstore_api.ApiClient(configuration) as api_client: Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- - **enum_ref** | [**EnumClass**](.md)| enum reference | [optional] [default to -efg] + **enum_ref** | [**EnumClass**](.md)| enum reference | [optional] [default to '-efg'] ### Return type diff --git a/samples/openapi3/client/petstore/python-httpx/docs/FakeApi.md b/samples/openapi3/client/petstore/python-httpx/docs/FakeApi.md index 8f79394afc28..2c434569b655 100644 --- a/samples/openapi3/client/petstore/python-httpx/docs/FakeApi.md +++ b/samples/openapi3/client/petstore/python-httpx/docs/FakeApi.md @@ -131,7 +131,7 @@ configuration = petstore_api.Configuration( async with petstore_api.ApiClient(configuration) as api_client: # Create an instance of the API class api_instance = petstore_api.FakeApi(api_client) - enum_ref = -efg # EnumClass | enum reference (optional) (default to -efg) + enum_ref = '-efg' # EnumClass | enum reference (optional) (default to '-efg') try: # test enum reference query parameter @@ -147,7 +147,7 @@ async with petstore_api.ApiClient(configuration) as api_client: Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- - **enum_ref** | [**EnumClass**](.md)| enum reference | [optional] [default to -efg] + **enum_ref** | [**EnumClass**](.md)| enum reference | [optional] [default to '-efg'] ### Return type diff --git a/samples/openapi3/client/petstore/python-lazyImports/docs/FakeApi.md b/samples/openapi3/client/petstore/python-lazyImports/docs/FakeApi.md index 7ef2af8af2c3..813bc5e71df5 100644 --- a/samples/openapi3/client/petstore/python-lazyImports/docs/FakeApi.md +++ b/samples/openapi3/client/petstore/python-lazyImports/docs/FakeApi.md @@ -131,7 +131,7 @@ configuration = petstore_api.Configuration( with petstore_api.ApiClient(configuration) as api_client: # Create an instance of the API class api_instance = petstore_api.FakeApi(api_client) - enum_ref = -efg # EnumClass | enum reference (optional) (default to -efg) + enum_ref = '-efg' # EnumClass | enum reference (optional) (default to '-efg') try: # test enum reference query parameter @@ -147,7 +147,7 @@ with petstore_api.ApiClient(configuration) as api_client: Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- - **enum_ref** | [**EnumClass**](.md)| enum reference | [optional] [default to -efg] + **enum_ref** | [**EnumClass**](.md)| enum reference | [optional] [default to '-efg'] ### Return type diff --git a/samples/openapi3/client/petstore/python/docs/FakeApi.md b/samples/openapi3/client/petstore/python/docs/FakeApi.md index 7ef2af8af2c3..813bc5e71df5 100644 --- a/samples/openapi3/client/petstore/python/docs/FakeApi.md +++ b/samples/openapi3/client/petstore/python/docs/FakeApi.md @@ -131,7 +131,7 @@ configuration = petstore_api.Configuration( with petstore_api.ApiClient(configuration) as api_client: # Create an instance of the API class api_instance = petstore_api.FakeApi(api_client) - enum_ref = -efg # EnumClass | enum reference (optional) (default to -efg) + enum_ref = '-efg' # EnumClass | enum reference (optional) (default to '-efg') try: # test enum reference query parameter @@ -147,7 +147,7 @@ with petstore_api.ApiClient(configuration) as api_client: Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- - **enum_ref** | [**EnumClass**](.md)| enum reference | [optional] [default to -efg] + **enum_ref** | [**EnumClass**](.md)| enum reference | [optional] [default to '-efg'] ### Return type