Skip to content

Commit 4d6d046

Browse files
committed
Replace DSync/DSyncModel print_detailed() methods with str() methods; add tests verifying these as well as serialization to dict/json
1 parent ad6e6c1 commit 4d6d046

File tree

6 files changed

+290
-47
lines changed

6 files changed

+290
-47
lines changed

dsync/__init__.py

Lines changed: 71 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
from collections.abc import Iterable as ABCIterable, Mapping as ABCMapping
1919
import enum
2020
from inspect import isclass
21-
from typing import ClassVar, Iterable, List, Mapping, MutableMapping, Optional, Tuple, Type, Union
21+
from typing import ClassVar, Dict, Iterable, List, Mapping, MutableMapping, Optional, Text, Tuple, Type, Union
2222

2323
from pydantic import BaseModel
2424
import structlog # type: ignore
@@ -187,28 +187,42 @@ def __repr__(self):
187187
def __str__(self):
188188
return self.get_unique_id()
189189

190-
def print_detailed(self, dsync: "Optional[DSync]" = None, indent: int = 0):
191-
"""Print this model and its children."""
190+
def dict(self, **kwargs) -> dict:
191+
"""Convert this DSyncModel to a dict, excluding the dsync field by default as it is not serializable."""
192+
if "exclude" not in kwargs:
193+
kwargs["exclude"] = {"dsync"}
194+
return super().dict(**kwargs)
195+
196+
def json(self, **kwargs) -> str:
197+
"""Convert this DSyncModel to a JSON string, excluding the dsync field by default as it is not serializable."""
198+
if "exclude" not in kwargs:
199+
kwargs["exclude"] = {"dsync"}
200+
if "exclude_defaults" not in kwargs:
201+
kwargs["exclude_defaults"] = True
202+
return super().json(**kwargs)
203+
204+
def str(self, include_children: bool = True, indent: int = 0) -> str:
205+
"""Build a detailed string representation of this DSyncModel and optionally its children."""
192206
margin = " " * indent
193-
if not dsync:
194-
dsync = self.dsync
195-
print(f"{margin}{self.get_type()}: {self.get_unique_id()}")
207+
output = f"{margin}{self.get_type()}: {self.get_unique_id()}: {self.get_attrs()}"
196208
for modelname, fieldname in self._children.items():
197-
print(f"{margin} {modelname}")
209+
output += f"\n{margin} {fieldname}"
198210
child_ids = getattr(self, fieldname)
199211
if not child_ids:
200-
print(f"{margin} (none)")
201-
for child_id in child_ids:
202-
child = None
203-
if dsync:
204-
child = dsync.get(modelname, child_id)
205-
if not child:
206-
print(f"{margin} {child_id} (no details available)")
207-
else:
208-
child.print_detailed(dsync, indent + 4)
212+
output += ": []"
213+
elif not self.dsync or not include_children:
214+
output += f": {child_ids}"
215+
else:
216+
for child_id in child_ids:
217+
child = self.dsync.get(modelname, child_id)
218+
if not child:
219+
output += f"\n{margin} {child_id} (details unavailable)"
220+
else:
221+
output += "\n" + child.str(include_children=include_children, indent=indent + 4)
222+
return output
209223

210224
@classmethod
211-
def create(cls, dsync: "DSync", ids: dict, attrs: dict) -> Optional["DSyncModel"]:
225+
def create(cls, dsync: "DSync", ids: Dict, attrs: Dict) -> Optional["DSyncModel"]:
212226
"""Instantiate this class, along with any platform-specific data creation.
213227
214228
Args:
@@ -225,7 +239,7 @@ def create(cls, dsync: "DSync", ids: dict, attrs: dict) -> Optional["DSyncModel"
225239
"""
226240
return cls(**ids, dsync=dsync, **attrs)
227241

