@@ -42,50 +42,79 @@ class ObjectDoesNotExist(Exception):
4242    """The requested object does not exist.""" 
4343
4444
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. 
4750
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**: 
5152
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'} 
5855
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')) 
6392
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')  
6695
96+     >>> keygetter(restaurant, "food__breakfast") 
97+     'cereal' 
98+     """ 
6799    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+ 
82108    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 }  )
84111        return  None 
85112
113+     return  dct 
114+ 
86115
87116def  parse_lookup (
88-     obj : Mapping [str , t .Any ]  |   t . Any ,
117+     obj : Mapping [str , t .Any ],
89118    path : str ,
90119    lookup : str ,
91120) ->  t .Any  |  None :
@@ -114,8 +143,8 @@ def parse_lookup(
114143    """ 
115144    try :
116145        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 :
119148                return  keygetter (obj , field_name )
120149    except  Exception  as  e :
121150        traceback .print_stack ()
@@ -161,8 +190,7 @@ def lookup_icontains(
161190        return  rhs .lower () in  data .lower ()
162191    if  isinstance (data , Mapping ):
163192        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+ 
166194    return  False 
167195
168196
@@ -212,11 +240,18 @@ def lookup_in(
212240    if  isinstance (rhs , list ):
213241        return  data  in  rhs 
214242
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 
220255    return  False 
221256
222257
@@ -227,11 +262,18 @@ def lookup_nin(
227262    if  isinstance (rhs , list ):
228263        return  data  not  in rhs 
229264
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 
235277    return  False 
236278
237279
@@ -272,39 +314,12 @@ def lookup_iregex(
272314
273315class  PKRequiredException (Exception ):
274316    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" )
276318
277319
278320class  OpNotFound (ValueError ):
279321    def  __init__ (self , op : str , * args : object ) ->  None :
280-         super ().__init__ (f"{ op }  )
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 }  )
308323
309324
310325class  QueryList (list [T ], t .Generic [T ]):
@@ -457,98 +472,80 @@ class QueryList(list[T], t.Generic[T]):
457472    """ 
458473
459474    data : Sequence [T ]
460-     pk_key : str  |  None   =   None 
475+     pk_key : str  |  None 
461476
462477    def  __init__ (self , items : Iterable [T ] |  None  =  None ) ->  None :
463478        super ().__init__ (items  if  items  is  not None  else  [])
464479
465480    def  items (self ) ->  list [tuple [str , T ]]:
466481        if  self .pk_key  is  None :
467482            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 ]
469484
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 
473490
474-         if  len (self )  !=   len ( other ):
491+         if  not   isinstance (self ,  list )  or   not   isinstance ( data ,  list ):
475492            return  False 
476493
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 :
497503                    return  False 
498-         return  True 
504+ 
505+             return  True 
506+         return  False 
499507
500508    def  filter (
501509        self ,
502510        matcher : Callable [[T ], bool ] |  T  |  None  =  None ,
503-         ** lookups : t .Any ,
511+         ** kwargs : t .Any ,
504512    ) ->  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.""" 
510514
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 ():
532517                try :
533-                     path , op  =  key .rsplit ("__" , 1 )
518+                     lhs , op  =  path .rsplit ("__" , 1 )
519+ 
534520                    if  op  not  in LOOKUP_NAME_MAP :
535-                         path  =  key 
536-                         op  =  "exact" 
521+                         raise  OpNotFound (op = op )
537522                except  ValueError :
538-                     path  =  key 
523+                     lhs  =  path 
539524                    op  =  "exact" 
540525
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 )
546529
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 )
550543
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 ))
552549
553550    def  get (
554551        self ,
@@ -560,18 +557,9 @@ def get(
560557
561558        Raises :exc:`MultipleObjectsReturned` if multiple objects found. 
562559
563-         Raises :exc:`ObjectDoesNotExist` if no object found, unless ``default`` is given . 
560+         Raises :exc:`ObjectDoesNotExist` if no object found, unless ``default`` stated . 
564561        """ 
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 )
575563        if  len (objs ) >  1 :
576564            raise  MultipleObjectsReturned 
577565        if  len (objs ) ==  0 :
0 commit comments