Skip to content

Commit 58f4806

Browse files
committed
Implement DiffSyncModelFlags.NATURAL_DELETION_ORDER.
1 parent 95fb491 commit 58f4806

File tree

4 files changed

+79
-14
lines changed

4 files changed

+79
-14
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

+14-8
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,14 +360,23 @@ 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+
if dst_model:
365+
natural_deletion_order = bool(dst_model.model_flags & DiffSyncModelFlags.NATURAL_DELETION_ORDER)
366+
367+
changed = False
368+
if natural_deletion_order and self.action == DiffSyncActions.DELETE:
369+
for child in element.get_children():
370+
changed |= self.sync_diff_element(child, parent_model=dst_model)
371+
367372
changed, modified_model = self.sync_model(src_model=src_model, dst_model=dst_model, ids=ids, attrs=attrs)
368373
dst_model = modified_model or dst_model
369374

370375
if not modified_model or not dst_model:
371376
self.logger.warning("No object resulted from sync, will not process child objects.")
372377
return changed
373378

374-
if self.action == DiffSyncActions.CREATE: # type: ignore
379+
if self.action == DiffSyncActions.CREATE:
375380
if parent_model:
376381
parent_model.add_child(dst_model)
377382
self.dst_diffsync.add(dst_model)
@@ -387,8 +392,9 @@ def sync_diff_element(self, element: DiffElement, parent_model: Optional["DiffSy
387392

388393
self.incr_elements_processed()
389394

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

393399
return changed
394400

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_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)