Skip to content

Commit 414319d

Browse files
author
Adam Gray
committed
Convert property and endpoint names to snake case in Python code
1 parent 5583d86 commit 414319d

File tree

18 files changed

+90
-36
lines changed

18 files changed

+90
-36
lines changed

openapi_python_client/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@
88
from pathlib import Path
99
from typing import Any, Dict, Optional
1010

11+
import stringcase
1112
import httpx
1213
import yaml
1314
from jinja2 import Environment, PackageLoader
1415

16+
from openapi_python_client import utils
1517
from .openapi_parser import OpenAPI, import_string_from_reference
1618

1719
__version__ = version(__package__)
@@ -61,6 +63,8 @@ def _get_json(*, url: Optional[str], path: Optional[Path]) -> Dict[str, Any]:
6163

6264

6365
class _Project:
66+
TEMPLATE_FILTERS = {"snakecase": utils.snake_case}
67+
6468
def __init__(self, *, openapi: OpenAPI) -> None:
6569
self.openapi: OpenAPI = openapi
6670
self.env: Environment = Environment(loader=PackageLoader(__package__), trim_blocks=True, lstrip_blocks=True)
@@ -72,6 +76,8 @@ def __init__(self, *, openapi: OpenAPI) -> None:
7276
self.package_dir: Path = self.project_dir / self.package_name
7377
self.package_description = f"A client library for accessing {self.openapi.title}"
7478

79+
self.env.filters.update(self.TEMPLATE_FILTERS)
80+
7581
def build(self) -> None:
7682
""" Create the project from templates """
7783

openapi_python_client/openapi_parser/properties.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from dataclasses import dataclass, field
22
from typing import Any, ClassVar, Dict, List, Optional
33

4+
from openapi_python_client import utils
45
from .reference import Reference
56

67

@@ -15,6 +16,10 @@ class Property:
1516
constructor_template: ClassVar[Optional[str]] = None
1617
_type_string: ClassVar[str]
1718

19+
@property
20+
def python_name(self):
21+
return utils.snake_case(self.name)
22+
1823
def get_type_string(self) -> str:
1924
""" Get a string representation of type that should be used when declaring this property """
2025
if self.required:
@@ -201,7 +206,7 @@ def get_type_string(self) -> str:
201206

202207
def transform(self) -> str:
203208
""" Convert this into a JSONable value """
204-
return f"{self.name}.to_dict()"
209+
return f"{self.python_name}.to_dict()"
205210

206211

207212
@dataclass

openapi_python_client/openapi_parser/reference.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from dataclasses import dataclass
66
from typing import Dict
77

8-
import stringcase
8+
from .. import utils
99

1010
class_overrides: Dict[str, Reference] = {}
1111

@@ -21,9 +21,9 @@ class Reference:
2121
def from_ref(ref: str) -> Reference:
2222
""" Get a Reference from the openapi #/schemas/blahblah string """
2323
ref_value = ref.split("/")[-1]
24-
class_name = stringcase.pascalcase(ref_value)
24+
class_name = utils.pascal_case(ref_value)
2525

2626
if class_name in class_overrides:
2727
return class_overrides[class_name]
2828

29-
return Reference(class_name=class_name, module_name=stringcase.snakecase(ref_value),)
29+
return Reference(class_name=class_name, module_name=utils.snake_case(ref_value),)

openapi_python_client/templates/async_endpoint_module.pyi

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ from ..errors import ApiResponseError
1212
{% for endpoint in collection.endpoints %}
1313

1414

