Skip to content

Commit 33de087

Browse files
committed
adding type hints to search
1 parent b6c93ca commit 33de087

File tree

1 file changed

+73
-59
lines changed

1 file changed

+73
-59
lines changed

deepdiff/search.py

Lines changed: 73 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,24 @@
11
#!/usr/bin/env python
22
import re
33
from collections.abc import MutableMapping, Iterable
4+
from typing import Any, Dict, FrozenSet, List, Optional, Pattern, Set, Union, TYPE_CHECKING
45
from deepdiff.helper import SetOrdered
56
import logging
67

78
from deepdiff.helper import (
89
strings, numbers, add_to_frozen_set, get_doc, dict_, RE_COMPILED_TYPE, ipranges
910
)
1011

12+
if TYPE_CHECKING:
13+
from typing_extensions import Self
14+
1115
logger = logging.getLogger(__name__)
1216

1317

1418
doc = get_doc('search_doc.rst')
1519

1620

17-
class DeepSearch(dict):
21+
class DeepSearch(Dict[str, Union[Dict[str, Any], SetOrdered, List[str]]]):
1822
r"""
1923
**DeepSearch**
2024
@@ -80,52 +84,56 @@ class DeepSearch(dict):
8084
8185
"""
8286

83-
warning_num = 0
87+
warning_num: int = 0
8488

8589
def __init__(self,
86-
obj,
87-
item,
88-
exclude_paths=SetOrdered(),
89-
exclude_regex_paths=SetOrdered(),
90-
exclude_types=SetOrdered(),
91-
verbose_level=1,
92-
case_sensitive=False,
93-
match_string=False,
94-
use_regexp=False,
95-
strict_checking=True,
96-
**kwargs):
90+
obj: Any,
91+
item: Any,
92+
exclude_paths: Union[SetOrdered, Set[str], List[str]] = SetOrdered(),
93+
exclude_regex_paths: Union[SetOrdered, Set[Union[str, Pattern[str]]], List[Union[str, Pattern[str]]]] = SetOrdered(),
94+
exclude_types: Union[SetOrdered, Set[type], List[type]] = SetOrdered(),
95+
verbose_level: int = 1,
96+
case_sensitive: bool = False,
97+
match_string: bool = False,
98+
use_regexp: bool = False,
99+
strict_checking: bool = True,
100+
**kwargs: Any) -> None:
97101
if kwargs:
98102
raise ValueError((
99103
"The following parameter(s) are not valid: %s\n"
100104
"The valid parameters are obj, item, exclude_paths, exclude_types,\n"
101105
"case_sensitive, match_string and verbose_level."
102106
) % ', '.join(kwargs.keys()))
103107

