7
7
import re
8
8
import traceback
9
9
import typing as t
10
- from collections .abc import Mapping , Sequence
10
+ from collections .abc import Iterable , Mapping , Sequence
11
11
12
12
if t .TYPE_CHECKING :
13
13
@@ -23,7 +23,7 @@ def __call__(
23
23
...
24
24
25
25
26
- T = t .TypeVar ("T" , t . Any , t . Any )
26
+ T = t .TypeVar ("T" )
27
27
28
28
no_arg = object ()
29
29
@@ -40,13 +40,55 @@ def keygetter(
40
40
obj : "Mapping[str, t.Any]" ,
41
41
path : str ,
42
42
) -> t .Union [None , t .Any , str , t .List [str ], "Mapping[str, str]" ]:
43
- """obj, "foods__breakfast", obj['foods']['breakfast'] .
43
+ """Fetch values in objects and keys, supported nested data .
44
44
45
- >>> keygetter({ "foods": { "breakfast": "cereal" } }, "foods__breakfast")
46
- 'cereal'
47
- >>> keygetter({ "foods ": { "breakfast": "cereal" } }, "foods ")
45
+ **With dictionaries**:
46
+
47
+ >>> keygetter({ "food ": { "breakfast": "cereal" } }, "food ")
48
48
{'breakfast': 'cereal'}
49
49
50
+ >>> keygetter({ "food": { "breakfast": "cereal" } }, "food__breakfast")
51
+ 'cereal'
52
+
53
+ **With objects**:
54
+
55
+ >>> from typing import List, Optional
56
+ >>> from dataclasses import dataclass, field
57
+
58
+ >>> @dataclass()
59
+ ... class Food:
60
+ ... fruit: List[str] = field(default_factory=list)
61
+ ... breakfast: Optional[str] = None
62
+
63
+
64
+ >>> @dataclass()
65
+ ... class Restaurant:
66
+ ... place: str
67
+ ... city: str
68
+ ... state: str
69
+ ... food: Food = field(default_factory=Food)
70
+
71
+
72
+ >>> restaurant = Restaurant(
73
+ ... place="Largo",
74
+ ... city="Tampa",
75
+ ... state="Florida",
76
+ ... food=Food(
77
+ ... fruit=["banana", "orange"], breakfast="cereal"
78
+ ... )
79
+ ... )
80
+
81
+ >>> restaurant
82
+ Restaurant(place='Largo',
83
+ city='Tampa',
84
+ state='Florida',
85
+ food=Food(fruit=['banana', 'orange'], breakfast='cereal'))
86
+
87
+ >>> keygetter(restaurant, "food")
88
+ Food(fruit=['banana', 'orange'], breakfast='cereal')
89
+
90
+ >>> keygetter(restaurant, "food__breakfast")
91
+ 'cereal'
50
92
"""
51
93
try :
52
94
sub_fields = path .split ("__" )
@@ -74,10 +116,24 @@ def parse_lookup(
74
116
75
117
If comparator not used or value not found, return None.
76
118
77
- mykey__endswith("mykey") -> "mykey" else None
78
-
79
119
>>> parse_lookup({ "food": "red apple" }, "food__istartswith", "__istartswith")
80
120
'red apple'
121
+
122
+ It can also look up objects:
123
+
124
+ >>> from dataclasses import dataclass
125
+
126
+ >>> @dataclass()
127
+ ... class Inventory:
128
+ ... food: str
129
+
130
+ >>> item = Inventory(food="red apple")
131
+
132
+ >>> item
133
+ Inventory(food='red apple')
134
+
135
+ >>> parse_lookup(item, "food__istartswith", "__istartswith")
136
+ 'red apple'
81
137
"""
82
138
try :
83
139
if isinstance (path , str ) and isinstance (lookup , str ) and path .endswith (lookup ):
@@ -259,11 +315,13 @@ def __init__(self, op: str, *args: object):
259
315
return super ().__init__ (f"{ op } not in LOOKUP_NAME_MAP" )
260
316
261
317
262
- class QueryList (t .List [T ]):
318
+ class QueryList (t .Generic [ T ], t . List [T ]):
263
319
"""Filter list of object/dictionaries. For small, local datasets.
264
320
265
321
*Experimental, unstable*.
266
322
323
+ **With dictionaries**:
324
+
267
325
>>> query = QueryList(
268
326
... [
269
327
... {
@@ -280,6 +338,7 @@ class QueryList(t.List[T]):
280
338
... },
281
339
... ]
282
340
... )
341
+
283
342
>>> query.filter(place="Chicago suburbs")[0]['city']
284
343
'Elmhurst'
285
344
>>> query.filter(place__icontains="chicago")[0]['city']
@@ -290,27 +349,135 @@ class QueryList(t.List[T]):
290
349
'Elmhurst'
291
350
>>> query.filter(foods__fruit__in="orange")[0]['city']
292
351
'Tampa'
293
- >>> query.get(foods__fruit__in="orange")['city']
352
+
353
+ >>> query.filter(foods__fruit__in="apple")
354
+ [{'place': 'Chicago suburbs',
355
+ 'city': 'Elmhurst',
356
+ 'state': 'Illinois',
357
+ 'foods':
358
+ {'fruit': ['apple', 'cantelope'], 'breakfast': 'waffles'}}]
359
+
360
+ >>> query.filter(foods__fruit__in="non_existent")
361
+ []
362
+
363
+ **With objects**:
364
+
365
+ >>> from typing import Any, Dict
366
+ >>> from dataclasses import dataclass, field
367
+
368
+ >>> @dataclass()
369
+ ... class Restaurant:
370
+ ... place: str
371
+ ... city: str
372
+ ... state: str
373
+ ... foods: Dict[str, Any]
374
+
375
+ >>> restaurant = Restaurant(
376
+ ... place="Largo",
377
+ ... city="Tampa",
378
+ ... state="Florida",
379
+ ... foods={
380
+ ... "fruit": ["banana", "orange"], "breakfast": "cereal"
381
+ ... }
382
+ ... )
383
+
384
+ >>> restaurant
385
+ Restaurant(place='Largo',
386
+ city='Tampa',
387
+ state='Florida',
388
+ foods={'fruit': ['banana', 'orange'], 'breakfast': 'cereal'})
389
+
390
+ >>> query = QueryList([restaurant])
391
+
392
+ >>> query.filter(foods__fruit__in="banana")
393
+ [Restaurant(place='Largo',
394
+ city='Tampa',
395
+ state='Florida',
396
+ foods={'fruit': ['banana', 'orange'], 'breakfast': 'cereal'})]
397
+
398
+ >>> query.filter(foods__fruit__in="banana")[0].city
294
399
'Tampa'
400
+
401
+ >>> query.get(foods__fruit__in="banana").city
402
+ 'Tampa'
403
+
404
+ **With objects (nested)**:
405
+
406
+ >>> from typing import List, Optional
407
+ >>> from dataclasses import dataclass, field
408
+
409
+ >>> @dataclass()
410
+ ... class Food:
411
+ ... fruit: List[str] = field(default_factory=list)
412
+ ... breakfast: Optional[str] = None
413
+
414
+
415
+ >>> @dataclass()
416
+ ... class Restaurant:
417
+ ... place: str
418
+ ... city: str
419
+ ... state: str
420
+ ... food: Food = field(default_factory=Food)
421
+
422
+
423
+ >>> query = QueryList([
424
+ ... Restaurant(
425
+ ... place="Largo",
426
+ ... city="Tampa",
427
+ ... state="Florida",
428
+ ... food=Food(
429
+ ... fruit=["banana", "orange"], breakfast="cereal"
430
+ ... )
431
+ ... ),
432
+ ... Restaurant(
433
+ ... place="Chicago suburbs",
434
+ ... city="Elmhurst",
435
+ ... state="Illinois",
436
+ ... food=Food(
437
+ ... fruit=["apple", "cantelope"], breakfast="waffles"
438
+ ... )
439
+ ... )
440
+ ... ])
441
+
442
+ >>> query.filter(food__fruit__in="banana")
443
+ [Restaurant(place='Largo',
444
+ city='Tampa',
445
+ state='Florida',
446
+ food=Food(fruit=['banana', 'orange'], breakfast='cereal'))]
447
+
448
+ >>> query.filter(food__fruit__in="banana")[0].city
449
+ 'Tampa'
450
+
451
+ >>> query.get(food__fruit__in="banana").city
452
+ 'Tampa'
453
+
454
+ >>> query.filter(food__breakfast="waffles")
455
+ [Restaurant(place='Chicago suburbs',
456
+ city='Elmhurst',
457
+ state='Illinois',
458
+ food=Food(fruit=['apple', 'cantelope'], breakfast='waffles'))]
459
+
460
+ >>> query.filter(food__breakfast="waffles")[0].city
461
+ 'Elmhurst'
462
+
463
+ >>> query.filter(food__breakfast="non_existent")
464
+ []
295
465
"""
296
466
297
467
data : "Sequence[T]"
298
468
pk_key : t .Optional [str ]
299
469
300
- def items (self ) -> t .List [T ]:
470
+ def __init__ (self , items : t .Optional ["Iterable[T]" ] = None ) -> None :
471
+ super ().__init__ (items if items is not None else [])
472
+
473
+ def items (self ) -> t .List [t .Tuple [str , T ]]:
301
474
if self .pk_key is None :
302
475
raise PKRequiredException ()
303
476
return [(getattr (item , self .pk_key ), item ) for item in self ]
304
477
305
478
def __eq__ (
306
479
self ,
307
480
other : object ,
308
- # other: t.Union[
309
- # "QueryList[T]",
310
- # t.List[Mapping[str, str]],
311
- # t.List[Mapping[str, int]],
312
- # t.List[Mapping[str, t.Union[str, Mapping[str, t.Union[List[str], str]]]]],
313
- # ],
314
481
) -> bool :
315
482
data = other
316
483
@@ -363,7 +530,7 @@ def filter_lookup(obj: t.Any) -> bool:
363
530
_filter = matcher
364
531
elif matcher is not None :
365
532
366
- def val_match (obj : t .Union [str , t .List [t .Any ]]) -> bool :
533
+ def val_match (obj : t .Union [str , t .List [t .Any ], T ]) -> bool :
367
534
if isinstance (matcher , list ):
368
535
return obj in matcher
369
536
else :
0 commit comments