Skip to content

Commit 6d0d2fd

Browse files
authored
docs,tests(query_list): Better object handling, docs, tests (#415)
Improve docs (examples), tests, and handling off nested values in objects.
2 parents 1efe871 + d98c54f commit 6d0d2fd

File tree

3 files changed

+169
-12
lines changed

3 files changed

+169
-12
lines changed

CHANGES

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,21 @@ $ pip install --user --upgrade --pre libvcs
1313

1414
- _Add your latest changes from PRs here_
1515

16+
## libvcs 0.16.3 (unreleased)
17+
18+
### Bug fixes
19+
20+
- `QueryList`: Fix lookups of objects (#415)
21+
1622
### Tests
1723

1824
- Basic pytest plugin test (#413)
25+
- Add test for object based lookups (#414)
26+
27+
### Documentation
28+
29+
- Improve doc examples / tests for `keygetter` and `QueryList` to show deep lookups for objects
30+
(#415)
1931

2032
## libvcs 0.16.2 (2022-09-11)
2133

src/libvcs/_internal/query_list.py

Lines changed: 151 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,22 +17,68 @@ def keygetter(
1717
obj: Mapping[str, Any],
1818
path: str,
1919
) -> Union[None, Any, str, list[str], Mapping[str, str]]:
20-
"""obj, "foods__breakfast", obj['foods']['breakfast']
20+
"""Fetch values in objects and keys, deeply.
2121
22-
>>> keygetter({ "foods": { "breakfast": "cereal" } }, "foods__breakfast")
23-
'cereal'
24-
>>> keygetter({ "foods": { "breakfast": "cereal" } }, "foods")
22+
**With dictionaries**:
23+
24+
>>> keygetter({ "menu": { "breakfast": "cereal" } }, "menu")
2525
{'breakfast': 'cereal'}
2626
27+
>>> keygetter({ "menu": { "breakfast": "cereal" } }, "menu__breakfast")
28+
'cereal'
29+
30+
**With objects**:
31+
32+
>>> from typing import Optional
33+
>>> from dataclasses import dataclass, field
34+
35+
>>> @dataclass()
36+
... class Menu:
37+
... fruit: list[str] = field(default_factory=list)
38+
... breakfast: Optional[str] = None
39+
40+
41+
>>> @dataclass()
42+
... class Restaurant:
43+
... place: str
44+
... city: str
45+
... state: str
46+
... menu: Menu = field(default_factory=Menu)
47+
48+
49+
>>> restaurant = Restaurant(
50+
... place="Largo",
51+
... city="Tampa",
52+
... state="Florida",
53+
... menu=Menu(
54+
... fruit=["banana", "orange"], breakfast="cereal"
55+
... )
56+
... )
57+
58+
>>> restaurant
59+
Restaurant(place='Largo',
60+
city='Tampa',
61+
state='Florida',
62+
menu=Menu(fruit=['banana', 'orange'], breakfast='cereal'))
63+
64+
>>> keygetter(restaurant, "menu")
65+
Menu(fruit=['banana', 'orange'], breakfast='cereal')
66+
67+
>>> keygetter(restaurant, "menu__breakfast")
68+
'cereal'
2769
"""
2870
try:
2971
sub_fields = path.split("__")
3072
dct = obj
3173
for sub_field in sub_fields:
32-
dct = dct[sub_field]
74+
if isinstance(dct, dict):
75+
dct = dct[sub_field]
76+
elif hasattr(dct, sub_field):
77+
dct = getattr(dct, sub_field)
3378
return dct
34-
except Exception:
79+
except Exception as e:
3580
traceback.print_stack()
81+
print(f"Above error was {e}")
3682
return None
3783

3884

@@ -41,10 +87,24 @@ def parse_lookup(obj: Mapping[str, Any], path: str, lookup: str) -> Optional[Any
4187
4288
If comparator not used or value not found, return None.
4389
44-
mykey__endswith("mykey") -> "mykey" else None
45-
4690
>>> parse_lookup({ "food": "red apple" }, "food__istartswith", "__istartswith")
4791
'red apple'
92+
93+
It can also look up objects:
94+
95+
>>> from dataclasses import dataclass
96+
97+
>>> @dataclass()
98+
... class Inventory:
99+
... food: str
100+
101+
>>> item = Inventory(food="red apple")
102+
103+
>>> item
104+
Inventory(food='red apple')
105+
106+
>>> parse_lookup(item, "food__istartswith", "__istartswith")
107+
'red apple'
48108
"""
49109
try:
50110
if isinstance(path, str) and isinstance(lookup, str) and path.endswith(lookup):
@@ -258,6 +318,89 @@ class QueryList(list[T]):
258318
'Elmhurst'
259319
>>> query.filter(foods__fruit__in="orange")[0]['city']
260320
'Tampa'
321+
322+
Examples of object lookups:
323+
324+
>>> from typing import Any
325+
>>> from dataclasses import dataclass, field
326+
327+
>>> @dataclass()
328+
... class Restaurant:
329+
... place: str
330+
... city: str
331+
... state: str
332+
... foods: dict[str, Any]
333+
334+
>>> restaurant = Restaurant(
335+
... place="Largo",
336+
... city="Tampa",
337+
... state="Florida",
338+
... foods={
339+
... "fruit": ["banana", "orange"], "breakfast": "cereal"
340+
... }
341+
... )
342+
343+
>>> restaurant
344+
Restaurant(place='Largo',
345+
city='Tampa',
346+
state='Florida',
347+
foods={'fruit': ['banana', 'orange'], 'breakfast': 'cereal'})
348+
349+
>>> query = QueryList([restaurant])
350+
351+
>>> query.filter(foods__fruit__in="banana")
352+
[Restaurant(place='Largo',
353+
city='Tampa',
354+
state='Florida',
355+
foods={'fruit': ['banana', 'orange'], 'breakfast': 'cereal'})]
356+
357+
>>> query.filter(foods__fruit__in="banana")[0].city
358+
'Tampa'
359+
360+
Example of deeper object lookups:
361+
362+
>>> from typing import Optional
363+
>>> from dataclasses import dataclass, field
364+
365+
>>> @dataclass()
366+
... class Menu:
367+
... fruit: list[str] = field(default_factory=list)
368+
... breakfast: Optional[str] = None
369+
370+
371+
>>> @dataclass()
372+
... class Restaurant:
373+
... place: str
374+
... city: str
375+
... state: str
376+
... menu: Menu = field(default_factory=Menu)
377+
378+
379+
>>> restaurant = Restaurant(
380+
... place="Largo",
381+
... city="Tampa",
382+
... state="Florida",
383+
... menu=Menu(
384+
... fruit=["banana", "orange"], breakfast="cereal"
385+
... )
386+
... )
387+
388+
>>> restaurant
389+
Restaurant(place='Largo',
390+
city='Tampa',
391+
state='Florida',
392+
menu=Menu(fruit=['banana', 'orange'], breakfast='cereal'))
393+
394+
>>> query = QueryList([restaurant])
395+
396+
>>> query.filter(menu__fruit__in="banana")
397+
[Restaurant(place='Largo',
398+
city='Tampa',
399+
state='Florida',
400+
menu=Menu(fruit=['banana', 'orange'], breakfast='cereal'))]
401+
402+
>>> query.filter(menu__fruit__in="banana")[0].city
403+
'Tampa'
261404
"""
262405

263406
data: Sequence[T]

tests/_internal/test_query_list.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,16 @@ class Obj:
1616
"items,filter_expr,expected_result",
1717
[
1818
[[Obj(test=1)], None, [Obj(test=1)]],
19-
[[{"test": 1}], None, [{"test": 1}]],
20-
[[{"test": 1}], None, QueryList([{"test": 1}])],
21-
[[{"fruit": "apple"}], None, QueryList([{"fruit": "apple"}])],
19+
[[Obj(test=1)], dict(test=1), [Obj(test=1)]],
20+
[[Obj(test=1)], dict(test=2), []],
2221
[
2322
[Obj(test=2, fruit=["apple"])],
24-
None,
23+
dict(fruit__in="apple"),
2524
QueryList([Obj(test=2, fruit=["apple"])]),
2625
],
26+
[[{"test": 1}], None, [{"test": 1}]],
27+
[[{"test": 1}], None, QueryList([{"test": 1}])],
28+
[[{"fruit": "apple"}], None, QueryList([{"fruit": "apple"}])],
2729
[
2830
[{"fruit": "apple", "banana": object()}],
2931
None,

0 commit comments

Comments
 (0)