Skip to content

Commit 2967c2e

Browse files
authored
Merge pull request #220 from networktocode/lk-natural-deletion-order
Implement DiffSyncModelFlags.NATURAL_DELETION_ORDER.
2 parents 34c210c + 3696421 commit 2967c2e

File tree

5 files changed

+83
-15
lines changed

5 files changed

+83
-15
lines changed

diffsync/enum.py

+7
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,13 @@ class DiffSyncModelFlags(enum.Flag):
4747
If this flag is set, the model will not be deleted from the target/"to" DiffSync.
4848
"""
4949

50+
NATURAL_DELETION_ORDER = 0b10000
51+
"""When deleting, delete children before instances of this this element.
52+
53+
If this flag is set, the models children will be deleted from the target/"to" DiffSync before the models instances
54+
themselves.
55+
"""
56+
5057
SKIP_UNMATCHED_BOTH = SKIP_UNMATCHED_SRC | SKIP_UNMATCHED_DST
5158

5259

diffsync/helpers.py

+17-9
Original file line numberDiff line numberDiff line change
@@ -350,11 +350,7 @@ def sync_diff_element(self, element: DiffElement, parent_model: Optional["DiffSy
350350
attrs = diffs.get("+", {})
351351

352352
# Retrieve Source Object to get its flags
353-
src_model: Optional["DiffSyncModel"]
354-
try:
355-
src_model = self.src_diffsync.get(self.model_class, ids)
356-
except ObjectNotFound:
357-
src_model = None
353+
src_model = self.src_diffsync.get_or_none(self.model_class, ids)
358354

359355
# Retrieve Dest (and primary) Object
360356
dst_model: Optional["DiffSyncModel"]
@@ -364,31 +360,43 @@ def sync_diff_element(self, element: DiffElement, parent_model: Optional["DiffSy
364360
except ObjectNotFound:
365361
dst_model = None
366362

363+
natural_deletion_order = False
364+
skip_children = False
365+
# Set up flag booleans
366+
if dst_model:
367+
natural_deletion_order = bool(dst_model.model_flags & DiffSyncModelFlags.NATURAL_DELETION_ORDER)
368+
skip_children = bool(dst_model.model_flags & DiffSyncModelFlags.SKIP_CHILDREN_ON_DELETE)
369+
370+
changed = False
371+
if natural_deletion_order and self.action == DiffSyncActions.DELETE and not skip_children:
372+
for child in element.get_children():
373+
changed |= self.sync_diff_element(child, parent_model=dst_model)
374+
367375
changed, modified_model = self.sync_model(src_model=src_model, dst_model=dst_model, ids=ids, attrs=attrs)
368376
dst_model = modified_model or dst_model
369377

370378
if not modified_model or not dst_model:
371379
self.logger.warning("No object resulted from sync, will not process child objects.")
372380
return changed
373381

374-
if self.action == DiffSyncActions.CREATE: # type: ignore
382+
if self.action == DiffSyncActions.CREATE:
375383
if parent_model:
376384
parent_model.add_child(dst_model)
377385
self.dst_diffsync.add(dst_model)
378386
elif self.action == DiffSyncActions.DELETE:
379387
if parent_model:
380388
parent_model.remove_child(dst_model)
381389

382-
skip_children = bool(dst_model.model_flags & DiffSyncModelFlags.SKIP_CHILDREN_ON_DELETE)
383390
self.dst_diffsync.remove(dst_model, remove_children=skip_children)
384391

385392
if skip_children:
386393
return changed
387394

388395
self.incr_elements_processed()
389396

390-
for child in element.get_children():
391-
changed |= self.sync_diff_element(child, parent_model=dst_model)
397+
if not natural_deletion_order:
398+
for child in element.get_children():
399+
changed |= self.sync_diff_element(child, parent_model=dst_model)
392400

393401
return changed
394402

docs/source/core_engine/01-flags.md

+7-6
Original file line numberDiff line numberDiff line change
@@ -53,13 +53,14 @@ class MyAdapter(DiffSync):
5353

5454
### Supported Model Flags
5555

56-
| Name | Description | Binary Value |
57-
|---|---|---|
56+
| Name | Description | Binary Value |
57+
|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---|
5858
| IGNORE | Do not render diffs containing this model; do not make any changes to this model when synchronizing. Can be used to indicate a model instance that exists but should not be changed by DiffSync. | 0b1 |
59-
| SKIP_CHILDREN_ON_DELETE | When deleting this model, do not recursively delete its children. Can be used for the case where deletion of a model results in the automatic deletion of all its children. | 0b10 |
60-
| SKIP_UNMATCHED_SRC | Ignore the model if it only exists in the source/"from" DiffSync when determining diffs and syncing. If this flag is set, no new model will be created in the target/"to" DiffSync. | 0b100 |
61-
| SKIP_UNMATCHED_DST | Ignore the model if it only exists in the target/"to" DiffSync when determining diffs and syncing. If this flag is set, the model will not be deleted from the target/"to" DiffSync. | 0b1000 |
62-
| SKIP_UNMATCHED_BOTH | Convenience value combining both SKIP_UNMATCHED_SRC and SKIP_UNMATCHED_DST into a single flag | 0b1100 |
59+
| SKIP_CHILDREN_ON_DELETE | When deleting this model, do not recursively delete its children. Can be used for the case where deletion of a model results in the automatic deletion of all its children. | 0b10 |
60+
| SKIP_UNMATCHED_SRC | Ignore the model if it only exists in the source/"from" DiffSync when determining diffs and syncing. If this flag is set, no new model will be created in the target/"to" DiffSync. | 0b100 |
61+
| SKIP_UNMATCHED_DST | Ignore the model if it only exists in the target/"to" DiffSync when determining diffs and syncing. If this flag is set, the model will not be deleted from the target/"to" DiffSync. | 0b1000 |
62+
| SKIP_UNMATCHED_BOTH | Convenience value combining both SKIP_UNMATCHED_SRC and SKIP_UNMATCHED_DST into a single flag | 0b1100 |
63+
| NATURAL_DELETION_ORDER | When deleting, delete children before instances of this model. | 0b10000 |
6364

6465
## Working with flags
6566

tests/unit/test_diffsync.py

+1
Original file line numberDiff line numberDiff line change
@@ -973,6 +973,7 @@ class NoDeleteInterfaceDiffSync(BackendA):
973973
extra_models.load()
974974
extra_device = extra_models.device(name="nyc-spine3", site_name="nyc", role="spine")
975975
extra_device.model_flags |= DiffSyncModelFlags.SKIP_CHILDREN_ON_DELETE
976+
extra_device.model_flags |= DiffSyncModelFlags.NATURAL_DELETION_ORDER
976977
extra_models.get(extra_models.site, "nyc").add_child(extra_device)
977978
extra_models.add(extra_device)
978979
extra_interface = extra_models.interface(name="eth0", device_name="nyc-spine3")

tests/unit/test_diffsync_model_flags.py

+51
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,11 @@
1414
See the License for the specific language governing permissions and
1515
limitations under the License.
1616
"""
17+
from typing import List
1718

