Skip to content

Commit 7febdc1

Browse files
committed
Change Diff/DiffElement print_detailed() to str() and add test coverage
1 parent 4d6d046 commit 7febdc1

File tree

7 files changed

+112
-49
lines changed

7 files changed

+112
-49
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ B.load()
1313

1414
# it will show the difference between both systems
1515
diff_a_b = A.diff_from(B)
16-
diff.print_detailed()
16+
print(diff.str())
1717

1818
# it will update System A to align with the current status of system B
1919
A.sync_from(B)

dsync/diff.py

Lines changed: 39 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
"""
1717

1818
from functools import total_ordering
19-
from typing import Any, Iterator, Iterable, Mapping, Optional
19+
from typing import Any, Iterator, Iterable, Mapping, Optional, Text
2020

2121
from .exceptions import ObjectAlreadyExists
2222
from .utils import intersection, OrderedDefaultDict
@@ -64,7 +64,7 @@ def has_diffs(self) -> bool:
6464
"""
6565
for group in self.groups():
6666
for child in self.children[group].values():
67-
if child.has_diffs():
67+
if child.has_diffs(include_children=True):
6868
return True
6969

7070
return False
@@ -95,26 +95,30 @@ def order_children_default(cls, children: dict) -> Iterator["DiffElement"]:
9595
for child in children.values():
9696
yield child
9797

98-
def print_detailed(self, indent: int = 0):
99-
"""Print all diffs to screen for all child elements.
100-
101-
Args:
102-
indent (int, optional): Indentation to use when printing to screen. Defaults to 0.
103-
"""
98+
def str(self, indent: int = 0):
99+
"""Build a detailed string representation of this Diff and its child DiffElements."""
104100
margin = " " * indent
101+
output = []
105102
for group in self.groups():
106-
print(f"{margin}{group}")
103+
group_heading_added = False
107104
for child in self.children[group].values():
108-
if child.has_diffs():
109-
child.print_detailed(indent + 2)
105+
if child.has_diffs(include_children=True):
106+
if not group_heading_added:
107+
output.append(f"{margin}{group}")
108+
group_heading_added = True
109+
output.append(child.str(indent + 2))
110+
result = "\n".join(output)
111+
if not result:
112+
result = "(no diffs)"
113+
return result
110114

111115

112116
@total_ordering
113117
class DiffElement: # pylint: disable=too-many-instance-attributes
114118
"""DiffElement object, designed to represent a single item/object that may or may not have any diffs."""
115119

116120
def __init__(
117-
self, obj_type: str, name: str, keys: dict, source_name: str = "source", dest_name: str = "dest"
121+
self, obj_type: Text, name: Text, keys: dict, source_name: Text = "source", dest_name: Text = "dest"
118122
): # pylint: disable=too-many-arguments
119123
"""Instantiate a DiffElement.
120124
@@ -170,7 +174,7 @@ def __str__(self):
170174
return f'{self.type} "{self.name}" : {self.keys} : {self.source_name}{self.dest_name} : {self.get_attrs_diffs()}'
171175

172176
@property
173-
def action(self) -> Optional[str]:
177+
def action(self) -> Optional[Text]:
174178
"""Action, if any, that should be taken to remediate the diffs described by this element.
175179
176180
Returns:
@@ -199,7 +203,7 @@ def add_attrs(self, source: Optional[dict] = None, dest: Optional[dict] = None):
199203
if dest is not None:
200204
self.dest_attrs = dest
201205

202-
def get_attrs_keys(self) -> Iterable[str]:
206+
def get_attrs_keys(self) -> Iterable[Text]:
203207
"""Get the list of shared attrs between source and dest, or the attrs of source or dest if only one is present.
204208
205209
- If source_attrs is not set, return the keys of dest_attrs
@@ -215,8 +219,8 @@ def get_attrs_keys(self) -> Iterable[str]:
215219
return []
216220

217221
# The below would be more accurate but typing.Literal is only in Python 3.8 and later
218-
# def get_attrs_diffs(self) -> Mapping[str, Mapping[Literal["src", "dst"], Any]]:
219-
def get_attrs_diffs(self) -> Mapping[str, Mapping[str, Any]]:
222+
# def get_attrs_diffs(self) -> Mapping[Text, Mapping[Literal["src", "dst"], Any]]:
223+
def get_attrs_diffs(self) -> Mapping[Text, Mapping[Text, Any]]:
220224
"""Get the dict of actual attribute diffs between source_attrs and dest_attrs.
221225
222226
Returns:
@@ -269,22 +273,25 @@ def has_diffs(self, include_children: bool = True) -> bool:
269273

270274
return False
271275

