Skip to content

Commit e7d52b6

Browse files
csymeonides-mfConstantinos Symeonides
and
Constantinos Symeonides
authored
fix: Problems with enum defaults in allOf (#363). Thanks @csymeonides-mf
* test: allOf should work without "type": "object" * test: allOf should work with enums, and get default from outer schema * fix: Get enum default from parent schema Co-authored-by: Constantinos Symeonides <[email protected]>
1 parent caf6814 commit e7d52b6

File tree

7 files changed

+144
-30
lines changed

7 files changed

+144
-30
lines changed

Diff for: end_to_end_tests/golden-record/my_test_api_client/models/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from .a_model import AModel
44
from .all_of_sub_model import AllOfSubModel
5+
from .an_all_of_enum import AnAllOfEnum
56
from .an_enum import AnEnum
67
from .an_int_enum import AnIntEnum
78
from .another_all_of_sub_model import AnotherAllOfSubModel

Diff for: end_to_end_tests/golden-record/my_test_api_client/models/a_model.py

+21
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import attr
55
from dateutil.parser import isoparse
66

7+
from ..models.an_all_of_enum import AnAllOfEnum
78
from ..models.an_enum import AnEnum
89
from ..models.different_enum import DifferentEnum
910
from ..models.free_form_model import FreeFormModel
@@ -27,6 +28,8 @@ class AModel:
2728
required_nullable: Optional[str]
2829
nullable_one_of_models: Union[FreeFormModel, ModelWithUnionProperty, None]
2930
nullable_model: Optional[ModelWithUnionProperty]
31+
an_allof_enum_with_overridden_default: AnAllOfEnum = AnAllOfEnum.OVERRIDDEN_DEFAULT
32+
an_optional_allof_enum: Union[Unset, AnAllOfEnum] = UNSET
3033
nested_list_of_enums: Union[Unset, List[List[DifferentEnum]]] = UNSET
3134
a_not_required_date: Union[Unset, datetime.date] = UNSET
3235
attr_1_leading_digit: Union[Unset, str] = UNSET
@@ -40,6 +43,8 @@ class AModel:
4043
def to_dict(self) -> Dict[str, Any]:
4144
an_enum_value = self.an_enum_value.value
4245

46+
an_allof_enum_with_overridden_default = self.an_allof_enum_with_overridden_default.value
47+
4348
if isinstance(self.a_camel_date_time, datetime.datetime):
4449
a_camel_date_time = self.a_camel_date_time.isoformat()
4550

@@ -56,6 +61,10 @@ def to_dict(self) -> Dict[str, Any]:
5661

5762
model = self.model.to_dict()
5863

64+
an_optional_allof_enum: Union[Unset, str] = UNSET
65+
if not isinstance(self.an_optional_allof_enum, Unset):
66+
an_optional_allof_enum = self.an_optional_allof_enum.value
67+
5968
nested_list_of_enums: Union[Unset, List[List[str]]] = UNSET
6069
if not isinstance(self.nested_list_of_enums, Unset):
6170
nested_list_of_enums = []
@@ -133,6 +142,7 @@ def to_dict(self) -> Dict[str, Any]:
133142
field_dict.update(
134143
{
135144
"an_enum_value": an_enum_value,
145+
"an_allof_enum_with_overridden_default": an_allof_enum_with_overridden_default,
136146
"aCamelDateTime": a_camel_date_time,
137147
"a_date": a_date,
138148
"required_not_nullable": required_not_nullable,
@@ -144,6 +154,8 @@ def to_dict(self) -> Dict[str, Any]:
144154
"nullable_model": nullable_model,
145155
}
146156
)
157+
if an_optional_allof_enum is not UNSET:
158+
field_dict["an_optional_allof_enum"] = an_optional_allof_enum
147159
if nested_list_of_enums is not UNSET:
148160
field_dict["nested_list_of_enums"] = nested_list_of_enums
149161
if a_not_required_date is not UNSET:
@@ -170,6 +182,8 @@ def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T:
170182
d = src_dict.copy()
171183
an_enum_value = AnEnum(d.pop("an_enum_value"))
172184

185+
an_allof_enum_with_overridden_default = AnAllOfEnum(d.pop("an_allof_enum_with_overridden_default"))
186+
173187
def _parse_a_camel_date_time(data: object) -> Union[datetime.date, datetime.datetime]:
174188
try:
175189
a_camel_date_time_type0: datetime.datetime
@@ -214,6 +228,11 @@ def _parse_one_of_models(data: object) -> Union[FreeFormModel, ModelWithUnionPro
214228

215229
model = ModelWithUnionProperty.from_dict(d.pop("model"))
216230

231+
an_optional_allof_enum: Union[Unset, AnAllOfEnum] = UNSET
232+
_an_optional_allof_enum = d.pop("an_optional_allof_enum", UNSET)
233+
if not isinstance(_an_optional_allof_enum, Unset):
234+
an_optional_allof_enum = AnAllOfEnum(_an_optional_allof_enum)
235+
217236
nested_list_of_enums = []
218237
_nested_list_of_enums = d.pop("nested_list_of_enums", UNSET)
219238
for nested_list_of_enums_item_data in _nested_list_of_enums or []:
@@ -350,11 +369,13 @@ def _parse_not_required_nullable_one_of_models(
350369

351370
a_model = cls(
352371
an_enum_value=an_enum_value,
372+
an_allof_enum_with_overridden_default=an_allof_enum_with_overridden_default,
353373
a_camel_date_time=a_camel_date_time,
354374
a_date=a_date,
355375
required_not_nullable=required_not_nullable,
356376
one_of_models=one_of_models,
357377
model=model,
378+
an_optional_allof_enum=an_optional_allof_enum,
358379
nested_list_of_enums=nested_list_of_enums,
359380
a_nullable_date=a_nullable_date,
360381
a_not_required_date=a_not_required_date,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from enum import Enum
2+
3+
4+
class AnAllOfEnum(str, Enum):
5+
FOO = "foo"
6+
BAR = "bar"
7+
A_DEFAULT = "a_default"
8+
OVERRIDDEN_DEFAULT = "overridden_default"
9+
10+
def __str__(self) -> str:
11+
return str(self.value)

Diff for: end_to_end_tests/openapi.json

+21-4
Original file line numberDiff line numberDiff line change
@@ -747,6 +747,7 @@
747747
"title": "AModel",
748748
"required": [
749749
"an_enum_value",
750+
"an_allof_enum_with_overridden_default",
750751
"aCamelDateTime",
751752
"a_date",
752753
"a_nullable_date",
@@ -762,6 +763,21 @@
762763
"an_enum_value": {
763764
"$ref": "#/components/schemas/AnEnum"
764765
},
766+
"an_allof_enum_with_overridden_default": {
767+
"allOf": [
768+
{
769+
"$ref": "#/components/schemas/AnAllOfEnum"
770+
}
771+
],
772+
"default": "overridden_default"
773+
},
774+
"an_optional_allof_enum": {
775+
"allOf": [
776+
{
777+
"$ref": "#/components/schemas/AnAllOfEnum"
778+
}
779+
]
780+
},
765781
"nested_list_of_enums": {
766782
"title": "Nested List Of Enums",
767783
"type": "array",
@@ -874,7 +890,6 @@
874890
"nullable": true
875891
},
876892
"model": {
877-
"type": "object",
878893
"allOf": [
879894
{
880895
"ref": "#/components/schemas/ModelWithUnionProperty"
@@ -883,7 +898,6 @@
883898
"nullable": false
884899
},
885900
"nullable_model": {
886-
"type": "object",
887901
"allOf": [
888902
{
889903
"ref": "#/components/schemas/ModelWithUnionProperty"
@@ -892,7 +906,6 @@
892906
"nullable": true
893907
},
894908
"not_required_model": {
895-
"type": "object",
896909
"allOf": [
897910
{
898911
"ref": "#/components/schemas/ModelWithUnionProperty"
@@ -901,7 +914,6 @@
901914
"nullable": false
902915
},
903916
"not_required_nullable_model": {
904-
"type": "object",
905917
"allOf": [
906918
{
907919
"ref": "#/components/schemas/ModelWithUnionProperty"
@@ -918,6 +930,11 @@
918930
"enum": ["FIRST_VALUE", "SECOND_VALUE"],
919931
"description": "For testing Enums in all the ways they can be used "
920932
},
933+
"AnAllOfEnum": {
934+
"title": "AnAllOfEnum",
935+
"enum": ["foo", "bar", "a_default", "overridden_default"],
936+
"default": "a_default"
937+
},
921938
"AnIntEnum": {
922939
"title": "AnIntEnum",
923940
"enum": [-1, 1, 2],

Diff for: end_to_end_tests/regen_golden_record.py

-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
""" Regenerate golden-record """
22
import shutil
3-
import sys
43
from pathlib import Path
54

65
from typer.testing import CliRunner

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

+36-25
Original file line numberDiff line numberDiff line change
@@ -318,30 +318,36 @@ def build_enum_property(
318318
else:
319319
return PropertyError(data=data, detail="No values provided for Enum"), schemas
320320

321-
default = None
322-
if data.default is not None:
323-
inverse_values = {v: k for k, v in values.items()}
324-
try:
325-
default = f"{class_info.name}.{inverse_values[data.default]}"
326-
except KeyError:
327-
return (
328-
PropertyError(detail=f"{data.default} is an invalid default for enum {class_info.name}", data=data),
329-
schemas,
330-
)
331-
332321
prop = EnumProperty(
333322
name=name,
334323
required=required,
335-
default=default,
336324
nullable=data.nullable,
337325
class_info=class_info,
338326
values=values,
339327
value_type=value_type,
328+
default=None,
340329
)
330+
331+
default = get_enum_default(prop, data)
332+
if isinstance(default, PropertyError):
333+
return default, schemas
334+
prop = attr.evolve(prop, default=default)
335+
341336
schemas = attr.evolve(schemas, classes_by_name={**schemas.classes_by_name, class_info.name: prop})
342337
return prop, schemas
343338

344339

340+
def get_enum_default(prop: EnumProperty, data: oai.Schema) -> Union[Optional[Any], PropertyError]:
341+
if data.default is None:
342+
return None
343+
344+
inverse_values = {v: k for k, v in prop.values.items()}
345+
try:
346+
return f"{prop.class_info.name}.{inverse_values[data.default]}"
347+
except KeyError:
348+
return PropertyError(detail=f"{data.default} is an invalid default for enum {prop.class_info.name}", data=data)
349+
350+
345351
def build_union_property(
346352
*, data: oai.Schema, name: str, required: bool, schemas: Schemas, parent_name: str, config: Config
347353
) -> Tuple[Union[UnionProperty, PropertyError], Schemas]:
@@ -397,20 +403,27 @@ def build_list_property(
397403
def _property_from_ref(
398404
name: str,
399405
required: bool,
400-
nullable: bool,
406+
parent: Union[oai.Schema, None],
401407
data: oai.Reference,
402408
schemas: Schemas,
403409
) -> Tuple[Union[Property, PropertyError], Schemas]:
404410
ref_path = parse_reference_path(data.ref)
405411
if isinstance(ref_path, ParseError):
406412
return PropertyError(data=data, detail=ref_path.detail), schemas
407413
existing = schemas.classes_by_reference.get(ref_path)
408-
if existing:
409-
return (
410-
attr.evolve(existing, required=required, name=name, nullable=nullable),
411-
schemas,
412-
)
413-
return PropertyError(data=data, detail="Could not find reference in parsed models or enums"), schemas
414+
if not existing:
415+
return PropertyError(data=data, detail="Could not find reference in parsed models or enums"), schemas
416+
417+
prop = attr.evolve(existing, required=required, name=name)
418+
if parent:
419+
prop = attr.evolve(prop, nullable=parent.nullable)
420+
if isinstance(prop, EnumProperty):
421+
default = get_enum_default(prop, parent)
422+
if isinstance(default, PropertyError):
423+
return default, schemas
424+
prop = attr.evolve(prop, default=default)
425+
426+
return prop, schemas
414427

415428

416429
def _property_from_data(
@@ -424,14 +437,12 @@ def _property_from_data(
424437
""" Generate a Property from the OpenAPI dictionary representation of it """
425438
name = utils.remove_string_escapes(name)
426439
if isinstance(data, oai.Reference):
427-
return _property_from_ref(name=name, required=required, nullable=False, data=data, schemas=schemas)
440+
return _property_from_ref(name=name, required=required, parent=None, data=data, schemas=schemas)
428441

429442
# A union of a single reference should just be passed through to that reference (don't create copy class)
430443
sub_data = (data.allOf or []) + data.anyOf + data.oneOf
431444
if len(sub_data) == 1 and isinstance(sub_data[0], oai.Reference):
432-
return _property_from_ref(
433-
name=name, required=required, nullable=data.nullable, data=sub_data[0], schemas=schemas
434-
)
445+
return _property_from_ref(name=name, required=required, parent=data, data=sub_data[0], schemas=schemas)
435446

436447
if data.enum:
437448
return build_enum_property(
@@ -443,11 +454,11 @@ def _property_from_data(
443454
parent_name=parent_name,
444455
config=config,
445456
)
446-
if data.anyOf or data.oneOf:
457+
elif data.anyOf or data.oneOf:
447458
return build_union_property(
448459
data=data, name=name, required=required, schemas=schemas, parent_name=parent_name, config=config
449460
)
450-
if data.type == "string":
461+
elif data.type == "string":
451462
return _string_based_property(name=name, required=required, data=data), schemas
452463
elif data.type == "number":
453464
return (

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

+54
Original file line numberDiff line numberDiff line change
@@ -555,6 +555,60 @@ def test_property_from_data_ref_enum(self):
555555
)
556556
assert schemas == new_schemas
557557

558+
def test_property_from_data_ref_enum_with_overridden_default(self):
559+
from openapi_python_client.parser.properties import Class, EnumProperty, Schemas, property_from_data
560+
561+
name = "some_enum"
562+
data = oai.Schema.construct(default="b", allOf=[oai.Reference.construct(ref="#/components/schemas/MyEnum")])
563+
existing_enum = EnumProperty(
564+
name="an_enum",
565+
required=True,
566+
nullable=False,
567+
default="MyEnum.A",
568+
values={"A": "a", "B": "b"},
569+
value_type=str,
570+
class_info=Class(name="MyEnum", module_name="my_enum"),
571+
)
572+
schemas = Schemas(classes_by_reference={"/components/schemas/MyEnum": existing_enum})
573+
574+
prop, new_schemas = property_from_data(
575+
name=name, required=False, data=data, schemas=schemas, parent_name="", config=Config()
576+
)
577+
578+
assert prop == EnumProperty(
579+
name="some_enum",
580+
required=False,
581+
nullable=False,
582+
default="MyEnum.B",
583+
values={"A": "a", "B": "b"},
584+
value_type=str,
585+
class_info=Class(name="MyEnum", module_name="my_enum"),
586+
)
587+
assert schemas == new_schemas
588+
589+
def test_property_from_data_ref_enum_with_invalid_default(self):
590+
from openapi_python_client.parser.properties import Class, EnumProperty, Schemas, property_from_data
591+
592+
name = "some_enum"
593+
data = oai.Schema.construct(default="x", allOf=[oai.Reference.construct(ref="#/components/schemas/MyEnum")])
594+
existing_enum = EnumProperty(
595+
name="an_enum",
596+
required=True,
597+
nullable=False,
598+
default="MyEnum.A",
599+
values={"A": "a", "B": "b"},
600+
value_type=str,
601+
class_info=Class(name="MyEnum", module_name="my_enum"),
602+
)
603+
schemas = Schemas(classes_by_reference={"/components/schemas/MyEnum": existing_enum})
604+
605+
prop, new_schemas = property_from_data(
606+
name=name, required=False, data=data, schemas=schemas, parent_name="", config=Config()
607+
)
608+
609+
assert schemas == new_schemas
610+
assert prop == PropertyError(data=data, detail="x is an invalid default for enum MyEnum")
611+
558612
def test_property_from_data_ref_model(self):
559613
from openapi_python_client.parser.properties import Class, ModelProperty, Schemas, property_from_data
560614

0 commit comments

Comments
 (0)