Skip to content

Commit c1341fc

Browse files
committed
Add dict() methods on Diff and DiffElement
1 parent 7febdc1 commit c1341fc

File tree

5 files changed

+108
-45
lines changed

5 files changed

+108
-45
lines changed

dsync/__init__.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ def str(self, include_children: bool = True, indent: int = 0) -> str:
222222
return output
223223

224224
@classmethod
225-
def create(cls, dsync: "DSync", ids: Dict, attrs: Dict) -> Optional["DSyncModel"]:
225+
def create(cls, dsync: "DSync", ids: Mapping, attrs: Mapping) -> Optional["DSyncModel"]:
226226
"""Instantiate this class, along with any platform-specific data creation.
227227
228228
Args:
@@ -239,7 +239,7 @@ def create(cls, dsync: "DSync", ids: Dict, attrs: Dict) -> Optional["DSyncModel"
239239
"""
240240
return cls(**ids, dsync=dsync, **attrs)
241241

242-
def update(self, attrs: Dict) -> Optional["DSyncModel"]:
242+
def update(self, attrs: Mapping) -> Optional["DSyncModel"]:
243243
"""Update the attributes of this instance, along with any platform-specific data updates.
244244
245245
Args:
@@ -292,15 +292,15 @@ def get_children_mapping(cls) -> Mapping[Text, Text]:
292292
"""Get the mapping of types to fieldnames for child models of this model."""
293293
return cls._children
294294

295-
def get_identifiers(self) -> Dict:
295+
def get_identifiers(self) -> Mapping:
296296
"""Get a dict of all identifiers (primary keys) and their values for this object.
297297
298298
Returns:
299299
dict: dictionary containing all primary keys for this device, as defined in _identifiers
300300
"""
301301
return self.dict(include=set(self._identifiers))
302302

303-
def get_attrs(self) -> Dict:
303+
def get_attrs(self) -> Mapping:
304304
"""Get all the non-primary-key attributes or parameters for this object.
305305
306306
Similar to Pydantic's `BaseModel.dict()` method, with the following key differences:
@@ -441,9 +441,9 @@ def load(self):
441441
"""Load all desired data from whatever backend data source into this instance."""
442442
# No-op in this generic class
443443

