Skip to content

Commit c4dda2c

Browse files
committed
misc fixes
1 parent b92fed6 commit c4dda2c

File tree

4 files changed

+162
-112
lines changed

4 files changed

+162
-112
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_unions.py

Lines changed: 110 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -10,32 +10,6 @@
1010
)
1111

1212

13-
@with_generated_client_fixture(
14-
"""
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-
38-
3913
@with_generated_client_fixture(
4014
"""
4115
# Various use cases for oneOf
@@ -271,88 +245,8 @@ def test_type_hints(self, MyModel, MyEnum, MyEnumIncludingNull, Unset):
271245

272246
@with_generated_client_fixture(
273247
"""
274-
# Test use cases where there's a union of types *and* an explicit list of multiple "type:"s
248+
# Tests for using a discriminator property
275249
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-
354-
@with_generated_client_fixture(
355-
"""
356250
components:
357251
schemas:
358252
ModelType1:
@@ -503,3 +397,112 @@ def test_nested_with_different_property(self, ModelType1, Schnauzer, WithNestedD
503397
{"unionProp": {"modelType": "irrelevant", "dogType": "Schnauzer", "name": "a"}},
504398
WithNestedDiscriminatorsDifferentProperty(union_prop=Schnauzer(model_type="irrelevant", dog_type="Schnauzer", name="a")),
505399
)
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/generated_client.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,9 @@ def import_symbol(self, module_path: str, name: str) -> Any:
5252
except AttributeError:
5353
existing = ", ".join(name for name in dir(module) if not name.startswith("_"))
5454
assert False, (
55-
f"Couldn't find import \"{name}\" in \"{self.base_module}{module_path}\"."
56-
f" Available imports in that module are: {existing}"
55+
f"Couldn't find import \"{name}\" in \"{self.base_module}{module_path}\".\n"
56+
f"Available imports in that module are: {existing}\n"
57+
f"Output from generator was: {self.generator_result.stdout}"
5758
)
5859

5960
def _run_command(

openapi_python_client/parser/properties/union.py

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,12 +77,26 @@ def build(
7777
"""
7878
from . import property_from_data
7979

80-
sub_properties: list[PropertyProtocol] = []
81-
8280
type_list_data = []
8381
if isinstance(data.type, list) and not (data.anyOf or data.oneOf):
82+
# The schema specifies "type:" with a list of allowable types. If there is *not* also an "anyOf"
83+
# or "oneOf", then we should treat that as a shorthand for a oneOf where each variant is just
84+
# a single "type:". For example, this--
85+
# type:
86+
# - string
87+
# - int
88+
# --becomes this:
89+
# oneOf:
90+
# - type: string
91+
# - type: int
92+
# (We don't copy any other attributes from the top-level schema into the variants, like "default:",
93+
# because they don't necessarily apply to every one of those types. The default will be handled
94+
# at the UnionProperty level, not in the variants. The same goes for description, example, etc.)
95+
#
96+
# However, if there *is* also an "anyOf" or "oneOf" list, then the information from "type:" is
97+
# redundant since every allowable variant type is already fully described in the list.
8498
for _type in data.type:
85-
type_list_data.append(data.model_copy(update={"type": _type, "default": None}))
99+
type_list_data.append(oai.Schema.model_construct(type=_type))
86100

87101
def process_items(
88102
preserve_name_for_item: oai.Schema | oai.Reference | None = None,

0 commit comments

Comments
 (0)