|
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