Skip to content

Commit 8059d24

Browse files
committed
functional tests for union type fix
1 parent 9056e92 commit 8059d24

File tree

4 files changed

+223
-75
lines changed

4 files changed

+223
-75
lines changed

end_to_end_tests/functional_tests/generated_code_execution/test_enums_and_consts.py

+13-73
Original file line numberDiff line numberDiff line change
@@ -133,88 +133,22 @@ 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.MyEnumIncludingNull",
156144
".models.MyModel",
157-
".types.Unset",
158145
)
159-
class TestNullableEnums:
160-
def test_nullable_enum_prop(self, MyModel, MyEnum, MyEnumIncludingNull):
161-
assert_model_decode_encode(MyModel, {"nullableEnumProp": "b"}, MyModel(nullable_enum_prop=MyEnum.B))
162-
assert_model_decode_encode(MyModel, {"nullableEnumProp": None}, MyModel(nullable_enum_prop=None))
163-
assert_model_decode_encode(
164-
MyModel,
165-
{"enumIncludingNullProp": "a"},
166-
MyModel(enum_including_null_prop=MyEnumIncludingNull.A),
167-
)
168-
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):
169148
assert_model_decode_encode(MyModel, {"nullOnlyEnumProp": None}, MyModel(null_only_enum_prop=None))
170149

171-
def test_type_hints(self, MyModel, MyEnum, MyEnumIncludingNull, Unset):
172-
assert_model_property_type_hint(MyModel, "nullable_enum_prop", Union[MyEnum, None, Unset])
173-
assert_model_property_type_hint(MyModel, "enum_including_null_prop", Union[MyEnumIncludingNull, None, Unset])
174-
assert_model_property_type_hint(MyModel, "null_only_enum_prop", Union[None, Unset])
175-
176-
177-
@with_generated_client_fixture(
178-
"""
179-
openapi: 3.0.0
180-
181-
components:
182-
schemas:
183-
MyEnum:
184-
type: string
185-
enum: ["a", "b"]
186-
MyEnumIncludingNull:
187-
type: string
188-
nullable: true
189-
enum: ["a", "b", null]
190-
MyModel:
191-
properties:
192-
nullableEnumProp:
193-
allOf:
194-
- {"$ref": "#/components/schemas/MyEnum"}
195-
nullable: true
196-
enumIncludingNullProp: {"$ref": "#/components/schemas/MyEnumIncludingNull"}
197-
""")
198-
@with_generated_code_imports(
199-
".models.MyEnum",
200-
".models.MyEnumIncludingNull",
201-
".models.MyModel",
202-
".types.Unset",
203-
)
204-
class TestNullableEnumsInOpenAPI30:
205-
def test_nullable_enum_prop(self, MyModel, MyEnum, MyEnumIncludingNull):
206-
assert_model_decode_encode(MyModel, {"nullableEnumProp": "b"}, MyModel(nullable_enum_prop=MyEnum.B))
207-
assert_model_decode_encode(MyModel, {"nullableEnumProp": None}, MyModel(nullable_enum_prop=None))
208-
assert_model_decode_encode(
209-
MyModel,
210-
{"enumIncludingNullProp": "a"},
211-
MyModel(enum_including_null_prop=MyEnumIncludingNull.A),
212-
)
213-
assert_model_decode_encode( MyModel, {"enumIncludingNullProp": None}, MyModel(enum_including_null_prop=None))
214-
215-
def test_type_hints(self, MyModel, MyEnum, MyEnumIncludingNull, Unset):
216-
assert_model_property_type_hint(MyModel, "nullable_enum_prop", Union[MyEnum, None, Unset])
217-
assert_model_property_type_hint(MyModel, "enum_including_null_prop", Union[MyEnumIncludingNull, None, Unset])
150+
def test_type_hints(self, MyModel):
151+
assert_model_property_type_hint(MyModel, "null_only_enum_prop", None)
218152

219153

