Skip to content

Commit 62e0c02

Browse files
committed
clarify special-casing logic for nullable object/enum
1 parent 203e58b commit 62e0c02

File tree

2 files changed

+58
-26
lines changed

2 files changed

+58
-26
lines changed

openapi_python_client/parser/properties/union.py

+54-20
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
from __future__ import annotations
22

33
from itertools import chain
4-
from typing import Any, ClassVar, cast
4+
from typing import Any, Callable, ClassVar, cast
55

66
from attr import define, evolve
77

88
from openapi_python_client.parser.properties.has_named_class import HasNamedClass
9-
from openapi_python_client.schema.openapi_schema_pydantic.reference import Reference
10-
from openapi_python_client.schema.openapi_schema_pydantic.schema import Schema
119

1210
from ... import Config
1311
from ... import schema as oai
@@ -66,14 +64,46 @@ def build(
6664
# of the type variants, like "format" for a string. But we don't copy "default" because
6765
# default values will be handled at the top level by the UnionProperty.
6866

67+
def _add_index_suffix_to_variant_names(index: int) -> str:
68+
return f"{name}_type_{index}"
69+
70+
def _add_index_suffix_to_variant_names(index: int) -> str:
71+
return f"{name}_type_{index}"
72+
6973
def process_items(
70-
use_original_name_for: oai.Schema | oai.Reference | None = None,
74+
variant_name_from_index_func: Callable[[int], str] = _add_index_suffix_to_variant_names,
7175
) -> tuple[list[PropertyProtocol] | PropertyError, Schemas]:
7276
props: list[PropertyProtocol] = []
7377
new_schemas = schemas
74-
schemas_with_classes: list[oai.Schema | oai.Reference] = []
7578
for i, sub_prop_data in enumerate(chain(data.anyOf, data.oneOf, type_list_data)):
76-
sub_prop_name = name if sub_prop_data is use_original_name_for else f"{name}_type_{i}"
79+
sub_prop_name = variant_name_from_index_func(i)
80+
81+
# The sub_prop_name logic is what makes this a bit complicated. That value is used only
82+
# if sub_prop is an *inline* schema and needs us to make up a name for it. For instance,
83+
# in the following schema--
84+
#
85+
# MyModel:
86+
# properties:
87+
# unionThing:
88+
# oneOf:
89+
# - type: object
90+
# properties: ...
91+
# - type: object
92+
# properties: ...
93+
#
94+
# --both of the variants under oneOf are inline schemas. And since they're objects, we
95+
# will be creating model classes for them, which need names. Inline schemas are named by
96+
# concatenating names of parents; so, when we're in UnionProperty.build() for unionThing,
97+
# the value of "name" is "my_model_union_thing", and then we set sub_prop_name to
98+
# "my_model_union_thing_type_0" and "my_model_union_thing_type_1" for the two variants,
99+
# and their model classes will be MyModelUnionThingType0 and MyModelUnionThingType1.
100+
#
101+
# However, in this example, if the second variant was just a scalar type instead of an
102+
# object (like "type: null" or "type: string"), so that the first variant is the only
103+
# one that needs a class... then it would be friendlier to call the first variant's
104+
# class just MyModelUnionThing, not MyModelUnionThingType0. We'll check for that special
105+
# case below; we can't know if that's the situation until after we've processed them all.
106+
77107
sub_prop, new_schemas = property_from_data(
78108
name=sub_prop_name,
79109
required=True,
@@ -84,26 +114,30 @@ def process_items(
84114
)
85115
if isinstance(sub_prop, PropertyError):
86116
return PropertyError(detail=f"Invalid property in union {name}", data=sub_prop_data), new_schemas
87-
if isinstance(sub_prop, HasNamedClass):
88-
schemas_with_classes.append(sub_prop_data)
89117
props.append(sub_prop)
90118

91-
if (not use_original_name_for) and len(schemas_with_classes) == 1:
92-
# An example of this scenario is a oneOf where one of the variants is an inline enum or
93-
# model, and the other is a simple value like null. If the name of the union property is
94-
# "foo" then it's desirable for the enum or model class to be named "Foo", not "FooType1".
95-
# So, we'll do a second pass where we tell ourselves to use the original property name
96-
# for that item instead of "{name}_type_{i}".
97-
# This only makes a functional difference if the variant was an inline schema, because
98-
# we wouldn't be generating a class otherwise, but even if it wasn't inline this will
99-
# save on pointlessly long variable names inside from_dict/to_dict.
100-
return process_items(use_original_name_for=schemas_with_classes[0])
101-
102119
return props, new_schemas
103120

104-
sub_properties, schemas = process_items()
121+
sub_properties, new_schemas = process_items()
122+
# Here's the check for the special case described above. If just one of the variants is
123+
# an inline schema whose name matters, then we'll re-process them to simplify the naming.
124+
# Unfortunately we do have to re-process them all; we can't just modify that one variant
125+
# in place, because new_schemas already contains several references to its old name.
126+
if (
127+
not isinstance(sub_properties, PropertyError)
128+
and len([p for p in sub_properties if isinstance(p, HasNamedClass)]) == 1
129+
):
130+
def _use_same_name_as_parent_for_that_one_variant(index: int) -> str:
131+
for i, p in enumerate(sub_properties):
132+
if i == index and isinstance(p, HasNamedClass):
133+
return name
134+
return _add_index_suffix_to_variant_names(index)
135+
136+
sub_properties, new_schemas = process_items(_use_same_name_as_parent_for_that_one_variant)
137+
105138
if isinstance(sub_properties, PropertyError):
106139
return sub_properties, schemas
140+
schemas = new_schemas
107141

108142
def flatten_union_properties(sub_properties: list[PropertyProtocol]) -> list[PropertyProtocol]:
109143
flattened = []

tests/test_parser/test_properties/test_union.py

+4-6
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ def test_property_from_data_union(union_property_factory, date_time_property_fac
3737
assert s == Schemas()
3838

3939

40-
def test_name_is_preserved_if_union_has_one_model(config):
40+
def test_name_is_preserved_if_union_is_nullable_model(config):
4141
from openapi_python_client.parser.properties import Schemas, property_from_data
4242

4343
parent_name = "parent"
@@ -46,7 +46,7 @@ def test_name_is_preserved_if_union_has_one_model(config):
4646
data = oai.Schema(
4747
oneOf=[
4848
oai.Schema(type=DataType.OBJECT),
49-
oai.Schema(type=DataType.STRING),
49+
oai.Schema(type=DataType.NULL),
5050
],
5151
)
5252
expected_model_class = Class(name=ClassName("ParentProp1", ""), module_name="parent_prop_1")
@@ -61,12 +61,11 @@ def test_name_is_preserved_if_union_has_one_model(config):
6161
assert isinstance(prop1, ModelProperty)
6262
assert prop1.name == name
6363
assert prop1.class_info == expected_model_class
64-
assert p.inner_properties[1].name == f"{name}_type_1"
6564

6665
assert s == Schemas(classes_by_name={expected_model_class.name: prop1}, models_to_process=[prop1])
6766

6867

69-
def test_name_is_preserved_if_union_has_one_enum(config):
68+
def test_name_is_preserved_if_union_is_nullable_enum(config):
7069
from openapi_python_client.parser.properties import Schemas, property_from_data
7170

7271
parent_name = "parent"
@@ -75,7 +74,7 @@ def test_name_is_preserved_if_union_has_one_enum(config):
7574
data = oai.Schema(
7675
oneOf=[
7776
oai.Schema(type=DataType.INTEGER, enum=[10, 20]),
78-
oai.Schema(type=DataType.STRING),
77+
oai.Schema(type=DataType.NULL),
7978
],
8079
)
8180
expected_enum_class = Class(name=ClassName("ParentProp1", ""), module_name="parent_prop_1")
@@ -90,7 +89,6 @@ def test_name_is_preserved_if_union_has_one_enum(config):
9089
assert isinstance(prop1, EnumProperty)
9190
assert prop1.name == name
9291
assert prop1.class_info == expected_enum_class
93-
assert p.inner_properties[1].name == f"{name}_type_1"
9492

9593
assert s == Schemas(classes_by_name={expected_enum_class.name: prop1})
9694

0 commit comments

Comments
 (0)