11from __future__ import annotations
22
33from itertools import chain
4- from typing import Any , ClassVar , cast
4+ from typing import Any , Callable , ClassVar , cast
55
66from attr import define , evolve
77
88from 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
1210from ... import Config
1311from ... 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 = []
0 commit comments