@@ -87,7 +87,7 @@ def get_children(self) -> Iterator["DiffElement"]:
8787 yield from order_method (self .children [group ])
8888
8989 @classmethod
90- def order_children_default (cls , children : dict ) -> Iterator ["DiffElement" ]:
90+ def order_children_default (cls , children : Mapping ) -> Iterator ["DiffElement" ]:
9191 """Default method to an Iterator for children.
9292
9393 Since children is already an OrderedDefaultDict, this method is not doing anything special.
@@ -112,23 +112,31 @@ def str(self, indent: int = 0):
112112 result = "(no diffs)"
113113 return result
114114
115+ def dict (self ) -> Mapping [Text , Mapping [Text , Mapping ]]:
116+ """Build a dictionary representation of this Diff."""
117+ result = OrderedDefaultDict (dict )
118+ for child in self .get_children ():
119+ if child .has_diffs (include_children = True ):
120+ result [child .type ][child .name ] = child .dict ()
121+ return dict (result )
122+
115123
116124@total_ordering
117125class DiffElement : # pylint: disable=too-many-instance-attributes
118126 """DiffElement object, designed to represent a single item/object that may or may not have any diffs."""
119127
120128 def __init__ (
121- self , obj_type : Text , name : Text , keys : dict , source_name : Text = "source" , dest_name : Text = "dest"
129+ self , obj_type : Text , name : Text , keys : Mapping , source_name : Text = "source" , dest_name : Text = "dest"
122130 ): # pylint: disable=too-many-arguments
123131 """Instantiate a DiffElement.
124132
125133 Args:
126- obj_type (str) : Name of the object type being described, as in DSyncModel.get_type().
127- name (str) : Human-readable name of the object being described, as in DSyncModel.get_shortname().
134+ obj_type: Name of the object type being described, as in DSyncModel.get_type().
135+ name: Human-readable name of the object being described, as in DSyncModel.get_shortname().
128136 This name must be unique within the context of the Diff that is the direct parent of this DiffElement.
129- keys (dict) : Primary keys and values uniquely describing this object, as in DSyncModel.get_identifiers().
130- source_name (str) : Name of the source DSync object
131- dest_name (str) : Name of the destination DSync object
137+ keys: Primary keys and values uniquely describing this object, as in DSyncModel.get_identifiers().
138+ source_name: Name of the source DSync object
139+ dest_name: Name of the destination DSync object
132140 """
133141 if not isinstance (obj_type , str ):
134142 raise ValueError (f"obj_type must be a string (not { type (obj_type )} )" )
@@ -142,8 +150,8 @@ def __init__(
142150 self .source_name = source_name
143151 self .dest_name = dest_name
144152 # Note: *_attrs == None if no target object exists; it'll be an empty dict if it exists but has no _attributes
145- self .source_attrs : Optional [dict ] = None
146- self .dest_attrs : Optional [dict ] = None
153+ self .source_attrs : Optional [Mapping ] = None
154+ self .dest_attrs : Optional [Mapping ] = None
147155 self .child_diff = Diff ()
148156
149157 def __lt__ (self , other ):
@@ -194,7 +202,7 @@ def action(self) -> Optional[Text]:
194202 return None
195203
196204 # TODO: separate into set_source_attrs() and set_dest_attrs() methods, or just use direct property access instead?
197- def add_attrs (self , source : Optional [dict ] = None , dest : Optional [dict ] = None ):
205+ def add_attrs (self , source : Optional [Mapping ] = None , dest : Optional [Mapping ] = None ):
198206 """Set additional attributes of a source and/or destination item that may result in diffs."""
199207 # TODO: should source_attrs and dest_attrs be "write-once" properties, or is it OK to overwrite them once set?
200208 if source is not None :
@@ -218,25 +226,31 @@ def get_attrs_keys(self) -> Iterable[Text]:
218226 return self .source_attrs .keys ()
219227 return []
220228
221- # The below would be more accurate but typing.Literal is only in Python 3.8 and later
222- # def get_attrs_diffs(self) -> Mapping[Text, Mapping[Literal["src", "dst"], Any]]:
223229 def get_attrs_diffs (self ) -> Mapping [Text , Mapping [Text , Any ]]:
224230 """Get the dict of actual attribute diffs between source_attrs and dest_attrs.
225231
226232 Returns:
227- dict: of the form `{key: {src: <value>, dst: <value>}, key2: ...}`
233+ dict: of the form `{src: {key1: <value>, key2: ...}, dst: {key1: <value>, key2: ...}}`,
234+ where the `src` or `dst` dicts may be empty.
228235 """
229236 if self .source_attrs is not None and self .dest_attrs is not None :
230237 return {
231- key : dict (src = self .source_attrs [key ], dst = self .dest_attrs [key ])
232- for key in self .get_attrs_keys ()
233- if self .source_attrs [key ] != self .dest_attrs [key ]
238+ "src" : {
239+ key : self .source_attrs [key ]
240+ for key in self .get_attrs_keys ()
241+ if self .source_attrs [key ] != self .dest_attrs [key ]
242+ },
243+ "dst" : {
244+ key : self .dest_attrs [key ]
245+ for key in self .get_attrs_keys ()
246+ if self .source_attrs [key ] != self .dest_attrs [key ]
247+ },
234248 }
235249 if self .source_attrs is None and self .dest_attrs is not None :
236- return {key : dict ( src = None , dst = self .dest_attrs [key ]) for key in self .get_attrs_keys ()}
250+ return {"src" : {}, " dst" : { key : self .dest_attrs [key ] for key in self .get_attrs_keys ()} }
237251 if self .source_attrs is not None and self .dest_attrs is None :
238- return {key : dict ( src = self .source_attrs [key ], dst = None ) for key in self .get_attrs_keys ()}
239- return {}
252+ return {"src" : { key : self .source_attrs [key ] for key in self .get_attrs_keys ()}, "dst" : {} }
253+ return {"src" : {}, "dst" : {} }
240254
241255 def add_child (self , element : "DiffElement" ):
242256 """Attach a child object of type DiffElement.
@@ -279,11 +293,12 @@ def str(self, indent: int = 0):
279293 result = f"{ margin } { self .type } : { self .name } "
280294 if self .source_attrs is not None and self .dest_attrs is not None :
281295 # Only print attrs that have meaning in both source and dest
282- for attr , item in self .get_attrs_diffs ().items ():
296+ attrs_diffs = self .get_attrs_diffs ()
297+ for attr in attrs_diffs ["src" ]:
283298 result += (
284299 f"\n { margin } { attr } "
285- f" { self .source_name } ({ item . get ( 'src' ) } )"
286- f" { self .dest_name } ({ item . get ( 'dst' ) } )"
300+ f" { self .source_name } ({ attrs_diffs [ 'src' ][ attr ] } )"
301+ f" { self .dest_name } ({ attrs_diffs [ 'dst' ][ attr ] } )"
287302 )
288303 elif self .dest_attrs is not None :
289304 result += f" MISSING in { self .source_name } "
@@ -295,3 +310,15 @@ def str(self, indent: int = 0):
295310 elif self .source_attrs is None and self .dest_attrs is None :
296311 result += " (no diffs)"
297312 return result
313+
314+ def dict (self ) -> Mapping [Text , Mapping [Text , Any ]]:
315+ """Build a dictionary representation of this DiffElement and its children."""
316+ attrs_diffs = self .get_attrs_diffs ()
317+ result = {}
318+ if attrs_diffs .get ("src" ):
319+ result ["_src" ] = attrs_diffs ["src" ]
320+ if attrs_diffs .get ("dst" ):
321+ result ["_dst" ] = attrs_diffs ["dst" ]
322+ if self .child_diff .has_diffs ():
323+ result .update (self .child_diff .dict ())
324+ return result
0 commit comments