Skip to content

Implement DiffSyncModelFlags.NATURAL_DELETION_ORDER. #220

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
May 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions diffsync/enum.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
26 changes: 17 additions & 9 deletions diffsync/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand All @@ -364,31 +360,43 @@ 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

if not modified_model or not dst_model:
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)
elif self.action == DiffSyncActions.DELETE:
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:
return changed

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

Expand Down
13 changes: 7 additions & 6 deletions docs/source/core_engine/01-flags.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions tests/unit/test_diffsync.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
51 changes: 51 additions & 0 deletions tests/unit/test_diffsync_model_flags.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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"]