diff --git a/diffsync/enum.py b/diffsync/enum.py index 57179c9..cb56532 100644 --- a/diffsync/enum.py +++ b/diffsync/enum.py @@ -47,6 +47,13 @@ class DiffSyncModelFlags(enum.Flag): If this flag is set, the model will not be deleted from the target/"to" DiffSync. """ + NATURAL_DELETION_ORDER = 0b10000 + """When deleting, delete children before instances of this this element. + + If this flag is set, the models children will be deleted from the target/"to" DiffSync before the models instances + themselves. + """ + SKIP_UNMATCHED_BOTH = SKIP_UNMATCHED_SRC | SKIP_UNMATCHED_DST diff --git a/diffsync/helpers.py b/diffsync/helpers.py index e940410..8da7fd5 100644 --- a/diffsync/helpers.py +++ b/diffsync/helpers.py @@ -350,11 +350,7 @@ def sync_diff_element(self, element: DiffElement, parent_model: Optional["DiffSy attrs = diffs.get("+", {}) # Retrieve Source Object to get its flags - src_model: Optional["DiffSyncModel"] - try: - src_model = self.src_diffsync.get(self.model_class, ids) - except ObjectNotFound: - src_model = None + src_model = self.src_diffsync.get_or_none(self.model_class, ids) # Retrieve Dest (and primary) Object dst_model: Optional["DiffSyncModel"] @@ -364,6 +360,18 @@ def sync_diff_element(self, element: DiffElement, parent_model: Optional["DiffSy except ObjectNotFound: dst_model = None + natural_deletion_order = False + skip_children = False + # Set up flag booleans + if dst_model: + natural_deletion_order = bool(dst_model.model_flags & DiffSyncModelFlags.NATURAL_DELETION_ORDER) + skip_children = bool(dst_model.model_flags & DiffSyncModelFlags.SKIP_CHILDREN_ON_DELETE) + + changed = False + if natural_deletion_order and self.action == DiffSyncActions.DELETE and not skip_children: + for child in element.get_children(): + changed |= self.sync_diff_element(child, parent_model=dst_model) + changed, modified_model = self.sync_model(src_model=src_model, dst_model=dst_model, ids=ids, attrs=attrs) dst_model = modified_model or dst_model @@ -371,7 +379,7 @@ def sync_diff_element(self, element: DiffElement, parent_model: Optional["DiffSy self.logger.warning("No object resulted from sync, will not process child objects.") return changed - if self.action == DiffSyncActions.CREATE: # type: ignore + if self.action == DiffSyncActions.CREATE: if parent_model: parent_model.add_child(dst_model) self.dst_diffsync.add(dst_model) @@ -379,7 +387,6 @@ def sync_diff_element(self, element: DiffElement, parent_model: Optional["DiffSy if parent_model: parent_model.remove_child(dst_model) - skip_children = bool(dst_model.model_flags & DiffSyncModelFlags.SKIP_CHILDREN_ON_DELETE) self.dst_diffsync.remove(dst_model, remove_children=skip_children) if skip_children: @@ -387,8 +394,9 @@ def sync_diff_element(self, element: DiffElement, parent_model: Optional["DiffSy self.incr_elements_processed() - for child in element.get_children(): - changed |= self.sync_diff_element(child, parent_model=dst_model) + if not natural_deletion_order: + for child in element.get_children(): + changed |= self.sync_diff_element(child, parent_model=dst_model) return changed diff --git a/docs/source/core_engine/01-flags.md b/docs/source/core_engine/01-flags.md index 0556145..542703e 100644 --- a/docs/source/core_engine/01-flags.md +++ b/docs/source/core_engine/01-flags.md @@ -53,13 +53,14 @@ class MyAdapter(DiffSync): ### Supported Model Flags -| Name | Description | Binary Value | -|---|---|---| +| Name | Description | Binary Value | +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---| | 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 | -| 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 | -| 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 | -| 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 | -| SKIP_UNMATCHED_BOTH | Convenience value combining both SKIP_UNMATCHED_SRC and SKIP_UNMATCHED_DST into a single flag | 0b1100 | +| 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 | +| 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 | +| 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 | +| SKIP_UNMATCHED_BOTH | Convenience value combining both SKIP_UNMATCHED_SRC and SKIP_UNMATCHED_DST into a single flag | 0b1100 | +| NATURAL_DELETION_ORDER | When deleting, delete children before instances of this model. | 0b10000 | ## Working with flags diff --git a/tests/unit/test_diffsync.py b/tests/unit/test_diffsync.py index ebf0e49..74727be 100644 --- a/tests/unit/test_diffsync.py +++ b/tests/unit/test_diffsync.py @@ -973,6 +973,7 @@ class NoDeleteInterfaceDiffSync(BackendA): extra_models.load() extra_device = extra_models.device(name="nyc-spine3", site_name="nyc", role="spine") extra_device.model_flags |= DiffSyncModelFlags.SKIP_CHILDREN_ON_DELETE + extra_device.model_flags |= DiffSyncModelFlags.NATURAL_DELETION_ORDER extra_models.get(extra_models.site, "nyc").add_child(extra_device) extra_models.add(extra_device) extra_interface = extra_models.interface(name="eth0", device_name="nyc-spine3") diff --git a/tests/unit/test_diffsync_model_flags.py b/tests/unit/test_diffsync_model_flags.py index b7950ae..76457cc 100644 --- a/tests/unit/test_diffsync_model_flags.py +++ b/tests/unit/test_diffsync_model_flags.py @@ -14,9 +14,11 @@ See the License for the specific language governing permissions and limitations under the License. """ +from typing import List import pytest +from diffsync import DiffSync, DiffSyncModel from diffsync.enum import DiffSyncModelFlags from diffsync.exceptions import ObjectNotFound @@ -111,3 +113,52 @@ def test_diffsync_diff_with_ignore_flag_on_target_models(backend_a, backend_a_mi diff = backend_a.diff_from(backend_a_minus_some_models) print(diff.str()) # for debugging of any failure assert not diff.has_diffs() + + +def test_diffsync_diff_with_natural_deletion_order(): + # This list will contain the order in which the delete methods were called + call_order = [] + + class TestModelChild(DiffSyncModel): # pylint: disable=missing-class-docstring + _modelname = "child" + _identifiers = ("name",) + + name: str + + def delete(self): + call_order.append(self.name) + return super().delete() + + class TestModelParent(DiffSyncModel): # pylint: disable=missing-class-docstring + _modelname = "parent" + _identifiers = ("name",) + _children = {"child": "children"} + + name: str + children: List[TestModelChild] = [] + + def delete(self): + call_order.append(self.name) + return super().delete() + + class TestBackend(DiffSync): # pylint: disable=missing-class-docstring + top_level = ["parent"] + + parent = TestModelParent + child = TestModelChild + + def load(self): + parent = self.parent(name="Test-Parent") + parent.model_flags |= DiffSyncModelFlags.NATURAL_DELETION_ORDER + self.add(parent) + child = self.child(name="Test-Child") + parent.add_child(child) + self.add(child) + + source = TestBackend() + source.load() + destination = TestBackend() + destination.load() + source.remove(source.get("parent", {"name": "Test-Parent"}), remove_children=True) + source.sync_to(destination) + assert call_order == ["Test-Child", "Test-Parent"]