Skip to content

Commit 24aef5c

Browse files
dbantywallagib
andauthored
Union variants use Title names instead of indices (#1301)
Replaces #962 with a rebase & a couple tweaks Co-authored-by: Gibryon Bhojraj <gib.bhojraj@wallaroo.ai>
1 parent 8f5f11f commit 24aef5c

File tree

10 files changed

+128
-34
lines changed

10 files changed

+128
-34
lines changed
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
default: major
3+
---
4+
5+
# Change some union variant names
6+
7+
When creating a union with `oneOf`, `anyOf`, or a list of `type`, the name of each variant used to be `type_{index}`
8+
where the index is based on the order of the types in the union.
9+
10+
This made some modules difficult to understand, what is a `my_type_type_0` after all?
11+
It also meant that reordering union members, while not a breaking change to the API, _would_ be a breaking change
12+
for generated clients.
13+
14+
Now, if an individual variant has a `title` attribute, that `title` will be used in the name instead.
15+
This is only an enhancement for documents which use `title` in union variants, and only a breaking change for
16+
_inline models_ (not `#/components/schemas` which should already have used more descriptive names).
17+
18+
Thanks @wallagib for PR #962!

end_to_end_tests/baseline_openapi_3.0.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2171,6 +2171,7 @@
21712171
"oneOf": [
21722172
{
21732173
"type": "object",
2174+
"title": "Apples",
21742175
"properties": {
21752176
"apples": {
21762177
"type": "string"
@@ -2179,6 +2180,7 @@
21792180
},
21802181
{
21812182
"type": "object",
2183+
"title": "Bananas",
21822184
"properties": {
21832185
"bananas": {
21842186
"type": "string"

end_to_end_tests/baseline_openapi_3.1.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2157,6 +2157,7 @@ info:
21572157
"oneOf": [
21582158
{
21592159
"type": "object",
2160+
"title": "Apples",
21602161
"properties": {
21612162
"apples": {
21622163
"type": "string"
@@ -2165,6 +2166,7 @@ info:
21652166
},
21662167
{
21672168
"type": "object",
2169+
"title": "Bananas",
21682170
"properties": {
21692171
"bananas": {
21702172
"type": "string"

end_to_end_tests/golden-record/my_test_api_client/models/__init__.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,8 @@
7272
from .model_with_recursive_ref_in_additional_properties import ModelWithRecursiveRefInAdditionalProperties
7373
from .model_with_union_property import ModelWithUnionProperty
7474
from .model_with_union_property_inlined import ModelWithUnionPropertyInlined
75-
from .model_with_union_property_inlined_fruit_type_0 import ModelWithUnionPropertyInlinedFruitType0
76-
from .model_with_union_property_inlined_fruit_type_1 import ModelWithUnionPropertyInlinedFruitType1
75+
from .model_with_union_property_inlined_apples import ModelWithUnionPropertyInlinedApples
76+
from .model_with_union_property_inlined_bananas import ModelWithUnionPropertyInlinedBananas
7777
from .none import None_
7878
from .octet_stream_tests_octet_stream_post_response_200 import OctetStreamTestsOctetStreamPostResponse200
7979
from .post_bodies_multiple_data_body import PostBodiesMultipleDataBody
@@ -155,8 +155,8 @@
155155
"ModelWithRecursiveRefInAdditionalProperties",
156156
"ModelWithUnionProperty",
157157
"ModelWithUnionPropertyInlined",
158-
"ModelWithUnionPropertyInlinedFruitType0",
159-
"ModelWithUnionPropertyInlinedFruitType1",
158+
"ModelWithUnionPropertyInlinedApples",
159+
"ModelWithUnionPropertyInlinedBananas",
160160
"None_",
161161
"OctetStreamTestsOctetStreamPostResponse200",
162162
"PostBodiesMultipleDataBody",

end_to_end_tests/golden-record/my_test_api_client/models/model_with_union_property_inlined.py

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
from ..types import UNSET, Unset
77

88
if TYPE_CHECKING:
9-
from ..models.model_with_union_property_inlined_fruit_type_0 import ModelWithUnionPropertyInlinedFruitType0
10-
from ..models.model_with_union_property_inlined_fruit_type_1 import ModelWithUnionPropertyInlinedFruitType1
9+
from ..models.model_with_union_property_inlined_apples import ModelWithUnionPropertyInlinedApples
10+
from ..models.model_with_union_property_inlined_bananas import ModelWithUnionPropertyInlinedBananas
1111

1212

1313
T = TypeVar("T", bound="ModelWithUnionPropertyInlined")
@@ -17,18 +17,18 @@
1717
class ModelWithUnionPropertyInlined:
1818
"""
1919
Attributes:
20-
fruit (Union['ModelWithUnionPropertyInlinedFruitType0', 'ModelWithUnionPropertyInlinedFruitType1', Unset]):
20+
fruit (Union['ModelWithUnionPropertyInlinedApples', 'ModelWithUnionPropertyInlinedBananas', Unset]):
2121
"""
2222

23-
fruit: Union["ModelWithUnionPropertyInlinedFruitType0", "ModelWithUnionPropertyInlinedFruitType1", Unset] = UNSET
23+
fruit: Union["ModelWithUnionPropertyInlinedApples", "ModelWithUnionPropertyInlinedBananas", Unset] = UNSET
2424

2525
def to_dict(self) -> dict[str, Any]:
26-
from ..models.model_with_union_property_inlined_fruit_type_0 import ModelWithUnionPropertyInlinedFruitType0
26+
from ..models.model_with_union_property_inlined_apples import ModelWithUnionPropertyInlinedApples
2727

2828
fruit: Union[Unset, dict[str, Any]]
2929
if isinstance(self.fruit, Unset):
3030
fruit = UNSET
31-
elif isinstance(self.fruit, ModelWithUnionPropertyInlinedFruitType0):
31+
elif isinstance(self.fruit, ModelWithUnionPropertyInlinedApples):
3232
fruit = self.fruit.to_dict()
3333
else:
3434
fruit = self.fruit.to_dict()
@@ -43,29 +43,29 @@ def to_dict(self) -> dict[str, Any]:
4343

4444
@classmethod
4545
def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
46-
from ..models.model_with_union_property_inlined_fruit_type_0 import ModelWithUnionPropertyInlinedFruitType0
47-
from ..models.model_with_union_property_inlined_fruit_type_1 import ModelWithUnionPropertyInlinedFruitType1
46+
from ..models.model_with_union_property_inlined_apples import ModelWithUnionPropertyInlinedApples
47+
from ..models.model_with_union_property_inlined_bananas import ModelWithUnionPropertyInlinedBananas
4848

4949
d = dict(src_dict)
5050

5151
def _parse_fruit(
5252
data: object,
53-
) -> Union["ModelWithUnionPropertyInlinedFruitType0", "ModelWithUnionPropertyInlinedFruitType1", Unset]:
53+
) -> Union["ModelWithUnionPropertyInlinedApples", "ModelWithUnionPropertyInlinedBananas", Unset]:
5454
if isinstance(data, Unset):
5555
return data
5656
try:
5757
if not isinstance(data, dict):
5858
raise TypeError()
59-
fruit_type_0 = ModelWithUnionPropertyInlinedFruitType0.from_dict(data)
59+
fruit_apples = ModelWithUnionPropertyInlinedApples.from_dict(data)
6060

61-
return fruit_type_0
61+
return fruit_apples
6262
except: # noqa: E722
6363
pass
6464
if not isinstance(data, dict):
6565
raise TypeError()
66-
fruit_type_1 = ModelWithUnionPropertyInlinedFruitType1.from_dict(data)
66+
fruit_bananas = ModelWithUnionPropertyInlinedBananas.from_dict(data)
6767

68-
return fruit_type_1
68+
return fruit_bananas
6969

7070
fruit = _parse_fruit(d.pop("fruit", UNSET))
7171

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@
66

77
from ..types import UNSET, Unset
88

9-
T = TypeVar("T", bound="ModelWithUnionPropertyInlinedFruitType0")
9+
T = TypeVar("T", bound="ModelWithUnionPropertyInlinedApples")
1010

1111

1212
@_attrs_define
13-
class ModelWithUnionPropertyInlinedFruitType0:
13+
class ModelWithUnionPropertyInlinedApples:
1414
"""
1515
Attributes:
1616
apples (Union[Unset, str]):
@@ -35,12 +35,12 @@ def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
3535
d = dict(src_dict)
3636
apples = d.pop("apples", UNSET)
3737

38-
model_with_union_property_inlined_fruit_type_0 = cls(
38+
model_with_union_property_inlined_apples = cls(
3939
apples=apples,
4040
)
4141

42-
model_with_union_property_inlined_fruit_type_0.additional_properties = d
43-
return model_with_union_property_inlined_fruit_type_0
42+
model_with_union_property_inlined_apples.additional_properties = d
43+
return model_with_union_property_inlined_apples
4444

4545
@property
4646
def additional_keys(self) -> list[str]:
Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@
66

77
from ..types import UNSET, Unset
88

9-
T = TypeVar("T", bound="ModelWithUnionPropertyInlinedFruitType1")
9+
T = TypeVar("T", bound="ModelWithUnionPropertyInlinedBananas")
1010

1111

1212
@_attrs_define
13-
class ModelWithUnionPropertyInlinedFruitType1:
13+
class ModelWithUnionPropertyInlinedBananas:
1414
"""
1515
Attributes:
1616
bananas (Union[Unset, str]):
@@ -35,12 +35,12 @@ def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
3535
d = dict(src_dict)
3636
bananas = d.pop("bananas", UNSET)
3737

38-
model_with_union_property_inlined_fruit_type_1 = cls(
38+
model_with_union_property_inlined_bananas = cls(
3939
bananas=bananas,
4040
)
4141

42-
model_with_union_property_inlined_fruit_type_1.additional_properties = d
43-
return model_with_union_property_inlined_fruit_type_1
42+
model_with_union_property_inlined_bananas.additional_properties = d
43+
return model_with_union_property_inlined_bananas
4444

4545
@property
4646
def additional_keys(self) -> list[str]:

openapi_python_client/parser/properties/union.py

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,14 @@ class UnionProperty(PropertyProtocol):
2828

2929
@classmethod
3030
def build(
31-
cls, *, data: oai.Schema, name: str, required: bool, schemas: Schemas, parent_name: str, config: Config
31+
cls,
32+
*,
33+
data: oai.Schema,
34+
name: str,
35+
required: bool,
36+
schemas: Schemas,
37+
parent_name: str,
38+
config: Config,
3239
) -> tuple[UnionProperty | PropertyError, Schemas]:
3340
"""
3441
Create a `UnionProperty` the right way.
@@ -55,16 +62,30 @@ def build(
5562
type_list_data.append(data.model_copy(update={"type": _type, "default": None}))
5663

5764
for i, sub_prop_data in enumerate(chain(data.anyOf, data.oneOf, type_list_data)):
65+
# If a schema has a unique title property, we can use that to carry forward a descriptive name instead of "type_0"
66+
subscript: str
67+
if (
68+
isinstance(sub_prop_data, oai.Schema)
69+
and sub_prop_data.title is not None
70+
and sub_prop_data.title != data.title
71+
):
72+
subscript = sub_prop_data.title
73+
else:
74+
subscript = f"type_{i}"
75+
5876
sub_prop, schemas = property_from_data(
59-
name=f"{name}_type_{i}",
77+
name=f"{name}_{subscript}",
6078
required=True,
6179
data=sub_prop_data,
6280
schemas=schemas,
6381
parent_name=parent_name,
6482
config=config,
6583
)
6684
if isinstance(sub_prop, PropertyError):
67-
return PropertyError(detail=f"Invalid property in union {name}", data=sub_prop_data), schemas
85+
return (
86+
PropertyError(detail=f"Invalid property in union {name}", data=sub_prop_data),
87+
schemas,
88+
)
6889
sub_properties.append(sub_prop)
6990

7091
def flatten_union_properties(sub_properties: list[PropertyProtocol]) -> list[PropertyProtocol]:
@@ -108,7 +129,12 @@ def convert_value(self, value: Any) -> Value | None | PropertyError:
108129

109130
def _get_inner_type_strings(self, json: bool) -> set[str]:
110131
return {
111-
p.get_type_string(no_optional=True, json=json, quoted=not p.is_base_type) for p in self.inner_properties
132+
p.get_type_string(
133+
no_optional=True,
134+
json=json,
135+
quoted=not p.is_base_type,
136+
)
137+
for p in self.inner_properties
112138
}
113139

114140
@staticmethod

tests/conftest.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import pytest
77
from mypy.semanal_shared import Protocol
88

9-
from openapi_python_client import Config, MetaType
9+
from openapi_python_client import Config, MetaType, utils
1010
from openapi_python_client import schema as oai
1111
from openapi_python_client.config import ConfigFile
1212
from openapi_python_client.parser.properties import (
@@ -321,5 +321,5 @@ def _common_kwargs(kwargs: dict[str, Any]) -> dict[str, Any]:
321321
**kwargs,
322322
}
323323
if not kwargs.get("python_name"):
324-
kwargs["python_name"] = kwargs["name"]
324+
kwargs["python_name"] = utils.PythonIdentifier(value=kwargs["name"], prefix="")
325325
return kwargs

tests/test_parser/test_properties/test_union.py

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import openapi_python_client.schema as oai
22
from openapi_python_client.parser.errors import ParseError
3-
from openapi_python_client.parser.properties import Schemas, UnionProperty
3+
from openapi_python_client.parser.properties import Schemas, UnionProperty, property_from_data
44
from openapi_python_client.schema import DataType, ParameterLocation
55

66

@@ -28,3 +28,49 @@ def test_not_required_in_path(config):
2828

2929
err = prop.validate_location(ParameterLocation.PATH)
3030
assert isinstance(err, ParseError)
31+
32+
33+
def test_union_oneOf_descriptive_type_name(
34+
union_property_factory, date_time_property_factory, string_property_factory, config
35+
):
36+
nested_schema_variant_A = oai.Schema(type=DataType.STRING, title="A")
37+
nested_schema_variant_B = oai.Schema(type=DataType.STRING, title="B")
38+
nested_schema_variant_2 = oai.Schema(type=DataType.STRING)
39+
nested_schema_variant_C = oai.Schema(type=DataType.STRING, title="C")
40+
nested_schema_variant_4 = oai.Schema(type=DataType.STRING)
41+
42+
name = "union_prop"
43+
required = True
44+
data = oai.Schema(
45+
anyOf=[
46+
# AnyOf retains the old naming convention
47+
nested_schema_variant_C,
48+
nested_schema_variant_4,
49+
],
50+
oneOf=[
51+
# OneOf fields that define their own titles will have those titles as their Type names
52+
nested_schema_variant_A,
53+
nested_schema_variant_B,
54+
nested_schema_variant_2,
55+
oai.Schema(type=DataType.STRING, schema_format="date-time"),
56+
],
57+
)
58+
expected = union_property_factory(
59+
name=name,
60+
required=required,
61+
inner_properties=[
62+
string_property_factory(name=f"{name}_C"),
63+
string_property_factory(name=f"{name}_type_1"),
64+
string_property_factory(name=f"{name}_A"),
65+
string_property_factory(name=f"{name}_B"),
66+
string_property_factory(name=f"{name}_type_4"),
67+
date_time_property_factory(name=f"{name}_type_5"),
68+
],
69+
)
70+
71+
p, s = property_from_data(
72+
name=name, required=required, data=data, schemas=Schemas(), parent_name="parent", config=config
73+
)
74+
75+
assert p == expected
76+
assert s == Schemas()

0 commit comments

Comments
 (0)