228-
def update(self, attrs: dict) -> Optional["DSyncModel"]:
242+
def update(self, attrs: Dict) -> Optional["DSyncModel"]:
229243
"""Update the attributes of this instance, along with any platform-specific data updates.
230244
231245
Args:
@@ -256,7 +270,7 @@ def delete(self) -> Optional["DSyncModel"]:
256270
return self
257271

258272
@classmethod
259-
def get_type(cls) -> str:
273+
def get_type(cls) -> Text:
260274
"""Return the type AKA modelname of the object or the class
261275
262276
Returns:
@@ -265,7 +279,7 @@ def get_type(cls) -> str:
265279
return cls._modelname
266280

267281
@classmethod
268-
def create_unique_id(cls, **identifiers) -> str:
282+
def create_unique_id(cls, **identifiers) -> Text:
269283
"""Construct a unique identifier for this model class.
270284
271285
Args:
@@ -274,19 +288,19 @@ def create_unique_id(cls, **identifiers) -> str:
274288
return "__".join(str(identifiers[key]) for key in cls._identifiers)
275289

276290
@classmethod
277-
def get_children_mapping(cls) -> Mapping[str, str]:
291+
def get_children_mapping(cls) -> Mapping[Text, Text]:
278292
"""Get the mapping of types to fieldnames for child models of this model."""
279293
return cls._children
280294

281-
def get_identifiers(self) -> dict:
295+
def get_identifiers(self) -> Dict:
282296
"""Get a dict of all identifiers (primary keys) and their values for this object.
283297
284298
Returns:
285299
dict: dictionary containing all primary keys for this device, as defined in _identifiers
286300
"""
287301
return self.dict(include=set(self._identifiers))
288302

289-
def get_attrs(self) -> dict:
303+
def get_attrs(self) -> Dict:
290304
"""Get all the non-primary-key attributes or parameters for this object.
291305
292306
Similar to Pydantic's `BaseModel.dict()` method, with the following key differences:
@@ -299,7 +313,7 @@ def get_attrs(self) -> dict:
299313
"""
300314
return self.dict(include=set(self._attributes))
301315

302-
def get_unique_id(self) -> str:
316+
def get_unique_id(self) -> Text:
303317
"""Get the unique ID of an object.
304318
305319
By default the unique ID is built based on all the primary keys defined in `_identifiers`.
@@ -309,7 +323,7 @@ def get_unique_id(self) -> str:
309323
"""
310324
return self.create_unique_id(**self.get_identifiers())
311325

312-
def get_shortname(self) -> str:
326+
def get_shortname(self) -> Text:
313327
"""Get the (not guaranteed-unique) shortname of an object, if any.
314328
315329
By default the shortname is built based on all the keys defined in `_shortname`.
@@ -427,16 +441,28 @@ def load(self):
427441
"""Load all desired data from whatever backend data source into this instance."""
428442
# No-op in this generic class
429443

430-
def print_detailed(self, indent: int = 0):
431-
"""Recursively print this DSync and its contained models."""
444+
def dict(self, exclude_defaults: bool = True, **kwargs) -> dict:
445+
"""Represent the DSync contents as a dict, as if it were a Pydantic model."""
446+
data: Dict[str, Dict[str, dict]] = {}
447+
for modelname in self._data:
448+
data[modelname] = {}
449+
for unique_id, model in self._data[modelname].items():
450+
data[modelname][unique_id] = model.dict(exclude_defaults=exclude_defaults, **kwargs)
451+
return data
452+
453+
def str(self, indent: int = 0) -> str:
454+
"""Build a detailed string representation of this DSync."""
432455
margin = " " * indent
456+
output = ""
433457
for modelname in self.top_level:
434-
print(f"{margin}{modelname}")
458+
output += f"{margin}{modelname}"
435459
models = self.get_all(modelname)
436460
if not models:
437-
print(f"{margin} (none)")
438-
for model in models:
439-
model.print_detailed(self, indent + 2)
461+
output += ": []"
462+
else:
463+
for model in models:
464+
output += "\n" + model.str(indent=indent + 2)
465+
return output
440466

