From 030895be6959c523ceddfebbb02b3b443f131b78 Mon Sep 17 00:00:00 2001 From: Jerry Date: Fri, 10 Jul 2026 06:19:38 +0800 Subject: [PATCH 1/2] [python-fastapi] Quote string enum defaults in Query/Header/Cookie parameters String enums with a default were emitted as bare identifiers (e.g. Query(uploadTime, ...)), causing NameError at import. Quote defaults like plain strings and unwrap before enum member matching so model property defaults still resolve correctly. Fixes #23774 --- .../languages/AbstractPythonCodegen.java | 64 +++++++++++++++++-- .../python/PythonClientCodegenTest.java | 20 ++++++ .../python/PythonFastapiCodegenTest.java | 30 +++++++++ 3 files changed, 107 insertions(+), 7 deletions(-) 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(); From b4a8488c1eb6312d88a49dfd7371feea94a5875b Mon Sep 17 00:00:00 2001 From: Jerry Date: Fri, 10 Jul 2026 06:24:25 +0800 Subject: [PATCH 2/2] [python] Regenerate samples after string enum default quoting fix Update FakeApi.md examples to show quoted enum default values in Python client docs, matching the corrected toDefaultValue() output. Co-authored-by: Cursor --- .../openapi3/client/petstore/python-aiohttp/docs/FakeApi.md | 4 ++-- .../client/petstore/python-httpx-sync/docs/FakeApi.md | 4 ++-- samples/openapi3/client/petstore/python-httpx/docs/FakeApi.md | 4 ++-- .../client/petstore/python-lazyImports/docs/FakeApi.md | 4 ++-- samples/openapi3/client/petstore/python/docs/FakeApi.md | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) 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