Skip to content

Commit a761eff

Browse files
committed
refactor: Switch _PythonIdentifier to a public custom class in utils
1 parent d679f49 commit a761eff

File tree

8 files changed

+56
-64
lines changed

8 files changed

+56
-64
lines changed

Diff for: openapi_python_client/parser/openapi.py

+4-5
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
from ..config import Config
1212
from .errors import GeneratorError, ParseError, PropertyError
1313
from .properties import Class, EnumProperty, ModelProperty, Property, Schemas, build_schemas, property_from_data
14-
from .properties.property import _PythonIdentifier, to_valid_python_identifier
1514
from .responses import Response, response_from_data
1615

1716

@@ -31,9 +30,9 @@ class EndpointCollection:
3130
@staticmethod
3231
def from_data(
3332
*, data: Dict[str, oai.PathItem], schemas: Schemas, config: Config
34-
) -> Tuple[Dict[_PythonIdentifier, "EndpointCollection"], Schemas]:
33+
) -> Tuple[Dict[utils.PythonIdentifier, "EndpointCollection"], Schemas]:
3534
"""Parse the openapi paths data to get EndpointCollections by tag"""
36-
endpoints_by_tag: Dict[_PythonIdentifier, EndpointCollection] = {}
35+
endpoints_by_tag: Dict[utils.PythonIdentifier, EndpointCollection] = {}
3736

3837
methods = ["get", "put", "post", "delete", "options", "head", "patch", "trace"]
3938

@@ -42,7 +41,7 @@ def from_data(
4241
operation: Optional[oai.Operation] = getattr(path_data, method)
4342
if operation is None:
4443
continue
45-
tag = to_valid_python_identifier(value=(operation.tags or ["default"])[0], prefix="tag")
44+
tag = utils.PythonIdentifier(value=(operation.tags or ["default"])[0], prefix="tag")
4645
collection = endpoints_by_tag.setdefault(tag, EndpointCollection(tag=tag))
4746
endpoint, schemas = Endpoint.from_data(
4847
data=operation, path=path, method=method, tag=tag, schemas=schemas, config=config
@@ -341,7 +340,7 @@ class GeneratorData:
341340
version: str
342341
models: Iterator[ModelProperty]
343342
errors: List[ParseError]
344-
endpoint_collections_by_tag: Dict[_PythonIdentifier, EndpointCollection]
343+
endpoint_collections_by_tag: Dict[utils.PythonIdentifier, EndpointCollection]
345344
enums: Iterator[EnumProperty]
346345

347346
@staticmethod

Diff for: openapi_python_client/parser/properties/__init__.py

+10-10
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
from .converter import convert, convert_chain
2222
from .enum_property import EnumProperty
2323
from .model_property import ModelProperty, build_model_property
24-
from .property import Property, to_valid_python_identifier
24+
from .property import Property
2525
from .schemas import Class, Schemas, parse_reference_path, update_schemas_with_data
2626

2727

@@ -238,7 +238,7 @@ def _string_based_property(
238238
) -> Union[StringProperty, DateProperty, DateTimeProperty, FileProperty]:
239239
"""Construct a Property from the type "string" """
240240
string_format = data.schema_format
241-
python_name = to_valid_python_identifier(value=name, prefix=config.field_prefix)
241+
python_name = utils.PythonIdentifier(value=name, prefix=config.field_prefix)
242242
if string_format == "date-time":
243243
return DateTimeProperty(
244244
name=name,
@@ -330,7 +330,7 @@ def build_enum_property(
330330
values=values,
331331
value_type=value_type,
332332
default=None,
333-
python_name=to_valid_python_identifier(value=name, prefix=config.field_prefix),
333+
python_name=utils.PythonIdentifier(value=name, prefix=config.field_prefix),
334334
)
335335

336336
default = get_enum_default(prop, data)
@@ -378,7 +378,7 @@ def build_union_property(
378378
default=default,
379379
inner_properties=sub_properties,
380380
nullable=data.nullable,
381-
python_name=to_valid_python_identifier(value=name, prefix=config.field_prefix),
381+
python_name=utils.PythonIdentifier(value=name, prefix=config.field_prefix),
382382
),
383383
schemas,
384384
)
@@ -401,7 +401,7 @@ def build_list_property(
401401
default=None,
402402
inner_property=inner_prop,
403403
nullable=data.nullable,
404-
python_name=to_valid_python_identifier(value=name, prefix=config.field_prefix),
404+
python_name=utils.PythonIdentifier(value=name, prefix=config.field_prefix),
405405
),
406406
schemas,
407407
)
@@ -426,7 +426,7 @@ def _property_from_ref(
426426
existing,
427427
required=required,
428428
name=name,
429-
python_name=to_valid_python_identifier(value=name, prefix=config.field_prefix),
429+
python_name=utils.PythonIdentifier(value=name, prefix=config.field_prefix),
430430
)
431431
if parent:
432432
prop = attr.evolve(prop, nullable=parent.nullable)
@@ -482,7 +482,7 @@ def _property_from_data(
482482
default=convert("float", data.default),
483483
required=required,
484484
nullable=data.nullable,
485-
python_name=to_valid_python_identifier(value=name, prefix=config.field_prefix),
485+
python_name=utils.PythonIdentifier(value=name, prefix=config.field_prefix),
486486
),
487487
schemas,
488488
)
@@ -493,7 +493,7 @@ def _property_from_data(
493493
default=convert("int", data.default),
494494
required=required,
495495
nullable=data.nullable,
496-
python_name=to_valid_python_identifier(value=name, prefix=config.field_prefix),
496+
python_name=utils.PythonIdentifier(value=name, prefix=config.field_prefix),
497497
),
498498
schemas,
499499
)
@@ -504,7 +504,7 @@ def _property_from_data(
504504
required=required,
505505
default=convert("bool", data.default),
506506
nullable=data.nullable,
507-
python_name=to_valid_python_identifier(value=name, prefix=config.field_prefix),
507+
python_name=utils.PythonIdentifier(value=name, prefix=config.field_prefix),
508508
),
509509
schemas,
510510
)
@@ -523,7 +523,7 @@ def _property_from_data(
523523
required=required,
524524
nullable=False,
525525
default=None,
526-
python_name=to_valid_python_identifier(value=name, prefix=config.field_prefix),
526+
python_name=utils.PythonIdentifier(value=name, prefix=config.field_prefix),
527527
),
528528
schemas,
529529
)

