|
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