Skip to content

Commit f40adf0

Browse files
authored
Improve handling of special characters in enum variants and property names. (#116)
1 parent 7eff32c commit f40adf0

File tree

10 files changed

+204
-12
lines changed

10 files changed

+204
-12
lines changed

src/openapi_python_generator/language_converters/python/common.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,10 @@ def normalize_symbol(symbol: str) -> str:
4949
:param symbol: name of the identifier
5050
:return: normalized identifier name
5151
"""
52-
symbol = symbol.replace("-", "_")
52+
symbol = symbol.replace("-", "_").replace(" ", "_")
5353
normalized_symbol = _symbol_ascii_strip_re.sub("", symbol)
5454
if normalized_symbol in keyword.kwlist:
5555
normalized_symbol = normalized_symbol + "_"
56+
if len(normalized_symbol) > 0 and normalized_symbol[0].isnumeric():
57+
normalized_symbol = "_" + normalized_symbol
5658
return normalized_symbol

src/openapi_python_generator/language_converters/python/jinja_config.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
def create_jinja_env():
1818
custom_template_path = common.get_custom_template_path()
19-
return Environment(
19+
environment = Environment(
2020
loader=(
2121
ChoiceLoader(
2222
[
@@ -30,3 +30,7 @@ def create_jinja_env():
3030
autoescape=True,
3131
trim_blocks=True,
3232
)
33+
34+
environment.filters["normalize_symbol"] = common.normalize_symbol
35+
36+
return environment

src/openapi_python_generator/language_converters/python/model_generator.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import itertools
2-
import re
32
from typing import List, Optional, Union
43

54
import click
@@ -414,10 +413,8 @@ def generate_models(
414413
name = common.normalize_symbol(schema_name)
415414
if schema_or_reference.enum is not None:
416415
value_dict = schema_or_reference.model_dump()
417-
regex = re.compile(r"[\s\/=\*\+]+")
418416
value_dict["enum"] = [
419-
re.sub(regex, "_", i) if isinstance(i, str) else f"value_{i}"
420-
for i in value_dict["enum"]
417+
(common.normalize_symbol(str(i)).upper(), i) for i in value_dict["enum"]
421418
]
422419
m = Model(
423420
file_name=name,
Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
from enum import Enum
22

33
class {{ name }}(str, Enum):
4-
{% for enumItem in enum %}
4+
{% for enumItemIdent, enumItem in enum %}
55

66
{% if enumItem is string %}
7-
{{ enumItem.upper() }} = '{{ enumItem }}'{% else %}
8-
value_{{ enumItem }} = {{ enumItem }}{% endif %}
7+
{{ enumItemIdent }} = '{{ enumItem }}'
8+
{% else %}
9+
{{ enumItemIdent }} = {{ enumItem }}
10+
{% endif %}
911
{% endfor %}

src/openapi_python_generator/language_converters/python/templates/models.jinja2

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,5 @@ class {{ schema_name }}(BaseModel):
2020
"""
2121
{% for property in properties %}
2222

23-
{{ property.name | replace("@","") | replace("-","_") }} : {{ property.type.converted_type | safe }} = Field(alias="{{ property.name }}" {% if not property.required %}, default = {{ property.default }} {% endif %})
23+
{{ property.name | normalize_symbol }} : {{ property.type.converted_type | safe }} = Field(alias="{{ property.name }}" {% if not property.required %}, default = {{ property.default }} {% endif %})
2424
{% endfor %}

src/openapi_python_generator/language_converters/python/templates/models_pydantic_2.jinja2

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,5 @@ class {{ schema_name }}(BaseModel):
2424
}
2525
{% for property in properties %}
2626

27-
{{ property.name | replace("@","") | replace("-","_") }} : {{ property.type.converted_type | safe }} = Field(validation_alias="{{ property.name }}" {% if not property.required %}, default = {{ property.default }} {% endif %})
28-
{% endfor %}
27+
{{ property.name | normalize_symbol }} : {{ property.type.converted_type | safe }} = Field(validation_alias="{{ property.name }}" {% if not property.required %}, default = {{ property.default }} {% endif %})
28+
{% endfor %}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import pytest
2+
3+
from openapi_python_generator.common import HTTPLibrary
4+
from openapi_python_generator.generate_data import get_open_api
5+
from openapi_python_generator.parsers import generate_code_3_1
6+
from tests.conftest import test_data_folder
7+
8+
9+
@pytest.mark.parametrize(
10+
"library",
11+
[HTTPLibrary.httpx, HTTPLibrary.aiohttp, HTTPLibrary.requests],
12+
)
13+
def test_issue_30_87(library) -> None:
14+
"""
15+
https://github.com/MarcoMuellner/openapi-python-generator/issues/30
16+
https://github.com/MarcoMuellner/openapi-python-generator/issues/87
17+
"""
18+
openapi_obj, version = get_open_api(str(test_data_folder / "issue_30_87.json"))
19+
result = generate_code_3_1(openapi_obj, library) # type: ignore
20+
21+
expected_model = [m for m in result.models if m.openapi_object.title == "UserType"][
22+
0
23+
]
24+
assert "ADMIN_USER = 'admin-user'" in expected_model.content