Diff for: openapi_python_client/parser/properties/model_property.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from ... import schema as oai
88
from ... import utils
99
from ..errors import ParseError, PropertyError
10-
from .property import Property, to_valid_python_identifier
10+
from .property import Property
1111
from .schemas import Class, Schemas, parse_reference_path
1212

1313

@@ -210,7 +210,7 @@ def build_model_property(
210210
required=required,
211211
name=name,
212212
additional_properties=additional_properties,
213-
python_name=to_valid_python_identifier(value=name, prefix=config.field_prefix),
213+
python_name=utils.PythonIdentifier(value=name, prefix=config.field_prefix),
214214
)
215215
if class_info.name in schemas.classes_by_name:
216216
error = PropertyError(data=data, detail=f'Attempted to generate duplicate models with name "{class_info.name}"')

Diff for: openapi_python_client/parser/properties/property.py

+6-22
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,11 @@
1-
from typing import ClassVar, NewType, Optional, Set
1+
__all__ = ["Property"]
2+
3+
from typing import ClassVar, Optional, Set
24

35
import attr
46

57
from ... import Config
6-
from ...utils import fix_keywords, fix_reserved_words, sanitize, snake_case
7-
8-
_PythonIdentifier = NewType("_PythonIdentifier", str)
9-
10-
11-
def to_valid_python_identifier(*, value: str, prefix: str) -> _PythonIdentifier:
12-
"""
13-
Given a string, attempt to coerce it into a valid Python identifier by stripping out invalid characters and, if
14-
necessary, prepending a prefix.
15-
16-
See:
17-
https://docs.python.org/3/reference/lexical_analysis.html#identifiers
18-
"""
19-
new_value = fix_reserved_words(fix_keywords(snake_case(sanitize(value))))
20-
21-
if new_value.isidentifier():
22-
return _PythonIdentifier(new_value)
23-
24-
return _PythonIdentifier(f"{prefix}{new_value}")
8+
from ...utils import PythonIdentifier
259

2610

2711
@attr.s(auto_attribs=True, frozen=True)
@@ -45,13 +29,13 @@ class Property:
4529
_type_string: ClassVar[str] = ""
4630
_json_type_string: ClassVar[str] = "" # Type of the property after JSON serialization
4731
default: Optional[str] = attr.ib()
48-
python_name: _PythonIdentifier
32+
python_name: PythonIdentifier
4933

5034
template: ClassVar[Optional[str]] = None
5135
json_is_dict: ClassVar[bool] = False
5236

