77import re
88import traceback
99import typing as t
10- from collections .abc import Mapping , Sequence
10+ from collections .abc import Iterable , Mapping , Sequence
1111
1212if t .TYPE_CHECKING :
1313
@@ -23,7 +23,7 @@ def __call__(
2323 ...
2424
2525
26- T = t .TypeVar ("T" , t . Any , t . Any )
26+ T = t .TypeVar ("T" )
2727
2828no_arg = object ()
2929
@@ -40,13 +40,55 @@ def keygetter(
4040 obj : "Mapping[str, t.Any]" ,
4141 path : str ,
4242) -> 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 .
4444
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 ")
4848 {'breakfast': 'cereal'}
4949
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'
5092 """
5193 try :
5294 sub_fields = path .split ("__" )
@@ -74,10 +116,24 @@ def parse_lookup(
74116
75117 If comparator not used or value not found, return None.
76118
77- mykey__endswith("mykey") -> "mykey" else None
78-
79119 >>> parse_lookup({ "food": "red apple" }, "food__istartswith", "__istartswith")
80120 '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'
81137 """
82138 try :
83139 if isinstance (path , str ) and isinstance (lookup , str ) and path .endswith (lookup ):
@@ -259,11 +315,13 @@ def __init__(self, op: str, *args: object):
259315 return super ().__init__ (f"{ op } not in LOOKUP_NAME_MAP" )
260316
261317
262- class QueryList (t .List [T ]):
318+ class QueryList (t .Generic [ T ], t . List [T ]):
263319 """Filter list of object/dictionaries. For small, local datasets.
264320
265321 *Experimental, unstable*.
266322
323+ **With dictionaries**:
324+
267325 >>> query = QueryList(
268326 ... [
269327 ... {
@@ -280,6 +338,7 @@ class QueryList(t.List[T]):
280338 ... },
281339 ... ]
282340 ... )
341+
283342 >>> query.filter(place="Chicago suburbs")[0]['city']
284343 'Elmhurst'
285344 >>> query.filter(place__icontains="chicago")[0]['city']
@@ -290,27 +349,135 @@ class QueryList(t.List[T]):
290349 'Elmhurst'
291350 >>> query.filter(foods__fruit__in="orange")[0]['city']
292351 '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
294399 '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+ []
295465 """
296466
297467 data : "Sequence[T]"
298468 pk_key : t .Optional [str ]
299469
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 ]]:
301474 if self .pk_key is None :
302475 raise PKRequiredException ()
303476 return [(getattr (item , self .pk_key ), item ) for item in self ]
304477
305478 def __eq__ (
306479 self ,
307480 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- # ],
314481 ) -> bool :
315482 data = other
316483
@@ -363,7 +530,7 @@ def filter_lookup(obj: t.Any) -> bool:
363530 _filter = matcher
364531 elif matcher is not None :
365532
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 :
367534 if isinstance (matcher , list ):
368535 return obj in matcher
369536 else :
0 commit comments