Skip to content

Commit 92ceca9

Browse files
Extend sync_to|from to accept a Diff (#80)
* Allow Diff object to be passed into sync_to/from. Update tests and examples. * Update examples/01-multiple-data-sources/README.md Co-authored-by: Glenn Matthews <glenn.matthews@networktocode.com> * Update diffsync/__init__.py Co-authored-by: Glenn Matthews <glenn.matthews@networktocode.com> * Update diffsync/__init__.py Co-authored-by: Glenn Matthews <glenn.matthews@networktocode.com> * Add better testing. Add new exception if provided diff_class and diff do not match. * Pylint fails within the pipeline, but not locally. Co-authored-by: Glenn Matthews <glenn.matthews@networktocode.com>
1 parent b59a362 commit 92ceca9

File tree

7 files changed

+84
-10
lines changed

7 files changed

+84
-10
lines changed

diffsync/__init__.py

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323

2424
from .diff import Diff
2525
from .enum import DiffSyncModelFlags, DiffSyncFlags, DiffSyncStatus
26-
from .exceptions import ObjectAlreadyExists, ObjectStoreWrongType, ObjectNotFound
26+
from .exceptions import DiffClassMismatch, ObjectAlreadyExists, ObjectStoreWrongType, ObjectNotFound
2727
from .helpers import DiffSyncDiffer, DiffSyncSyncer
2828

2929

@@ -461,7 +461,8 @@ def sync_from(
461461
diff_class: Type[Diff] = Diff,
462462
flags: DiffSyncFlags = DiffSyncFlags.NONE,
463463
callback: Optional[Callable[[Text, int, int], None]] = None,
464-
):
464+
diff: Optional[Diff] = None,
465+
): # pylint: disable=too-many-arguments:
465466
"""Synchronize data from the given source DiffSync object into the current DiffSync object.
466467
467468
Args:
@@ -470,8 +471,17 @@ def sync_from(
470471
flags (DiffSyncFlags): Flags influencing the behavior of this sync.
471472
callback (function): Function with parameters (stage, current, total), to be called at intervals as the
472473
calculation of the diff and subsequent sync proceed.
474+
diff (Diff): An existing diff to be used rather than generating a completely new diff.
473475
"""
474-
diff = self.diff_from(source, diff_class=diff_class, flags=flags, callback=callback)
476+
if diff_class and diff:
477+
if not isinstance(diff, diff_class):
478+
raise DiffClassMismatch(
479+
f"The provided diff's class ({diff.__class__.__name__}) does not match the diff_class: {diff_class.__name__}",
480+
)
481+
482+
# Generate the diff if an existing diff was not provided
483+
if not diff:
484+
diff = self.diff_from(source, diff_class=diff_class, flags=flags, callback=callback)
475485
syncer = DiffSyncSyncer(diff=diff, src_diffsync=source, dst_diffsync=self, flags=flags, callback=callback)
476486
result = syncer.perform_sync()
477487
if result:
@@ -483,7 +493,8 @@ def sync_to(
483493
diff_class: Type[Diff] = Diff,
484494
flags: DiffSyncFlags = DiffSyncFlags.NONE,
485495
callback: Optional[Callable[[Text, int, int], None]] = None,
486-
):
496+
diff: Optional[Diff] = None,
497+
): # pylint: disable=too-many-arguments
487498
"""Synchronize data from the current DiffSync object into the given target DiffSync object.
488499
489500
Args:
@@ -492,8 +503,9 @@ def sync_to(
492503
flags (DiffSyncFlags): Flags influencing the behavior of this sync.
493504
callback (function): Function with parameters (stage, current, total), to be called at intervals as the
494505
calculation of the diff and subsequent sync proceed.
506+
diff (Diff): An existing diff that will be used when determining what needs to be synced.
495507
"""
496-
target.sync_from(self, diff_class=diff_class, flags=flags, callback=callback)
508+
target.sync_from(self, diff_class=diff_class, flags=flags, callback=callback, diff=diff)
497509

