11from __future__ import annotations
2+ from itertools import chain
23
4+ from openapi_python_client import utils
5+ from openapi_python_client .config import Config
36from openapi_python_client .parser .properties .date import DateProperty
47from openapi_python_client .parser .properties .datetime import DateTimeProperty
58from 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
711
812__all__ = ["merge_properties" ]
913
2731STRING_WITH_FORMAT_TYPES = (DateProperty , DateTimeProperty , FileProperty )
2832
2933
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
3140 """Attempt to create a new property that incorporates the behavior of both.
3241
3342 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
5463 if isinstance (prop1 , EnumProperty ) or isinstance (prop2 , EnumProperty ):
5564 return _merge_with_enum (prop1 , prop2 )
5665
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 :
5867 return merged
5968
6069 if (merged := _merge_numeric (prop1 , prop2 )) is not None :
@@ -68,7 +77,7 @@ def merge_properties(prop1: Property, prop2: Property) -> Property | PropertyErr
6877 )
6978
7079
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 :
7281 if type (prop1 ) is not type (prop2 ):
7382 return None
7483
@@ -77,10 +86,10 @@ def _merge_same_type(prop1: Property, prop2: Property) -> Property | None | Prop
7786 return prop1
7887
7988 if isinstance (prop1 , ModelProperty ) and isinstance (prop2 , ModelProperty ):
80- return _merge_models (prop1 , prop2 )
89+ return _merge_models (prop1 , prop2 , parent_name , config )
8190
8291 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
8493 if isinstance (inner_property , PropertyError ):
8594 return PropertyError (detail = f"can't merge list properties: { inner_property .detail } " )
8695 prop1 .inner_property = inner_property
@@ -90,22 +99,67 @@ def _merge_same_type(prop1: Property, prop2: Property) -> Property | None | Prop
9099 return _merge_common_attributes (prop1 , prop2 )
91100
92101
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 :
94103 # Ideally, we would treat this case the same as a schema that consisted of "allOf: [prop1, prop2]",
95104 # applying the property merge logic recursively and creating a new third schema if the result could
96105 # not be fully described by one or the other. But for now we will just handle the common case where
97106 # B is an object type that extends A and fully includes it, with no changes to any of A's properties;
98107 # in that case, it is valid to just reuse the model class for B.
99108 for prop in [prop1 , prop2 ]:
100109 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.
101112 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
109163
110164
111165def _merge_string_with_format (prop1 : Property , prop2 : Property ) -> Property | None | PropertyError :
@@ -190,17 +244,19 @@ def _values_are_subset(prop1: EnumProperty, prop2: EnumProperty) -> bool:
190244 return set (prop1 .values .items ()) <= set (prop2 .values .items ())
191245
192246
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 :
195249 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 )]:
197251 return False
198252 return True
199253
200- return _list_is_extension_of (
254+ return _properties_are_extension_of (
201255 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 )
203257
204258
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