Skip to content

Commit 853eb9c

Browse files
authored
eng: fix bug in type generation for nullable/union props BNCH-111776 (#219)
* fix bug in type generation for nullable props BNCH-111776 * functional tests for union type fix * misc fixes * clarify nullable special-case logic * further clarification
1 parent b758daa commit 853eb9c

18 files changed

+443
-140
lines changed

end_to_end_tests/functional_tests/generated_code_execution/test_defaults.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,3 +112,35 @@ def test_enum_default(self, MyEnum, MyModel):
112112
class TestLiteralEnumDefaults:
113113
def test_default_value(self, MyModel):
114114
assert MyModel().enum_prop == "A"
115+
116+
117+
@with_generated_client_fixture(
118+
"""
119+
# Test the ability to specify a default value for a union type as long as that value is
120+
# supported by at least one of the variants
121+
122+
components:
123+
schemas:
124+
MyModel:
125+
type: object
126+
properties:
127+
simpleTypeProp1:
128+
type: ["integer", "boolean", "string"]
129+
default: 3
130+
simpleTypeProp2:
131+
type: ["integer", "boolean", "string"]
132+
default: true
133+
simpleTypeProp3:
134+
type: ["integer", "boolean", "string"]
135+
default: abc
136+
"""
137+
)
138+
@with_generated_code_imports(".models.MyModel")
139+
class TestUnionDefaults:
140+
def test_simple_type(self, MyModel):
141+
instance = MyModel()
142+
assert instance == MyModel(
143+
simple_type_prop_1=3,
144+
simple_type_prop_2=True,
145+
simple_type_prop_3="abc",
146+
)

end_to_end_tests/functional_tests/generated_code_execution/test_enums_and_consts.py

Lines changed: 14 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -133,47 +133,23 @@ def test_invalid_values(self, MyModel):
133133
"""
134134
components:
135135
schemas:
136-
MyEnum:
137-
type: string
138-
enum: ["a", "b"]
139-
MyEnumIncludingNull:
140-
type: ["string", "null"]
141-
enum: ["a", "b", null]
142-
MyNullOnlyEnum:
136+
EnumOfNullOnly:
143137
enum: [null]
144138
MyModel:
145139
properties:
146-
nullableEnumProp:
147-
oneOf:
148-
- {"$ref": "#/components/schemas/MyEnum"}
149-
- type: "null"
150-
enumIncludingNullProp: {"$ref": "#/components/schemas/MyEnumIncludingNull"}
151-
nullOnlyEnumProp: {"$ref": "#/components/schemas/MyNullOnlyEnum"}
140+
nullOnlyEnumProp: {"$ref": "#/components/schemas/EnumOfNullOnly"}
141+
required: ["nullOnlyEnumProp"]
152142
""")
153143
@with_generated_code_imports(
154-
".models.MyEnum",
155-
".models.MyEnumIncludingNullType1", # see comment in test_nullable_enum_prop
156144
".models.MyModel",
157-
".types.Unset",
158145
)
159-
class TestNullableEnums:
160-
def test_nullable_enum_prop(self, MyModel, MyEnum, MyEnumIncludingNullType1):
161-
# Note, MyEnumIncludingNullType1 should be named just MyEnumIncludingNull -
162-
# known bug: https://github.com/openapi-generators/openapi-python-client/issues/1120
163-
assert_model_decode_encode(MyModel, {"nullableEnumProp": "b"}, MyModel(nullable_enum_prop=MyEnum.B))
164-
assert_model_decode_encode(MyModel, {"nullableEnumProp": None}, MyModel(nullable_enum_prop=None))
165-
assert_model_decode_encode(
166-
MyModel,
167-
{"enumIncludingNullProp": "a"},
168-
MyModel(enum_including_null_prop=MyEnumIncludingNullType1.A),
169-
)
170-
assert_model_decode_encode( MyModel, {"enumIncludingNullProp": None}, MyModel(enum_including_null_prop=None))
146+
class TestSingleValueNullEnum:
147+
def test_enum_of_null_only(self, MyModel):
171148
assert_model_decode_encode(MyModel, {"nullOnlyEnumProp": None}, MyModel(null_only_enum_prop=None))
172149

