6
6
"""
7
7
import re
8
8
import traceback
9
- from typing import Any , Callable , Optional , Protocol , Sequence , TypeVar , Union
9
+ from typing import (
10
+ Any ,
11
+ Callable ,
12
+ List ,
13
+ Mapping ,
14
+ Optional ,
15
+ Pattern ,
16
+ Protocol ,
17
+ Sequence ,
18
+ TypeVar ,
19
+ Union ,
20
+ )
10
21
11
22
T = TypeVar ("T" , Any , Any )
12
23
13
24
14
- def keygetter (obj , path ):
25
+ def keygetter (
26
+ obj : Mapping [str , Any ],
27
+ path : str ,
28
+ ) -> Union [None , Any , str , List [str ], Mapping [str , str ]]:
15
29
"""obj, "foods__breakfast", obj['foods']['breakfast']
16
30
17
31
>>> keygetter({ "foods": { "breakfast": "cereal" } }, "foods__breakfast")
@@ -26,12 +40,12 @@ def keygetter(obj, path):
26
40
for sub_field in sub_fields :
27
41
dct = dct [sub_field ]
28
42
return dct
29
- except Exception as e :
30
- traceback .print_exception ( e )
43
+ except Exception :
44
+ traceback .print_stack ( )
31
45
return None
32
46
33
47
34
- def parse_lookup (obj , path , lookup ) :
48
+ def parse_lookup (obj : Mapping [ str , Any ], path : str , lookup : str ) -> Optional [ Any ] :
35
49
"""Check if field lookup key, e.g. "my__path__contains" has comparator, return val.
36
50
37
51
If comparator not used or value not found, return None.
@@ -42,74 +56,170 @@ def parse_lookup(obj, path, lookup):
42
56
'red apple'
43
57
"""
44
58
try :
45
- if path .endswith (lookup ):
59
+ if isinstance ( path , str ) and isinstance ( lookup , str ) and path .endswith (lookup ):
46
60
if field_name := path .rsplit (lookup )[0 ]:
47
61
return keygetter (obj , field_name )
48
- except Exception as e :
49
- traceback .print_exception ( e )
62
+ except Exception :
63
+ traceback .print_stack ( )
50
64
return None
51
65
52
66
53
67
class LookupProtocol (Protocol ):
54
68
"""Protocol for :class:`QueryList` filtering operators."""
55
69
56
- def __call__ (self , data : Union [list [str ], str ], rhs : Union [list [str ], str ]):
70
+ def __call__ (
71
+ self ,
72
+ data : Union [str , list [str ], Mapping [str , str ]],
73
+ rhs : Union [str , list [str ], Mapping [str , str ], Pattern [str ]],
74
+ ) -> bool :
57
75
"""Callback for :class:`QueryList` filtering operators."""
76
+ ...
58
77
59
78
60
- def lookup_exact (data , rhs ):
79
+ def lookup_exact (
80
+ data : Union [str , list [str ], Mapping [str , str ]],
81
+ rhs : Union [str , list [str ], Mapping [str , str ], Pattern [str ]],
82
+ ) -> bool :
61
83
return rhs == data
62
84
63
85
64
- def lookup_iexact (data , rhs ):
86
+ def lookup_iexact (
87
+ data : Union [str , list [str ], Mapping [str , str ]],
88
+ rhs : Union [str , list [str ], Mapping [str , str ], Pattern [str ]],
89
+ ) -> bool :
90
+ if not isinstance (rhs , str ) or not isinstance (data , str ):
91
+ return False
92
+
65
93
return rhs .lower () == data .lower ()
66
94
67
95
68
- def lookup_contains (data , rhs ):
96
+ def lookup_contains (
97
+ data : Union [str , list [str ], Mapping [str , str ]],
98
+ rhs : Union [str , list [str ], Mapping [str , str ], Pattern [str ]],
99
+ ) -> bool :
100
+ if not isinstance (rhs , str ) or not isinstance (data , (str , Mapping , list )):
101
+ return False
102
+
69
103
return rhs in data
70
104
71
105
72
- def lookup_icontains (data , rhs ):
73
- return rhs .lower () in data .lower ()
106
+ def lookup_icontains (
107
+ data : Union [str , list [str ], Mapping [str , str ]],
108
+ rhs : Union [str , list [str ], Mapping [str , str ], Pattern [str ]],
109
+ ) -> bool :
110
+ if not isinstance (rhs , str ) or not isinstance (data , (str , Mapping , list )):
111
+ return False
112
+
113
+ if isinstance (data , str ):
114
+ return rhs .lower () in data .lower ()
115
+ if isinstance (data , Mapping ):
116
+ return rhs .lower () in [k .lower () for k in data .keys ()]
74
117
118
+ return False
119
+
120
+
121
+ def lookup_startswith (
122
+ data : Union [str , list [str ], Mapping [str , str ]],
123
+ rhs : Union [str , list [str ], Mapping [str , str ], Pattern [str ]],
124
+ ) -> bool :
125
+ if not isinstance (rhs , str ) or not isinstance (data , str ):
126
+ return False
75
127
76
- def lookup_startswith (data , rhs ):
77
128
return data .startswith (rhs )
78
129
79
130
80
- def lookup_istartswith (data , rhs ):
131
+ def lookup_istartswith (
132
+ data : Union [str , list [str ], Mapping [str , str ]],
133
+ rhs : Union [str , list [str ], Mapping [str , str ], Pattern [str ]],
134
+ ) -> bool :
135
+ if not isinstance (rhs , str ) or not isinstance (data , str ):
136
+ return False
137
+
81
138
return data .lower ().startswith (rhs .lower ())
82
139
83
140
84
- def lookup_endswith (data , rhs ):
141
+ def lookup_endswith (
142
+ data : Union [str , list [str ], Mapping [str , str ]],
143
+ rhs : Union [str , list [str ], Mapping [str , str ], Pattern [str ]],
144
+ ) -> bool :
145
+ if not isinstance (rhs , str ) or not isinstance (data , str ):
146
+ return False
147
+
85
148
return data .endswith (rhs )
86
149
87
150
88
- def lookup_iendswith (data , rhs ):
151
+ def lookup_iendswith (
152
+ data : Union [str , list [str ], Mapping [str , str ]],
153
+ rhs : Union [str , list [str ], Mapping [str , str ], Pattern [str ]],
154
+ ) -> bool :
155
+ if not isinstance (rhs , str ) or not isinstance (data , str ):
156
+ return False
89
157
return data .lower ().endswith (rhs .lower ())
90
158
91
159
92
- def lookup_in (data , rhs ):
160
+ def lookup_in (
161
+ data : Union [str , list [str ], Mapping [str , str ]],
162
+ rhs : Union [str , list [str ], Mapping [str , str ], Pattern [str ]],
163
+ ) -> bool :
93
164
if isinstance (rhs , list ):
94
165
return data in rhs
95
- return rhs in data
166
+
167
+ try :
168
+ if isinstance (rhs , str ) and isinstance (data , Mapping ):
169
+ return rhs in data
170
+ if isinstance (rhs , str ) and isinstance (data , (str , list )):
171
+ return rhs in data
172
+ if isinstance (rhs , str ) and isinstance (data , Mapping ):
173
+ return rhs in data
174
+ # TODO: Add a deep Mappingionary matcher
175
+ # if isinstance(rhs, Mapping) and isinstance(data, Mapping):
176
+ # return rhs.items() not in data.items()
177
+ except Exception :
178
+ return False
179
+ return False
96
180
97
181
98
- def lookup_nin (data , rhs ):
182
+ def lookup_nin (
183
+ data : Union [str , list [str ], Mapping [str , str ]],
184
+ rhs : Union [str , list [str ], Mapping [str , str ], Pattern [str ]],
185
+ ) -> bool :
99
186
if isinstance (rhs , list ):
100
187
return data not in rhs
101
- return rhs not in data
188
+
189
+ try :
190
+ if isinstance (rhs , str ) and isinstance (data , Mapping ):
191
+ return rhs not in data
192
+ if isinstance (rhs , str ) and isinstance (data , (str , list )):
193
+ return rhs not in data
194
+ if isinstance (rhs , str ) and isinstance (data , Mapping ):
195
+ return rhs not in data
196
+ # TODO: Add a deep Mappingionary matcher
197
+ # if isinstance(rhs, Mapping) and isinstance(data, Mapping):
198
+ # return rhs.items() not in data.items()
199
+ except Exception :
200
+ return False
201
+ return False
102
202
103
203
104
- def lookup_regex (data , rhs ):
105
- return re .search (rhs , data )
204
+ def lookup_regex (
205
+ data : Union [str , list [str ], Mapping [str , str ]],
206
+ rhs : Union [str , list [str ], Mapping [str , str ], Pattern [str ]],
207
+ ) -> bool :
208
+ if isinstance (data , (str , bytes , re .Pattern )) and isinstance (rhs , (str , bytes )):
209
+ return bool (re .search (rhs , data ))
210
+ return False
106
211
107
212
108
- def lookup_iregex (data , rhs ):
109
- return re .search (rhs , data , re .IGNORECASE )
213
+ def lookup_iregex (
214
+ data : Union [str , list [str ], Mapping [str , str ]],
215
+ rhs : Union [str , list [str ], Mapping [str , str ], Pattern [str ]],
216
+ ) -> bool :
217
+ if isinstance (data , (str , bytes , re .Pattern )) and isinstance (rhs , (str , bytes )):
218
+ return bool (re .search (rhs , data , re .IGNORECASE ))
219
+ return False
110
220
111
221
112
- LOOKUP_NAME_MAP : dict [str , LookupProtocol ] = {
222
+ LOOKUP_NAME_MAP : Mapping [str , LookupProtocol ] = {
113
223
"eq" : lookup_exact ,
114
224
"exact" : lookup_exact ,
115
225
"iexact" : lookup_iexact ,
@@ -127,7 +237,7 @@ def lookup_iregex(data, rhs):
127
237
128
238
129
239
class QueryList (list [T ]):
130
- """Filter list of object/dicts . For small, local datasets. *Experimental, unstable*.
240
+ """Filter list of object/Mappings . For small, local datasets. *Experimental, unstable*.
131
241
132
242
>>> query = QueryList(
133
243
... [
@@ -158,23 +268,33 @@ class QueryList(list[T]):
158
268
"""
159
269
160
270
data : Sequence [T ]
271
+ pk_key : Optional [str ]
161
272
162
- def items (self ):
273
+ def items (self ) -> list [ T ] :
163
274
data : Sequence [T ]
164
275
165
276
if self .pk_key is None :
166
277
raise Exception ("items() require a pk_key exists" )
167
278
return [(getattr (item , self .pk_key ), item ) for item in self ]
168
279
169
- def __eq__ (self , other ):
280
+ def __eq__ (
281
+ self ,
282
+ other : object ,
283
+ # other: Union[
284
+ # "QueryList[T]",
285
+ # List[Mapping[str, str]],
286
+ # List[Mapping[str, int]],
287
+ # List[Mapping[str, Union[str, Mapping[str, Union[List[str], str]]]]],
288
+ # ],
289
+ ) -> bool :
170
290
data = other
171
291
172
292
if not isinstance (self , list ) or not isinstance (data , list ):
173
293
return False
174
294
175
295
if len (self ) == len (data ):
176
296
for (a , b ) in zip (self , data ):
177
- if isinstance (a , dict ):
297
+ if isinstance (a , Mapping ):
178
298
a_keys = a .keys ()
179
299
if a .keys == b .keys ():
180
300
for key in a_keys :
@@ -187,8 +307,10 @@ def __eq__(self, other):
187
307
return True
188
308
return False
189
309
190
- def filter (self , matcher : Optional [Union [Callable [[T ], bool ], T ]] = None , ** kwargs ):
191
- def filter_lookup (obj ) -> bool :
310
+ def filter (
311
+ self , matcher : Optional [Union [Callable [[T ], bool ], T ]] = None , ** kwargs : Any
312
+ ) -> "QueryList[T]" :
313
+ def filter_lookup (obj : Any ) -> bool :
192
314
for path , v in kwargs .items ():
193
315
try :
194
316
lhs , op = path .rsplit ("__" , 1 )
@@ -203,7 +325,7 @@ def filter_lookup(obj) -> bool:
203
325
path = lhs
204
326
data = keygetter (obj , path )
205
327
206
- if not LOOKUP_NAME_MAP [op ](data , v ):
328
+ if data is None or not LOOKUP_NAME_MAP [op ](data , v ):
207
329
return False
208
330
209
331
return True
@@ -212,7 +334,7 @@ def filter_lookup(obj) -> bool:
212
334
_filter = matcher
213
335
elif matcher is not None :
214
336
215
- def val_match (obj ) :
337
+ def val_match (obj : Union [ str , list [ Any ]]) -> bool :
216
338
if isinstance (matcher , list ):
217
339
return obj in matcher
218
340
else :
0 commit comments