Skip to content

Commit 2ef30b4

Browse files
committed
Support conflicting enum names. Closes #54
1 parent 3c7d395 commit 2ef30b4

File tree

18 files changed

+116
-209
lines changed

18 files changed

+116
-209
lines changed

CHANGELOG.md

+9-2
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
- Classes generated to be included within lists will now be named like <ListName>Item. For example, if a property
1111
named "statuses" is an array of enum values, previously the `Enum` class declared would be called "Statuses". Now it
1212
will be called "StatusesItem". If a "title" attribute was used in the OpenAPI document, that should still be respected
13-
and used instead of the generated name.
14-
- Clients now require httpx ^0.13.0 (up from ^0.12.1)
13+
and used instead of the generated name. You can restore previous names by adding "StatusesItem" to the `class_overrides`
14+
section of a config file.
15+
- Clients now require httpx ^0.13.0 (up from ^0.12.1). See [httpx release notes](https://github.com/encode/httpx/releases/tag/0.13.0)
16+
for details.
1517

1618
### Additions
1719
- Support for binary format strings (file payloads)
1820
- Support for multipart/form bodies
1921
- Support for any supported property within a list (array), including other lists.
2022
- Support for Union types ("anyOf" in OpenAPI document)
2123
- Support for more basic response types (integer, number, boolean)
24+
- Support for duplicate enums. Instead of erroring, enums with the same name (title) but differing values
25+
will have a number appended to the end. So if you have two conflicting enums named `MyEnum`, one of them
26+
will now be named `MyEnum1`. Note that the order in which these are processed and therefore named is entirely
27+
dependent on the order they are read from the OpenAPI document, so changes to the document could result
28+
in swapping the names of conflicting Enums.
2229

2330
### Changes
2431
- The way most imports are handled was changed which *should* lead to fewer unused imports in generated files.

end_to_end_tests/config.yml

+6
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,9 @@ class_overrides:
22
_ABCResponse:
33
class_name: ABCResponse
44
module_name: abc_response
5+
AnEnumValueItem:
6+
class_name: AnEnumValue
7+
module_name: an_enum_value
8+
NestedListOfEnumsItemItem:
9+
class_name: AnEnumValue
10+
module_name: an_enum_value

end_to_end_tests/fastapi_app/__init__.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@ class AnEnum(Enum):
2727
SECOND_VALUE = "SECOND_VALUE"
2828

2929

30+
class DifferentEnum(Enum):
31+
FIRST_VALUE = "DIFFERENT"
32+
SECOND_VALUE = "OTHER"
33+
34+
3035
class OtherModel(BaseModel):
3136
""" A different model for calling from TestModel """
3237

@@ -37,7 +42,7 @@ class AModel(BaseModel):
3742
""" A Model for testing all the ways custom objects can be used """
3843

3944
an_enum_value: AnEnum
40-
nested_list_of_enums: List[List[AnEnum]]
45+
nested_list_of_enums: List[List[DifferentEnum]]
4146
aCamelDateTime: Union[datetime, date]
4247
a_date: date
4348

end_to_end_tests/fastapi_app/openapi.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -164,8 +164,8 @@
164164
"type": "array",
165165
"items": {
166166
"enum": [
167-
"FIRST_VALUE",
168-
"SECOND_VALUE"
167+
"DIFFERENT",
168+
"OTHER"
169169
]
170170
}
171171
}

end_to_end_tests/golden-master/my_test_api_client/api/users.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@
77
from ..client import AuthenticatedClient, Client
88
from ..errors import ApiResponseError
99
from ..models.a_model import AModel
10-
from ..models.an_enum_value_item import AnEnumValueItem
10+
from ..models.an_enum_value import AnEnumValue
1111
from ..models.body_upload_file_tests_upload_post import BodyUploadFileTestsUploadPost
1212
from ..models.http_validation_error import HTTPValidationError
1313

1414

1515
def get_user_list(
16-
*, client: Client, an_enum_value: List[AnEnumValueItem], some_date: Union[date, datetime],
16+
*, client: Client, an_enum_value: List[AnEnumValue], some_date: Union[date, datetime],
1717
) -> Union[
1818
List[AModel], HTTPValidationError,
1919
]:

end_to_end_tests/golden-master/my_test_api_client/async_api/users.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@
77
from ..client import AuthenticatedClient, Client
88
from ..errors import ApiResponseError
99
from ..models.a_model import AModel
10-
from ..models.an_enum_value_item import AnEnumValueItem
10+
from ..models.an_enum_value import AnEnumValue
1111
from ..models.body_upload_file_tests_upload_post import BodyUploadFileTestsUploadPost
1212
from ..models.http_validation_error import HTTPValidationError
1313

1414

1515
async def get_user_list(
16-
*, client: Client, an_enum_value: List[AnEnumValueItem], some_date: Union[date, datetime],
16+
*, client: Client, an_enum_value: List[AnEnumValue], some_date: Union[date, datetime],
1717
) -> Union[
1818
List[AModel], HTTPValidationError,
1919
]:

end_to_end_tests/golden-master/my_test_api_client/models/__init__.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,8 @@
22

33
from .a_model import AModel
44
from .an_enum_value import AnEnumValue
5-
from .an_enum_value_item import AnEnumValueItem
5+
from .an_enum_value1 import AnEnumValue1
66
from .body_upload_file_tests_upload_post import BodyUploadFileTestsUploadPost
77
from .http_validation_error import HTTPValidationError
8-
from .nested_list_of_enums_item_item import NestedListOfEnumsItemItem
98
from .types import *
109
from .validation_error import ValidationError

end_to_end_tests/golden-master/my_test_api_client/models/a_model.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,15 @@
55
from typing import Any, Dict, List, Union, cast
66

77
from .an_enum_value import AnEnumValue
8-
from .nested_list_of_enums_item_item import NestedListOfEnumsItemItem
8+
from .an_enum_value1 import AnEnumValue1
99

1010

1111
@dataclass
1212
class AModel:
1313
""" A Model for testing all the ways custom objects can be used """
1414

1515
an_enum_value: AnEnumValue
16-
nested_list_of_enums: List[List[NestedListOfEnumsItemItem]]
16+
nested_list_of_enums: List[List[AnEnumValue1]]
1717
a_camel_date_time: Union[datetime, date]
1818
a_date: date
1919

@@ -53,7 +53,7 @@ def from_dict(d: Dict[str, Any]) -> AModel:
5353
for nested_list_of_enums_item_data in d["nested_list_of_enums"]:
5454
nested_list_of_enums_item = []
5555
for nested_list_of_enums_item_item_data in nested_list_of_enums_item_data:
56-
nested_list_of_enums_item_item = NestedListOfEnumsItemItem(nested_list_of_enums_item_item_data)
56+
nested_list_of_enums_item_item = AnEnumValue1(nested_list_of_enums_item_item_data)
5757

5858
nested_list_of_enums_item.append(nested_list_of_enums_item_item)
5959

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from enum import Enum
2+
3+
4+
class AnEnumValue1(str, Enum):
5+
DIFFERENT = "DIFFERENT"
6+
OTHER = "OTHER"

end_to_end_tests/golden-master/my_test_api_client/models/an_enum_value_item.py

-6
This file was deleted.

end_to_end_tests/golden-master/my_test_api_client/models/nested_list_of_enums_item_item.py

-6
This file was deleted.

openapi_python_client/openapi_parser/openapi.py

+1-41
Original file line numberDiff line numberDiff line change
@@ -220,52 +220,12 @@ class OpenAPI:
220220
endpoint_collections_by_tag: Dict[str, EndpointCollection]
221221
enums: Dict[str, EnumProperty]
222222

223-
@staticmethod
224-
def _check_enums(schemas: Iterable[Schema], collections: Iterable[EndpointCollection]) -> Dict[str, EnumProperty]:
225-
"""
226-
Create EnumProperties for every enum in any schema or collection.
227-
Enums are deduplicated by class name.
228-
229-
:raises AssertionError: if two Enums with the same name but different values are detected
230-
"""
231-
enums: Dict[str, EnumProperty] = {}
232-
233-
def _unpack_list_property(list_prop: ListProperty[Property]) -> Property:
234-
inner = list_prop.inner_property
235-
if isinstance(inner, ListProperty):
236-
return _unpack_list_property(inner)
237-
return inner
238-
239-
def _iterate_properties() -> Generator[Property, None, None]:
240-
for schema in schemas:
241-
yield from schema.required_properties
242-
yield from schema.optional_properties
243-
for collection in collections:
244-
for endpoint in collection.endpoints:
245-
yield from endpoint.path_parameters
246-
yield from endpoint.query_parameters
247-
248-
for prop in _iterate_properties():
249-
if isinstance(prop, ListProperty):
250-
prop = _unpack_list_property(prop)
251-
if not isinstance(prop, EnumProperty):
252-
continue
253-
254-
if prop.reference.class_name in enums:
255-
# We already have an enum with this name, make sure the values match
256-
assert (
257-
prop.values == enums[prop.reference.class_name].values
258-
), f"Encountered conflicting enum named {prop.reference.class_name}"
259-
260-
enums[prop.reference.class_name] = prop
261-
return enums
262-
263223
@staticmethod
264224
def from_dict(d: Dict[str, Dict[str, Any]], /) -> OpenAPI:
265225
""" Create an OpenAPI from dict """
266226
schemas = Schema.dict(d["components"]["schemas"])
267227
endpoint_collections_by_tag = EndpointCollection.from_dict(d["paths"])
268-
enums = OpenAPI._check_enums(schemas.values(), endpoint_collections_by_tag.values())
228+
enums = EnumProperty.get_all_enums()
269229