498510
def sync_complete(
499511
self,

diffsync/exceptions.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,11 @@ class ObjectNotFound(ObjectStoreException):
5151

5252
class ObjectStoreWrongType(ObjectStoreException):
5353
"""Exception raised when trying to store a DiffSyncModel of the wrong type."""
54+
55+
56+
class DiffException(Exception):
57+
"""Base class for various failures related to Diff operations."""
58+
59+
60+
class DiffClassMismatch(DiffException):
61+
"""Exception raised when a diff object is not the same as the expected diff_class."""

examples/01-multiple-data-sources/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ Synchronize A and B (update B with the contents of A):
5757
```python
5858
a.sync_to(b)
5959
print(a.diff_to(b).str())
60+
# Alternatively you can pass in the diff object from above to prevent another diff calculation
61+
# a.sync_to(b, diff=diff_a_b)
6062
```
6163

6264
Now A and B will show no differences:

examples/01-multiple-data-sources/main.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ def main():
6969
pprint.pprint(diff_a_b.dict(), width=120)
7070

7171
print("Syncing changes from Backend A to Backend B...")
72-
backend_a.sync_to(backend_b)
72+
backend_a.sync_to(backend_b, diff=diff_a_b)
7373
print("Getting updated diffs from Backend A to Backend B...")
7474
print(backend_a.diff_to(backend_b).str())
7575

examples/03-remote-system/main.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,10 @@ def main():
4040
print(diff.str())
4141

4242
if args.sync:
43+
if not args.diff:
44+
diff = None
4345
print("Updating the list of countries in Nautobot ...")
44-
nautobot.sync_from(local, flags=flags, diff_class=AlphabeticalOrderDiff)
46+
nautobot.sync_from(local, flags=flags, diff_class=AlphabeticalOrderDiff, diff=diff)
4547

4648

4749
if __name__ == "__main__":

examples/04-get-update-instantiate/backends.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
limitations under the License.
1616
"""
1717

18-
from models import Site, Device, Interface
18+
from models import Site, Device, Interface # pylint: disable=no-name-in-module
1919
from diffsync import DiffSync
2020

2121
BACKEND_DATA_A = [

tests/unit/test_diffsync.py

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
import pytest
2121

2222
from diffsync import DiffSync, DiffSyncModel, DiffSyncFlags, DiffSyncModelFlags
23-
from diffsync.exceptions import ObjectAlreadyExists, ObjectNotFound, ObjectCrudException
23+
from diffsync.exceptions import DiffClassMismatch, ObjectAlreadyExists, ObjectNotFound, ObjectCrudException
2424

2525
from .conftest import Site, Device, Interface, TrackedDiff, BackendA, PersonA
2626

@@ -468,6 +468,57 @@ def callback(stage, current, total):
468468
assert last_value == {"current": expected, "total": expected}
469469

470470

471+
def test_diffsync_sync_to_w_different_diff_class_raises(backend_a, backend_b):
472+
diff = backend_b.diff_to(backend_a)
473+
with pytest.raises(DiffClassMismatch) as failure:
474+
backend_b.sync_to(backend_a, diff_class=TrackedDiff, diff=diff)
475+
assert failure.value.args[0] == "The provided diff's class (Diff) does not match the diff_class: TrackedDiff"
476+
477+
478+
def test_diffsync_sync_to_w_diff_no_mocks(backend_a, backend_b):
479+
diff = backend_b.diff_to(backend_a)
480+
assert diff.has_diffs()
481+
# Perform full sync
482+
backend_b.sync_to(backend_a, diff=diff)
483+
# Assert there are no diffs after synchronization
484+
post_diff = backend_b.diff_to(backend_a)
485+
assert not post_diff.has_diffs()
486+
487+
488+
def test_diffsync_sync_to_w_diff(backend_a, backend_b):
489+
diff = backend_b.diff_to(backend_a)
490+
assert diff.has_diffs()
491+
# Mock diff_from to make sure it's not called when passing in an existing diff
492+
backend_b.diff_from = mock.Mock()
493+
backend_b.diff_to = mock.Mock()
494+
backend_a.diff_from = mock.Mock()
495+
backend_a.diff_to = mock.Mock()
496+
# Perform full sync
497+
backend_b.sync_to(backend_a, diff=diff)
498+
# Assert none of the diff methods have been called
499+
assert not backend_b.diff_from.called
500+
assert not backend_b.diff_to.called
501+
assert not backend_a.diff_from.called
502+
assert not backend_a.diff_to.called
503+
504+
505+
def test_diffsync_sync_from_w_diff(backend_a, backend_b):
506+
diff = backend_a.diff_from(backend_b)
507+
assert diff.has_diffs()
508+
# Mock diff_from to make sure it's not called when passing in an existing diff
509+
backend_a.diff_from = mock.Mock()
510+
backend_a.diff_to = mock.Mock()
511+
backend_b.diff_from = mock.Mock()
512+
backend_b.diff_to = mock.Mock()
513+
# Perform full sync
514+
backend_a.sync_from(backend_b, diff=diff)
515+
# Assert none of the diff methods have been called
516+
assert not backend_a.diff_from.called
517+
assert not backend_a.diff_to.called
518+
assert not backend_b.diff_from.called
519+
assert not backend_b.diff_to.called
520+
521+
471522
def test_diffsync_sync_from(backend_a, backend_b):
472523
backend_a.sync_complete = mock.Mock()
473524
backend_b.sync_complete = mock.Mock()
@@ -542,7 +593,6 @@ def check_successful_sync_log_sanity(log, src, dst, flags):
542593
def check_sync_logs_against_diff(diffsync, diff, log, errors_permitted=False):
543594
"""Given a Diff, make sure the captured structlogs correctly correspond to its contents/actions."""
544595
for element in diff.get_children():
545-
print(element)
546596
# This is kinda gross, but needed since a DiffElement stores a shortname and keys, not a unique_id
547597
uid = getattr(diffsync, element.type).create_unique_id(**element.keys)
548598

0 commit comments

Comments
 (0)