Skip to content

Commit 1183e0c

Browse files
committed
Change DiffSync.get() to also raise ObjectNotFound instead of returning None
1 parent aab30b2 commit 1183e0c

File tree

3 files changed

+56
-23
lines changed

3 files changed

+56
-23
lines changed

diffsync/__init__.py

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -214,11 +214,11 @@ def str(self, include_children: bool = True, indent: int = 0) -> str:
214214
output += f": {child_ids}"
215215
else:
216216
for child_id in child_ids:
217-
child = self.diffsync.get(modelname, child_id)
218-
if not child:
219-
output += f"\n{margin} {child_id} (details unavailable)"
220-
else:
217+
try:
218+
child = self.diffsync.get(modelname, child_id)
221219
output += "\n" + child.str(include_children=include_children, indent=indent + 4)
220+
except ObjectNotFound:
221+
output += f"\n{margin} {child_id} (ERROR: details unavailable)"
222222
return output
223223

224224
@classmethod
@@ -539,7 +539,10 @@ def _sync_from_diff_element(
539539
# So let's live with the slightly too high number of branches (14/12) for now.
540540
log = logger or self._log
541541
object_class = getattr(self, element.type)
542-
obj = self.get(object_class, element.keys)
542+
try:
543+
obj = self.get(object_class, element.keys)
544+
except ObjectNotFound:
545+
obj = None
543546
# Get the attributes that actually differ between source and dest
544547
diffs = element.get_attrs_diffs()
545548
log = log.bind(
@@ -634,12 +637,15 @@ def diff_to(
634637

635638
def get(
636639
self, obj: Union[Text, DiffSyncModel, Type[DiffSyncModel]], identifier: Union[Text, Mapping]
637-
) -> Optional[DiffSyncModel]:
640+
) -> DiffSyncModel:
638641
"""Get one object from the data store based on its unique id.
639642
640643
Args:
641644
obj: DiffSyncModel class or instance, or modelname string, that defines the type of the object to retrieve
642645
identifier: Unique ID of the object to retrieve, or dict of unique identifier keys/values
646+
647+
Raises:
648+
ObjectNotFound: if the requested object is not present
643649
"""
644650
if isinstance(obj, str):
645651
modelname = obj
@@ -662,7 +668,9 @@ def get(
662668
)
663669
return None
664670

665-
return self._data[modelname].get(uid)
671+
if uid not in self._data[modelname]:
672+
raise ObjectNotFound(f"{modelname} {uid} not present in {self.name}")
673+
return self._data[modelname][uid]
666674

667675
def get_all(self, obj: Union[Text, DiffSyncModel, Type[DiffSyncModel]]):
668676
"""Get all objects of a given type.
@@ -700,7 +708,7 @@ def get_by_uids(
700708
results = []
701709
for uid in uids:
702710
if uid not in self._data[modelname]:
703-
raise ObjectNotFound(f"{modelname} {uid} not present")
711+
raise ObjectNotFound(f"{modelname} {uid} not present in {self.name}")
704712
results.append(self._data[modelname][uid])
705713
return results
706714

@@ -738,7 +746,7 @@ def remove(self, obj: DiffSyncModel, remove_children: bool = False):
738746
uid = obj.get_unique_id()
739747

740748
if uid not in self._data[modelname]:
741-
raise ObjectNotFound(f"{modelname} {uid} not present")
749+
raise ObjectNotFound(f"{modelname} {uid} not present in {self.name}")
742750

743751
if obj.diffsync is self:
744752
obj.diffsync = None
@@ -748,9 +756,12 @@ def remove(self, obj: DiffSyncModel, remove_children: bool = False):
748756
if remove_children:
749757
for child_type, child_fieldname in obj.get_children_mapping().items():
750758
for child_id in getattr(obj, child_fieldname):
751-
child_obj = self.get(child_type, child_id)
752-
if child_obj:
759+
try:
760+
child_obj = self.get(child_type, child_id)
753761
self.remove(child_obj, remove_children=remove_children)
762+
except ObjectNotFound:
763+
# Since this is "cleanup" code, log an error and continue, instead of letting the exception raise
764+
self._log.error(f"Unable to remove child {child_id} of {modelname} {uid} - not found!")
754765

755766

756767
# DiffSyncModel references DiffSync and DiffSync references DiffSyncModel. Break the typing loop:

tests/unit/test_diffsync.py

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,11 @@ def test_diffsync_sync_self_with_no_data_is_noop(generic_diffsync):
5656
assert not generic_diffsync.sync_complete.called
5757

5858

59-
def test_diffsync_get_with_no_data_is_none(generic_diffsync):
60-
assert generic_diffsync.get("anything", "myname") is None
61-
assert generic_diffsync.get(DiffSyncModel, "") is None
59+
def test_diffsync_get_with_no_data_fails(generic_diffsync):
60+
with pytest.raises(ObjectNotFound):
61+
generic_diffsync.get("anything", "myname")
62+
with pytest.raises(ObjectNotFound):
63+
generic_diffsync.get(DiffSyncModel, "")
6264

6365

6466
def test_diffsync_get_all_with_no_data_is_empty_list(generic_diffsync):
@@ -90,9 +92,11 @@ def test_diffsync_get_with_generic_model(generic_diffsync, generic_diffsync_mode
9092
# DiffSync doesn't know how to construct a uid str for a "diffsyncmodel"
9193
assert generic_diffsync.get(DiffSyncModel.get_type(), {}) is None
9294
# Wrong object-type - no match
93-
assert generic_diffsync.get("", "") is None
95+
with pytest.raises(ObjectNotFound):
96+
generic_diffsync.get("", "")
9497
# Wrong unique-id - no match
95-
assert generic_diffsync.get(DiffSyncModel, "myname") is None
98+
with pytest.raises(ObjectNotFound):
99+
generic_diffsync.get(DiffSyncModel, "myname")
96100

97101

98102
def test_diffsync_get_all_with_generic_model(generic_diffsync, generic_diffsync_model):
@@ -121,7 +125,8 @@ def test_diffsync_remove_with_generic_model(generic_diffsync, generic_diffsync_m
121125
with pytest.raises(ObjectNotFound):
122126
generic_diffsync.remove(generic_diffsync_model)
123127

124-
assert generic_diffsync.get(DiffSyncModel, "") is None
128+
with pytest.raises(ObjectNotFound):
129+
generic_diffsync.get(DiffSyncModel, "")
125130
assert list(generic_diffsync.get_all(DiffSyncModel)) == []
126131
with pytest.raises(ObjectNotFound):
127132
generic_diffsync.get_by_uids([""], DiffSyncModel)
@@ -315,8 +320,10 @@ def test_diffsync_sync_from(backend_a, backend_b):
315320
site_atl_a = backend_a.get("site", "atl")
316321
assert isinstance(site_atl_a, Site)
317322
assert site_atl_a.name == "atl"
318-
assert backend_a.get(Site, "rdu") is None
319-
assert backend_a.get("nothing", "") is None
323+
with pytest.raises(ObjectNotFound):
324+
backend_a.get(Site, "rdu")
325+
with pytest.raises(ObjectNotFound):
326+
backend_a.get("nothing", "")
320327

321328
assert list(backend_a.get_all(Site)) == [site_nyc_a, site_sfo_a, site_atl_a]
322329
assert list(backend_a.get_all("site")) == [site_nyc_a, site_sfo_a, site_atl_a]
@@ -363,6 +370,18 @@ def test_diffsync_add_get_remove_with_subclass_and_data(backend_a):
363370
backend_a.remove(site_atl_a)
364371

365372

373+
def test_diffsync_remove_missing_child(log, backend_a):
374+
rdu_spine1 = backend_a.get(Device, "rdu-spine1")
375+
rdu_spine1_eth0 = backend_a.get(Interface, "rdu-spine1__eth0")
376+
# Usage error - remove rdu_spine1_eth0 from backend_a, but rdu_spine1 still has a reference to it
377+
backend_a.remove(rdu_spine1_eth0)
378+
# Should log an error but continue removing other child objects
379+
backend_a.remove(rdu_spine1, remove_children=True)
380+
assert log.has("Unable to remove child rdu-spine1__eth0 of device rdu-spine1 - not found!", diffsync=backend_a)
381+
with pytest.raises(ObjectNotFound):
382+
backend_a.get(Interface, "rdu-spine1__eth1")
383+
384+
366385
def test_diffsync_sync_from_exceptions_are_not_caught_by_default(error_prone_backend_a, backend_b):
367386
with pytest.raises(ObjectCrudException):
368387
error_prone_backend_a.sync_from(backend_b)
@@ -439,8 +458,10 @@ def test_diffsync_diff_with_skip_unmatched_both_flag(
439458
def test_diffsync_sync_with_skip_unmatched_src_flag(backend_a, backend_a_with_extra_models):
440459
backend_a.sync_from(backend_a_with_extra_models, flags=DiffSyncFlags.SKIP_UNMATCHED_SRC)
441460
# New objects should not have been created
442-
assert backend_a.get(backend_a.site, "lax") is None
443-
assert backend_a.get(backend_a.device, "nyc-spine3") is None
461+
with pytest.raises(ObjectNotFound):
462+
backend_a.get(backend_a.site, "lax")
463+
with pytest.raises(ObjectNotFound):
464+
backend_a.get(backend_a.device, "nyc-spine3")
444465
assert "nyc-spine3" not in backend_a.get(backend_a.site, "nyc").devices
445466

446467

@@ -500,7 +521,8 @@ class NoDeleteInterfaceDiffSync(BackendA):
500521
# NoDeleteInterface.delete() should not be called since we're deleting its parent only
501522
extra_models.sync_from(backend_a)
502523
# The extra interface should have been removed from the DiffSync without calling its delete() method
503-
assert extra_models.get(extra_models.interface, extra_interface.get_unique_id()) is None
524+
with pytest.raises(ObjectNotFound):
525+
extra_models.get(extra_models.interface, extra_interface.get_unique_id())
504526
# The sync should be complete, regardless
505527
diff = extra_models.diff_from(backend_a)
506528
print(diff.str()) # for debugging of any failure

tests/unit/test_diffsync_model.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ def test_diffsync_model_str_with_children(generic_diffsync, make_site, make_devi
195195
devices
196196
device: device1: {'role': 'default'}
197197
interfaces
198-
device1__eth0 (details unavailable)\
198+
device1__eth0 (ERROR: details unavailable)\
199199
"""
200200
)
201201

0 commit comments

Comments
 (0)