Skip to content

Commit 2c1577f

Browse files
authored
Merge pull request #219 from networktocode/typing-overhaul
Type hinting overhaul.
2 parents 6a0949e + 4fa39b5 commit 2c1577f

File tree

12 files changed

+302
-228
lines changed

12 files changed

+302
-228
lines changed

diffsync/__init__.py

Lines changed: 98 additions & 91 deletions
Large diffs are not rendered by default.

diffsync/diff.py

Lines changed: 42 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -16,40 +16,44 @@
1616
"""
1717

1818
from functools import total_ordering
19-
from typing import Any, Iterator, Iterable, Mapping, Optional, Text, Type
19+
from typing import Any, Iterator, Optional, Type, List, Dict, Iterable
2020

2121
from .exceptions import ObjectAlreadyExists
2222
from .utils import intersection, OrderedDefaultDict
2323
from .enum import DiffSyncActions
2424

25+
# This workaround is used because we are defining a method called `str` in our class definition, which therefore renders
26+
# the builtin `str` type unusable.
27+
StrType = str
28+
2529

2630
class Diff:
2731
"""Diff Object, designed to store multiple DiffElement object and organize them in a group."""
2832

29-
def __init__(self):
33+
def __init__(self) -> None:
3034
"""Initialize a new, empty Diff object."""
31-
self.children = OrderedDefaultDict(dict)
35+
self.children = OrderedDefaultDict[StrType, Dict[StrType, DiffElement]](dict)
3236
"""DefaultDict for storing DiffElement objects.
3337
3438
`self.children[group][unique_id] == DiffElement(...)`
3539
"""
3640
self.models_processed = 0
3741

38-
def __len__(self):
42+
def __len__(self) -> int:
3943
"""Total number of DiffElements stored herein."""
4044
total = 0
4145
for child in self.get_children():
4246
total += len(child)
4347
return total
4448

45-
def complete(self):
49+
def complete(self) -> None:
4650
"""Method to call when this Diff has been fully populated with data and is "complete".
4751
4852
The default implementation does nothing, but a subclass could use this, for example, to save
4953
the completed Diff to a file or database record.
5054
"""
5155

52-
def add(self, element: "DiffElement"):
56+
def add(self, element: "DiffElement") -> None:
5357
"""Add a new DiffElement to the changeset of this Diff.
5458
5559
Raises:
@@ -61,15 +65,15 @@ def add(self, element: "DiffElement"):
6165

6266
self.children[element.type][element.name] = element
6367

64-
def groups(self):
68+
def groups(self) -> List[StrType]:
6569
"""Get the list of all group keys in self.children."""
66-
return self.children.keys()
70+
return list(self.children.keys())
6771

6872
def has_diffs(self) -> bool:
6973
"""Indicate if at least one of the child elements contains some diff.
7074
7175
Returns:
72-
bool: True if at least one child element contains some diff
76+
True if at least one child element contains some diff
7377
"""
7478
for group in self.groups():
7579
for child in self.children[group].values():
@@ -96,15 +100,15 @@ def get_children(self) -> Iterator["DiffElement"]:
96100
yield from order_method(self.children[group])
97101

98102
@classmethod
99-
def order_children_default(cls, children: Mapping) -> Iterator["DiffElement"]:
103+
def order_children_default(cls, children: Dict[StrType, "DiffElement"]) -> Iterator["DiffElement"]:
100104
"""Default method to an Iterator for children.
101105
102106
Since children is already an OrderedDefaultDict, this method is not doing anything special.
103107
"""
104108
for child in children.values():
105109
yield child
106110

107-
def summary(self) -> Mapping[Text, int]:
111+
def summary(self) -> Dict[StrType, int]:
108112
"""Build a dict summary of this Diff and its child DiffElements."""
109113
summary = {
110114
DiffSyncActions.CREATE: 0,
@@ -127,7 +131,7 @@ def summary(self) -> Mapping[Text, int]:
127131
)
128132
return summary
129133

130-
def str(self, indent: int = 0):
134+
def str(self, indent: int = 0) -> StrType:
131135
"""Build a detailed string representation of this Diff and its child DiffElements."""
132136
margin = " " * indent
133137
output = []
@@ -144,9 +148,9 @@ def str(self, indent: int = 0):
144148
result = "(no diffs)"
145149
return result
146150

147-
def dict(self) -> Mapping[Text, Mapping[Text, Mapping]]:
151+
def dict(self) -> Dict[StrType, Dict[StrType, Dict]]:
148152
"""Build a dictionary representation of this Diff."""
149-
result = OrderedDefaultDict(dict)
153+
result = OrderedDefaultDict[str, Dict](dict)
150154
for child in self.get_children():
151155
if child.has_diffs(include_children=True):
152156
result[child.type][child.name] = child.dict()
@@ -159,11 +163,11 @@ class DiffElement: # pylint: disable=too-many-instance-attributes
159163

