Skip to content

Commit 60b5ac9

Browse files
committed
refactor: Change null-only enums to simply be None
1 parent 70886ba commit 60b5ac9

File tree

9 files changed

+58
-109
lines changed

9 files changed

+58
-109
lines changed

end_to_end_tests/golden-record/my_test_api_client/api/tests/get_user_list.py

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
from ...models.a_model import AModel
88
from ...models.an_enum import AnEnum
99
from ...models.an_enum_with_null import AnEnumWithNull
10-
from ...models.an_enum_with_only_null import AnEnumWithOnlyNull
1110
from ...models.http_validation_error import HTTPValidationError
1211
from ...types import UNSET, Response
1312

@@ -17,7 +16,7 @@ def _get_kwargs(
1716
client: Client,
1817
an_enum_value: List[AnEnum],
1918
an_enum_value_with_null: List[Optional[AnEnumWithNull]],
20-
an_enum_value_with_only_null: List[AnEnumWithOnlyNull],
19+
an_enum_value_with_only_null: List[None],
2120
some_date: Union[datetime.date, datetime.datetime],
2221
) -> Dict[str, Any]:
2322
url = "{}/tests/".format(client.base_url)
@@ -39,11 +38,7 @@ def _get_kwargs(
3938

4039
json_an_enum_value_with_null.append(an_enum_value_with_null_item)
4140

42-
json_an_enum_value_with_only_null = []
43-
for an_enum_value_with_only_null_item_data in an_enum_value_with_only_null:
44-
an_enum_value_with_only_null_item = an_enum_value_with_only_null_item_data.value
45-
46-
json_an_enum_value_with_only_null.append(an_enum_value_with_only_null_item)
41+
json_an_enum_value_with_only_null = an_enum_value_with_only_null
4742

4843
if isinstance(some_date, datetime.date):
4944
json_some_date = some_date.isoformat()
@@ -103,7 +98,7 @@ def sync_detailed(
10398
client: Client,
10499
an_enum_value: List[AnEnum],
105100
an_enum_value_with_null: List[Optional[AnEnumWithNull]],
106-
an_enum_value_with_only_null: List[AnEnumWithOnlyNull],
101+
an_enum_value_with_only_null: List[None],
107102
some_date: Union[datetime.date, datetime.datetime],
108103
) -> Response[Union[HTTPValidationError, List[AModel]]]:
109104
kwargs = _get_kwargs(
@@ -126,7 +121,7 @@ def sync(
126121
client: Client,
127122
an_enum_value: List[AnEnum],
128123
an_enum_value_with_null: List[Optional[AnEnumWithNull]],
129-
an_enum_value_with_only_null: List[AnEnumWithOnlyNull],
124+
an_enum_value_with_only_null: List[None],
130125
some_date: Union[datetime.date, datetime.datetime],
131126
) -> Optional[Union[HTTPValidationError, List[AModel]]]:
132127
"""Get a list of things"""
@@ -145,7 +140,7 @@ async def asyncio_detailed(
145140
client: Client,
146141
an_enum_value: List[AnEnum],
147142
an_enum_value_with_null: List[Optional[AnEnumWithNull]],
148-
an_enum_value_with_only_null: List[AnEnumWithOnlyNull],
143+
an_enum_value_with_only_null: List[None],
149144
some_date: Union[datetime.date, datetime.datetime],
150145
) -> Response[Union[HTTPValidationError, List[AModel]]]:
151146
kwargs = _get_kwargs(
@@ -167,7 +162,7 @@ async def asyncio(
167162
client: Client,
168163
an_enum_value: List[AnEnum],
169164
an_enum_value_with_null: List[Optional[AnEnumWithNull]],
170-
an_enum_value_with_only_null: List[AnEnumWithOnlyNull],
165+
an_enum_value_with_only_null: List[None],
171166
some_date: Union[datetime.date, datetime.datetime],
172167
) -> Optional[Union[HTTPValidationError, List[AModel]]]:
173168
"""Get a list of things"""

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
from .an_all_of_enum import AnAllOfEnum
99
from .an_enum import AnEnum
1010
from .an_enum_with_null import AnEnumWithNull
11-
from .an_enum_with_only_null import AnEnumWithOnlyNull
1211
from .an_int_enum import AnIntEnum
1312
from .another_all_of_sub_model import AnotherAllOfSubModel
1413
from .another_all_of_sub_model_type import AnotherAllOfSubModelType

end_to_end_tests/golden-record/my_test_api_client/models/an_enum_with_null.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,3 @@ class AnEnumWithNull(str, Enum):
77

88
def __str__(self) -> str:
99
return str(self.value)
10-

end_to_end_tests/golden-record/my_test_api_client/models/an_enum_with_only_null.py

Lines changed: 0 additions & 8 deletions
This file was deleted.

openapi_python_client/__init__.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,6 @@ def _build_models(self) -> None:
227227
models_dir.mkdir()
228228
models_init = models_dir / "__init__.py"
229229
imports = []
230-
NoneType = type(None)
231230

232231
model_template = self.env.get_template("model.py.jinja")
233232
for model in self.openapi.models:
@@ -237,14 +236,11 @@ def _build_models(self) -> None:
237236

238237
# Generate enums
239238
str_enum_template = self.env.get_template("str_enum.py.jinja")
240-
null_enum_template = self.env.get_template("null_enum.py.jinja")
241239
int_enum_template = self.env.get_template("int_enum.py.jinja")
242240
for enum in self.openapi.enums:
243241
module_path = models_dir / f"{enum.class_info.module_name}.py"
244242
if enum.value_type is int:
245243
module_path.write_text(int_enum_template.render(enum=enum), encoding=self.file_encoding)
246-
elif enum.value_type is NoneType:
247-
module_path.write_text(null_enum_template.render(enum=enum), encoding=self.file_encoding)
248244
else:
249245
module_path.write_text(str_enum_template.render(enum=enum), encoding=self.file_encoding)
250246
imports.append(import_string_from_class(enum.class_info))

openapi_python_client/parser/properties/__init__.py

Lines changed: 34 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,14 @@ class AnyProperty(Property):
3434
template: ClassVar[Optional[str]] = "any_property.py.jinja"
3535

3636

37+
@attr.s(auto_attribs=True, frozen=True)
38+
class NoneProperty(Property):
39+
"""A property that can only be None"""
40+
41+
_type_string: ClassVar[str] = "None"
42+
_json_type_string: ClassVar[str] = "None"
43+
44+
3745
@attr.s(auto_attribs=True, frozen=True)
3846
class StringProperty(Property):
3947
"""A property of type str"""
@@ -299,7 +307,7 @@ def build_enum_property(
299307
enum: Union[List[Optional[str]], List[Optional[int]]],
300308
parent_name: Optional[str],
301309
config: Config,
302-
) -> Tuple[Union[EnumProperty, PropertyError], Schemas]:
310+
) -> Tuple[Union[EnumProperty, NoneProperty, PropertyError], Schemas]:
303311
"""
304312
Create an EnumProperty from schema data.
305313
@@ -316,11 +324,34 @@ def build_enum_property(
316324
A tuple containing either the created property or a PropertyError describing what went wrong AND update schemas.
317325
"""
318326

327+
if len(enum) == 0:
328+
return PropertyError(detail="No values provided for Enum", data=data), schemas
329+
319330
class_name = data.title or name
320331
if parent_name:
321332
class_name = f"{utils.pascal_case(parent_name)}{utils.pascal_case(class_name)}"
322333
class_info = Class.from_string(string=class_name, config=config)
323-
values = EnumProperty.values_from_list(enum)
334+
335+
# OpenAPI allows for null as an enum value, but it doesn't make sense with how enums are constructed in Python.
336+
# So instead, if null is a possible value, make the property nullable.
337+
# Mypy is not smart enough to know that the type is right though
338+
value_list: Union[List[str], List[int]] = [value for value in enum if value is not None] # type: ignore
339+
if len(value_list) < len(enum):
340+
data.nullable = True
341+
342+
# It's legal to have an enum that only contains null as a value, we don't bother constructing an enum in that case
343+
if len(value_list) == 0:
344+
return (
345+
NoneProperty(
346+
name=name,
347+
required=required,
348+
nullable=False,
349+
default="None",
350+
python_name=utils.PythonIdentifier(value=name, prefix=config.field_prefix),
351+
),
352+
schemas,
353+
)
354+
values = EnumProperty.values_from_list(value_list)
324355

325356
if class_info.name in schemas.classes_by_name:
326357
existing = schemas.classes_by_name[class_info.name]
@@ -332,19 +363,7 @@ def build_enum_property(
332363
schemas,
333364
)
334365

335-
# Remove None from str / int list, if present, and mark property as nullable
336-
# If list only has None, with no str or int, make special None enum instead
337-
keys_to_remove = [key for key, value in values.items() if value is None]
338-
if keys_to_remove and len(keys_to_remove) < len(values.items()):
339-
data.nullable = True
340-
for key in keys_to_remove:
341-
values.pop(key)
342-
343-
for value in values.values():
344-
value_type = type(value)
345-
break
346-
else:
347-
return PropertyError(data=data, detail="No values provided for Enum"), schemas
366+
value_type = type(next(iter(values.values())))
348367

349368
prop = EnumProperty(
350369
name=name,

openapi_python_client/parser/properties/enum_property.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from .property import Property
99
from .schemas import Class
1010

11-
ValueType = Union[str, int, None]
11+
ValueType = Union[str, int]
1212

1313

1414
@attr.s(auto_attribs=True, frozen=True)
@@ -41,7 +41,7 @@ def get_imports(self, *, prefix: str) -> Set[str]:
4141
return imports
4242

4343
@staticmethod
44-
def values_from_list(values: Union[List[Optional[str]], List[Optional[int]]]) -> Dict[str, ValueType]:
44+
def values_from_list(values: Union[List[str], List[int]]) -> Dict[str, ValueType]:
4545
"""Convert a list of values into dict of {name: value}, where value can sometimes be None"""
4646
output: Dict[str, ValueType] = {}
4747

@@ -60,6 +60,5 @@ def values_from_list(values: Union[List[Optional[str]], List[Optional[int]]]) ->
6060
if key in output:
6161
raise ValueError(f"Duplicate key {key} in Enum")
6262
sanitized_key = utils.snake_case(key).upper()
63-
output[sanitized_key] = utils.remove_string_escapes(value) if isinstance(value, str) else value
64-
# If value is the string "null", this becomes Python's None instead of a str, so must special-case it
63+
output[sanitized_key] = utils.remove_string_escapes(value)
6564
return output

openapi_python_client/templates/null_enum.py.jinja

Lines changed: 0 additions & 9 deletions
This file was deleted.

tests/test_parser/test_properties/test_init.py

Lines changed: 15 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import openapi_python_client.schema as oai
77
from openapi_python_client import Config
88
from openapi_python_client.parser.errors import PropertyError, ValidationError
9-
from openapi_python_client.parser.properties import BooleanProperty, FloatProperty, IntProperty, Schemas
9+
from openapi_python_client.parser.properties import BooleanProperty, FloatProperty, IntProperty, NoneProperty, Schemas
1010

1111
MODULE_NAME = "openapi_python_client.parser.properties"
1212

@@ -222,8 +222,6 @@ class TestEnumProperty:
222222
(True, False, "{}"),
223223
(False, True, "Union[Unset, None, {}]"),
224224
(True, True, "Optional[{}]"),
225-
(True, False, "Optional[{}]"),
226-
(True, False, "{}"),
227225
),
228226
)
229227
def test_get_type_string(self, mocker, enum_property_factory, required, nullable, expected):
@@ -275,37 +273,6 @@ def test_values_from_list_duplicate(self):
275273
with pytest.raises(ValueError):
276274
EnumProperty.values_from_list(data)
277275

278-
def test_values_from_list_with_null(self):
279-
from openapi_python_client.parser.properties import EnumProperty
280-
281-
data = ["abc", "123", "a23", "1bc", 4, -3, "a Thing WIth spaces", "", "null"]
282-
283-
result = EnumProperty.values_from_list(data)
284-
285-
# None / null is removed from result, and result is now Optional[{}]
286-
assert result == {
287-
"ABC": "abc",
288-
"VALUE_1": "123",
289-
"A23": "a23",
290-
"VALUE_3": "1bc",
291-
"VALUE_4": 4,
292-
"VALUE_NEGATIVE_3": -3,
293-
"A_THING_WITH_SPACES": "a Thing WIth spaces",
294-
"VALUE_7": "",
295-
}
296-
297-
def test_values_from_list_with_only_null(self):
298-
from openapi_python_client.parser.properties import EnumProperty
299-
300-
data = ["null"]
301-
302-
result = EnumProperty.values_from_list(data)
303-
304-
# None / null is not removed from result since it's the only value
305-
assert result == {
306-
"VALUE_0": None,
307-
}
308-
309276

310277
class TestPropertyFromData:
311278
def test_property_from_data_str_enum(self, enum_property_factory):
@@ -342,7 +309,7 @@ def test_property_from_data_str_enum_with_null(self, enum_property_factory):
342309
from openapi_python_client.schema import Schema
343310

344311
existing = enum_property_factory()
345-
data = Schema(title="AnEnumWithNull", enum=["A", "B", "C", "null"], nullable=False, default="B")
312+
data = Schema(title="AnEnum", enum=["A", "B", "C", None], nullable=False, default="B")
346313
name = "my_enum"
347314
required = True
348315

@@ -356,6 +323,7 @@ def test_property_from_data_str_enum_with_null(self, enum_property_factory):
356323
assert prop == enum_property_factory(
357324
name=name,
358325
required=required,
326+
nullable=True,
359327
values={"A": "A", "B": "B", "C": "C"},
360328
class_info=Class(name="ParentAnEnum", module_name="parent_an_enum"),
361329
value_type=str,
@@ -364,42 +332,30 @@ def test_property_from_data_str_enum_with_null(self, enum_property_factory):
364332
assert prop.nullable is True
365333
assert schemas != new_schemas, "Provided Schemas was mutated"
366334
assert new_schemas.classes_by_name == {
367-
"AnEnumWithNull": existing,
335+
"AnEnum": existing,
368336
"ParentAnEnum": prop,
369337
}
370338

371339
def test_property_from_data_null_enum(self, enum_property_factory):
372340
from openapi_python_client.parser.properties import Class, Schemas, property_from_data
373341
from openapi_python_client.schema import Schema
374342

375-
existing = enum_property_factory()
376-
data = Schema(title="AnEnumWithOnlyNull", enum=["null"], nullable=False, default=None)
343+
data = Schema(title="AnEnumWithOnlyNull", enum=[None], nullable=False, default=None)
377344
name = "my_enum"
378345
required = True
379346

380-
schemas = Schemas(classes_by_name={"AnEnum": existing})
347+
schemas = Schemas()
381348

382349
prop, new_schemas = property_from_data(
383350
name=name, required=required, data=data, schemas=schemas, parent_name="parent", config=Config()
384351
)
385352

386-
assert prop == enum_property_factory(
387-
name=name,
388-
required=required,
389-
values={"VALUE_0": None},
390-
class_info=Class(name="ParentAnEnum", module_name="parent_an_enum"),
391-
value_type=type(None),
392-
default=None,
353+
assert prop == NoneProperty(
354+
name="my_enum", required=required, nullable=False, default="None", python_name="my_enum"
393355
)
394-
assert prop.nullable is False
395-
assert schemas != new_schemas, "Provided Schemas was mutated"
396-
assert new_schemas.classes_by_name == {
397-
"AnEnumWithOnlyNull": existing,
398-
"ParentAnEnum": prop,
399-
}
400356

401357
def test_property_from_data_int_enum(self, enum_property_factory):
402-
from openapi_python_client.parser.properties import Class, EnumProperty, Schemas, property_from_data
358+
from openapi_python_client.parser.properties import Class, Schemas, property_from_data
403359
from openapi_python_client.schema import Schema
404360

405361
name = "my_enum"
@@ -1017,14 +973,17 @@ def test_retries_failing_properties_while_making_progress(self, mocker):
1017973
assert result.errors == [PropertyError()]
1018974

1019975

1020-
def test_build_enum_property_conflict(mocker):
976+
def test_build_enum_property_conflict():
1021977
from openapi_python_client.parser.properties import Schemas, build_enum_property
1022978

1023979
data = oai.Schema()
1024-
schemas = Schemas(classes_by_name={"Existing": mocker.MagicMock()})
980+
schemas = Schemas()
1025981

982+
_, schemas = build_enum_property(
983+
data=data, name="Existing", required=True, schemas=schemas, enum=["a"], parent_name=None, config=Config()
984+
)
1026985
err, schemas = build_enum_property(
1027-
data=data, name="Existing", required=True, schemas=schemas, enum=[], parent_name=None, config=Config()
986+
data=data, name="Existing", required=True, schemas=schemas, enum=["a", "b"], parent_name=None, config=Config()
1028987
)
1029988

1030989
assert schemas == schemas

0 commit comments

Comments
 (0)