220154
@with_generated_client_fixture(
@@ -259,6 +193,8 @@ def test_invalid_int(self, MyModel):
259193

260194
@with_generated_client_fixture(
261195
"""
196+
# Tests of literal_enums mode, where enums become a typing.Literal type instead of a class
197+
262198
components:
263199
schemas:
264200
MyEnum:
@@ -303,6 +239,8 @@ def test_invalid_values(self, MyModel):
303239

304240
@with_generated_client_fixture(
305241
"""
242+
# Tests of literal_enums mode, where enums become a typing.Literal type instead of a class
243+
306244
components:
307245
schemas:
308246
MyEnum:
@@ -347,6 +285,8 @@ def test_invalid_values(self, MyModel):
347285

348286
@with_generated_client_fixture(
349287
"""
288+
# Similar to some of the "union with null" tests in test_unions.py, but in literal_enums mode
289+
350290
components:
351291
schemas:
352292
MyEnum:

end_to_end_tests/functional_tests/generated_code_execution/test_unions.py

+199
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ def test_type_hints(self, MyModel, Unset):
3838

3939
@with_generated_client_fixture(
4040
"""
41+
# Various use cases for oneOf
42+
4143
components:
4244
schemas:
4345
ThingA:
@@ -152,6 +154,203 @@ def test_type_hints(self, ModelWithUnion, ModelWithRequiredUnion, ModelWithUnion
152154
)
153155

154156

157+
@with_generated_client_fixture(
158+
"""
159+
# Various use cases for a oneOf where one of the variants is null, since these are handled
160+
# a bit differently in the generator
161+
162+
components:
163+
schemas:
164+
MyEnum:
165+
type: string
166+
enum: ["a", "b"]
167+
MyObject:
168+
type: object
169+
properties:
170+
name:
171+
type: string
172+
MyModel:
173+
properties:
174+
nullableEnumProp:
175+
oneOf:
176+
- {"$ref": "#/components/schemas/MyEnum"}
177+
- type: "null"
178+
nullableObjectProp:
179+
oneOf:
180+
- {"$ref": "#/components/schemas/MyObject"}
181+
- type: "null"
182+
inlineNullableObject:
183+
# Note, the generated class for this should be called "MyModelInlineNullableObject",
184+
# since the generator's rule for inline schemas that require their own class is to
185+
# concatenate the property name to the parent schema name.
186+
oneOf:
187+
- type: object
188+
properties:
189+
name:
190+
type: string
191+
- type: "null"
192+
""")
193+
@with_generated_code_imports(
194+
".models.MyEnum",
195+
".models.MyObject",
196+
".models.MyModel",
197+
".models.MyModelInlineNullableObject",
198+
".types.Unset",
199+
)
200+
class TestUnionsWithNull:
201+
def test_nullable_enum_prop(self, MyModel, MyEnum):
202+
assert_model_decode_encode(MyModel, {"nullableEnumProp": "b"}, MyModel(nullable_enum_prop=MyEnum.B))
203+
assert_model_decode_encode(MyModel, {"nullableEnumProp": None}, MyModel(nullable_enum_prop=None))
204+
205+
def test_nullable_object_prop(self, MyModel, MyObject):
206+
assert_model_decode_encode( MyModel, {"nullableObjectProp": None}, MyModel(nullable_object_prop=None))
207+
assert_model_decode_encode( MyModel, {"nullableObjectProp": None}, MyModel(nullable_object_prop=None))
208+
209+
def test_nullable_object_prop_with_inline_schema(self, MyModel, MyModelInlineNullableObject):
210+
assert_model_decode_encode(
211+
MyModel,
212+
{"inlineNullableObject": {"name": "a"}},
213+
MyModel(inline_nullable_object=MyModelInlineNullableObject(name="a")),
214+
)
215+
assert_model_decode_encode( MyModel, {"inlineNullableObject": None}, MyModel(inline_nullable_object=None))
216+
217+
def test_type_hints(self, MyModel, MyEnum, Unset):
218+
assert_model_property_type_hint(MyModel, "nullable_enum_prop", Union[MyEnum, None, Unset])
219+
assert_model_property_type_hint(MyModel, "nullable_object_prop", Union[ForwardRef("MyObject"), None, Unset])
220+
assert_model_property_type_hint(
221+
MyModel,
222+
"inline_nullable_object",
223+
Union[ForwardRef("MyModelInlineNullableObject"), None, Unset],
224+
)
225+
226+
227+
@with_generated_client_fixture(
228+
"""
229+
# Tests for combining the OpenAPI 3.0 "nullable" attribute with an enum
230+
231+
openapi: 3.0.0
232+
233+
components:
234+
schemas:
235+
MyEnum:
236+
type: string
237+
enum: ["a", "b"]
238+
MyEnumIncludingNull:
239+
type: string
240+
nullable: true
241+
enum: ["a", "b", null]
242+
MyModel:
243+
properties:
244+
nullableEnumProp:
245+
allOf:
246+
- {"$ref": "#/components/schemas/MyEnum"}
247+
nullable: true
248+
enumIncludingNullProp: {"$ref": "#/components/schemas/MyEnumIncludingNull"}
249+
""")
250+
@with_generated_code_imports(
251+
".models.MyEnum",
252+
".models.MyEnumIncludingNull",
253+
".models.MyModel",
254+
".types.Unset",
255+
)
256+
class TestNullableEnumsInOpenAPI30:
257+
def test_nullable_enum_prop(self, MyModel, MyEnum, MyEnumIncludingNull):
258+
assert_model_decode_encode(MyModel, {"nullableEnumProp": "b"}, MyModel(nullable_enum_prop=MyEnum.B))
259+
assert_model_decode_encode(MyModel, {"nullableEnumProp": None}, MyModel(nullable_enum_prop=None))
260+
assert_model_decode_encode(
261+
MyModel,
262+
{"enumIncludingNullProp": "a"},
263+
MyModel(enum_including_null_prop=MyEnumIncludingNull.A),
264+
)
265+
assert_model_decode_encode( MyModel, {"enumIncludingNullProp": None}, MyModel(enum_including_null_prop=None))
266+
267+
def test_type_hints(self, MyModel, MyEnum, MyEnumIncludingNull, Unset):
268+
assert_model_property_type_hint(MyModel, "nullable_enum_prop", Union[MyEnum, None, Unset])
269+
assert_model_property_type_hint(MyModel, "enum_including_null_prop", Union[MyEnumIncludingNull, None, Unset])
270+
271+
272+
@with_generated_client_fixture(
273+
"""
274+
# Test use cases where there's a union of types *and* an explicit list of multiple "type:"s
275+
276+
components:
277+
schemas:
278+
MyStringEnum:
279+
type: string
280+
enum: ["a", "b"]
281+
MyIntEnum:
282+
type: integer
283+
enum: [1, 2]
284+
MyEnumIncludingNull:
285+
type: ["string", "null"]
286+
enum: ["a", "b", null]
287+
MyObject:
288+
type: object
289+
properties:
290+
name:
291+
type: string
292+
MyModel:
293+
properties:
294+
enumsWithListOfTypesProp:
295+
type: ["string", "integer"]
296+
oneOf:
297+
- {"$ref": "#/components/schemas/MyStringEnum"}
298+
- {"$ref": "#/components/schemas/MyIntEnum"}
299+
enumIncludingNullProp: {"$ref": "#/components/schemas/MyEnumIncludingNull"}
300+
nullableObjectWithListOfTypesProp:
301+
type: ["string", "object"]
302+
oneOf:
303+
- {"$ref": "#/components/schemas/MyObject"}
304+
- type: "null"
305+
""")
306+
@with_generated_code_imports(
307+
".models.MyStringEnum",
308+
".models.MyIntEnum",
309+
".models.MyEnumIncludingNull",
310+
".models.MyObject",
311+
".models.MyModel",
312+
".types.Unset",
313+
)
314+
class TestUnionsWithExplicitListOfTypes:
315+
# This covers some use cases where combining "oneOf" with "type: [list of types]" (which is fine
316+
# to do in OpenAPI) used to generate enum/model classes incorrectly.
317+
318+
def test_union_of_enums(self, MyModel, MyStringEnum, MyIntEnum):
319+
assert_model_decode_encode(
320+
MyModel,
321+
{"enumsWithListOfTypesProp": "b"},
322+
MyModel(enums_with_list_of_types_prop=MyStringEnum.B),
323+
)
324+
assert_model_decode_encode(
325+
MyModel,
326+
{"enumsWithListOfTypesProp": 2},
327+
MyModel(enums_with_list_of_types_prop=MyIntEnum.VALUE_2),
328+
)
329+
330+
def test_union_of_enum_with_null(self, MyModel, MyEnumIncludingNull):
331+
assert_model_decode_encode(
332+
MyModel,
333+
{"enumIncludingNullProp": "b"},
334+
MyModel(enum_including_null_prop=MyEnumIncludingNull.B),
335+
)
336+
assert_model_decode_encode(
337+
MyModel,
338+
{"enumIncludingNullProp": None},
339+
MyModel(enum_including_null_prop=None),
340+
)
341+
342+
def test_nullable_object_with_list_of_types(self, MyModel, MyObject):
343+
assert_model_decode_encode(
344+
MyModel,
345+
{"nullableObjectWithListOfTypesProp": {"name": "a"}},
346+
MyModel(nullable_object_with_list_of_types_prop=MyObject(name="a")),
347+
)
348+
assert_model_decode_encode(
349+
MyModel,
350+
{"nullableObjectWithListOfTypesProp": None},
351+
MyModel(nullable_object_with_list_of_types_prop=None),
352+
)
353+
155354
@with_generated_client_fixture(
156355
"""
157356
components:

end_to_end_tests/functional_tests/helpers.py

+1-2
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

end_to_end_tests/generated_client.py

+10
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,16 @@ def import_module(self, module_path: str) -> Any:
4545
"""Attempt to import a module from the generated code."""
4646
return importlib.import_module(f"{self.base_module}{module_path}")
4747

48+
def import_symbol(self, module_path: str, name: str) -> Any:
49+
module = self.import_module(module_path)
50+
try:
51+
return getattr(module, name)
52+
except AttributeError:
53+
existing = ", ".join(name for name in dir(module) if not name.startswith("_"))
54+
assert False, (
55+
f"Couldn't find import \"{name}\" in \"{self.base_module}{module_path}\"."
56+
f" Available imports in that module are: {existing}"
57+
)
4858

4959
def _run_command(
5060
command: str,

0 commit comments

Comments
 (0)