173-
def test_type_hints(self, MyModel, MyEnum, Unset):
174-
expected_type = Union[MyEnum, None, Unset]
175-
assert_model_property_type_hint(MyModel, "nullable_enum_prop", expected_type)
176-
150+
def test_type_hints(self, MyModel):
151+
assert_model_property_type_hint(MyModel, "null_only_enum_prop", None)
152+
177153

178154
@with_generated_client_fixture(
179155
"""
@@ -217,6 +193,8 @@ def test_invalid_int(self, MyModel):
217193

218194
@with_generated_client_fixture(
219195
"""
196+
# Tests of literal_enums mode, where enums become a typing.Literal type instead of a class
197+
220198
components:
221199
schemas:
222200
MyEnum:
@@ -261,6 +239,8 @@ def test_invalid_values(self, MyModel):
261239

262240
@with_generated_client_fixture(
263241
"""
242+
# Tests of literal_enums mode, where enums become a typing.Literal type instead of a class
243+
264244
components:
265245
schemas:
266246
MyEnum:
@@ -305,6 +285,8 @@ def test_invalid_values(self, MyModel):
305285

306286
@with_generated_client_fixture(
307287
"""
288+
# Similar to some of the "union with null" tests in test_unions.py, but in literal_enums mode
289+
308290
components:
309291
schemas:
310292
MyEnum:

end_to_end_tests/functional_tests/generated_code_execution/test_unions.py

Lines changed: 227 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -12,32 +12,8 @@
1212

1313
@with_generated_client_fixture(
1414
"""
15-
components:
16-
schemas:
17-
StringOrInt:
18-
type: ["string", "integer"]
19-
MyModel:
20-
type: object
21-
properties:
22-
stringOrIntProp:
23-
type: ["string", "integer"]
24-
"""
25-
)
26-
@with_generated_code_imports(
27-
".models.MyModel",
28-
".types.Unset"
29-
)
30-
class TestSimpleTypeList:
31-
def test_decode_encode(self, MyModel):
32-
assert_model_decode_encode(MyModel, {"stringOrIntProp": "a"}, MyModel(string_or_int_prop="a"))
33-
assert_model_decode_encode(MyModel, {"stringOrIntProp": 1}, MyModel(string_or_int_prop=1))
34-
35-
def test_type_hints(self, MyModel, Unset):
36-
assert_model_property_type_hint(MyModel, "string_or_int_prop", Union[str, int, Unset])
37-
15+
# Various use cases for oneOf
3816
39-
@with_generated_client_fixture(
40-
"""
4117
components:
4218
schemas:
4319
ThingA:
@@ -154,6 +130,123 @@ def test_type_hints(self, ModelWithUnion, ModelWithRequiredUnion, ModelWithUnion
154130

155131
@with_generated_client_fixture(
156132
"""
133+
# Various use cases for a oneOf where one of the variants is null, since these are handled
134+
# a bit differently in the generator
135+
136+
components:
137+
schemas:
138+
MyEnum:
139+
type: string
140+
enum: ["a", "b"]
141+
MyObject:
142+
type: object
143+
properties:
144+
name:
145+
type: string
146+
MyModel:
147+
properties:
148+
nullableEnumProp:
149+
oneOf:
150+
- {"$ref": "#/components/schemas/MyEnum"}
151+
- type: "null"
152+
nullableObjectProp:
153+
oneOf:
154+
- {"$ref": "#/components/schemas/MyObject"}
155+
- type: "null"
156+
inlineNullableObject:
157+
# Note, the generated class for this should be called "MyModelInlineNullableObject",
158+
# since the generator's rule for inline schemas that require their own class is to
159+
# concatenate the property name to the parent schema name.
160+
oneOf:
161+
- type: object
162+
properties:
163+
name:
164+
type: string
165+
- type: "null"
166+
""")
167+
@with_generated_code_imports(
168+
".models.MyEnum",
169+
".models.MyObject",
170+
".models.MyModel",
171+
".models.MyModelInlineNullableObject",
172+
".types.Unset",
173+
)
174+
class TestUnionsWithNull:
175+
def test_nullable_enum_prop(self, MyModel, MyEnum):
176+
assert_model_decode_encode(MyModel, {"nullableEnumProp": "b"}, MyModel(nullable_enum_prop=MyEnum.B))
177+
assert_model_decode_encode(MyModel, {"nullableEnumProp": None}, MyModel(nullable_enum_prop=None))
178+
179+
def test_nullable_object_prop(self, MyModel, MyObject):
180+
assert_model_decode_encode( MyModel, {"nullableObjectProp": None}, MyModel(nullable_object_prop=None))
181+
assert_model_decode_encode( MyModel, {"nullableObjectProp": None}, MyModel(nullable_object_prop=None))
182+
183+
def test_nullable_object_prop_with_inline_schema(self, MyModel, MyModelInlineNullableObject):
184+
assert_model_decode_encode(
185+
MyModel,
186+
{"inlineNullableObject": {"name": "a"}},
187+
MyModel(inline_nullable_object=MyModelInlineNullableObject(name="a")),
188+
)
189+
assert_model_decode_encode( MyModel, {"inlineNullableObject": None}, MyModel(inline_nullable_object=None))
190+
191+
def test_type_hints(self, MyModel, MyEnum, Unset):
192+
assert_model_property_type_hint(MyModel, "nullable_enum_prop", Union[MyEnum, None, Unset])
193+
assert_model_property_type_hint(MyModel, "nullable_object_prop", Union[ForwardRef("MyObject"), None, Unset])
194+
assert_model_property_type_hint(
195+
MyModel,
196+
"inline_nullable_object",
197+
Union[ForwardRef("MyModelInlineNullableObject"), None, Unset],
198+
)
199+
200+
201+
@with_generated_client_fixture(
202+
"""
203+
# Tests for combining the OpenAPI 3.0 "nullable" attribute with an enum
204+
205+
openapi: 3.0.0
206+
207+
components:
208+
schemas:
209+
MyEnum:
210+
type: string
211+
enum: ["a", "b"]
212+
MyEnumIncludingNull:
213+
type: string
214+
nullable: true
215+
enum: ["a", "b", null]
216+
MyModel:
217+
properties:
218+
nullableEnumProp:
219+
allOf:
220+
- {"$ref": "#/components/schemas/MyEnum"}
221+
nullable: true
222+
enumIncludingNullProp: {"$ref": "#/components/schemas/MyEnumIncludingNull"}
223+
""")
224+
@with_generated_code_imports(
225+
".models.MyEnum",
226+
".models.MyEnumIncludingNull",
227+
".models.MyModel",
228+
".types.Unset",
229+
)
230+
class TestNullableEnumsInOpenAPI30:
231+
def test_nullable_enum_prop(self, MyModel, MyEnum, MyEnumIncludingNull):
232+
assert_model_decode_encode(MyModel, {"nullableEnumProp": "b"}, MyModel(nullable_enum_prop=MyEnum.B))
233+
assert_model_decode_encode(MyModel, {"nullableEnumProp": None}, MyModel(nullable_enum_prop=None))
234+
assert_model_decode_encode(
235+
MyModel,
236+
{"enumIncludingNullProp": "a"},
237+
MyModel(enum_including_null_prop=MyEnumIncludingNull.A),
238+
)
239+
assert_model_decode_encode( MyModel, {"enumIncludingNullProp": None}, MyModel(enum_including_null_prop=None))
240+
241+
def test_type_hints(self, MyModel, MyEnum, MyEnumIncludingNull, Unset):
242+
assert_model_property_type_hint(MyModel, "nullable_enum_prop", Union[MyEnum, None, Unset])
243+
assert_model_property_type_hint(MyModel, "enum_including_null_prop", Union[MyEnumIncludingNull, None, Unset])
244+
245+
246+
@with_generated_client_fixture(
247+
"""
248+
# Tests for using a discriminator property
249+
157250
components:
158251
schemas:
159252
ModelType1:
@@ -304,3 +397,112 @@ def test_nested_with_different_property(self, ModelType1, Schnauzer, WithNestedD
304397
{"unionProp": {"modelType": "irrelevant", "dogType": "Schnauzer", "name": "a"}},
305398
WithNestedDiscriminatorsDifferentProperty(union_prop=Schnauzer(model_type="irrelevant", dog_type="Schnauzer", name="a")),
306399
)
400+
401+
402+
@with_generated_client_fixture(
403+
"""
404+
# Tests for using multiple values of "type:" in one schema (OpenAPI 3.1)
405+
406+
components:
407+
schemas:
408+
StringOrInt:
409+
type: ["string", "integer"]
410+
MyModel:
411+
type: object
412+
properties:
413+
stringOrIntProp:
414+
type: ["string", "integer"]
415+
"""
416+
)
417+
@with_generated_code_imports(
418+
".models.MyModel",
419+
".types.Unset"
420+
)
421+
class TestListOfSimpleTypes:
422+
def test_decode_encode(self, MyModel):
423+
assert_model_decode_encode(MyModel, {"stringOrIntProp": "a"}, MyModel(string_or_int_prop="a"))
424+
assert_model_decode_encode(MyModel, {"stringOrIntProp": 1}, MyModel(string_or_int_prop=1))
425+
426+
def test_type_hints(self, MyModel, Unset):
427+
assert_model_property_type_hint(MyModel, "string_or_int_prop", Union[str, int, Unset])
428+
429+
430+
@with_generated_client_fixture(
431+
"""
432+
# Test cases where there's a union of types *and* an explicit list of multiple "type:"s -
433+
# there was a bug where this could cause enum/model classes to be generated incorrectly
434+
435+
components:
436+
schemas:
437+
MyStringEnum:
438+
type: string
439+
enum: ["a", "b"]
440+
MyIntEnum:
441+
type: integer
442+
enum: [1, 2]
443+
MyEnumIncludingNull:
444+
type: ["string", "null"]
445+
enum: ["a", "b", null]
446+
MyObject:
447+
type: object
448+
properties:
449+
name:
450+
type: string
451+
MyModel:
452+
properties:
453+
enumsWithListOfTypesProp:
454+
type: ["string", "integer"]
455+
oneOf:
456+
- {"$ref": "#/components/schemas/MyStringEnum"}
457+
- {"$ref": "#/components/schemas/MyIntEnum"}
458+
enumIncludingNullProp: {"$ref": "#/components/schemas/MyEnumIncludingNull"}
459+
nullableObjectWithListOfTypesProp:
460+
type: ["string", "object"]
461+
oneOf:
462+
- {"$ref": "#/components/schemas/MyObject"}
463+
- type: "null"
464+
""")
465+
@with_generated_code_imports(
466+
".models.MyStringEnum",
467+
".models.MyIntEnum",
468+
".models.MyEnumIncludingNull",
469+
".models.MyObject",
470+
".models.MyModel",
471+
".types.Unset",
472+
)
473+
class TestUnionsWithListOfSimpleTypes:
474+
def test_union_of_enums(self, MyModel, MyStringEnum, MyIntEnum):
475+
assert_model_decode_encode(
476+
MyModel,
477+
{"enumsWithListOfTypesProp": "b"},
478+
MyModel(enums_with_list_of_types_prop=MyStringEnum.B),
479+
)
480+
assert_model_decode_encode(
481+
MyModel,
482+
{"enumsWithListOfTypesProp": 2},
483+
MyModel(enums_with_list_of_types_prop=MyIntEnum.VALUE_2),
484+
)
485+
486+
def test_union_of_enum_with_null(self, MyModel, MyEnumIncludingNull):
487+
assert_model_decode_encode(
488+
MyModel,
489+
{"enumIncludingNullProp": "b"},
490+
MyModel(enum_including_null_prop=MyEnumIncludingNull.B),
491+
)
492+
assert_model_decode_encode(
493+
MyModel,
494+
{"enumIncludingNullProp": None},
495+
MyModel(enum_including_null_prop=None),
496+
)
497+
498+
def test_nullable_object_with_list_of_types(self, MyModel, MyObject):
499+
assert_model_decode_encode(
500+
MyModel,
501+
{"nullableObjectWithListOfTypesProp": {"name": "a"}},
502+
MyModel(nullable_object_with_list_of_types_prop=MyObject(name="a")),
503+
)
504+
assert_model_decode_encode(
505+
MyModel,
506+
{"nullableObjectWithListOfTypesProp": None},
507+
MyModel(nullable_object_with_list_of_types_prop=None),
508+
)

end_to_end_tests/functional_tests/helpers.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,7 @@ def _decorator(cls):
4747
nonlocal alias
4848

4949
def _func(self, generated_client):
50-
module = generated_client.import_module(module_name)
51-
return getattr(module, import_name)
50+
return generated_client.import_symbol(module_name, import_name)
5251

5352
alias = alias or import_name
5453
_func.__name__ = alias

0 commit comments

Comments
 (0)