tests/regression/test_issue_55.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import pytest
2+
3+
from openapi_python_generator.common import HTTPLibrary
4+
from openapi_python_generator.generate_data import get_open_api
5+
from openapi_python_generator.parsers import generate_code_3_1
6+
from tests.conftest import test_data_folder
7+
8+
9+
@pytest.mark.parametrize(
10+
"library",
11+
[HTTPLibrary.httpx, HTTPLibrary.aiohttp, HTTPLibrary.requests],
12+
)
13+
def test_issue_55(library) -> None:
14+
"""
15+
https://github.com/MarcoMuellner/openapi-python-generator/issues/55
16+
"""
17+
openapi_obj, version = get_open_api(str(test_data_folder / "issue_55.json"))
18+
result = generate_code_3_1(openapi_obj, library) # type: ignore
19+
20+
expected_model = [m for m in result.models if m.openapi_object.title == "UserType"][
21+
0
22+
]
23+
assert "ADMIN_USER = 'admin user'" in expected_model.content

tests/test_data/issue_30_87.json

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
{
2+
"openapi": "3.0.2",
3+
"info": {
4+
"title": "Title",
5+
"version": "1.0"
6+
},
7+
"paths": {
8+
"/users": {
9+
"get": {
10+
"summary": "Get users",
11+
"description": "Returns a list of users.",
12+
"operationId": "users_get",
13+
"parameters": [
14+
{
15+
"name": "type",
16+
"in": "query",
17+
"required": true,
18+
"schema": {
19+
"$ref": "#/components/schemas/UserType"
20+
}
21+
}
22+
],
23+
"responses": {
24+
"200": {
25+
"description": "Successful response",
26+
"content": {
27+
"application/json": {
28+
"schema": {
29+
"type": "array",
30+
"items": {
31+
"$ref": "#/components/schemas/User"
32+
}
33+
}
34+
}
35+
}
36+
}
37+
}
38+
}
39+
}
40+
},
41+
"components": {
42+
"schemas": {
43+
"UserType": {
44+
"title": "UserType",
45+
"description": "An enumeration.",
46+
"enum": ["admin-user", "regular-user"]
47+
},
48+
"User": {
49+
"title": "User",
50+
"description": "A user.",
51+
"type": "object",
52+
"properties": {
53+
"id": {
54+
"type": "string",
55+
"format": "uuid"
56+
},
57+
"name": {
58+
"type": "string"
59+
},
60+
"type": {
61+
"$ref": "#/components/schemas/UserType"
62+
},
63+
"30d_active": {
64+
"type": "boolean"
65+
}
66+
}
67+
}
68+
}
69+
}
70+
}

tests/test_data/issue_55.json

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
{
2+
"openapi": "3.0.2",
3+
"info": {
4+
"title": "Title",
5+
"version": "1.0"
6+
},
7+
"paths": {
8+
"/users": {
9+
"get": {
10+
"summary": "Get users",
11+
"description": "Returns a list of users.",
12+
"operationId": "users_get",
13+
"parameters": [
14+
{
15+
"name": "type",
16+
"in": "query",
17+
"required": true,
18+
"schema": {
19+
"$ref": "#/components/schemas/UserType"
20+
}
21+
}
22+
],
23+
"responses": {
24+
"200": {
25+
"description": "Successful response",
26+
"content": {
27+
"application/json": {
28+
"schema": {
29+
"type": "array",
30+
"items": {
31+
"$ref": "#/components/schemas/User"
32+
}
33+
}
34+
}
35+
}
36+
}
37+
}
38+
}
39+
}
40+
},
41+
"components": {
42+
"schemas": {
43+
"UserType": {
44+
"title": "UserType",
45+
"description": "An enumeration.",
46+
"enum": ["admin user", "regular user"]
47+
},
48+
"User": {
49+
"title": "User",
50+
"description": "A user.",
51+
"type": "object",
52+
"properties": {
53+
"id": {
54+
"type": "string",
55+
"format": "uuid"
56+
},
57+
"name": {
58+
"type": "string"
59+
},
60+
"type": {
61+
"$ref": "#/components/schemas/UserType"
62+
},
63+
"30d_active": {
64+
"type": "boolean"
65+
}
66+
}
67+
}
68+
}
69+
}
70+
}

0 commit comments

Comments
 (0)