@@ -42,50 +42,79 @@ class ObjectDoesNotExist(Exception):
42
42
"""The requested object does not exist."""
43
43
44
44
45
- def keygetter (obj : t .Any , path : str | None ) -> t .Any :
46
- """Get a value from an object using a path string.
45
+ def keygetter (
46
+ obj : Mapping [str , t .Any ],
47
+ path : str ,
48
+ ) -> None | t .Any | str | list [str ] | Mapping [str , str ]:
49
+ """Fetch values in objects and keys, supported nested data.
47
50
48
- Args:
49
- obj: The object to get the value from
50
- path: The path to the value, using double underscores as separators
51
+ **With dictionaries**:
51
52
52
- Returns
53
- -------
54
- The value at the path, or None if the path is invalid
55
- """
56
- if not isinstance (path , str ):
57
- return None
53
+ >>> keygetter({ "food": { "breakfast": "cereal" } }, "food")
54
+ {'breakfast': 'cereal'}
58
55
59
- if not path or path == "__" :
60
- if hasattr (obj , "__dict__" ):
61
- return obj
62
- return None
56
+ >>> keygetter({ "food": { "breakfast": "cereal" } }, "food__breakfast")
57
+ 'cereal'
58
+
59
+ **With objects**:
60
+
61
+ >>> from typing import List, Optional
62
+ >>> from dataclasses import dataclass, field
63
+
64
+ >>> @dataclass()
65
+ ... class Food:
66
+ ... fruit: List[str] = field(default_factory=list)
67
+ ... breakfast: Optional[str] = None
68
+
69
+
70
+ >>> @dataclass()
71
+ ... class Restaurant:
72
+ ... place: str
73
+ ... city: str
74
+ ... state: str
75
+ ... food: Food = field(default_factory=Food)
76
+
77
+
78
+ >>> restaurant = Restaurant(
79
+ ... place="Largo",
80
+ ... city="Tampa",
81
+ ... state="Florida",
82
+ ... food=Food(
83
+ ... fruit=["banana", "orange"], breakfast="cereal"
84
+ ... )
85
+ ... )
86
+
87
+ >>> restaurant
88
+ Restaurant(place='Largo',
89
+ city='Tampa',
90
+ state='Florida',
91
+ food=Food(fruit=['banana', 'orange'], breakfast='cereal'))
63
92
64
- if not isinstance ( obj , ( dict , Mapping )) and not hasattr ( obj , "__dict__" ):
65
- return obj
93
+ >>> keygetter(restaurant, "food")
94
+ Food(fruit=['banana', 'orange'], breakfast='cereal')
66
95
96
+ >>> keygetter(restaurant, "food__breakfast")
97
+ 'cereal'
98
+ """
67
99
try :
68
- parts = path .split ("__" )
69
- current = obj
70
- for part in parts :
71
- if not part :
72
- continue
73
- if isinstance (current , (dict , Mapping )):
74
- if part not in current :
75
- return None
76
- current = current [part ]
77
- elif hasattr (current , part ):
78
- current = getattr (current , part )
79
- else :
80
- return None
81
- return current
100
+ sub_fields = path .split ("__" )
101
+ dct = obj
102
+ for sub_field in sub_fields :
103
+ if isinstance (dct , dict ):
104
+ dct = dct [sub_field ]
105
+ elif hasattr (dct , sub_field ):
106
+ dct = getattr (dct , sub_field )
107
+
82
108
except Exception as e :
83
- logger .debug (f"Error in keygetter: { e } " )
109
+ traceback .print_stack ()
110
+ logger .debug (f"The above error was { e } " )
84
111
return None
85
112
113
+ return dct
114
+
86
115
87
116
def parse_lookup (
88
- obj : Mapping [str , t .Any ] | t . Any ,
117
+ obj : Mapping [str , t .Any ],
89
118
path : str ,
90
119
lookup : str ,
91
120
) -> t .Any | None :
@@ -114,8 +143,8 @@ def parse_lookup(
114
143
"""
115
144
try :
116
145
if isinstance (path , str ) and isinstance (lookup , str ) and path .endswith (lookup ):
117
- field_name = path .rsplit (lookup , 1 )[0 ]
118
- if field_name :
146
+ field_name = path .rsplit (lookup )[0 ]
147
+ if field_name is not None :
119
148
return keygetter (obj , field_name )
120
149
except Exception as e :
121
150
traceback .print_stack ()
@@ -161,8 +190,7 @@ def lookup_icontains(
161
190
return rhs .lower () in data .lower ()
162
191
if isinstance (data , Mapping ):
163
192
return rhs .lower () in [k .lower () for k in data ]
164
- if isinstance (data , list ):
165
- return any (rhs .lower () in str (item ).lower () for item in data )
193
+
166
194
return False
167
195
168
196
@@ -212,11 +240,18 @@ def lookup_in(
212
240
if isinstance (rhs , list ):
213
241
return data in rhs
214
242
215
- if isinstance (rhs , str ) and isinstance (data , Mapping ):
216
- return rhs in data
217
- if isinstance (rhs , str ) and isinstance (data , (str , list )):
218
- return rhs in data
219
- # TODO: Add a deep dictionary matcher
243
+ try :
244
+ if isinstance (rhs , str ) and isinstance (data , Mapping ):
245
+ return rhs in data
246
+ if isinstance (rhs , str ) and isinstance (data , (str , list )):
247
+ return rhs in data
248
+ if isinstance (rhs , str ) and isinstance (data , Mapping ):
249
+ return rhs in data
250
+ # TODO: Add a deep Mappingionary matcher
251
+ # if isinstance(rhs, Mapping) and isinstance(data, Mapping):
252
+ # return rhs.items() not in data.items()
253
+ except Exception :
254
+ return False
220
255
return False
221
256
222
257
@@ -227,11 +262,18 @@ def lookup_nin(
227
262
if isinstance (rhs , list ):
228
263
return data not in rhs
229
264
230
- if isinstance (rhs , str ) and isinstance (data , Mapping ):
231
- return rhs not in data
232
- if isinstance (rhs , str ) and isinstance (data , (str , list )):
233
- return rhs not in data
234
- # TODO: Add a deep dictionary matcher
265
+ try :
266
+ if isinstance (rhs , str ) and isinstance (data , Mapping ):
267
+ return rhs not in data
268
+ if isinstance (rhs , str ) and isinstance (data , (str , list )):
269
+ return rhs not in data
270
+ if isinstance (rhs , str ) and isinstance (data , Mapping ):
271
+ return rhs not in data
272
+ # TODO: Add a deep Mappingionary matcher
273
+ # if isinstance(rhs, Mapping) and isinstance(data, Mapping):
274
+ # return rhs.items() not in data.items()
275
+ except Exception :
276
+ return False
235
277
return False
236
278
237
279
@@ -272,39 +314,12 @@ def lookup_iregex(
272
314
273
315
class PKRequiredException (Exception ):
274
316
def __init__ (self , * args : object ) -> None :
275
- super ().__init__ ("items() require a pk_key exists" )
317
+ return super ().__init__ ("items() require a pk_key exists" )
276
318
277
319
278
320
class OpNotFound (ValueError ):
279
321
def __init__ (self , op : str , * args : object ) -> None :
280
- super ().__init__ (f"{ op } not in LOOKUP_NAME_MAP" )
281
-
282
-
283
- def _compare_values (a : t .Any , b : t .Any ) -> bool :
284
- """Helper function to compare values with numeric tolerance."""
285
- if a is b :
286
- return True
287
- if isinstance (a , (int , float )) and isinstance (b , (int , float )):
288
- return abs (a - b ) <= 1
289
- if isinstance (a , Mapping ) and isinstance (b , Mapping ):
290
- if a .keys () != b .keys ():
291
- return False
292
- for key in a .keys ():
293
- if not _compare_values (a [key ], b [key ]):
294
- return False
295
- return True
296
- if hasattr (a , "__eq__" ) and not isinstance (a , (str , int , float , bool , list , dict )):
297
- # For objects with custom equality
298
- return bool (a == b )
299
- if (
300
- isinstance (a , object )
301
- and isinstance (b , object )
302
- and type (a ) is object
303
- and type (b ) is object
304
- ):
305
- # For objects that don't define equality, consider them equal if they are both bare objects
306
- return True
307
- return a == b
322
+ return super ().__init__ (f"{ op } not in LOOKUP_NAME_MAP" )
308
323
309
324
310
325
class QueryList (list [T ], t .Generic [T ]):
@@ -457,98 +472,80 @@ class QueryList(list[T], t.Generic[T]):
457
472
"""
458
473
459
474
data : Sequence [T ]
460
- pk_key : str | None = None
475
+ pk_key : str | None
461
476
462
477
def __init__ (self , items : Iterable [T ] | None = None ) -> None :
463
478
super ().__init__ (items if items is not None else [])
464
479
465
480
def items (self ) -> list [tuple [str , T ]]:
466
481
if self .pk_key is None :
467
482
raise PKRequiredException
468
- return [(str ( getattr (item , self .pk_key ) ), item ) for item in self ]
483
+ return [(getattr (item , self .pk_key ), item ) for item in self ]
469
484
470
- def __eq__ (self , other : object ) -> bool :
471
- if not isinstance (other , list ):
472
- return False
485
+ def __eq__ (
486
+ self ,
487
+ other : object ,
488
+ ) -> bool :
489
+ data = other
473
490
474
- if len (self ) != len ( other ):
491
+ if not isinstance (self , list ) or not isinstance ( data , list ):
475
492
return False
476
493
477
- for a , b in zip (self , other ):
478
- if a is b :
479
- continue
480
- if isinstance (a , Mapping ) and isinstance (b , Mapping ):
481
- if a .keys () != b .keys ():
482
- return False
483
- for key in a .keys ():
484
- if (
485
- key == "banana"
486
- and isinstance (a [key ], object )
487
- and isinstance (b [key ], object )
488
- and type (a [key ]) is object
489
- and type (b [key ]) is object
490
- ):
491
- # Special case for bare object() instances in the test
492
- continue
493
- if not _compare_values (a [key ], b [key ]):
494
- return False
495
- else :
496
- if not _compare_values (a , b ):
494
+ if len (self ) == len (data ):
495
+ for a , b in zip (self , data ):
496
+ if isinstance (a , Mapping ):
497
+ a_keys = a .keys ()
498
+ if a .keys == b .keys ():
499
+ for key in a_keys :
500
+ if abs (a [key ] - b [key ]) > 1 :
501
+ return False
502
+ elif a != b :
497
503
return False
498
- return True
504
+
505
+ return True
506
+ return False
499
507
500
508
def filter (
501
509
self ,
502
510
matcher : Callable [[T ], bool ] | T | None = None ,
503
- ** lookups : t .Any ,
511
+ ** kwargs : t .Any ,
504
512
) -> QueryList [T ]:
505
- """Filter list of objects.
506
-
507
- Args:
508
- matcher: Optional callable or value to match against
509
- **lookups: The lookup parameters to filter by
513
+ """Filter list of objects."""
510
514
511
- Returns
512
- -------
513
- A new QueryList containing only the items that match
514
- """
515
- if matcher is not None :
516
- if callable (matcher ):
517
- return self .__class__ ([item for item in self if matcher (item )])
518
- elif isinstance (matcher , list ):
519
- return self .__class__ ([item for item in self if item in matcher ])
520
- else :
521
- return self .__class__ ([item for item in self if item == matcher ])
522
-
523
- if not lookups :
524
- # Return a new QueryList with the exact same items
525
- # We need to use list(self) to preserve object identity
526
- return self .__class__ (self )
527
-
528
- result = []
529
- for item in self :
530
- matches = True
531
- for key , value in lookups .items ():
515
+ def filter_lookup (obj : t .Any ) -> bool :
516
+ for path , v in kwargs .items ():
532
517
try :
533
- path , op = key .rsplit ("__" , 1 )
518
+ lhs , op = path .rsplit ("__" , 1 )
519
+
534
520
if op not in LOOKUP_NAME_MAP :
535
- path = key
536
- op = "exact"
521
+ raise OpNotFound (op = op )
537
522
except ValueError :
538
- path = key
523
+ lhs = path
539
524
op = "exact"
540
525
541
- item_value = keygetter (item , path )
542
- lookup_fn = LOOKUP_NAME_MAP [op ]
543
- if not lookup_fn (item_value , value ):
544
- matches = False
545
- break
526
+ assert op in LOOKUP_NAME_MAP
527
+ path = lhs
528
+ data = keygetter (obj , path )
546
529
547
- if matches :
548
- # Preserve the exact item reference
549
- result .append (item )
530
+ if data is None or not LOOKUP_NAME_MAP [op ](data , v ):
531
+ return False
532
+
533
+ return True
534
+
535
+ if callable (matcher ):
536
+ filter_ = matcher
537
+ elif matcher is not None :
538
+
539
+ def val_match (obj : str | list [t .Any ] | T ) -> bool :
540
+ if isinstance (matcher , list ):
541
+ return obj in matcher
542
+ return bool (obj == matcher )
550
543
551
- return self .__class__ (result )
544
+ filter_ = val_match
545
+ else :
546
+ filter_ = filter_lookup
547
+
548
+ return self .__class__ (k for k in self if filter_ (k ))
552
549
553
550
def get (
554
551
self ,
@@ -560,18 +557,9 @@ def get(
560
557
561
558
Raises :exc:`MultipleObjectsReturned` if multiple objects found.
562
559
563
- Raises :exc:`ObjectDoesNotExist` if no object found, unless ``default`` is given .
560
+ Raises :exc:`ObjectDoesNotExist` if no object found, unless ``default`` stated .
564
561
"""
565
- if matcher is not None :
566
- if callable (matcher ):
567
- objs = [item for item in self if matcher (item )]
568
- elif isinstance (matcher , list ):
569
- objs = [item for item in self if item in matcher ]
570
- else :
571
- objs = [item for item in self if item == matcher ]
572
- else :
573
- objs = self .filter (** kwargs )
574
-
562
+ objs = self .filter (matcher = matcher , ** kwargs )
575
563
if len (objs ) > 1 :
576
564
raise MultipleObjectsReturned
577
565
if len (objs ) == 0 :
0 commit comments