15-
async def {{ endpoint.name }}(
15+
async def {{ endpoint.name | snakecase }}(
1616
*,
1717
{# Proper client based on whether or not the endpoint requires authentication #}
1818
{% if endpoint.requires_security %}
@@ -42,7 +42,12 @@ async def {{ endpoint.name }}(
4242
{% endfor %}
4343
]:
4444
""" {{ endpoint.description }} """
45-
url = f"{client.base_url}{{ endpoint.path }}"
45+
url = "{}{{ endpoint.path }}".format(
46+
client.base_url
47+
{%- for parameter in endpoint.path_parameters -%}
48+
,{{parameter.name}}={{parameter.python_name}}
49+
{%- endfor -%}
50+
)
4651

4752
{% if endpoint.query_parameters %}
4853
params = {
@@ -54,8 +59,8 @@ async def {{ endpoint.name }}(
5459
}
5560
{% for parameter in endpoint.query_parameters %}
5661
{% if not parameter.required %}
57-
if {{ parameter.name }} is not None:
58-
params["{{ parameter.name }}"] = {{ parameter.transform() }}
62+
if {{ parameter.python_name }} is not None:
63+
params["{{ parameter.name }}"] = str({{ parameter.transform() }})
5964
{% endif %}
6065
{% endfor %}
6166
{% endif %}
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{% if property.required %}
2-
{{ property.name }} = datetime.fromisoformat(d["{{ property.name }}"])
2+
{{ property.python_name }} = datetime.fromisoformat(d["{{ property.name }}"])
33
{% else %}
4-
{{ property.name }} = None
5-
if ({{ property.name }}_string := d.get("{{ property.name }}")) is not None:
6-
{{ property.name }} = datetime.fromisoformat(cast(str, {{ property.name }}_string))
4+
{{ property.python_name }} = None
5+
if ({{ property.python_name }}_string := d.get("{{ property.name }}")) is not None:
6+
{{ property.python_name }} = datetime.fromisoformat(cast(str, {{ property.python_name }}_string))
77
{% endif %}

openapi_python_client/templates/endpoint_module.pyi

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ from ..errors import ApiResponseError
1212
{% for endpoint in collection.endpoints %}
1313

1414

15-
def {{ endpoint.name }}(
15+
def {{ endpoint.name | snakecase }}(
1616
*,
1717
{# Proper client based on whether or not the endpoint requires authentication #}
1818
{% if endpoint.requires_security %}
@@ -42,7 +42,12 @@ def {{ endpoint.name }}(
4242
{% endfor %}
4343
]:
4444
""" {{ endpoint.description }} """
45-
url = f"{client.base_url}{{ endpoint.path }}"
45+
url = "{}{{ endpoint.path }}".format(
46+
client.base_url
47+
{%- for parameter in endpoint.path_parameters -%}
48+
,{{parameter.name}}={{parameter.python_name}}
49+
{%- endfor -%}
50+
)
4651

4752
{% if endpoint.query_parameters %}
4853
params = {
@@ -54,8 +59,8 @@ def {{ endpoint.name }}(
5459
}
5560
{% for parameter in endpoint.query_parameters %}
5661
{% if not parameter.required %}
57-
if {{ parameter.name }} is not None:
58-
params["{{ parameter.name }}"] = {{ parameter.transform() }}
62+
if {{ parameter.python_name }} is not None:
63+
params["{{ parameter.name }}"] = str({{ parameter.transform() }})
5964
{% endif %}
6065
{% endfor %}
6166
{% endif %}
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
{{ property.name }} = []
2-
for {{ property.name }}_item in d.get("{{ property.name }}", []):
3-
{{ property.name }}.append({{ property.reference.class_name }}({{ property.name }}_item))
1+
{{ property.python_name }} = []
2+
for {{ property.python_name }}_item in d.get("{{ property.name }}", []):
3+
{{ property.python_name }}.append({{ property.reference.class_name }}({{ property.python_name }}_item))

openapi_python_client/templates/model.pyi

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ class {{ schema.reference.class_name }}:
2222
"{{ property.name }}": self.{{ property.transform() }},
2323
{% endfor %}
2424
{% for property in schema.optional_properties %}
25-
"{{ property.name }}": self.{{ property.transform() }} if self.{{ property.name }} is not None else None,
26-
{% endfor %}
25+
"{{ property.name }}": self.{{ property.transform() }} if self.{{property.python_name}} is not None else None,
26+
{% endfor %}
2727
}
2828

2929
@staticmethod
@@ -33,12 +33,12 @@ class {{ schema.reference.class_name }}:
3333
{% if property.constructor_template %}
3434
{% include property.constructor_template %}
3535
{% else %}
36-
{{ property.name }} = {{ property.constructor_from_dict("d") }}
36+
{{property.python_name}} = {{property.constructor_from_dict("d")}}
3737
{% endif %}
3838

3939
{% endfor %}
4040
return {{ schema.reference.class_name }}(
4141
{% for property in schema.required_properties + schema.optional_properties %}
42-
{{ property.name }}={{ property.name }},
42+
{{ property.python_name }}={{ property.python_name }},
4343
{% endfor %}
4444
)
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{% if property.required %}
2-
{{ property.name }} = {{ property.reference.class_name }}.from_dict(d["{{ property.name }}"])
2+
{{ property.python_name }} = {{ property.reference.class_name }}.from_dict(d["{{ property.name }}"])
33
{% else %}
4-
{{ property.name }} = None
5-
if ({{ property.name }}_data := d.get("{{ property.name }}")) is not None:
6-
{{ property.name }} = {{ property.reference.class_name }}.from_dict(cast(Dict, {{ property.name }}_data))
4+
{{ property.python_name }} = None
5+
if ({{ property.python_name }}_data := d.get("{{ property.name }}")) is not None:
6+
{{ property.python_name }} = {{ property.reference.class_name }}.from_dict(cast(Dict[str, Any], {{ property.python_name }}_data))
77
{% endif %}
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
{{ property.name }} = []
2-
for {{ property.name }}_item in d.get("{{ property.name }}", []):
3-
{{ property.name }}.append({{ property.reference.class_name }}.from_dict({{ property.name }}_item))
1+
{{ property.python_name }} = []
2+
for {{ property.python_name }}_item in d.get("{{ property.python_name }}", []):
3+
{{ property.python_name }}.append({{ property.reference.class_name }}.from_dict({{ property.python_name }}_item))

openapi_python_client/utils.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import stringcase
2+
import re
3+
4+
5+
def snake_case(value: str) -> str:
6+
value = re.sub(r"([A-Z]{2,})([A-Z][a-z]|[ -_]|$)", lambda m: m.group(1).title() + m.group(2), value.strip())
7+
value = re.sub(r"(^|[ _-])([A-Z])", lambda m: m.group(1) + m.group(2).lower(), value)
8+
return stringcase.snakecase(value)
9+
10+
11+
def pascal_case(value: str) -> str:
12+
return stringcase.pascalcase(value)

tests/test_end_to_end/golden-master/my_test_api_client/api/users.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from ..client import AuthenticatedClient, Client
77
from ..errors import ApiResponseError
88
from ..models.a_model import AModel
9-
from ..models.h_t_t_p_validation_error import HTTPValidationError
9+
from ..models.http_validation_error import HTTPValidationError
1010
from ..models.statuses import Statuses
1111

1212

tests/test_end_to_end/golden-master/my_test_api_client/async_api/users.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from ..client import AuthenticatedClient, Client
77
from ..errors import ApiResponseError
88
from ..models.a_model import AModel
9-
from ..models.h_t_t_p_validation_error import HTTPValidationError
9+
from ..models.http_validation_error import HTTPValidationError
1010
from ..models.statuses import Statuses
1111

1212

tests/test_end_to_end/golden-master/my_test_api_client/models/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from .a_model import AModel
55
from .abc_response import ABCResponse
66
from .an_enum_value import AnEnumValue
7-
from .h_t_t_p_validation_error import HTTPValidationError
7+
from .http_validation_error import HTTPValidationError
88
from .other_model import OtherModel
99
from .statuses import Statuses
1010
from .validation_error import ValidationError

tests/test_end_to_end/test_end_to_end.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ def _compare_directories(first: Path, second: Path, /):
1818

1919
match, mismatch, errors = cmpfiles(first, second, dc.common_files, shallow=False)
2020
if mismatch:
21-
pytest.fail(f"{first_printable} and {second_printable} had differing files: {mismatch}", pytrace=False)
21+
for error in errors:
22+
pytest.fail(f"{first_printable} and {second_printable} had differing files: {mismatch}, first error is {error}", pytrace=False)
2223

2324
for sub_path in dc.common_dirs:
2425
_compare_directories(first / sub_path, second / sub_path)

tests/test_end_to_end/test_utils.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
from openapi_python_client import utils
2+
3+
4+
def test_snake_case_uppercase_str():
5+
assert utils.snake_case("HTTP") == "http"
6+
assert utils.snake_case("HTTP RESPONSE") == "http_response"
7+
8+
9+
def test_snake_case_from_pascal_with_acronums():
10+
assert utils.snake_case("HTTPResponse") == "http_response"
11+
assert utils.snake_case("APIClientHTTPResponse") == "api_client_http_response"
12+
assert utils.snake_case("OAuthClientHTTPResponse") == "o_auth_client_http_response"
13+
14+
15+
def test_snake_Case_from_pascal():
16+
assert utils.snake_case("HttpResponsePascalCase") == "http_response_pascal_case"
17+
18+
19+
def test_snake_case_from_camel():
20+
assert utils.snake_case("httpResponseLowerCamel") == "http_response_lower_camel"

tests/test_openapi_parser/test_openapi.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ def test__check_enums(self, mocker):
4343
from openapi_python_client.openapi_parser.properties import EnumProperty, StringProperty
4444

4545
def _make_enum():
46-
return EnumProperty(name=mocker.MagicMock(), required=True, default=None, values=mocker.MagicMock(),)
46+
return EnumProperty(name=str(mocker.MagicMock()), required=True, default=None, values=mocker.MagicMock(),)
4747

4848
# Multiple schemas with both required and optional properties for making sure iteration works correctly
4949
schema_1 = mocker.MagicMock()
@@ -119,7 +119,7 @@ def test__check_enums_bad_duplicate(self, mocker):
119119

120120
schema = mocker.MagicMock()
121121

122-
enum_1 = EnumProperty(name=mocker.MagicMock(), required=True, default=None, values=mocker.MagicMock(),)
122+
enum_1 = EnumProperty(name=str(mocker.MagicMock()), required=True, default=None, values=mocker.MagicMock(),)
123123
enum_2 = replace(enum_1, values=mocker.MagicMock())
124124
schema.required_properties = [enum_1, enum_2]
125125

0 commit comments

Comments
 (0)