160164
def __init__(
161165
self,
162-
obj_type: Text,
163-
name: Text,
164-
keys: Mapping,
165-
source_name: Text = "source",
166-
dest_name: Text = "dest",
166+
obj_type: StrType,
167+
name: StrType,
168+
keys: Dict,
169+
source_name: StrType = "source",
170+
dest_name: StrType = "dest",
167171
diff_class: Type[Diff] = Diff,
168172
): # pylint: disable=too-many-arguments
169173
"""Instantiate a DiffElement.
@@ -177,10 +181,10 @@ def __init__(
177181
dest_name: Name of the destination DiffSync object
178182
diff_class: Diff or subclass thereof to use to calculate the diffs to use for synchronization
179183
"""
180-
if not isinstance(obj_type, str):
184+
if not isinstance(obj_type, StrType):
181185
raise ValueError(f"obj_type must be a string (not {type(obj_type)})")
182186

183-
if not isinstance(name, str):
187+
if not isinstance(name, StrType):
184188
raise ValueError(f"name must be a string (not {type(name)})")
185189

186190
self.type = obj_type
@@ -189,18 +193,18 @@ def __init__(
189193
self.source_name = source_name
190194
self.dest_name = dest_name
191195
# Note: *_attrs == None if no target object exists; it'll be an empty dict if it exists but has no _attributes
192-
self.source_attrs: Optional[Mapping] = None
193-
self.dest_attrs: Optional[Mapping] = None
196+
self.source_attrs: Optional[Dict] = None
197+
self.dest_attrs: Optional[Dict] = None
194198
self.child_diff = diff_class()
195199

196-
def __lt__(self, other):
200+
def __lt__(self, other: "DiffElement") -> bool:
197201
"""Logical ordering of DiffElements.
198202
199203
Other comparison methods (__gt__, __le__, __ge__, etc.) are created by our use of the @total_ordering decorator.
200204
"""
201205
return (self.type, self.name) < (other.type, other.name)
202206

203-
def __eq__(self, other):
207+
def __eq__(self, other: object) -> bool:
204208
"""Logical equality of DiffElements.
205209
206210
Other comparison methods (__gt__, __le__, __ge__, etc.) are created by our use of the @total_ordering decorator.
@@ -216,26 +220,26 @@ def __eq__(self, other):
216220
# TODO also check that self.child_diff == other.child_diff, needs Diff to implement __eq__().
217221
)
218222

219-
def __str__(self):
223+
def __str__(self) -> StrType:
220224
"""Basic string representation of a DiffElement."""
221225
return (
222226
f'{self.type} "{self.name}" : {self.keys} : '
223227
f"{self.source_name}{self.dest_name} : {self.get_attrs_diffs()}"
224228
)
225229

226-
def __len__(self):
230+
def __len__(self) -> int:
227231
"""Total number of DiffElements in this one, including itself."""
228232
total = 1 # self
229233
for child in self.get_children():
230234
total += len(child)
231235
return total
232236

233237
@property
234-
def action(self) -> Optional[Text]:
238+
def action(self) -> Optional[StrType]:
235239
"""Action, if any, that should be taken to remediate the diffs described by this element.
236240
237241
Returns:
238-
str: DiffSyncActions ("create", "update", "delete", or None)
242+
"create", "update", "delete", or None)
239243
"""
240244
if self.source_attrs is not None and self.dest_attrs is None:
241245
return DiffSyncActions.CREATE
@@ -251,7 +255,7 @@ def action(self) -> Optional[Text]:
251255
return None
252256

253257
# TODO: separate into set_source_attrs() and set_dest_attrs() methods, or just use direct property access instead?
254-
def add_attrs(self, source: Optional[Mapping] = None, dest: Optional[Mapping] = None):
258+
def add_attrs(self, source: Optional[Dict] = None, dest: Optional[Dict] = None) -> None:
255259
"""Set additional attributes of a source and/or destination item that may result in diffs."""
256260
# TODO: should source_attrs and dest_attrs be "write-once" properties, or is it OK to overwrite them once set?
257261
if source is not None:
@@ -260,26 +264,26 @@ def add_attrs(self, source: Optional[Mapping] = None, dest: Optional[Mapping] =
260264
if dest is not None:
261265
self.dest_attrs = dest
262266

263-
def get_attrs_keys(self) -> Iterable[Text]:
267+
def get_attrs_keys(self) -> Iterable[StrType]:
264268
"""Get the list of shared attrs between source and dest, or the attrs of source or dest if only one is present.
265269
266270
- If source_attrs is not set, return the keys of dest_attrs
267271
- If dest_attrs is not set, return the keys of source_attrs
268272
- If both are defined, return the intersection of both keys
269273
"""
270274
if self.source_attrs is not None and self.dest_attrs is not None:
271-
return intersection(self.dest_attrs.keys(), self.source_attrs.keys())
275+
return intersection(list(self.dest_attrs.keys()), list(self.source_attrs.keys()))
272276
if self.source_attrs is None and self.dest_attrs is not None:
273277
return self.dest_attrs.keys()
274278
if self.source_attrs is not None and self.dest_attrs is None:
275279
return self.source_attrs.keys()
276280
return []
277281

278-
def get_attrs_diffs(self) -> Mapping[Text, Mapping[Text, Any]]:
282+
def get_attrs_diffs(self) -> Dict[StrType, Dict[StrType, Any]]:
279283
"""Get the dict of actual attribute diffs between source_attrs and dest_attrs.
280284
281285
Returns:
282-
dict: of the form `{"-": {key1: <value>, key2: ...}, "+": {key1: <value>, key2: ...}}`,
286+
Dictionary of the form `{"-": {key1: <value>, key2: ...}, "+": {key1: <value>, key2: ...}}`,
283287
where the `"-"` or `"+"` dicts may be absent.
284288
"""
285289
if self.source_attrs is not None and self.dest_attrs is not None:
@@ -301,13 +305,10 @@ def get_attrs_diffs(self) -> Mapping[Text, Mapping[Text, Any]]:
301305
return {"+": {key: self.source_attrs[key] for key in self.get_attrs_keys()}}
302306
return {}
303307

304-
def add_child(self, element: "DiffElement"):
308+
def add_child(self, element: "DiffElement") -> None:
305309
"""Attach a child object of type DiffElement.
306310
307311
Childs are saved in a Diff object and are organized by type and name.
308-
309-
Args:
310-
element: DiffElement
311312
"""
312313
self.child_diff.add(element)
313314

@@ -336,7 +337,7 @@ def has_diffs(self, include_children: bool = True) -> bool:
336337

337338
return False
338339

339-
def summary(self) -> Mapping[Text, int]:
340+
def summary(self) -> Dict[StrType, int]:
340341
"""Build a summary of this DiffElement and its children."""
341342
summary = {
342343
DiffSyncActions.CREATE: 0,
@@ -353,7 +354,7 @@ def summary(self) -> Mapping[Text, int]:
353354
summary[key] += child_summary[key]
354355
return summary
355356

356-
def str(self, indent: int = 0):
357+
def str(self, indent: int = 0) -> StrType:
357358
"""Build a detailed string representation of this DiffElement and its children."""
358359
margin = " " * indent
359360
result = f"{margin}{self.type}: {self.name}"
@@ -377,7 +378,7 @@ def str(self, indent: int = 0):
377378
result += " (no diffs)"
378379
return result
379380

380-
def dict(self) -> Mapping[Text, Mapping[Text, Any]]:
381+
def dict(self) -> Dict[StrType, Dict[StrType, Any]]:
381382
"""Build a dictionary representation of this DiffElement and its children."""
382383
attrs_diffs = self.get_attrs_diffs()
383384
result = {}

diffsync/exceptions.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@
1414
See the License for the specific language governing permissions and
1515
limitations under the License.
1616
"""
17+
from typing import TYPE_CHECKING, Union, Any
18+
19+
if TYPE_CHECKING:
20+
from diffsync import DiffSyncModel
21+
from diffsync.diff import DiffElement
1722

