|
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,24 @@ 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: |
| 440 | + skip = True |
| 441 | + if self.include_paths and level_path not in self.include_paths: |
436 | 442 | skip = True
|
437 | 443 | elif self.exclude_regex_paths and any(
|
438 |
| - [exclude_regex_path.search(level.path()) for exclude_regex_path in self.exclude_regex_paths]): |
| 444 | + [exclude_regex_path.search(level_path) for exclude_regex_path in self.exclude_regex_paths]): |
439 | 445 | skip = True
|
440 | 446 | elif self.exclude_types_tuple and \
|
441 | 447 | (isinstance(level.t1, self.exclude_types_tuple) or isinstance(level.t2, self.exclude_types_tuple)):
|
442 | 448 | skip = True
|
443 | 449 | elif self.exclude_obj_callback and \
|
444 |
| - (self.exclude_obj_callback(level.t1, level.path()) or self.exclude_obj_callback(level.t2, level.path())): |
| 450 | + (self.exclude_obj_callback(level.t1, level_path) or self.exclude_obj_callback(level.t2, level_path)): |
445 | 451 | skip = True
|
446 | 452 | 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())): |
| 453 | + (self.exclude_obj_callback_strict(level.t1, level_path) and |
| 454 | + self.exclude_obj_callback_strict(level.t2, level_path)): |
449 | 455 | skip = True
|
450 | 456 |
|
451 | 457 | return skip
|
@@ -477,12 +483,12 @@ def _get_clean_to_keys_mapping(self, keys, level):
|
477 | 483 | return result
|
478 | 484 |
|
479 | 485 | 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): |
| 486 | + level, |
| 487 | + parents_ids=frozenset([]), |
| 488 | + print_as_attribute=False, |
| 489 | + override=False, |
| 490 | + override_t1=None, |
| 491 | + override_t2=None): |
486 | 492 | """Difference of 2 dictionaries"""
|
487 | 493 | if override:
|
488 | 494 | # for special stuff like custom objects and named tuples we receive preprocessed t1 and t2
|
@@ -1097,7 +1103,7 @@ def get_other_pair(hash_value, in_t1=True):
|
1097 | 1103 | old_indexes=t1_indexes,
|
1098 | 1104 | new_indexes=t2_indexes)
|
1099 | 1105 | self._report_result('repetition_change',
|
1100 |
| - repetition_change_level) |
| 1106 | + repetition_change_level) |
1101 | 1107 |
|
1102 | 1108 | else:
|
1103 | 1109 | for hash_value in hashes_added:
|
@@ -1423,6 +1429,22 @@ def get_stats(self):
|
1423 | 1429 | """
|
1424 | 1430 | return self._stats
|
1425 | 1431 |
|
| 1432 | + @property |
| 1433 | + def affected_paths(self): |
| 1434 | + """ |
| 1435 | + Get the list of paths that were affected. |
| 1436 | + Whether a value was changed or they were added or removed. |
| 1437 | + """ |
| 1438 | + result = OrderedSet() |
| 1439 | + for key in REPORT_KEYS: |
| 1440 | + value = self.get(key) |
| 1441 | + if value: |
| 1442 | + if isinstance(value, PrettyOrderedSet): |
| 1443 | + result |= value |
| 1444 | + else: |
| 1445 | + result |= OrderedSet(value.keys()) |
| 1446 | + return result |
| 1447 | + |
1426 | 1448 |
|
1427 | 1449 | if __name__ == "__main__": # pragma: no cover
|
1428 | 1450 | import doctest
|
|
0 commit comments