Skip to content

Commit 591890a

Browse files
committed
Add validation of top_level class attribute, fix a bug in str() output
1 parent 1183e0c commit 591890a

File tree

3 files changed

+52
-5
lines changed

3 files changed

+52
-5
lines changed

diffsync/__init__.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -428,6 +428,13 @@ def __init_subclass__(cls):
428428
f'Incorrect field name - {value.__name__} has type name "{value.get_type()}", not "{name}"'
429429
)
430430

431+
for name in cls.top_level:
432+
if not hasattr(cls, name):
433+
raise AttributeError(f'top_level references attribute "{name}" but it is not a class attribute!')
434+
value = getattr(cls, name)
435+
if not isclass(value) or not issubclass(value, DiffSyncModel):
436+
raise AttributeError(f'top_level references attribute "{name}" but it is not a DiffSyncModel subclass!')
437+
431438
def __str__(self):
432439
"""String representation of a DiffSync."""
433440
if self.type != self.name:
@@ -455,6 +462,8 @@ def str(self, indent: int = 0) -> str:
455462
margin = " " * indent
456463
output = ""
457464
for modelname in self.top_level:
465+
if output:
466+
output += "\n"
458467
output += f"{margin}{modelname}"
459468
models = self.get_all(modelname)
460469
if not models:

tests/unit/conftest.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,14 +140,24 @@ def generic_diffsync():
140140
return DiffSync()
141141

142142

143+
class UnusedModel(DiffSyncModel):
144+
"""Concrete DiffSyncModel subclass that can be referenced as a class attribute but never has any data."""
145+
146+
_modelname = "unused"
147+
_identifiers = ("name",)
148+
149+
name: str
150+
151+
143152
class GenericBackend(DiffSync):
144153
"""An example semi-abstract subclass of DiffSync."""
145154

146155
site = Site # to be overridden by subclasses
147156
device = Device
148157
interface = Interface
158+
unused = UnusedModel
149159

150-
top_level = ["site"]
160+
top_level = ["site", "unused"]
151161

152162
DATA: dict = {}
153163

tests/unit/test_diffsync.py

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -132,13 +132,12 @@ def test_diffsync_remove_with_generic_model(generic_diffsync, generic_diffsync_m
132132
generic_diffsync.get_by_uids([""], DiffSyncModel)
133133

134134

135-
def test_diffsync_subclass_validation():
136-
"""Test the declaration-time checks on a DiffSync subclass."""
135+
def test_diffsync_subclass_validation_name_mismatch():
137136
# pylint: disable=unused-variable
138137
with pytest.raises(AttributeError) as excinfo:
139138

140139
class BadElementName(DiffSync):
141-
"""Model with a DiffSyncModel attribute whose name does not match the modelname."""
140+
"""DiffSync with a DiffSyncModel attribute whose name does not match the modelname."""
142141

143142
dev_class = Device # should be device = Device
144143

@@ -147,6 +146,33 @@ class BadElementName(DiffSync):
147146
assert "dev_class" in str(excinfo.value)
148147

149148

149+
def test_diffsync_subclass_validation_missing_top_level():
150+
with pytest.raises(AttributeError) as excinfo:
151+
152+
class MissingTopLevel(DiffSync):
153+
"""DiffSync whose top_level references an attribute that does not exist on the class."""
154+
155+
top_level = ["missing"]
156+
157+
assert "top_level" in str(excinfo.value)
158+
assert "missing" in str(excinfo.value)
159+
assert "is not a class attribute" in str(excinfo.value)
160+
161+
162+
def test_diffsync_subclass_validation_top_level_not_diffsyncmodel():
163+
with pytest.raises(AttributeError) as excinfo:
164+
165+
class TopLevelNotDiffSyncModel(DiffSync):
166+
"""DiffSync whose top_level references an attribute that is not a DiffSyncModel subclass."""
167+
168+
age = 0
169+
top_level = ["age"]
170+
171+
assert "top_level" in str(excinfo.value)
172+
assert "age" in str(excinfo.value)
173+
assert "is not a DiffSyncModel" in str(excinfo.value)
174+
175+
150176
def test_diffsync_dict_with_data(backend_a):
151177
assert backend_a.dict() == {
152178
"device": {
@@ -250,7 +276,9 @@ def test_diffsync_str_with_data(backend_a):
250276
interface: rdu-spine2__eth0: {'interface_type': 'ethernet', 'description': 'Interface 0'}
251277
interface: rdu-spine2__eth1: {'interface_type': 'ethernet', 'description': 'Interface 1'}
252278
people
253-
person: Glenn Matthews: {}"""
279+
person: Glenn Matthews: {}
280+
unused: []\
281+
"""
254282
)
255283

256284

0 commit comments

Comments
 (0)