1819
import pytest
1920

21+
from diffsync import DiffSync, DiffSyncModel
2022
from diffsync.enum import DiffSyncModelFlags
2123
from diffsync.exceptions import ObjectNotFound
2224

@@ -111,3 +113,52 @@ def test_diffsync_diff_with_ignore_flag_on_target_models(backend_a, backend_a_mi
111113
diff = backend_a.diff_from(backend_a_minus_some_models)
112114
print(diff.str()) # for debugging of any failure
113115
assert not diff.has_diffs()
116+
117+
118+
def test_diffsync_diff_with_natural_deletion_order():
119+
# This list will contain the order in which the delete methods were called
120+
call_order = []
121+
122+
class TestModelChild(DiffSyncModel): # pylint: disable=missing-class-docstring
123+
_modelname = "child"
124+
_identifiers = ("name",)
125+
126+
name: str
127+
128+
def delete(self):
129+
call_order.append(self.name)
130+
return super().delete()
131+
132+
class TestModelParent(DiffSyncModel): # pylint: disable=missing-class-docstring
133+
_modelname = "parent"
134+
_identifiers = ("name",)
135+
_children = {"child": "children"}
136+
137+
name: str
138+
children: List[TestModelChild] = []
139+
140+
def delete(self):
141+
call_order.append(self.name)
142+
return super().delete()
143+
144+
class TestBackend(DiffSync): # pylint: disable=missing-class-docstring
145+
top_level = ["parent"]
146+
147+
parent = TestModelParent
148+
child = TestModelChild
149+
150+
def load(self):
151+
parent = self.parent(name="Test-Parent")
152+
parent.model_flags |= DiffSyncModelFlags.NATURAL_DELETION_ORDER
153+
self.add(parent)
154+
child = self.child(name="Test-Child")
155+
parent.add_child(child)
156+
self.add(child)
157+
158+
source = TestBackend()
159+
source.load()
160+
destination = TestBackend()
161+
destination.load()
162+
source.remove(source.get("parent", {"name": "Test-Parent"}), remove_children=True)
163+
source.sync_to(destination)
164+
assert call_order == ["Test-Child", "Test-Parent"]

0 commit comments

Comments
 (0)