Skip to content

Commit 4cfd56e

Browse files
committed
feat: QueryList: Filter lists of dictionaries w/ nested support
1 parent 03c9300 commit 4cfd56e

File tree

2 files changed

+27
-13
lines changed

2 files changed

+27
-13
lines changed

libvcs/utils/query_list.py

+26-12
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1+
import dataclasses
12
import re
23
import traceback
3-
from typing import Any, Callable, Optional, Protocol, Sequence, TypeVar, Union
4+
from typing import Any, Callable, Generic, Optional, Protocol, Sequence, TypeVar, Union
45

56
T = TypeVar("T", Any, Any)
67

@@ -120,9 +121,13 @@ def lookup_iregex(data, rhs):
120121
}
121122

122123

123-
class QueryList(list[T]):
124+
@dataclasses.dataclass(eq=False)
125+
class QueryList(Generic[T]):
124126
"""Filter list of object/dicts. For small, local datasets. *Experimental, unstable*.
125127
128+
:py:func:`dataclasses.dataclass` is only used for ``__repr__`` and pytest comparison
129+
details.
130+
126131
>>> query = QueryList(
127132
... [
128133
... {
@@ -139,35 +144,44 @@ class QueryList(list[T]):
139144
... },
140145
... ]
141146
... )
142-
>>> query.filter(place="Chicago suburbs")[0]['city']
147+
>>> query.filter(place="Chicago suburbs").data[0]['city']
143148
'Elmhurst'
144-
>>> query.filter(place__icontains="chicago")[0]['city']
149+
>>> query.filter(place__icontains="chicago").data[0]['city']
145150
'Elmhurst'
146-
>>> query.filter(foods__breakfast="waffles")[0]['city']
151+
>>> query.filter(foods__breakfast="waffles").data[0]['city']
147152
'Elmhurst'
148-
>>> query.filter(foods__fruit__in="cantelope")[0]['city']
153+
>>> query.filter(foods__fruit__in="cantelope").data[0]['city']
149154
'Elmhurst'
150-
>>> query.filter(foods__fruit__in="orange")[0]['city']
155+
>>> query.filter(foods__fruit__in="orange").data[0]['city']
151156
'Tampa'
152157
"""
153158

159+
__slots__ = ("data", "pk_key")
154160
data: Sequence[T]
155161

162+
# def __init__(self, data, pk_key: Optional[str] = None):
163+
# self.data: Sequence[T] = data
164+
# #: Primary key for objects, optional.
165+
# #: Use for .get(), .items()
166+
# self.pk_key: Optional[Any] = pk_key
167+
156168
def items(self):
157169
data: Sequence[T]
158170

159171
if self.pk_key is None:
160172
raise Exception("items() require a pk_key exists")
161-
return [(getattr(item, self.pk_key), item) for item in self]
173+
return [(getattr(item, self.pk_key), item) for item in self.data]
162174

163175
def __eq__(self, other):
164176
data = other
177+
if hasattr(data, "data"):
178+
data = getattr(data, "data")
165179

166-
if not isinstance(self, list) or not isinstance(data, list):
180+
if not isinstance(self.data, list) or not isinstance(data, list):
167181
return False
168182

169-
if len(self) == len(data):
170-
for (a, b) in zip(self, data):
183+
if len(self.data) == len(data):
184+
for (a, b) in zip(self.data, data):
171185
if isinstance(a, dict):
172186
a_keys = a.keys()
173187
if a.keys == b.keys():
@@ -216,4 +230,4 @@ def val_match(obj):
216230
else:
217231
_filter = filter_lookup
218232

219-
return self.__class__(k for k in self if _filter(k))
233+
return self.__class__(data=[k for k in self.data if _filter(k)])

tests/utils/test_query_list.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,7 @@
230230
],
231231
)
232232
def test_filter(items: list, filter_expr: Optional[dict], expected_result: list):
233-
qs = QueryList(items)
233+
qs = QueryList(data=items)
234234
if filter_expr is not None:
235235
if isinstance(filter_expr, dict):
236236
assert qs.filter(**filter_expr) == expected_result

0 commit comments

Comments
 (0)