270230
return OpenAPI(
271231
title=d["info"]["title"],

openapi_python_client/openapi_parser/properties.py

+26-4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
from dataclasses import dataclass, field
1+
from __future__ import annotations
2+
3+
from dataclasses import InitVar, dataclass, field
24
from typing import Any, ClassVar, Dict, Generic, List, Optional, Set, TypeVar, Union
35

46
from openapi_python_client import utils
@@ -222,20 +224,40 @@ def get_imports(self, *, prefix: str) -> Set[str]:
222224
return imports
223225

224226

227+
_existing_enums: Dict[str, EnumProperty] = {}
228+
229+
225230
@dataclass
226231
class EnumProperty(Property):
227232
""" A property that should use an enum """
228233

229234
values: Dict[str, str]
230-
reference: Reference
235+
reference: Reference = field(init=False)
236+
title: InitVar[str]
231237

232238
template: ClassVar[str] = "enum_property.pyi"
233239

234-
def __post_init__(self) -> None:
240+
def __post_init__(self, title: str) -> None: # type: ignore
235241
super().__post_init__()
242+
reference = Reference.from_ref(title)
243+
dedup_counter = 0
244+
while reference.class_name in _existing_enums:
245+
existing = _existing_enums[reference.class_name]
246+
if self.values == existing.values:
247+
break # This is the same Enum, we're good
248+
dedup_counter += 1
249+
reference = Reference.from_ref(f"{reference.class_name}{dedup_counter}")
250+
251+
self.reference = reference
236252
inverse_values = {v: k for k, v in self.values.items()}
237253
if self.default is not None:
238254
self.default = f"{self.reference.class_name}.{inverse_values[self.default]}"
255+
_existing_enums[self.reference.class_name] = self
256+
257+
@staticmethod
258+
def get_all_enums() -> Dict[str, EnumProperty]:
259+
""" Get all the EnumProperties that have been registered keyed by class name """
260+
return _existing_enums
239261

240262
def get_type_string(self) -> str:
241263
""" Get a string representation of type that should be used when declaring this property """
@@ -345,7 +367,7 @@ def property_from_dict(name: str, required: bool, data: Dict[str, Any]) -> Prope
345367
name=name,
346368
required=required,
347369
values=EnumProperty.values_from_list(data["enum"]),
348-
reference=Reference.from_ref(data.get("title", name)),
370+
title=data.get("title", name),
349371
default=data.get("default"),
350372
)
351373
if "$ref" in data:

openapi_python_client/templates/async_endpoint_module.pyi

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ from ..errors import ApiResponseError
1111
{% endfor %}
1212
{% for endpoint in collection.endpoints %}
1313

14-
{% from "endpoint_macros.py.jinja" import query_params, json_body, return_type %}
14+
{% from "endpoint_macros.pyi" import query_params, json_body, return_type %}
1515

1616
async def {{ endpoint.name | snakecase }}(
1717
*,

openapi_python_client/templates/endpoint_module.pyi

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ from ..errors import ApiResponseError
1111
{% endfor %}
1212
{% for endpoint in collection.endpoints %}
1313

14-
{% from "endpoint_macros.py.jinja" import query_params, json_body, return_type %}
14+
{% from "endpoint_macros.pyi" import query_params, json_body, return_type %}
1515

1616
def {{ endpoint.name | snakecase }}(
1717
*,

0 commit comments

Comments
 (0)