444-
def dict(self, exclude_defaults: bool = True, **kwargs) -> dict:
444+
def dict(self, exclude_defaults: bool = True, **kwargs) -> Mapping:
445445
"""Represent the DSync contents as a dict, as if it were a Pydantic model."""
446-
data: Dict[str, Dict[str, dict]] = {}
446+
data: Dict[str, Dict[str, Dict]] = {}
447447
for modelname in self._data:
448448
data[modelname] = {}
449449
for unique_id, model in self._data[modelname].items():
@@ -531,13 +531,13 @@ def _sync_from_diff_element(
531531
log.debug("Attempting object creation")
532532
if obj:
533533
raise ObjectNotCreated(f"Failed to create {object_class.get_type()} {element.keys} - it exists!")
534-
obj = object_class.create(dsync=self, ids=element.keys, attrs={key: diffs[key]["src"] for key in diffs})
534+
obj = object_class.create(dsync=self, ids=element.keys, attrs=diffs["src"])
535535
log.info("Created successfully", status="success")
536536
elif element.action == "update":
537537
log.debug("Attempting object update")
538538
if not obj:
539539
raise ObjectNotUpdated(f"Failed to update {object_class.get_type()} {element.keys} - not found!")
540-
obj = obj.update(attrs={key: diffs[key]["src"] for key in diffs})
540+
obj = obj.update(attrs=diffs["src"])
541541
log.info("Updated successfully", status="success")
542542
elif element.action == "delete":
543543
log.debug("Attempting object deletion")
@@ -606,7 +606,7 @@ def diff_to(self, target: "DSync", diff_class: Type[Diff] = Diff, flags: DSyncFl
606606
# ------------------------------------------------------------------------------
607607

608608
def get(
609-
self, obj: Union[Text, DSyncModel, Type[DSyncModel]], identifier: Union[Text, Dict]
609+
self, obj: Union[Text, DSyncModel, Type[DSyncModel]], identifier: Union[Text, Mapping]
610610
) -> Optional[DSyncModel]:
611611
"""Get one object from the data store based on its unique id.
612612

dsync/diff.py

Lines changed: 49 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -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
117125
class 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

tests/unit/conftest.py

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
See the License for the specific language governing permissions and
1515
limitations under the License.
1616
"""
17-
from typing import ClassVar, List, Optional, Tuple
17+
from typing import ClassVar, List, Mapping, Optional, Tuple
1818

1919
import pytest
2020

@@ -23,17 +23,6 @@
2323
from dsync.exceptions import ObjectNotCreated, ObjectNotUpdated, ObjectNotDeleted
2424

2525

26-
@pytest.fixture()
27-
def give_me_success():
28-
"""
29-
Provides True to make tests pass
30-
31-
Returns:
32-
(bool): Returns True
33-
"""
34-
return True
35-
36-
3726
@pytest.fixture
3827
def generic_dsync_model():
3928
"""Provide a generic DSyncModel instance."""
@@ -46,14 +35,14 @@ class ErrorProneModel(DSyncModel):
4635
_counter: ClassVar[int] = 0
4736

4837
@classmethod
49-
def create(cls, dsync: DSync, ids: dict, attrs: dict):
38+
def create(cls, dsync: DSync, ids: Mapping, attrs: Mapping):
5039
"""As DSyncModel.create(), but periodically throw exceptions."""
5140
cls._counter += 1
5241
if not cls._counter % 3:
5342
raise ObjectNotCreated("Random creation error!")
5443
return super().create(dsync, ids, attrs)
5544

56-
def update(self, attrs: dict):
45+
def update(self, attrs: Mapping):
5746
"""As DSyncModel.update(), but periodically throw exceptions."""
5847
# pylint: disable=protected-access
5948
self.__class__._counter += 1

tests/unit/test_diff.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@ def test_diff_str_with_no_diffs():
3737
assert diff.str() == "(no diffs)"
3838

3939

40+
def test_diff_dict_with_no_diffs():
41+
diff = Diff()
42+
43+
assert diff.dict() == {}
44+
45+
4046
def test_diff_children():
4147
"""Test the basic functionality of the Diff class when adding child elements."""
4248
diff = Diff()
@@ -89,6 +95,21 @@ def test_diff_str_with_diffs():
8995
)
9096

9197

98+
def test_diff_dict_with_diffs():
99+
diff = Diff()
100+
device_element = DiffElement("device", "device1", {"name": "device1"})
101+
diff.add(device_element)
102+
intf_element = DiffElement("interface", "eth0", {"device_name": "device1", "name": "eth0"})
103+
source_attrs = {"interface_type": "ethernet", "description": "my interface"}
104+
dest_attrs = {"description": "your interface"}
105+
intf_element.add_attrs(source=source_attrs, dest=dest_attrs)
106+
diff.add(intf_element)
107+
108+
assert diff.dict() == {
109+
"interface": {"eth0": {"_dst": {"description": "your interface"}, "_src": {"description": "my interface"}}},
110+
}
111+
112+
92113
def test_order_children_default(backend_a, backend_b):
93114
"""Test that order_children_default is properly called when calling get_children."""
94115

tests/unit/test_diff_element.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,11 @@ def test_diff_element_str_with_no_diffs():
4747
assert element.str() == "interface: eth0 (no diffs)"
4848

4949

50+
def test_diff_element_dict_with_no_diffs():
51+
element = DiffElement("interface", "eth0", {"device_name": "device1", "name": "eth0"})
52+
assert element.dict() == {}
53+
54+
5055
def test_diff_element_attrs():
5156
"""Test the basic functionality of the DiffElement class when setting and retrieving attrs."""
5257
element = DiffElement("interface", "eth0", {"device_name": "device1", "name": "eth0"})
@@ -85,6 +90,14 @@ def test_diff_element_str_with_diffs():
8590
)
8691

8792

93+
def test_diff_element_dict_with_diffs():
94+
element = DiffElement("interface", "eth0", {"device_name": "device1", "name": "eth0"})
95+
element.add_attrs(source={"interface_type": "ethernet", "description": "my interface"})
96+
assert element.dict() == {"_src": {"description": "my interface", "interface_type": "ethernet"}}
97+
element.add_attrs(dest={"description": "your interface"})
98+
assert element.dict() == {"_dst": {"description": "your interface"}, "_src": {"description": "my interface"}}
99+
100+
88101
def test_diff_element_children():
89102
"""Test the basic functionality of the DiffElement class when storing and retrieving child elements."""
90103
child_element = DiffElement("interface", "eth0", {"device_name": "device1", "name": "eth0"})
@@ -122,3 +135,16 @@ def test_diff_element_str_with_child_diffs():
122135
description source(my interface) dest(your interface)\
123136
"""
124137
)
138+
139+
140+
def test_diff_element_dict_with_child_diffs():
141+
child_element = DiffElement("interface", "eth0", {"device_name": "device1", "name": "eth0"})
142+
parent_element = DiffElement("device", "device1", {"name": "device1"})
143+
parent_element.add_child(child_element)
144+
source_attrs = {"interface_type": "ethernet", "description": "my interface"}
145+
dest_attrs = {"description": "your interface"}
146+
child_element.add_attrs(source=source_attrs, dest=dest_attrs)
147+
148+
assert parent_element.dict() == {
149+
"interface": {"eth0": {"_dst": {"description": "your interface"}, "_src": {"description": "my interface"}}},
150+
}

0 commit comments

Comments
 (0)