1823

1924
class ObjectCrudException(Exception):
@@ -39,7 +44,7 @@ class ObjectStoreException(Exception):
3944
class ObjectAlreadyExists(ObjectStoreException):
4045
"""Exception raised when trying to store a DiffSyncModel or DiffElement that is already being stored."""
4146

42-
def __init__(self, message, existing_object, *args, **kwargs):
47+
def __init__(self, message: str, existing_object: Union["DiffSyncModel", "DiffElement"], *args: Any, **kwargs: Any):
4348
"""Add existing_object to the exception to provide user with existing object."""
4449
self.existing_object = existing_object
4550
super().__init__(message, existing_object, *args, **kwargs)

diffsync/helpers.py

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
limitations under the License.
1616
"""
1717
from collections.abc import Iterable as ABCIterable, Mapping as ABCMapping
18-
from typing import Callable, Iterable, List, Mapping, Optional, Tuple, Type, TYPE_CHECKING
18+
from typing import Callable, List, Optional, Tuple, Type, TYPE_CHECKING, Dict, Iterable
1919

2020
import structlog # type: ignore
2121

@@ -57,7 +57,7 @@ def __init__( # pylint: disable=too-many-arguments
5757
self.total_models = len(src_diffsync) + len(dst_diffsync)
5858
self.logger.debug(f"Diff calculation between these two datasets will involve {self.total_models} models")
5959

60-
def incr_models_processed(self, delta: int = 1):
60+
def incr_models_processed(self, delta: int = 1) -> None:
6161
"""Increment self.models_processed, then call self.callback if present."""
6262
if delta:
6363
self.models_processed += delta
@@ -136,7 +136,9 @@ def diff_object_list(self, src: List["DiffSyncModel"], dst: List["DiffSyncModel"
136136
return diff_elements
137137

138138
@staticmethod
139-
def validate_objects_for_diff(object_pairs: Iterable[Tuple[Optional["DiffSyncModel"], Optional["DiffSyncModel"]]]):
139+
def validate_objects_for_diff(
140+
object_pairs: Iterable[Tuple[Optional["DiffSyncModel"], Optional["DiffSyncModel"]]]
141+
) -> None:
140142
"""Check whether all DiffSyncModels in the given dictionary are valid for comparison to one another.
141143
142144
Helper method for `diff_object_list`.
@@ -234,15 +236,15 @@ def diff_child_objects(
234236
diff_element: DiffElement,
235237
src_obj: Optional["DiffSyncModel"],
236238
dst_obj: Optional["DiffSyncModel"],
237-
):
239+
) -> DiffElement:
238240
"""For all children of the given DiffSyncModel pair, diff recursively, adding diffs to the given diff_element.
239241
240242
Helper method to `calculate_diffs`, usually doesn't need to be called directly.
241243
242244
These helper methods work in a recursive cycle:
243245
diff_object_list -> diff_object_pair -> diff_child_objects -> diff_object_list -> etc.
244246
"""
245-
children_mapping: Mapping[str, str]
247+
children_mapping: Dict[str, str]
246248
if src_obj and dst_obj:
247249
# Get the subset of child types common to both src_obj and dst_obj
248250
src_mapping = src_obj.get_children_mapping()
@@ -308,7 +310,7 @@ def __init__( # pylint: disable=too-many-arguments
308310
self.model_class: Type["DiffSyncModel"]
309311
self.action: Optional[str] = None
310312

311-
def incr_elements_processed(self, delta: int = 1):
313+
def incr_elements_processed(self, delta: int = 1) -> None:
312314
"""Increment self.elements_processed, then call self.callback if present."""
313315
if delta:
314316
self.elements_processed += delta
@@ -319,7 +321,7 @@ def perform_sync(self) -> bool:
319321
"""Perform data synchronization based on the provided diff.
320322
321323
Returns:
322-
bool: True if any changes were actually performed, else False.
324+
True if any changes were actually performed, else False.
323325
"""
324326
changed = False
325327
self.base_logger.info("Beginning sync")
@@ -401,14 +403,14 @@ def sync_diff_element(self, element: DiffElement, parent_model: Optional["DiffSy
401403
return changed
402404

403405
def sync_model( # pylint: disable=too-many-branches, unused-argument
404-
self, src_model: Optional["DiffSyncModel"], dst_model: Optional["DiffSyncModel"], ids: Mapping, attrs: Mapping
406+
self, src_model: Optional["DiffSyncModel"], dst_model: Optional["DiffSyncModel"], ids: Dict, attrs: Dict
405407
) -> Tuple[bool, Optional["DiffSyncModel"]]:
406408
"""Create/update/delete the current DiffSyncModel with current ids/attrs, and update self.status and self.message.
407409
408410
Helper method to `sync_diff_element`.
409411
410412
Returns:
411-
tuple: (changed, model) where model may be None if an error occurred
413+
(changed, model) where model may be None if an error occurred
412414
"""
413415
if self.action is None:
414416
status = DiffSyncStatus.SUCCESS
@@ -451,7 +453,7 @@ def sync_model( # pylint: disable=too-many-branches, unused-argument
451453

452454
return (True, dst_model)
453455

454-
def log_sync_status(self, action: Optional[str], status: DiffSyncStatus, message: str):
456+
def log_sync_status(self, action: Optional[str], status: DiffSyncStatus, message: str) -> None:
455457
"""Log the current sync status at the appropriate verbosity with appropriate context.
456458
457459
Helper method to `sync_diff_element`/`sync_model`.

0 commit comments

Comments
 (0)