2929 TEXT_VIEW , TREE_VIEW , DELTA_VIEW , COLORED_VIEW , COLORED_COMPACT_VIEW ,
3030 detailed__dict__ , add_root_to_paths ,
3131 np , get_truncate_datetime , dict_ , CannotCompare , ENUM_INCLUDE_KEYS ,
32- PydanticBaseModel , Opcode , SetOrdered , ipranges )
32+ PydanticBaseModel , Opcode , SetOrdered , ipranges ,
33+ separate_wildcard_and_exact_paths )
3334from deepdiff .serialization import SerializationMixin
3435from deepdiff .distance import DistanceMixin , logarithmic_similarity
3536from deepdiff .model import (
@@ -110,7 +111,9 @@ def _report_progress(_stats: Dict[str, Any], progress_logger: Callable[[str], No
110111DEEPHASH_PARAM_KEYS = (
111112 'exclude_types' ,
112113 'exclude_paths' ,
114+ 'exclude_glob_paths' ,
113115 'include_paths' ,
116+ 'include_glob_paths' ,
114117 'exclude_regex_paths' ,
115118 'hasher' ,
116119 'significant_digits' ,
@@ -209,6 +212,10 @@ def __init__(self,
209212 _shared_parameters : Optional [Dict [str , Any ]]= None ,
210213 ** kwargs ):
211214 super ().__init__ ()
215+ # Defaults for glob path attributes — needed for non-root instances
216+ # that may receive _parameters without these keys.
217+ self .exclude_glob_paths = None
218+ self .include_glob_paths = None
212219 if kwargs :
213220 raise ValueError ((
214221 "The following parameter(s) are not valid: %s\n "
@@ -257,8 +264,12 @@ def __init__(self,
257264 ignore_type_subclasses = ignore_type_subclasses ,
258265 ignore_uuid_types = ignore_uuid_types )
259266 self .report_repetition = report_repetition
260- self .exclude_paths = add_root_to_paths (convert_item_or_items_into_set_else_none (exclude_paths ))
261- self .include_paths = add_root_to_paths (convert_item_or_items_into_set_else_none (include_paths ))
267+ _exclude_set = convert_item_or_items_into_set_else_none (exclude_paths )
268+ _exclude_exact , self .exclude_glob_paths = separate_wildcard_and_exact_paths (_exclude_set )
269+ self .exclude_paths = add_root_to_paths (_exclude_exact )
270+ _include_set = convert_item_or_items_into_set_else_none (include_paths )
271+ _include_exact , self .include_glob_paths = separate_wildcard_and_exact_paths (_include_set )
272+ self .include_paths = add_root_to_paths (_include_exact )
262273 self .exclude_regex_paths = convert_item_or_items_into_compiled_regexes_else_none (exclude_regex_paths )
263274 self .exclude_types = set (exclude_types ) if exclude_types else None
264275 self .exclude_types_tuple = tuple (exclude_types ) if exclude_types else None # we need tuple for checking isinstance
@@ -423,7 +434,7 @@ def _group_by_sort_key(x):
423434 self .__dict__ .clear ()
424435
425436 def _get_deephash_params (self ):
426- result = {key : self ._parameters [ key ] for key in DEEPHASH_PARAM_KEYS }
437+ result = {key : self ._parameters . get ( key ) for key in DEEPHASH_PARAM_KEYS }
427438 result ['ignore_repetition' ] = not self .report_repetition
428439 result ['number_to_string_func' ] = self .number_to_string
429440 return result
@@ -442,6 +453,8 @@ def _report_result(self, report_type, change_level, local_tree=None):
442453 """
443454
444455 if not self ._skip_this (change_level ):
456+ if self ._skip_report_for_include_glob (change_level ):
457+ return
445458 change_level .report_type = report_type
446459 tree = self .tree if local_tree is None else local_tree
447460 tree [report_type ].add (change_level )
@@ -461,10 +474,33 @@ def custom_report_result(self, report_type, level, extra_info=None):
461474 """
462475
463476 if not self ._skip_this (level ):
477+ if self ._skip_report_for_include_glob (level ):
478+ return
464479 level .report_type = report_type
465480 level .additional [CUSTOM_FIELD ] = extra_info
466481 self .tree [report_type ].add (level )
467482
483+ def _skip_report_for_include_glob (self , level ):
484+ """When include_glob_paths is set, _skip_this allows ancestors through for traversal.
485+ This method does a stricter check at report time: only report if the path
486+ actually matches a glob pattern or is a descendant of a matching path,
487+ or if it already matches an exact include_path."""
488+ if not self .include_glob_paths :
489+ return False
490+ level_path = level .path ()
491+ # If exact include_paths already matched, don't skip
492+ if self .include_paths :
493+ if level_path in self .include_paths :
494+ return False
495+ for prefix in self .include_paths :
496+ if prefix in level_path :
497+ return False
498+ # Check glob patterns: match or descendant
499+ for gp in self .include_glob_paths :
500+ if gp .match_or_is_descendant (level_path ):
501+ return False
502+ return True
503+
468504 @staticmethod
469505 def _dict_from_slots (object : Any ) -> Dict [str , Any ]:
470506 def unmangle (attribute : str ) -> str :
@@ -552,11 +588,21 @@ def _skip_this(self, level: Any) -> bool:
552588 skip = False
553589 if self .exclude_paths and level_path in self .exclude_paths :
554590 skip = True
555- if self .include_paths and level_path != 'root' :
556- if level_path not in self .include_paths :
557- skip = True
558- for prefix in self .include_paths :
559- if prefix in level_path or level_path in prefix :
591+ elif self .exclude_glob_paths and any (gp .match (level_path ) for gp in self .exclude_glob_paths ):
592+ skip = True
593+ if not skip and (self .include_paths or self .include_glob_paths ) and level_path != 'root' :
594+ skip = True
595+ if self .include_paths :
596+ if level_path in self .include_paths :
597+ skip = False
598+ else :
599+ for prefix in self .include_paths :
600+ if prefix in level_path or level_path in prefix :
601+ skip = False
602+ break
603+ if skip and self .include_glob_paths :
604+ for gp in self .include_glob_paths :
605+ if gp .match_or_is_ancestor (level_path ):
560606 skip = False
561607 break
562608 elif self .exclude_regex_paths and any (
@@ -586,28 +632,34 @@ def _skip_this(self, level: Any) -> bool:
586632
587633 def _skip_this_key (self , level : Any , key : Any ) -> bool :
588634 # if include_paths is not set, than treet every path as included
589- if self .include_paths is None :
590- return False
591- if "{}['{}']" .format (level .path (), key ) in self .include_paths :
592- return False
593- if level .path () in self .include_paths :
594- # matches e.g. level+key root['foo']['bar']['veg'] include_paths ["root['foo']['bar']"]
635+ if self .include_paths is None and self .include_glob_paths is None :
595636 return False
596- for prefix in self .include_paths :
597- if "{}['{}']" .format (level .path (), key ) in prefix :
598- # matches as long the prefix is longer than this object key
599- # eg.: level+key root['foo']['bar'] matches prefix root['foo']['bar'] from include paths
600- # level+key root['foo'] matches prefix root['foo']['bar'] from include_paths
601- # level+key root['foo']['bar'] DOES NOT match root['foo'] from include_paths This needs to be handled afterwards
637+ key_path = "{}['{}']" .format (level .path (), key )
638+ if self .include_paths :
639+ if key_path in self .include_paths :
602640 return False
603- # check if a higher level is included as a whole (=without any sublevels specified)
604- # matches e.g. level+key root['foo']['bar']['veg'] include_paths ["root['foo']"]
605- # but does not match, if it is level+key root['foo']['bar']['veg'] include_paths ["root['foo']['bar']['fruits']"]
606- up = level .up
607- while up is not None :
608- if up .path () in self .include_paths :
641+ if level .path () in self .include_paths :
642+ # matches e.g. level+key root['foo']['bar']['veg'] include_paths ["root['foo']['bar']"]
609643 return False
610- up = up .up
644+ for prefix in self .include_paths :
645+ if key_path in prefix :
646+ # matches as long the prefix is longer than this object key
647+ # eg.: level+key root['foo']['bar'] matches prefix root['foo']['bar'] from include paths
648+ # level+key root['foo'] matches prefix root['foo']['bar'] from include_paths
649+ # level+key root['foo']['bar'] DOES NOT match root['foo'] from include_paths This needs to be handled afterwards
650+ return False
651+ # check if a higher level is included as a whole (=without any sublevels specified)
652+ # matches e.g. level+key root['foo']['bar']['veg'] include_paths ["root['foo']"]
653+ # but does not match, if it is level+key root['foo']['bar']['veg'] include_paths ["root['foo']['bar']['fruits']"]
654+ up = level .up
655+ while up is not None :
656+ if up .path () in self .include_paths :
657+ return False
658+ up = up .up
659+ if self .include_glob_paths :
660+ for gp in self .include_glob_paths :
661+ if gp .match_or_is_ancestor (key_path ):
662+ return False
611663 return True
612664
613665 def _get_clean_to_keys_mapping (self , keys : Any , level : Any ) -> Dict [Any , Any ]:
@@ -701,9 +753,13 @@ def _diff_dict(
701753 t_keys_removed = t1_keys - t_keys_intersect
702754
703755 if self .threshold_to_diff_deeper :
704- if self .exclude_paths :
756+ if self .exclude_paths or self . exclude_glob_paths :
705757 t_keys_union = {f"{ level .path ()} [{ repr (key )} ]" for key in (t2_keys | t1_keys )}
706- t_keys_union -= self .exclude_paths
758+ if self .exclude_paths :
759+ t_keys_union -= self .exclude_paths
760+ if self .exclude_glob_paths :
761+ t_keys_union = {k for k in t_keys_union
762+ if not any (gp .match (k ) for gp in self .exclude_glob_paths )}
707763 t_keys_union_len = len (t_keys_union )
708764 else :
709765 t_keys_union_len = len (t2_keys | t1_keys )
0 commit comments