1
1
from __future__ import annotations
2
+ from itertools import chain
2
3
4
+ from openapi_python_client import utils
5
+ from openapi_python_client .config import Config
3
6
from openapi_python_client .parser .properties .date import DateProperty
4
7
from openapi_python_client .parser .properties .datetime import DateTimeProperty
5
8
from openapi_python_client .parser .properties .file import FileProperty
6
- from openapi_python_client .parser .properties .model_property import ModelProperty
9
+ from openapi_python_client .parser .properties .model_property import ModelDetails , ModelProperty , _gather_property_data
10
+ from openapi_python_client .parser .properties .schemas import Class , Schemas
7
11
8
12
__all__ = ["merge_properties" ]
9
13
27
31
STRING_WITH_FORMAT_TYPES = (DateProperty , DateTimeProperty , FileProperty )
28
32
29
33
30
- def merge_properties (prop1 : Property , prop2 : Property ) -> Property | PropertyError : # noqa: PLR0911
34
+ def merge_properties (
35
+ prop1 : Property ,
36
+ prop2 : Property ,
37
+ parent_name : str ,
38
+ config : Config ,
39
+ ) -> Property | PropertyError : # noqa: PLR0911
31
40
"""Attempt to create a new property that incorporates the behavior of both.
32
41
33
42
This is used when merging schemas with allOf, when two schemas define a property with the same name.
@@ -54,7 +63,7 @@ def merge_properties(prop1: Property, prop2: Property) -> Property | PropertyErr
54
63
if isinstance (prop1 , EnumProperty ) or isinstance (prop2 , EnumProperty ):
55
64
return _merge_with_enum (prop1 , prop2 )
56
65
57
- if (merged := _merge_same_type (prop1 , prop2 )) is not None :
66
+ if (merged := _merge_same_type (prop1 , prop2 , parent_name , config )) is not None :
58
67
return merged
59
68
60
69
if (merged := _merge_numeric (prop1 , prop2 )) is not None :
@@ -68,7 +77,7 @@ def merge_properties(prop1: Property, prop2: Property) -> Property | PropertyErr
68
77
)
69
78
70
79
71
- def _merge_same_type (prop1 : Property , prop2 : Property ) -> Property | None | PropertyError :
80
+ def _merge_same_type (prop1 : Property , prop2 : Property , parent_name : str , config : Config ) -> Property | None | PropertyError :
72
81
if type (prop1 ) is not type (prop2 ):
73
82
return None
74
83
@@ -77,10 +86,10 @@ def _merge_same_type(prop1: Property, prop2: Property) -> Property | None | Prop
77
86
return prop1
78
87
79
88
if isinstance (prop1 , ModelProperty ) and isinstance (prop2 , ModelProperty ):
80
- return _merge_models (prop1 , prop2 )
89
+ return _merge_models (prop1 , prop2 , parent_name , config )
81
90
82
91
if isinstance (prop1 , ListProperty ) and isinstance (prop2 , ListProperty ):
83
- inner_property = merge_properties (prop1 .inner_property , prop2 .inner_property ) # type: ignore
92
+ inner_property = merge_properties (prop1 .inner_property , prop2 .inner_property , "" , config ) # type: ignore
84
93
if isinstance (inner_property , PropertyError ):
85
94
return PropertyError (detail = f"can't merge list properties: { inner_property .detail } " )
86
95
prop1 .inner_property = inner_property
@@ -90,22 +99,67 @@ def _merge_same_type(prop1: Property, prop2: Property) -> Property | None | Prop
90
99
return _merge_common_attributes (prop1 , prop2 )
91
100
92
101
93
- def _merge_models (prop1 : ModelProperty , prop2 : ModelProperty ) -> Property | PropertyError :
102
+ def _merge_models (prop1 : ModelProperty , prop2 : ModelProperty , parent_name : str , config : Config ) -> Property | PropertyError :
94
103
# Ideally, we would treat this case the same as a schema that consisted of "allOf: [prop1, prop2]",
95
104
# applying the property merge logic recursively and creating a new third schema if the result could
96
105
# not be fully described by one or the other. But for now we will just handle the common case where
97
106
# B is an object type that extends A and fully includes it, with no changes to any of A's properties;
98
107
# in that case, it is valid to just reuse the model class for B.
99
108
for prop in [prop1 , prop2 ]:
100
109
if prop .needs_processing ():
110
+ # This means not all of the details of the schema have been filled in, possibly due to a
111
+ # forward reference. That may be resolved in a later pass, but for now we can't proceed.
101
112
return PropertyError (f"Schema for { prop } in allOf was not processed" , data = prop )
102
- if _model_is_extension_of (prop1 , prop2 ):
103
- extended_model = prop1
104
- elif _model_is_extension_of (prop2 , prop1 ):
105
- extended_model = prop2
106
- else :
107
- return PropertyError (detail = "unable to merge two unrelated object types for this property" )
108
- return _merge_common_attributes (extended_model , prop1 , prop2 )
113
+
114
+ # Detect whether one of the schemas is derived from the other-- that is, if it is (or is equivalent
115
+ # to) the result of taking the other type and adding/modifying properties with allOf. If so, then
116
+ # we can simply use the class of the derived type. We will still call _merge_common_attributes in
117
+ # case any metadata like "description" has been modified.
118
+ if _model_is_extension_of (prop1 , prop2 , parent_name , config ):
119
+ return _merge_common_attributes (prop1 , prop2 )
120
+ elif _model_is_extension_of (prop2 , prop1 , parent_name , config ):
121
+ return _merge_common_attributes (prop2 , prop1 , prop2 )
122
+
123
+ # Neither of the schemas is a superset of the other, so merging them will result in a new type.
124
+ merged_props : dict [str , Property ] = {p .name : p for p in chain (prop1 .required_properties , prop1 .optional_properties )}
125
+ for model in [prop1 , prop2 ]:
126
+ for sub_prop in chain (model .required_properties , model .optional_properties ):
127
+ if sub_prop .name in merged_props :
128
+ merged_prop = merge_properties (merged_props [sub_prop .name ], sub_prop , parent_name , config )
129
+ if isinstance (merged_prop , PropertyError ):
130
+ return merged_prop
131
+ merged_props [sub_prop .name ] = merged_prop
132
+ else :
133
+ merged_props [sub_prop .name ] = sub_prop
134
+
135
+ prop_data = _gather_property_data (merged_props .values (), Schemas ())
136
+
137
+ name = prop2 .name
138
+ class_string = f"{ utils .pascal_case (parent_name )} { utils .pascal_case (name )} "
139
+ class_info = Class .from_string (string = class_string , config = config )
140
+ roots = prop1 .roots .union (prop2 .roots ).difference ({prop1 .class_info .name , prop2 .class_info .name })
141
+ roots .add (class_info .name )
142
+ prop_details = ModelDetails (
143
+ required_properties = prop_data .required_props ,
144
+ optional_properties = prop_data .optional_props ,
145
+ additional_properties = None ,
146
+ relative_imports = prop_data .relative_imports ,
147
+ lazy_imports = prop_data .lazy_imports ,
148
+ )
149
+ prop = ModelProperty (
150
+ class_info = class_info ,
151
+ data = prop2 .data , # TODO: not sure what this should be
152
+ roots = roots ,
153
+ details = prop_details ,
154
+ description = prop2 .description or prop1 .description ,
155
+ default = None ,
156
+ required = prop2 .required or prop1 .required ,
157
+ name = name ,
158
+ python_name = utils .PythonIdentifier (value = name , prefix = config .field_prefix ),
159
+ example = prop2 .example or prop1 .example ,
160
+ )
161
+
162
+ return prop
109
163
110
164
111
165
def _merge_string_with_format (prop1 : Property , prop2 : Property ) -> Property | None | PropertyError :
@@ -190,17 +244,19 @@ def _values_are_subset(prop1: EnumProperty, prop2: EnumProperty) -> bool:
190
244
return set (prop1 .values .items ()) <= set (prop2 .values .items ())
191
245
192
246
193
- def _model_is_extension_of (extended_model : ModelProperty , base_model : ModelProperty ) -> bool :
194
- def _list_is_extension_of (extended_list : list [Property ], base_list : list [Property ]) -> bool :
247
+ def _model_is_extension_of (extended_model : ModelProperty , base_model : ModelProperty , parent_name : str , config : Config ) -> bool :
248
+ def _properties_are_extension_of (extended_list : list [Property ], base_list : list [Property ]) -> bool :
195
249
for p2 in base_list :
196
- if not [p1 for p1 in extended_list if _property_is_extension_of (p2 , p1 )]:
250
+ if not [p1 for p1 in extended_list if _property_is_extension_of (p2 , p1 , parent_name , config )]:
197
251
return False
198
252
return True
199
253
200
- return _list_is_extension_of (
254
+ return _properties_are_extension_of (
201
255
extended_model .required_properties , base_model .required_properties
202
- ) and _list_is_extension_of (extended_model .optional_properties , base_model .optional_properties )
256
+ ) and _properties_are_extension_of (extended_model .optional_properties , base_model .optional_properties )
203
257
204
258
205
- def _property_is_extension_of (extended_prop : PropertyProtocol , base_prop : PropertyProtocol ) -> bool :
206
- return base_prop .name == extended_prop .name and merge_properties (base_prop , extended_prop ) == extended_prop
259
+ def _property_is_extension_of (extended_prop : PropertyProtocol , base_prop : PropertyProtocol , parent_name : str , config : Config ) -> bool :
260
+ return base_prop .name == extended_prop .name and (
261
+ base_prop == extended_prop or merge_properties (base_prop , extended_prop , parent_name , config ) == extended_prop
262
+ )
0 commit comments