272-
def print_detailed(self, indent: int = 0):
273-
"""Print status on screen for current object and all children.
274-
275-
Args:
276-
indent: Default value = 0
277-
"""
276+
def str(self, indent: int = 0):
277+
"""Build a detailed string representation of this DiffElement and its children."""
278278
margin = " " * indent
279-
280-
if self.source_attrs is None:
281-
print(f"{margin}{self.type}: {self.name} MISSING in {self.source_name}")
282-
elif self.dest_attrs is None:
283-
print(f"{margin}{self.type}: {self.name} MISSING in {self.dest_name}")
284-
else:
285-
print(f"{margin}{self.type}: {self.name}")
279+
result = f"{margin}{self.type}: {self.name}"
280+
if self.source_attrs is not None and self.dest_attrs is not None:
286281
# Only print attrs that have meaning in both source and dest
287282
for attr, item in self.get_attrs_diffs().items():
288-
print(f"{margin} {attr} {self.source_name}({item.get('src')}) {self.dest_name}({item.get('dst')})")
289-
290-
self.child_diff.print_detailed(indent + 2)
283+
result += (
284+
f"\n{margin} {attr}"
285+
f" {self.source_name}({item.get('src')})"
286+
f" {self.dest_name}({item.get('dst')})"
287+
)
288+
elif self.dest_attrs is not None:
289+
result += f" MISSING in {self.source_name}"
290+
elif self.source_attrs is not None:
291+
result += f" MISSING in {self.dest_name}"
292+
293+
if self.child_diff.has_diffs():
294+
result += "\n" + self.child_diff.str(indent + 2)
295+
elif self.source_attrs is None and self.dest_attrs is None:
296+
result += " (no diffs)"
297+
return result

examples/example1/README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,25 +38,25 @@ enable_console_logging(verbosity=0) # Show WARNING and ERROR logs only
3838
Show the differences between A and B
3939
```python
4040
diff_a_b = a.diff_to(b)
41-
diff_a_b.print_detailed()
41+
print(diff_a_b.str())
4242
```
4343

4444
Show the differences between B and C
4545
```python
4646
diff_b_c = c.diff_from(b)
47-
diff_b_c.print_detailed()
47+
print(diff_b_c.str())
4848
```
4949

5050
Synchronize A and B (update B with the contents of A)
5151
```python
5252
a.sync_to(b)
53-
a.diff_to(b).print_detailed()
53+
print(a.diff_to(b).str())
5454
```
5555

5656
Now A and B will show no differences
5757
```python
5858
diff_a_b = a.diff_to(b)
59-
diff_a_b.print_detailed()
59+
print(diff_a_b.str())
6060
```
6161

6262
> In the Device model, the `site_name` and `role` are not included in the `_attributes`, so they are not shown when we are comparing the different objects, even if the value is different.

examples/example1/main.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,12 +61,12 @@ def main():
6161

6262
print("Getting diffs from Backend A to Backend B...")
6363
diff_a_b = backend_a.diff_to(backend_b, diff_class=MyDiff)
64-
diff_a_b.print_detailed()
64+
print(diff_a_b.str())
6565

6666
print("Syncing changes from Backend A to Backend B...")
6767
backend_a.sync_to(backend_b)
6868
print("Getting updated diffs from Backend A to Backend B...")
69-
backend_a.diff_to(backend_b).print_detailed()
69+
print(backend_a.diff_to(backend_b).str())
7070

7171

7272
if __name__ == "__main__":

tests/unit/test_diff.py

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,11 @@ def test_diff_empty():
3030
assert not diff.has_diffs()
3131
assert list(diff.get_children()) == []
3232

33-
# TODO: test print_detailed
33+
34+
def test_diff_str_with_no_diffs():
35+
diff = Diff()
36+
37+
assert diff.str() == "(no diffs)"
3438

3539

3640
def test_diff_children():
@@ -63,7 +67,26 @@ def test_diff_children():
6367

6468
assert diff.has_diffs()
6569

66-
# TODO: test print_detailed
70+
71+
def test_diff_str_with_diffs():
72+
diff = Diff()
73+
device_element = DiffElement("device", "device1", {"name": "device1"})
74+
diff.add(device_element)
75+
intf_element = DiffElement("interface", "eth0", {"device_name": "device1", "name": "eth0"})
76+
source_attrs = {"interface_type": "ethernet", "description": "my interface"}
77+
dest_attrs = {"description": "your interface"}
78+
intf_element.add_attrs(source=source_attrs, dest=dest_attrs)
79+
diff.add(intf_element)
80+
81+
# Since device_element has no diffs, we don't have any "device" entry in the diff string:
82+
assert (
83+
diff.str()
84+
== """\
85+
interface
86+
interface: eth0
87+
description source(my interface) dest(your interface)\
88+
"""
89+
)
6790

6891

6992
def test_order_children_default(backend_a, backend_b):