104-
self.obj = obj
105-
self.case_sensitive = case_sensitive if isinstance(item, strings) else True
106-
item = item if self.case_sensitive else item.lower()
107-
self.exclude_paths = SetOrdered(exclude_paths)
108-
self.exclude_regex_paths = [re.compile(exclude_regex_path) for exclude_regex_path in exclude_regex_paths]
109-
self.exclude_types = SetOrdered(exclude_types)
110-
self.exclude_types_tuple = tuple(
108+
self.obj: Any = obj
109+
self.case_sensitive: bool = case_sensitive if isinstance(item, strings) else True
110+
item = item if self.case_sensitive else (item.lower() if isinstance(item, str) else item)
111+
self.exclude_paths: SetOrdered = SetOrdered(exclude_paths)
112+
self.exclude_regex_paths: List[Pattern[str]] = [re.compile(exclude_regex_path) for exclude_regex_path in exclude_regex_paths]
113+
self.exclude_types: SetOrdered = SetOrdered(exclude_types)
114+
self.exclude_types_tuple: tuple[type, ...] = tuple(
111115
exclude_types) # we need tuple for checking isinstance
112-
self.verbose_level = verbose_level
116+
self.verbose_level: int = verbose_level
113117
self.update(
114118
matched_paths=self.__set_or_dict(),
115119
matched_values=self.__set_or_dict(),
116120
unprocessed=[])
117-
self.use_regexp = use_regexp
121+
# Type narrowing for mypy/pyright
122+
self.matched_paths: Union[Dict[str, Any], SetOrdered]
123+
self.matched_values: Union[Dict[str, Any], SetOrdered]
124+
self.unprocessed: List[str]
125+
self.use_regexp: bool = use_regexp
118126
if not strict_checking and (isinstance(item, numbers) or isinstance(item, ipranges)):
119127
item = str(item)
120128
if self.use_regexp:
121129
try:
122130
item = re.compile(item)
123131
except TypeError as e:
124132
raise TypeError(f"The passed item of {item} is not usable for regex: {e}") from None
125-
self.strict_checking = strict_checking
133+
self.strict_checking: bool = strict_checking
126134

127135
# Cases where user wants to match exact string item
128-
self.match_string = match_string
136+
self.match_string: bool = match_string
129137

130138
self.__search(obj, item, parents_ids=frozenset({id(obj)}))
131139

@@ -134,21 +142,25 @@ def __init__(self,
134142
for k in empty_keys:
135143
del self[k]
136144

137-
def __set_or_dict(self):
145+
def __set_or_dict(self) -> Union[Dict[str, Any], SetOrdered]:
138146
return dict_() if self.verbose_level >= 2 else SetOrdered()
139147

140-
def __report(self, report_key, key, value):
148+
def __report(self, report_key: str, key: str, value: Any) -> None:
141149
if self.verbose_level >= 2:
142-
self[report_key][key] = value
150+
report_dict = self[report_key]
151+
if isinstance(report_dict, dict):
152+
report_dict[key] = value
143153
else:
144-
self[report_key].add(key)
154+
report_set = self[report_key]
155+
if isinstance(report_set, SetOrdered):
156+
report_set.add(key)
145157

146158
def __search_obj(self,
147-
obj,
148-
item,
149-
parent,
150-
parents_ids=frozenset(),
151-
is_namedtuple=False):
159+
obj: Any,
160+
item: Any,
161+
parent: str,
162+
parents_ids: FrozenSet[int] = frozenset(),
163+
is_namedtuple: bool = False) -> None:
152164
"""Search objects"""
153165
found = False
154166
if obj == item:
@@ -170,14 +182,16 @@ def __search_obj(self,
170182
obj = {i: getattr(obj, i) for i in obj.__slots__}
171183
except AttributeError:
172184
if not found:
173-
self['unprocessed'].append("%s" % parent)
185+
unprocessed = self.get('unprocessed', [])
186+
if isinstance(unprocessed, list):
187+
unprocessed.append("%s" % parent)
174188

175189
return
176190

177191
self.__search_dict(
178192
obj, item, parent, parents_ids, print_as_attribute=True)
179193

180-
def __skip_this(self, item, parent):
194+
def __skip_this(self, item: Any, parent: str) -> bool:
181195
skip = False
182196
if parent in self.exclude_paths:
183197
skip = True
@@ -191,11 +205,11 @@ def __skip_this(self, item, parent):
191205
return skip
192206

193207
def __search_dict(self,
194-
obj,
195-
item,
196-
parent,
197-
parents_ids=frozenset(),
198-
print_as_attribute=False):
208+
obj: Union[Dict[Any, Any], MutableMapping[Any, Any]],
209+
item: Any,
210+
parent: str,
211+
parents_ids: FrozenSet[int] = frozenset(),
212+
print_as_attribute: bool = False) -> None:
199213
"""Search dictionaries"""
200214
if print_as_attribute:
201215
parent_text = "%s.%s"
@@ -238,10 +252,10 @@ def __search_dict(self,
238252
parents_ids=parents_ids_added)
239253

240254
def __search_iterable(self,
241-
obj,
242-
item,
243-
parent="root",
244-
parents_ids=frozenset()):
255+
obj: Iterable[Any],
256+
item: Any,
257+
parent: str = "root",
258+
parents_ids: FrozenSet[int] = frozenset()) -> None:
245259
"""Search iterables except dictionaries, sets and strings."""
246260
for i, thing in enumerate(obj):
247261
new_parent = "{}[{}]".format(parent, i)
@@ -251,7 +265,7 @@ def __search_iterable(self,
251265
if self.case_sensitive or not isinstance(thing, strings):
252266
thing_cased = thing
253267
else:
254-
thing_cased = thing.lower()
268+
thing_cased = thing.lower() if isinstance(thing, str) else thing
255269

256270
if not self.use_regexp and thing_cased == item:
257271
self.__report(
@@ -264,19 +278,19 @@ def __search_iterable(self,
264278
self.__search(thing, item, "%s[%s]" %
265279
(parent, i), parents_ids_added)
266280

267-
def __search_str(self, obj, item, parent):
281+
def __search_str(self, obj: Union[str, bytes, memoryview], item: Union[str, bytes, memoryview, Pattern[str]], parent: str) -> None:
268282
"""Compare strings"""
269-
obj_text = obj if self.case_sensitive else obj.lower()
283+
obj_text = obj if self.case_sensitive else (obj.lower() if isinstance(obj, str) else obj)
270284

271285
is_matched = False
272-
if self.use_regexp:
273-
is_matched = item.search(obj_text)
274-
elif (self.match_string and item == obj_text) or (not self.match_string and item in obj_text):
286+
if self.use_regexp and isinstance(item, type(re.compile(''))):
287+
is_matched = bool(item.search(str(obj_text)))
288+
elif (self.match_string and str(item) == str(obj_text)) or (not self.match_string and str(item) in str(obj_text)):
275289
is_matched = True
276290
if is_matched:
277291
self.__report(report_key='matched_values', key=parent, value=obj)
278292

279-
def __search_numbers(self, obj, item, parent):
293+
def __search_numbers(self, obj: Any, item: Any, parent: str) -> None:
280294
if (
281295
item == obj or (
282296
not self.strict_checking and (
@@ -288,11 +302,11 @@ def __search_numbers(self, obj, item, parent):
288302
):
289303
self.__report(report_key='matched_values', key=parent, value=obj)
290304

291-
def __search_tuple(self, obj, item, parent, parents_ids):
305+
def __search_tuple(self, obj: tuple[Any, ...], item: Any, parent: str, parents_ids: FrozenSet[int]) -> None:
292306
# Checking to see if it has _fields. Which probably means it is a named
293307
# tuple.
294308
try:
295-
obj._asdict
309+
getattr(obj, '_asdict')
296310
# It must be a normal tuple
297311
except AttributeError:
298312
self.__search_iterable(obj, item, parent, parents_ids)
@@ -301,7 +315,7 @@ def __search_tuple(self, obj, item, parent, parents_ids):
301315
self.__search_obj(
302316
obj, item, parent, parents_ids, is_namedtuple=True)
303317

304-
def __search(self, obj, item, parent="root", parents_ids=frozenset()):
318+
def __search(self, obj: Any, item: Any, parent: str = "root", parents_ids: FrozenSet[int] = frozenset()) -> None:
305319
"""The main search method"""
306320
if self.__skip_this(item, parent):
307321
return
@@ -344,12 +358,12 @@ class grep:
344358
__doc__ = doc
345359

346360
def __init__(self,
347-
item,
348-
**kwargs):
349-
self.item = item
350-
self.kwargs = kwargs
361+
item: Any,
362+
**kwargs: Any) -> None:
363+
self.item: Any = item
364+
self.kwargs: Dict[str, Any] = kwargs
351365

352-
def __ror__(self, other):
366+
def __ror__(self, other: Any) -> "DeepSearch":
353367
return DeepSearch(obj=other, item=self.item, **self.kwargs)
354368

355369

0 commit comments

Comments
 (0)