441467
# ------------------------------------------------------------------------------
442468
# Synchronization between DSync instances
@@ -579,7 +605,9 @@ def diff_to(self, target: "DSync", diff_class: Type[Diff] = Diff, flags: DSyncFl
579605
# Object Storage Management
580606
# ------------------------------------------------------------------------------
581607

582-
def get(self, obj: Union[str, DSyncModel, Type[DSyncModel]], identifier: Union[str, dict]) -> Optional[DSyncModel]:
608+
def get(
609+
self, obj: Union[Text, DSyncModel, Type[DSyncModel]], identifier: Union[Text, Dict]
610+
) -> Optional[DSyncModel]:
583611
"""Get one object from the data store based on its unique id.
584612
585613
Args:
@@ -589,16 +617,23 @@ def get(self, obj: Union[str, DSyncModel, Type[DSyncModel]], identifier: Union[s
589617
if isinstance(obj, str):
590618
modelname = obj
591619
if not hasattr(self, obj):
592-
return None
593-
object_class = getattr(self, obj)
620+
object_class = None
621+
else:
622+
object_class = getattr(self, obj)
594623
else:
595624
object_class = obj
596625
modelname = obj.get_type()
597626

598627
if isinstance(identifier, str):
599628
uid = identifier
600-
else:
629+
elif object_class:
601630
uid = object_class.create_unique_id(**identifier)
631+
else:
632+
self._log.warning(
633+
f"Tried to look up a {modelname} by identifier {identifier}, "
634+
"but don't know how to convert that to a uid string",
635+
)
636+
return None
602637

603638
return self._data[modelname].get(uid)
604639

@@ -618,7 +653,7 @@ def get_all(self, obj):
618653

619654
return self._data[modelname].values()
620655

621-
def get_by_uids(self, uids: List[str], obj) -> List[DSyncModel]:
656+
def get_by_uids(self, uids: List[Text], obj) -> List[DSyncModel]:
622657
"""Get multiple objects from the store by their unique IDs/Keys and type.
623658
624659
Args:

examples/example1/README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,15 @@ from backend_c import BackendC
1616

1717
a = BackendA()
1818
a.load()
19-
a.print_detailed()
19+
print(a.str())
2020

2121
b = BackendB()
2222
b.load()
23-
b.print_detailed()
23+
print(b.str())
2424

2525
c = BackendC()
2626
c.load()
27-
c.print_detailed()
27+
print(c.str())
2828
```
2929

3030
Configure verbosity of DSync's structured logging to console; the default is full verbosity (all logs including debugging)

examples/example1/main.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,17 +47,17 @@ def main():
4747
print("Initializing and loading Backend A...")
4848
backend_a = BackendA(name="Backend-A")
4949
backend_a.load()
50-
backend_a.print_detailed()
50+
print(backend_a.str())
5151

5252
print("Initializing and loading Backend B...")
5353
backend_b = BackendB(name="Backend-B")
5454
backend_b.load()
55-
backend_b.print_detailed()
55+
print(backend_b.str())
5656

5757
print("Initializing and loading Backend C...")
5858
backend_c = BackendC()
5959
backend_c.load()
60-
backend_c.print_detailed()
60+
print(backend_c.str())
6161

6262
print("Getting diffs from Backend A to Backend B...")
6363
diff_a_b = backend_a.diff_to(backend_b, diff_class=MyDiff)

tests/unit/conftest.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,11 +85,11 @@ class Site(DSyncModel):
8585
def make_site():
8686
"""Factory for Site instances."""
8787

88-
def site(name="site1", devices=None):
88+
def site(name="site1", devices=None, **kwargs):
8989
"""Provide an instance of a Site model."""
9090
if not devices:
9191
devices = []
92-
return Site(name=name, devices=devices)
92+
return Site(name=name, devices=devices, **kwargs)
9393

9494
return site
9595

0 commit comments

Comments
 (0)