tests/unit/test_diff_element.py

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,11 @@ def test_diff_element_empty():
4040
)
4141
assert element2.source_name == "S1"
4242
assert element2.dest_name == "D1"
43-
# TODO: test print_detailed
43+
44+
45+
def test_diff_element_str_with_no_diffs():
46+
element = DiffElement("interface", "eth0", {"device_name": "device1", "name": "eth0"})
47+
assert element.str() == "interface: eth0 (no diffs)"
4448

4549

4650
def test_diff_element_attrs():
@@ -66,7 +70,19 @@ def test_diff_element_attrs():
6670
assert element.has_diffs(include_children=False)
6771
assert element.get_attrs_keys() == ["description"] # intersection of source_attrs.keys() and dest_attrs.keys()
6872

69-
# TODO: test print_detailed
73+
74+
def test_diff_element_str_with_diffs():
75+
element = DiffElement("interface", "eth0", {"device_name": "device1", "name": "eth0"})
76+
element.add_attrs(source={"interface_type": "ethernet", "description": "my interface"})
77+
assert element.str() == "interface: eth0 MISSING in dest"
78+
element.add_attrs(dest={"description": "your interface"})
79+
assert (
80+
element.str()
81+
== """\
82+
interface: eth0
83+
description source(my interface) dest(your interface)\
84+
"""
85+
)
7086

7187

7288
def test_diff_element_children():
@@ -88,4 +104,21 @@ def test_diff_element_children():
88104
assert parent_element.has_diffs(include_children=True)
89105
assert not parent_element.has_diffs(include_children=False)
90106

91-
# TODO: test print_detailed
107+
108+
def test_diff_element_str_with_child_diffs():
109+
child_element = DiffElement("interface", "eth0", {"device_name": "device1", "name": "eth0"})
110+
parent_element = DiffElement("device", "device1", {"name": "device1"})
111+
parent_element.add_child(child_element)
112+
source_attrs = {"interface_type": "ethernet", "description": "my interface"}
113+
dest_attrs = {"description": "your interface"}
114+
child_element.add_attrs(source=source_attrs, dest=dest_attrs)
115+
116+
assert (
117+
parent_element.str()
118+
== """\
119+
device: device1
120+
interface
121+
interface: eth0
122+
description source(my interface) dest(your interface)\
123+
"""
124+
)

tests/unit/test_dsync.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -346,7 +346,7 @@ def test_dsync_sync_from_with_continue_on_failure_flag(log, error_prone_backend_
346346
error_prone_backend_a.sync_from(backend_b, flags=DSyncFlags.CONTINUE_ON_FAILURE)
347347
# Not all sync operations succeeded on the first try
348348
remaining_diffs = error_prone_backend_a.diff_from(backend_b)
349-
remaining_diffs.print_detailed()
349+
print(remaining_diffs.str()) # for debugging of any failure
350350
assert remaining_diffs.has_diffs()
351351

352352
# At least some operations of each type should have succeeded
@@ -364,7 +364,7 @@ def test_dsync_sync_from_with_continue_on_failure_flag(log, error_prone_backend_
364364
print(f"Sync retry #{i}")
365365
error_prone_backend_a.sync_from(backend_b, flags=DSyncFlags.CONTINUE_ON_FAILURE)
366366
remaining_diffs = error_prone_backend_a.diff_from(backend_b)
367-
remaining_diffs.print_detailed()
367+
print(remaining_diffs.str()) # for debugging of any failure
368368
if remaining_diffs.has_diffs():
369369
# If we still have diffs, some ERROR messages should have been logged
370370
assert [event for event in log.events if event["level"] == "error"] != []
@@ -427,7 +427,7 @@ def test_dsync_diff_with_ignore_flag_on_source_models(backend_a, backend_a_with_
427427
backend_a_with_extra_models.get(backend_a_with_extra_models.site, "nyc").model_flags |= DSyncModelFlags.IGNORE
428428

429429
diff = backend_a.diff_from(backend_a_with_extra_models)
430-
diff.print_detailed()
430+
print(diff.str()) # for debugging of any failure
431431
assert not diff.has_diffs()
432432

433433

@@ -438,7 +438,7 @@ def test_dsync_diff_with_ignore_flag_on_target_models(backend_a, backend_a_minus
438438
backend_a.get(backend_a.site, "sfo").model_flags |= DSyncModelFlags.IGNORE
439439

440440
diff = backend_a.diff_from(backend_a_minus_some_models)
441-
diff.print_detailed()
441+
print(diff.str()) # for debugging of any failure
442442
assert not diff.has_diffs()
443443

444444

@@ -471,5 +471,5 @@ class NoDeleteInterfaceDSync(BackendA):
471471
assert extra_models.get(extra_models.interface, extra_interface.get_unique_id()) is None
472472
# The sync should be complete, regardless
473473
diff = extra_models.diff_from(backend_a)
474-
diff.print_detailed()
474+
print(diff.str()) # for debugging of any failure
475475
assert not diff.has_diffs()

0 commit comments

Comments
 (0)