5337
def set_python_name(self, new_name: str, config: Config) -> None:
54-
object.__setattr__(self, "python_name", to_valid_python_identifier(value=new_name, prefix=config.field_prefix))
38+
object.__setattr__(self, "python_name", PythonIdentifier(value=new_name, prefix=config.field_prefix))
5539

5640
def get_base_type_string(self) -> str:
5741
return self._type_string

Diff for: openapi_python_client/parser/responses.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@
66

77
from .. import Config
88
from .. import schema as oai
9+
from ..utils import PythonIdentifier
910
from .errors import ParseError, PropertyError
1011
from .properties import AnyProperty, Property, Schemas, property_from_data
11-
from .properties.property import to_valid_python_identifier
1212

1313

1414
@attr.s(auto_attribs=True, frozen=True)
@@ -37,7 +37,7 @@ def empty_response(*, status_code: int, response_name: str, config: Config) -> R
3737
default=None,
3838
nullable=False,
3939
required=True,
40-
python_name=to_valid_python_identifier(value=response_name, prefix=config.field_prefix),
40+
python_name=PythonIdentifier(value=response_name, prefix=config.field_prefix),
4141
),
4242
source="None",
4343
)

Diff for: openapi_python_client/utils.py

+15-1
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,25 @@
11
import builtins
22
import re
33
from keyword import iskeyword
4-
from typing import List
4+
from typing import Any, List
55

66
delimiters = " _-"
77

88

9+
class PythonIdentifier(str):
10+
"""A string which has been validated / transformed into a valid identifier for Python"""
11+
12+
def __new__(cls, value: str, prefix: str) -> "PythonIdentifier":
13+
new_value = fix_reserved_words(fix_keywords(snake_case(sanitize(value))))
14+
15+
if not new_value.isidentifier():
16+
new_value = f"{prefix}{new_value}"
17+
return str.__new__(cls, new_value)
18+
19+
def __deepcopy__(self, _: Any) -> "PythonIdentifier":
20+
return self
21+
22+
923
def sanitize(value: str) -> str:
1024
"""Removes every character that isn't 0-9, A-Z, a-z, or a known delimiter"""
1125
return re.sub(rf"[^\w{delimiters}]+", "", value)

Diff for: tests/test_parser/test_properties/test_property.py

-22
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,5 @@
1-
import attr
21
import pytest
32

4-
from openapi_python_client.parser.properties.property import to_valid_python_identifier
5-
6-
7-
class TestToValidPythonIdentifier:
8-
def test_valid_identifier_is_not_changed(self):
9-
assert to_valid_python_identifier(value="valid_field", prefix="field") == "valid_field"
10-
11-
def test_numbers_are_prefixed(self):
12-
assert to_valid_python_identifier(value="1", prefix="field") == "field1"
13-
14-
def test_invalid_symbols_are_stripped(self):
15-
assert to_valid_python_identifier(value="$abc", prefix="prefix") == "abc"
16-
17-
def test_keywords_are_postfixed(self):
18-
assert to_valid_python_identifier(value="for", prefix="prefix") == "for_"
19-
20-
def test_empty_is_prefixed(self):
21-
assert to_valid_python_identifier(value="", prefix="something") == "something"
22-
233

244
class TestProperty:
255
@pytest.mark.parametrize(
@@ -68,8 +48,6 @@ def test_to_string(self, mocker, default, required, expected, property_factory):
6848
assert p.to_string() == expected
6949

7050
def test_get_imports(self, property_factory):
71-
from openapi_python_client.parser.properties import Property
72-
7351
p = property_factory()
7452
assert p.get_imports(prefix="") == set()
7553

Diff for: tests/test_utils.py

+17
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,23 @@
33
from openapi_python_client import utils
44

55

6+
class TestPythonIdentifier:
7+
def test_valid_identifier_is_not_changed(self):
8+
assert utils.PythonIdentifier(value="valid_field", prefix="field") == "valid_field"
9+
10+
def test_numbers_are_prefixed(self):
11+
assert utils.PythonIdentifier(value="1", prefix="field") == "field1"
12+
13+
def test_invalid_symbols_are_stripped(self):
14+
assert utils.PythonIdentifier(value="$abc", prefix="prefix") == "abc"
15+
16+
def test_keywords_are_postfixed(self):
17+
assert utils.PythonIdentifier(value="for", prefix="prefix") == "for_"
18+
19+
def test_empty_is_prefixed(self):
20+
assert utils.PythonIdentifier(value="", prefix="something") == "something"
21+
22+
623
@pytest.mark.parametrize(
724
"before, after",
825
[

0 commit comments

Comments
 (0)