|
1 | 1 | import dataclasses
|
2 | 2 | import re
|
3 | 3 | import traceback
|
4 |
| -from typing import Any, Callable, Generic, Optional, Sequence, TypeVar, Union |
| 4 | +from typing import Any, Callable, Generic, Optional, Protocol, Sequence, TypeVar, Union |
5 | 5 |
|
6 | 6 | T = TypeVar("T", Any, Any)
|
7 | 7 |
|
@@ -45,6 +45,80 @@ def parse_lookup(obj, path, lookup):
|
45 | 45 | return None
|
46 | 46 |
|
47 | 47 |
|
| 48 | +class LookupProtocol(Protocol): |
| 49 | + def __call__(self, data: Union[list[str], str], rhs: Union[list[str], str]): |
| 50 | + pass |
| 51 | + |
| 52 | + |
| 53 | +def lookup_exact(data, rhs): |
| 54 | + return rhs == data |
| 55 | + |
| 56 | + |
| 57 | +def lookup_iexact(data, rhs): |
| 58 | + return rhs.lower() == data.lower() |
| 59 | + |
| 60 | + |
| 61 | +def lookup_contains(data, rhs): |
| 62 | + return rhs in data |
| 63 | + |
| 64 | + |
| 65 | +def lookup_icontains(data, rhs): |
| 66 | + return rhs.lower() in data.lower() |
| 67 | + |
| 68 | + |
| 69 | +def lookup_startswith(data, rhs): |
| 70 | + return data.startswith(rhs) |
| 71 | + |
| 72 | + |
| 73 | +def lookup_istartswith(data, rhs): |
| 74 | + return data.lower().startswith(rhs.lower()) |
| 75 | + |
| 76 | + |
| 77 | +def lookup_endswith(data, rhs): |
| 78 | + return data.endswith(rhs) |
| 79 | + |
| 80 | + |
| 81 | +def lookup_iendswith(data, rhs): |
| 82 | + return data.lower().endswith(rhs.lower()) |
| 83 | + |
| 84 | + |
| 85 | +def lookup_in(data, rhs): |
| 86 | + if isinstance(rhs, list): |
| 87 | + return data in rhs |
| 88 | + return rhs in data |
| 89 | + |
| 90 | + |
| 91 | +def lookup_nin(data, rhs): |
| 92 | + if isinstance(rhs, list): |
| 93 | + return data not in rhs |
| 94 | + return rhs not in data |
| 95 | + |
| 96 | + |
| 97 | +def lookup_regex(data, rhs): |
| 98 | + return re.search(rhs, data) |
| 99 | + |
| 100 | + |
| 101 | +def lookup_iregex(data, rhs): |
| 102 | + return re.search(rhs, data, re.IGNORECASE) |
| 103 | + |
| 104 | + |
| 105 | +LOOKUP_NAME_MAP: dict[str, LookupProtocol] = { |
| 106 | + "eq": lookup_exact, |
| 107 | + "exact": lookup_exact, |
| 108 | + "iexact": lookup_iexact, |
| 109 | + "contains": lookup_contains, |
| 110 | + "icontains": lookup_icontains, |
| 111 | + "startswith": lookup_startswith, |
| 112 | + "istartswith": lookup_istartswith, |
| 113 | + "endswith": lookup_endswith, |
| 114 | + "iendswith": lookup_iendswith, |
| 115 | + "in": lookup_in, |
| 116 | + "nin": lookup_nin, |
| 117 | + "regex": lookup_regex, |
| 118 | + "iregex": lookup_iregex, |
| 119 | +} |
| 120 | + |
| 121 | + |
48 | 122 | @dataclasses.dataclass(eq=False)
|
49 | 123 | class ListQuery(Generic[T]):
|
50 | 124 | """Filter a list of dicts. *Experimental and unstable*.
|
@@ -119,93 +193,25 @@ def __eq__(self, other):
|
119 | 193 | return True
|
120 | 194 | return False
|
121 | 195 |
|
122 |
| - def lookup_exact(self, data, rhs): |
123 |
| - return rhs == data |
124 |
| - |
125 |
| - def lookup_iexact(self, data, rhs): |
126 |
| - return rhs.lower() == data.lower() |
127 |
| - |
128 |
| - def lookup_contains(self, data, rhs): |
129 |
| - return rhs in data |
130 |
| - |
131 |
| - def lookup_icontains(self, data, rhs): |
132 |
| - return rhs.lower() in data.lower() |
133 |
| - |
134 |
| - LOOKUP_NAME_MAP = { |
135 |
| - "contains": lookup_contains, |
136 |
| - "icontains": lookup_icontains, |
137 |
| - "exact": lookup_exact, |
138 |
| - "iexact": lookup_iexact, |
139 |
| - } |
140 |
| - |
141 | 196 | def filter(self, matcher: Optional[Union[Callable[[T], bool], T]] = None, **kwargs):
|
142 | 197 | def filter_lookup(obj) -> bool:
|
143 | 198 |
|
144 | 199 | for path, v in kwargs.items():
|
145 | 200 | try:
|
146 | 201 | lhs, op = path.rsplit("__", 1)
|
| 202 | + |
| 203 | + if op not in LOOKUP_NAME_MAP: |
| 204 | + raise ValueError(f"{op} not in LOOKUP_NAME_MAP") |
147 | 205 | except ValueError:
|
148 | 206 | lhs = path
|
149 | 207 | op = "exact"
|
150 | 208 |
|
151 |
| - # assert op in self.LOOKUP_NAME_MAP |
152 |
| - # path = lhs |
| 209 | + assert op in LOOKUP_NAME_MAP |
| 210 | + path = lhs |
| 211 | + data = keygetter(obj, path) |
153 | 212 |
|
154 |
| - if (field := parse_lookup(obj, path, "__contains")) is not None: |
155 |
| - if v not in field: |
156 |
| - return False |
157 |
| - elif (field := parse_lookup(obj, path, "__icontains")) is not None: |
158 |
| - if v.lower() not in field.lower(): |
159 |
| - return False |
160 |
| - elif (field := parse_lookup(obj, path, "__in")) is not None: |
161 |
| - if isinstance(v, list): |
162 |
| - if field not in v: |
163 |
| - return False |
164 |
| - else: |
165 |
| - if v not in field: |
166 |
| - return False |
167 |
| - elif (field := parse_lookup(obj, path, "__nin")) is not None: |
168 |
| - if isinstance(v, list): |
169 |
| - if field in v: |
170 |
| - return False |
171 |
| - else: |
172 |
| - if v in field: |
173 |
| - return False |
174 |
| - elif (field := parse_lookup(obj, path, "__startswith")) is not None: |
175 |
| - if not field.startswith(v): |
176 |
| - return False |
177 |
| - elif (field := parse_lookup(obj, path, "__istartswith")) is not None: |
178 |
| - if not field.lower().startswith(v.lower()): |
179 |
| - return False |
180 |
| - elif (field := parse_lookup(obj, path, "__endswith")) is not None: |
181 |
| - if not field.endswith(v): |
182 |
| - return False |
183 |
| - elif (field := parse_lookup(obj, path, "__iendswith")) is not None: |
184 |
| - if not field.lower().endswith(v.lower()): |
185 |
| - return False |
186 |
| - elif (field := parse_lookup(obj, path, "__regex")) is not None: |
187 |
| - if not re.search(v, field): |
188 |
| - return False |
189 |
| - elif (field := parse_lookup(obj, path, "__iregex")) is not None: |
190 |
| - if not re.search(v, field, re.IGNORECASE): |
191 |
| - return False |
192 |
| - elif (field := parse_lookup(obj, path, "__iexact")) is not None: |
193 |
| - if field.lower() != v.lower(): |
194 |
| - return False |
195 |
| - elif ( |
196 |
| - field := parse_lookup(obj, path, "__exact") |
197 |
| - ) is not None: # same as else |
198 |
| - if field != v: |
199 |
| - return False |
200 |
| - elif ( |
201 |
| - field := parse_lookup(obj, path, "__eq") |
202 |
| - ) is not None: # same as else |
203 |
| - if field != v: |
204 |
| - return False |
205 |
| - else: |
206 |
| - if (field := keygetter(obj, path)) is not None: # same as else |
207 |
| - if field != v: |
208 |
| - return False |
| 213 | + if not LOOKUP_NAME_MAP[op](data, v): |
| 214 | + return False |
209 | 215 |
|
210 | 216 | return True
|
211 | 217 |
|
|
0 commit comments