|
21 | 21 | type_is_subclass_of_type_group, type_in_type_group, get_doc, |
22 | 22 | number_to_string, datetime_normalize, KEY_TO_VAL_STR, booleans, |
23 | 23 | np_ndarray, get_numpy_ndarray_rows, OrderedSetPlus, RepeatedTimer, |
24 | | - TEXT_VIEW, TREE_VIEW, DELTA_VIEW, detailed__dict__, |
| 24 | + TEXT_VIEW, TREE_VIEW, DELTA_VIEW, detailed__dict__, add_root_to_paths, |
25 | 25 | np, get_truncate_datetime, dict_, CannotCompare, ENUM_IGNORE_KEYS) |
26 | 26 | from deepdiff.serialization import SerializationMixin |
27 | 27 | from deepdiff.distance import DistanceMixin |
28 | 28 | from deepdiff.model import ( |
29 | 29 | RemapDict, ResultDict, TextResult, TreeResult, DiffLevel, |
30 | | - DictRelationship, AttributeRelationship, |
| 30 | + DictRelationship, AttributeRelationship, REPORT_KEYS, |
31 | 31 | SubscriptableIterableRelationship, NonSubscriptableIterableRelationship, |
32 | | - SetRelationship, NumpyArrayRelationship, CUSTOM_FIELD) |
| 32 | + SetRelationship, NumpyArrayRelationship, CUSTOM_FIELD, PrettyOrderedSet, ) |
33 | 33 | from deepdiff.deephash import DeepHash, combine_hashes_lists |
34 | 34 | from deepdiff.base import Base |
35 | 35 | from deepdiff.lfucache import LFUCache, DummyLFU |
@@ -85,6 +85,7 @@ def _report_progress(_stats, progress_logger, duration): |
85 | 85 | DEEPHASH_PARAM_KEYS = ( |
86 | 86 | 'exclude_types', |
87 | 87 | 'exclude_paths', |
| 88 | + 'include_paths', |
88 | 89 | 'exclude_regex_paths', |
89 | 90 | 'hasher', |
90 | 91 | 'significant_digits', |
@@ -119,6 +120,7 @@ def __init__(self, |
119 | 120 | exclude_obj_callback=None, |
120 | 121 | exclude_obj_callback_strict=None, |
121 | 122 | exclude_paths=None, |
| 123 | + include_paths=None, |
122 | 124 | exclude_regex_paths=None, |
123 | 125 | exclude_types=None, |
124 | 126 | get_deep_distance=False, |
@@ -157,7 +159,7 @@ def __init__(self, |
157 | 159 | raise ValueError(( |
158 | 160 | "The following parameter(s) are not valid: %s\n" |
159 | 161 | "The valid parameters are ignore_order, report_repetition, significant_digits, " |
160 | | - "number_format_notation, exclude_paths, exclude_types, exclude_regex_paths, ignore_type_in_groups, " |
| 162 | + "number_format_notation, exclude_paths, include_paths, exclude_types, exclude_regex_paths, ignore_type_in_groups, " |
161 | 163 | "ignore_string_type_changes, ignore_numeric_type_changes, ignore_type_subclasses, truncate_datetime, " |
162 | 164 | "ignore_private_variables, ignore_nan_inequality, number_to_string_func, verbose_level, " |
163 | 165 | "view, hasher, hashes, max_passes, max_diffs, " |
@@ -188,7 +190,8 @@ def __init__(self, |
188 | 190 | ignore_numeric_type_changes=ignore_numeric_type_changes, |
189 | 191 | ignore_type_subclasses=ignore_type_subclasses) |
190 | 192 | self.report_repetition = report_repetition |
191 | | - self.exclude_paths = convert_item_or_items_into_set_else_none(exclude_paths) |
| 193 | + self.exclude_paths = add_root_to_paths(convert_item_or_items_into_set_else_none(exclude_paths)) |
| 194 | + self.include_paths = add_root_to_paths(convert_item_or_items_into_set_else_none(include_paths)) |
192 | 195 | self.exclude_regex_paths = convert_item_or_items_into_compiled_regexes_else_none(exclude_regex_paths) |
193 | 196 | self.exclude_types = set(exclude_types) if exclude_types else None |
194 | 197 | self.exclude_types_tuple = tuple(exclude_types) if exclude_types else None # we need tuple for checking isinstance |
@@ -431,21 +434,29 @@ def _skip_this(self, level): |
431 | 434 | Check whether this comparison should be skipped because one of the objects to compare meets exclusion criteria. |
432 | 435 | :rtype: bool |
433 | 436 | """ |
| 437 | + level_path = level.path() |
434 | 438 | skip = False |
435 | | - if self.exclude_paths and level.path() in self.exclude_paths: |
| 439 | + if self.exclude_paths and level_path in self.exclude_paths: |
436 | 440 | skip = True |
| 441 | + if self.include_paths and level_path != 'root': |
| 442 | + if level_path not in self.include_paths: |
| 443 | + skip = True |
| 444 | + for prefix in self.include_paths: |
| 445 | + if level_path.startswith(prefix): |
| 446 | + skip = False |
| 447 | + break |
437 | 448 | elif self.exclude_regex_paths and any( |
438 | | - [exclude_regex_path.search(level.path()) for exclude_regex_path in self.exclude_regex_paths]): |
| 449 | + [exclude_regex_path.search(level_path) for exclude_regex_path in self.exclude_regex_paths]): |
439 | 450 | skip = True |
440 | 451 | elif self.exclude_types_tuple and \ |
441 | 452 | (isinstance(level.t1, self.exclude_types_tuple) or isinstance(level.t2, self.exclude_types_tuple)): |
442 | 453 | skip = True |
443 | 454 | elif self.exclude_obj_callback and \ |
444 | | - (self.exclude_obj_callback(level.t1, level.path()) or self.exclude_obj_callback(level.t2, level.path())): |
| 455 | + (self.exclude_obj_callback(level.t1, level_path) or self.exclude_obj_callback(level.t2, level_path)): |
445 | 456 | skip = True |
446 | 457 | elif self.exclude_obj_callback_strict and \ |
447 | | - (self.exclude_obj_callback_strict(level.t1, level.path()) and |
448 | | - self.exclude_obj_callback_strict(level.t2, level.path())): |
| 458 | + (self.exclude_obj_callback_strict(level.t1, level_path) and |
| 459 | + self.exclude_obj_callback_strict(level.t2, level_path)): |
449 | 460 | skip = True |
450 | 461 |
|
451 | 462 | return skip |
@@ -477,12 +488,12 @@ def _get_clean_to_keys_mapping(self, keys, level): |
477 | 488 | return result |
478 | 489 |
|
479 | 490 | def _diff_dict(self, |
480 | | - level, |
481 | | - parents_ids=frozenset([]), |
482 | | - print_as_attribute=False, |
483 | | - override=False, |
484 | | - override_t1=None, |
485 | | - override_t2=None): |
| 491 | + level, |
| 492 | + parents_ids=frozenset([]), |
| 493 | + print_as_attribute=False, |
| 494 | + override=False, |
| 495 | + override_t1=None, |
| 496 | + override_t2=None): |
486 | 497 | """Difference of 2 dictionaries""" |
487 | 498 | if override: |
488 | 499 | # for special stuff like custom objects and named tuples we receive preprocessed t1 and t2 |
@@ -1097,7 +1108,7 @@ def get_other_pair(hash_value, in_t1=True): |
1097 | 1108 | old_indexes=t1_indexes, |
1098 | 1109 | new_indexes=t2_indexes) |
1099 | 1110 | self._report_result('repetition_change', |
1100 | | - repetition_change_level) |
| 1111 | + repetition_change_level) |
1101 | 1112 |
|
1102 | 1113 | else: |
1103 | 1114 | for hash_value in hashes_added: |
@@ -1423,6 +1434,69 @@ def get_stats(self): |
1423 | 1434 | """ |
1424 | 1435 | return self._stats |
1425 | 1436 |
|
| 1437 | + @property |
| 1438 | + def affected_paths(self): |
| 1439 | + """ |
| 1440 | + Get the list of paths that were affected. |
| 1441 | + Whether a value was changed or they were added or removed. |
| 1442 | +
|
| 1443 | + Example |
| 1444 | + >>> t1 = {1: 1, 2: 2, 3: [3], 4: 4} |
| 1445 | + >>> t2 = {1: 1, 2: 4, 3: [3, 4], 5: 5, 6: 6} |
| 1446 | + >>> ddiff = DeepDiff(t1, t2) |
| 1447 | + >>> ddiff |
| 1448 | + >>> pprint(ddiff, indent=4) |
| 1449 | + { 'dictionary_item_added': [root[5], root[6]], |
| 1450 | + 'dictionary_item_removed': [root[4]], |
| 1451 | + 'iterable_item_added': {'root[3][1]': 4}, |
| 1452 | + 'values_changed': {'root[2]': {'new_value': 4, 'old_value': 2}}} |
| 1453 | + >>> ddiff.affected_paths |
| 1454 | + OrderedSet(['root[3][1]', 'root[4]', 'root[5]', 'root[6]', 'root[2]']) |
| 1455 | + >>> ddiff.affected_root_keys |
| 1456 | + OrderedSet([3, 4, 5, 6, 2]) |
| 1457 | +
|
| 1458 | + """ |
| 1459 | + result = OrderedSet() |
| 1460 | + for key in REPORT_KEYS: |
| 1461 | + value = self.get(key) |
| 1462 | + if value: |
| 1463 | + if isinstance(value, PrettyOrderedSet): |
| 1464 | + result |= value |
| 1465 | + else: |
| 1466 | + result |= OrderedSet(value.keys()) |
| 1467 | + return result |
| 1468 | + |
| 1469 | + @property |
| 1470 | + def affected_root_keys(self): |
| 1471 | + """ |
| 1472 | + Get the list of root keys that were affected. |
| 1473 | + Whether a value was changed or they were added or removed. |
| 1474 | +
|
| 1475 | + Example |
| 1476 | + >>> t1 = {1: 1, 2: 2, 3: [3], 4: 4} |
| 1477 | + >>> t2 = {1: 1, 2: 4, 3: [3, 4], 5: 5, 6: 6} |
| 1478 | + >>> ddiff = DeepDiff(t1, t2) |
| 1479 | + >>> ddiff |
| 1480 | + >>> pprint(ddiff, indent=4) |
| 1481 | + { 'dictionary_item_added': [root[5], root[6]], |
| 1482 | + 'dictionary_item_removed': [root[4]], |
| 1483 | + 'iterable_item_added': {'root[3][1]': 4}, |
| 1484 | + 'values_changed': {'root[2]': {'new_value': 4, 'old_value': 2}}} |
| 1485 | + >>> ddiff.affected_paths |
| 1486 | + OrderedSet(['root[3][1]', 'root[4]', 'root[5]', 'root[6]', 'root[2]']) |
| 1487 | + >>> ddiff.affected_root_keys |
| 1488 | + OrderedSet([3, 4, 5, 6, 2]) |
| 1489 | + """ |
| 1490 | + result = OrderedSet() |
| 1491 | + for key in REPORT_KEYS: |
| 1492 | + value = self.tree.get(key) |
| 1493 | + if value: |
| 1494 | + if isinstance(value, PrettyOrderedSet): |
| 1495 | + result |= OrderedSet([i.get_root_key() for i in value]) |
| 1496 | + else: |
| 1497 | + result |= OrderedSet([i.get_root_key() for i in value.keys()]) |
| 1498 | + return result |
| 1499 | + |
1426 | 1500 |
|
1427 | 1501 | if __name__ == "__main__": # pragma: no cover |
1428 | 1502 | import doctest |
|
0 commit comments