From fd945ae9e2629e54b06eee4d2cb217a23907671b Mon Sep 17 00:00:00 2001 From: Jonathan Doda Date: Wed, 10 Sep 2025 23:34:08 -0400 Subject: [PATCH 1/5] Handle symbols with leading numbers and convert spaces to _ in normalize_symbol. --- .../language_converters/python/common.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/openapi_python_generator/language_converters/python/common.py b/src/openapi_python_generator/language_converters/python/common.py index 72da991..49019a5 100644 --- a/src/openapi_python_generator/language_converters/python/common.py +++ b/src/openapi_python_generator/language_converters/python/common.py @@ -49,8 +49,10 @@ def normalize_symbol(symbol: str) -> str: :param symbol: name of the identifier :return: normalized identifier name """ - symbol = symbol.replace("-", "_") + symbol = symbol.replace("-", "_").replace(" ", "_") normalized_symbol = _symbol_ascii_strip_re.sub("", symbol) if normalized_symbol in keyword.kwlist: normalized_symbol = normalized_symbol + "_" + if len(normalized_symbol) > 0 and normalized_symbol[0].isnumeric(): + normalized_symbol = "_" + normalized_symbol return normalized_symbol From d8a2879ac72430cadb5a363deb0f7e740ba72efa Mon Sep 17 00:00:00 2001 From: Jonathan Doda Date: Wed, 10 Sep 2025 23:35:10 -0400 Subject: [PATCH 2/5] Use normalize_symbol to fix property names in templates. --- .../language_converters/python/jinja_config.py | 6 +++++- .../language_converters/python/templates/models.jinja2 | 2 +- .../python/templates/models_pydantic_2.jinja2 | 4 ++-- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/openapi_python_generator/language_converters/python/jinja_config.py b/src/openapi_python_generator/language_converters/python/jinja_config.py index 172ca70..48ae32c 100644 --- a/src/openapi_python_generator/language_converters/python/jinja_config.py +++ b/src/openapi_python_generator/language_converters/python/jinja_config.py @@ -16,7 +16,7 @@ def create_jinja_env(): custom_template_path = common.get_custom_template_path() - return Environment( + environment = Environment( loader=( ChoiceLoader( [ @@ -30,3 +30,7 @@ def create_jinja_env(): autoescape=True, trim_blocks=True, ) + + environment.filters["normalize_symbol"] = common.normalize_symbol + + return environment diff --git a/src/openapi_python_generator/language_converters/python/templates/models.jinja2 b/src/openapi_python_generator/language_converters/python/templates/models.jinja2 index fbf2e4e..3ff37e4 100644 --- a/src/openapi_python_generator/language_converters/python/templates/models.jinja2 +++ b/src/openapi_python_generator/language_converters/python/templates/models.jinja2 @@ -20,5 +20,5 @@ class {{ schema_name }}(BaseModel): """ {% for property in properties %} - {{ property.name | replace("@","") | replace("-","_") }} : {{ property.type.converted_type | safe }} = Field(alias="{{ property.name }}" {% if not property.required %}, default = {{ property.default }} {% endif %}) + {{ property.name | normalize_symbol }} : {{ property.type.converted_type | safe }} = Field(alias="{{ property.name }}" {% if not property.required %}, default = {{ property.default }} {% endif %}) {% endfor %} diff --git a/src/openapi_python_generator/language_converters/python/templates/models_pydantic_2.jinja2 b/src/openapi_python_generator/language_converters/python/templates/models_pydantic_2.jinja2 index 36edd3b..99bdfcd 100644 --- a/src/openapi_python_generator/language_converters/python/templates/models_pydantic_2.jinja2 +++ b/src/openapi_python_generator/language_converters/python/templates/models_pydantic_2.jinja2 @@ -24,5 +24,5 @@ class {{ schema_name }}(BaseModel): } {% for property in properties %} - {{ property.name | replace("@","") | replace("-","_") }} : {{ property.type.converted_type | safe }} = Field(validation_alias="{{ property.name }}" {% if not property.required %}, default = {{ property.default }} {% endif %}) - {% endfor %} \ No newline at end of file + {{ property.name | normalize_symbol }} : {{ property.type.converted_type | safe }} = Field(validation_alias="{{ property.name }}" {% if not property.required %}, default = {{ property.default }} {% endif %}) + {% endfor %} From db988b637aaaa8ae46408a2011dc6fedcb0b394f Mon Sep 17 00:00:00 2001 From: Jonathan Doda Date: Wed, 10 Sep 2025 23:36:46 -0400 Subject: [PATCH 3/5] Ensure enum varient idents are valid python symbols, and enum values are unaltered. Fixes https://github.com/MarcoMuellner/openapi-python-generator/issues/30 , https://github.com/MarcoMuellner/openapi-python-generator/issues/55 , and https://github.com/MarcoMuellner/openapi-python-generator/issues/87 . --- .../python/model_generator.py | 3 +- .../python/templates/enum.jinja2 | 8 ++- tests/regression/test_issue_30_87.py | 25 +++++++ tests/regression/test_issue_55.py | 24 +++++++ tests/test_data/issue_30_87.json | 70 +++++++++++++++++++ tests/test_data/issue_55.json | 70 +++++++++++++++++++ 6 files changed, 195 insertions(+), 5 deletions(-) create mode 100644 tests/regression/test_issue_30_87.py create mode 100644 tests/regression/test_issue_55.py create mode 100644 tests/test_data/issue_30_87.json create mode 100644 tests/test_data/issue_55.json diff --git a/src/openapi_python_generator/language_converters/python/model_generator.py b/src/openapi_python_generator/language_converters/python/model_generator.py index 0b7943e..8f8d9cd 100644 --- a/src/openapi_python_generator/language_converters/python/model_generator.py +++ b/src/openapi_python_generator/language_converters/python/model_generator.py @@ -414,9 +414,8 @@ def generate_models( name = common.normalize_symbol(schema_name) if schema_or_reference.enum is not None: value_dict = schema_or_reference.model_dump() - regex = re.compile(r"[\s\/=\*\+]+") value_dict["enum"] = [ - re.sub(regex, "_", i) if isinstance(i, str) else f"value_{i}" + (common.normalize_symbol(str(i)).upper(), i) for i in value_dict["enum"] ] m = Model( diff --git a/src/openapi_python_generator/language_converters/python/templates/enum.jinja2 b/src/openapi_python_generator/language_converters/python/templates/enum.jinja2 index f88bdd6..848f159 100644 --- a/src/openapi_python_generator/language_converters/python/templates/enum.jinja2 +++ b/src/openapi_python_generator/language_converters/python/templates/enum.jinja2 @@ -1,9 +1,11 @@ from enum import Enum class {{ name }}(str, Enum): - {% for enumItem in enum %} + {% for enumItemIdent, enumItem in enum %} {% if enumItem is string %} - {{ enumItem.upper() }} = '{{ enumItem }}'{% else %} - value_{{ enumItem }} = {{ enumItem }}{% endif %} + {{ enumItemIdent }} = '{{ enumItem }}' + {% else %} + {{ enumItemIdent }} = {{ enumItem }} + {% endif %} {% endfor %} diff --git a/tests/regression/test_issue_30_87.py b/tests/regression/test_issue_30_87.py new file mode 100644 index 0000000..446b65f --- /dev/null +++ b/tests/regression/test_issue_30_87.py @@ -0,0 +1,25 @@ +import pytest + +from openapi_python_generator.common import HTTPLibrary +from openapi_python_generator.generate_data import get_open_api +from openapi_python_generator.parsers import generate_code_3_1 +from tests.conftest import test_data_folder + + +@pytest.mark.parametrize( + "library", + [HTTPLibrary.httpx, HTTPLibrary.aiohttp, HTTPLibrary.requests], +) +def test_issue_30_87(library) -> None: + """ + https://github.com/MarcoMuellner/openapi-python-generator/issues/30 + https://github.com/MarcoMuellner/openapi-python-generator/issues/87 + """ + openapi_obj, version = get_open_api(str(test_data_folder / "issue_30_87.json")) + result = generate_code_3_1( + openapi_obj, # type: ignore + library + ) + + expected_model = [m for m in result.models if m.openapi_object.title == "UserType"][0] + assert "ADMIN_USER = 'admin-user'" in expected_model.content diff --git a/tests/regression/test_issue_55.py b/tests/regression/test_issue_55.py new file mode 100644 index 0000000..26b8ee0 --- /dev/null +++ b/tests/regression/test_issue_55.py @@ -0,0 +1,24 @@ +import pytest + +from openapi_python_generator.common import HTTPLibrary +from openapi_python_generator.generate_data import get_open_api +from openapi_python_generator.parsers import generate_code_3_1 +from tests.conftest import test_data_folder + + +@pytest.mark.parametrize( + "library", + [HTTPLibrary.httpx, HTTPLibrary.aiohttp, HTTPLibrary.requests], +) +def test_issue_55(library) -> None: + """ + https://github.com/MarcoMuellner/openapi-python-generator/issues/55 + """ + openapi_obj, version = get_open_api(str(test_data_folder / "issue_55.json")) + result = generate_code_3_1( + openapi_obj, # type: ignore + library + ) + + expected_model = [m for m in result.models if m.openapi_object.title == "UserType"][0] + assert "ADMIN_USER = 'admin user'" in expected_model.content diff --git a/tests/test_data/issue_30_87.json b/tests/test_data/issue_30_87.json new file mode 100644 index 0000000..4f4f15a --- /dev/null +++ b/tests/test_data/issue_30_87.json @@ -0,0 +1,70 @@ +{ + "openapi": "3.0.2", + "info": { + "title": "Title", + "version": "1.0" + }, + "paths": { + "/users": { + "get": { + "summary": "Get users", + "description": "Returns a list of users.", + "operationId": "users_get", + "parameters": [ + { + "name": "type", + "in": "query", + "required": true, + "schema": { + "$ref": "#/components/schemas/UserType" + } + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/User" + } + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "UserType": { + "title": "UserType", + "description": "An enumeration.", + "enum": ["admin-user", "regular-user"] + }, + "User": { + "title": "User", + "description": "A user.", + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "name": { + "type": "string" + }, + "type": { + "$ref": "#/components/schemas/UserType" + }, + "30d_active": { + "type": "boolean" + } + } + } + } + } +} diff --git a/tests/test_data/issue_55.json b/tests/test_data/issue_55.json new file mode 100644 index 0000000..3de3259 --- /dev/null +++ b/tests/test_data/issue_55.json @@ -0,0 +1,70 @@ +{ + "openapi": "3.0.2", + "info": { + "title": "Title", + "version": "1.0" + }, + "paths": { + "/users": { + "get": { + "summary": "Get users", + "description": "Returns a list of users.", + "operationId": "users_get", + "parameters": [ + { + "name": "type", + "in": "query", + "required": true, + "schema": { + "$ref": "#/components/schemas/UserType" + } + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/User" + } + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "UserType": { + "title": "UserType", + "description": "An enumeration.", + "enum": ["admin user", "regular user"] + }, + "User": { + "title": "User", + "description": "A user.", + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "name": { + "type": "string" + }, + "type": { + "$ref": "#/components/schemas/UserType" + }, + "30d_active": { + "type": "boolean" + } + } + } + } + } +} From 2516efd496ea692582c1c8584c19ac941c2d0e93 Mon Sep 17 00:00:00 2001 From: Jonathan Doda Date: Thu, 11 Sep 2025 08:44:13 -0400 Subject: [PATCH 4/5] Reformat with black. --- .../language_converters/python/jinja_config.py | 2 +- .../language_converters/python/model_generator.py | 3 +-- tests/regression/test_issue_30_87.py | 9 ++++----- tests/regression/test_issue_55.py | 9 ++++----- 4 files changed, 10 insertions(+), 13 deletions(-) diff --git a/src/openapi_python_generator/language_converters/python/jinja_config.py b/src/openapi_python_generator/language_converters/python/jinja_config.py index 48ae32c..1a0704b 100644 --- a/src/openapi_python_generator/language_converters/python/jinja_config.py +++ b/src/openapi_python_generator/language_converters/python/jinja_config.py @@ -16,7 +16,7 @@ def create_jinja_env(): custom_template_path = common.get_custom_template_path() - environment = Environment( + environment = Environment( loader=( ChoiceLoader( [ diff --git a/src/openapi_python_generator/language_converters/python/model_generator.py b/src/openapi_python_generator/language_converters/python/model_generator.py index 8f8d9cd..dc69617 100644 --- a/src/openapi_python_generator/language_converters/python/model_generator.py +++ b/src/openapi_python_generator/language_converters/python/model_generator.py @@ -415,8 +415,7 @@ def generate_models( if schema_or_reference.enum is not None: value_dict = schema_or_reference.model_dump() value_dict["enum"] = [ - (common.normalize_symbol(str(i)).upper(), i) - for i in value_dict["enum"] + (common.normalize_symbol(str(i)).upper(), i) for i in value_dict["enum"] ] m = Model( file_name=name, diff --git a/tests/regression/test_issue_30_87.py b/tests/regression/test_issue_30_87.py index 446b65f..7456f2f 100644 --- a/tests/regression/test_issue_30_87.py +++ b/tests/regression/test_issue_30_87.py @@ -16,10 +16,9 @@ def test_issue_30_87(library) -> None: https://github.com/MarcoMuellner/openapi-python-generator/issues/87 """ openapi_obj, version = get_open_api(str(test_data_folder / "issue_30_87.json")) - result = generate_code_3_1( - openapi_obj, # type: ignore - library - ) + result = generate_code_3_1(openapi_obj, library) # type: ignore - expected_model = [m for m in result.models if m.openapi_object.title == "UserType"][0] + expected_model = [m for m in result.models if m.openapi_object.title == "UserType"][ + 0 + ] assert "ADMIN_USER = 'admin-user'" in expected_model.content diff --git a/tests/regression/test_issue_55.py b/tests/regression/test_issue_55.py index 26b8ee0..572d610 100644 --- a/tests/regression/test_issue_55.py +++ b/tests/regression/test_issue_55.py @@ -15,10 +15,9 @@ def test_issue_55(library) -> None: https://github.com/MarcoMuellner/openapi-python-generator/issues/55 """ openapi_obj, version = get_open_api(str(test_data_folder / "issue_55.json")) - result = generate_code_3_1( - openapi_obj, # type: ignore - library - ) + result = generate_code_3_1(openapi_obj, library) # type: ignore - expected_model = [m for m in result.models if m.openapi_object.title == "UserType"][0] + expected_model = [m for m in result.models if m.openapi_object.title == "UserType"][ + 0 + ] assert "ADMIN_USER = 'admin user'" in expected_model.content From 87be4cc137e284abe4aaf7cc487868b4f69935e8 Mon Sep 17 00:00:00 2001 From: Jonathan Doda Date: Sat, 20 Sep 2025 11:17:37 -0400 Subject: [PATCH 5/5] Remove unused import. --- .../language_converters/python/model_generator.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/openapi_python_generator/language_converters/python/model_generator.py b/src/openapi_python_generator/language_converters/python/model_generator.py index dc69617..d494b1d 100644 --- a/src/openapi_python_generator/language_converters/python/model_generator.py +++ b/src/openapi_python_generator/language_converters/python/model_generator.py @@ -1,5 +1,4 @@ import itertools -import re from typing import List, Optional, Union import click