Skip to content

Commit a415bd6

Browse files
committed
refactor(types): Strict mypy types
1 parent 89204f0 commit a415bd6

34 files changed

+855
-442
lines changed

docs/conf.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import sys
55
from os.path import dirname, relpath
66
from pathlib import Path
7+
from typing import Union
78

89
import libvcs
910

@@ -15,7 +16,7 @@
1516
sys.path.insert(0, str(doc_path / "_ext"))
1617

1718
# package data
18-
about: dict = {}
19+
about: dict[str, str] = {}
1920
with open(project_root / "libvcs" / "__about__.py") as fp:
2021
exec(fp.read(), about)
2122

@@ -58,8 +59,8 @@
5859
html_extra_path = ["manifest.json"]
5960
html_favicon = "_static/favicon.ico"
6061
html_theme = "furo"
61-
html_theme_path: list = []
62-
html_theme_options: dict = {
62+
html_theme_path: list[str] = []
63+
html_theme_options: dict[str, Union[str, list[dict[str, str]]]] = {
6364
"light_logo": "img/libvcs.svg",
6465
"dark_logo": "img/libvcs-dark.svg",
6566
"footer_icons": [
@@ -164,7 +165,9 @@
164165
}
165166

166167

167-
def linkcode_resolve(domain, info): # NOQA: C901
168+
def linkcode_resolve(
169+
domain: str, info: dict[str, str]
170+
) -> Union[None, str]: # NOQA: C901
168171
"""
169172
Determine the URL corresponding to Python object
170173
@@ -197,7 +200,8 @@ def linkcode_resolve(domain, info): # NOQA: C901
197200
except AttributeError:
198201
pass
199202
else:
200-
obj = unwrap(obj)
203+
if callable(obj):
204+
obj = unwrap(obj)
201205

202206
try:
203207
fn = inspect.getsourcefile(obj)

libvcs/_internal/dataclasses.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ class SkipDefaultFieldsReprMixin:
7272
ItemWithMixin(name=Test, unit_price=2.05)
7373
"""
7474

75-
def __repr__(self):
75+
def __repr__(self) -> str:
7676
"""Omit default fields in object representation."""
7777
nodef_f_vals = (
7878
(f.name, attrgetter(f.name)(self))

libvcs/_internal/query_list.py

Lines changed: 157 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,26 @@
66
"""
77
import re
88
import traceback
9-
from typing import Any, Callable, Optional, Protocol, Sequence, TypeVar, Union
9+
from typing import (
10+
Any,
11+
Callable,
12+
List,
13+
Mapping,
14+
Optional,
15+
Pattern,
16+
Protocol,
17+
Sequence,
18+
TypeVar,
19+
Union,
20+
)
1021

1122
T = TypeVar("T", Any, Any)
1223

1324

14-
def keygetter(obj, path):
25+
def keygetter(
26+
obj: Mapping[str, Any],
27+
path: str,
28+
) -> Union[None, Any, str, List[str], Mapping[str, str]]:
1529
"""obj, "foods__breakfast", obj['foods']['breakfast']
1630
1731
>>> keygetter({ "foods": { "breakfast": "cereal" } }, "foods__breakfast")
@@ -26,12 +40,12 @@ def keygetter(obj, path):
2640
for sub_field in sub_fields:
2741
dct = dct[sub_field]
2842
return dct
29-
except Exception as e:
30-
traceback.print_exception(e)
43+
except Exception:
44+
traceback.print_stack()
3145
return None
3246

3347

34-
def parse_lookup(obj, path, lookup):
48+
def parse_lookup(obj: Mapping[str, Any], path: str, lookup: str) -> Optional[Any]:
3549
"""Check if field lookup key, e.g. "my__path__contains" has comparator, return val.
3650
3751
If comparator not used or value not found, return None.
@@ -42,74 +56,170 @@ def parse_lookup(obj, path, lookup):
4256
'red apple'
4357
"""
4458
try:
45-
if path.endswith(lookup):
59+
if isinstance(path, str) and isinstance(lookup, str) and path.endswith(lookup):
4660
if field_name := path.rsplit(lookup)[0]:
4761
return keygetter(obj, field_name)
48-
except Exception as e:
49-
traceback.print_exception(e)
62+
except Exception:
63+
traceback.print_stack()
5064
return None
5165

5266

5367
class LookupProtocol(Protocol):
5468
"""Protocol for :class:`QueryList` filtering operators."""
5569

56-
def __call__(self, data: Union[list[str], str], rhs: Union[list[str], str]):
70+
def __call__(
71+
self,
72+
data: Union[str, list[str], Mapping[str, str]],
73+
rhs: Union[str, list[str], Mapping[str, str], Pattern[str]],
74+
) -> bool:
5775
"""Callback for :class:`QueryList` filtering operators."""
76+
...
5877

5978

60-
def lookup_exact(data, rhs):
79+
def lookup_exact(
80+
data: Union[str, list[str], Mapping[str, str]],
81+
rhs: Union[str, list[str], Mapping[str, str], Pattern[str]],
82+
) -> bool:
6183
return rhs == data
6284

6385

64-
def lookup_iexact(data, rhs):
86+
def lookup_iexact(
87+
data: Union[str, list[str], Mapping[str, str]],
88+
rhs: Union[str, list[str], Mapping[str, str], Pattern[str]],
89+
) -> bool:
90+
if not isinstance(rhs, str) or not isinstance(data, str):
91+
return False
92+
6593
return rhs.lower() == data.lower()
6694

6795

68-
def lookup_contains(data, rhs):
96+
def lookup_contains(
97+
data: Union[str, list[str], Mapping[str, str]],
98+
rhs: Union[str, list[str], Mapping[str, str], Pattern[str]],
99+
) -> bool:
100+
if not isinstance(rhs, str) or not isinstance(data, (str, Mapping, list)):
101+
return False
102+
69103
return rhs in data
70104

71105

72-
def lookup_icontains(data, rhs):
73-
return rhs.lower() in data.lower()
106+
def lookup_icontains(
107+
data: Union[str, list[str], Mapping[str, str]],
108+
rhs: Union[str, list[str], Mapping[str, str], Pattern[str]],
109+
) -> bool:
110+
if not isinstance(rhs, str) or not isinstance(data, (str, Mapping, list)):
111+
return False
112+
113+
if isinstance(data, str):
114+
return rhs.lower() in data.lower()
115+
if isinstance(data, Mapping):
116+
return rhs.lower() in [k.lower() for k in data.keys()]
74117

118+
return False
119+
120+
121+
def lookup_startswith(
122+
data: Union[str, list[str], Mapping[str, str]],
123+
rhs: Union[str, list[str], Mapping[str, str], Pattern[str]],
124+
) -> bool:
125+
if not isinstance(rhs, str) or not isinstance(data, str):
126+
return False
75127

76-
def lookup_startswith(data, rhs):
77128
return data.startswith(rhs)
78129

79130

80-
def lookup_istartswith(data, rhs):
131+
def lookup_istartswith(
132+
data: Union[str, list[str], Mapping[str, str]],
133+
rhs: Union[str, list[str], Mapping[str, str], Pattern[str]],
134+
) -> bool:
135+
if not isinstance(rhs, str) or not isinstance(data, str):
136+
return False
137+
81138
return data.lower().startswith(rhs.lower())
82139

83140

84-
def lookup_endswith(data, rhs):
141+
def lookup_endswith(
142+
data: Union[str, list[str], Mapping[str, str]],
143+
rhs: Union[str, list[str], Mapping[str, str], Pattern[str]],
144+
) -> bool:
145+
if not isinstance(rhs, str) or not isinstance(data, str):
146+
return False
147+
85148
return data.endswith(rhs)
86149

87150

88-
def lookup_iendswith(data, rhs):
151+
def lookup_iendswith(
152+
data: Union[str, list[str], Mapping[str, str]],
153+
rhs: Union[str, list[str], Mapping[str, str], Pattern[str]],
154+
) -> bool:
155+
if not isinstance(rhs, str) or not isinstance(data, str):
156+
return False
89157
return data.lower().endswith(rhs.lower())
90158

91159

92-
def lookup_in(data, rhs):
160+
def lookup_in(
161+
data: Union[str, list[str], Mapping[str, str]],
162+
rhs: Union[str, list[str], Mapping[str, str], Pattern[str]],
163+
) -> bool:
93164
if isinstance(rhs, list):
94165
return data in rhs
95-
return rhs in data
166+
167+
try:
168+
if isinstance(rhs, str) and isinstance(data, Mapping):
169+
return rhs in data
170+
if isinstance(rhs, str) and isinstance(data, (str, list)):
171+
return rhs in data
172+
if isinstance(rhs, str) and isinstance(data, Mapping):
173+
return rhs in data
174+
# TODO: Add a deep Mappingionary matcher
175+
# if isinstance(rhs, Mapping) and isinstance(data, Mapping):
176+
# return rhs.items() not in data.items()
177+
except Exception:
178+
return False
179+
return False
96180

97181

98-
def lookup_nin(data, rhs):
182+
def lookup_nin(
183+
data: Union[str, list[str], Mapping[str, str]],
184+
rhs: Union[str, list[str], Mapping[str, str], Pattern[str]],
185+
) -> bool:
99186
if isinstance(rhs, list):
100187
return data not in rhs
101-
return rhs not in data
188+
189+
try:
190+
if isinstance(rhs, str) and isinstance(data, Mapping):
191+
return rhs not in data
192+
if isinstance(rhs, str) and isinstance(data, (str, list)):
193+
return rhs not in data
194+
if isinstance(rhs, str) and isinstance(data, Mapping):
195+
return rhs not in data
196+
# TODO: Add a deep Mappingionary matcher
197+
# if isinstance(rhs, Mapping) and isinstance(data, Mapping):
198+
# return rhs.items() not in data.items()
199+
except Exception:
200+
return False
201+
return False
102202

103203

104-
def lookup_regex(data, rhs):
105-
return re.search(rhs, data)
204+
def lookup_regex(
205+
data: Union[str, list[str], Mapping[str, str]],
206+
rhs: Union[str, list[str], Mapping[str, str], Pattern[str]],
207+
) -> bool:
208+
if isinstance(data, (str, bytes, re.Pattern)) and isinstance(rhs, (str, bytes)):
209+
return bool(re.search(rhs, data))
210+
return False
106211

107212

108-
def lookup_iregex(data, rhs):
109-
return re.search(rhs, data, re.IGNORECASE)
213+
def lookup_iregex(
214+
data: Union[str, list[str], Mapping[str, str]],
215+
rhs: Union[str, list[str], Mapping[str, str], Pattern[str]],
216+
) -> bool:
217+
if isinstance(data, (str, bytes, re.Pattern)) and isinstance(rhs, (str, bytes)):
218+
return bool(re.search(rhs, data, re.IGNORECASE))
219+
return False
110220

111221

112-
LOOKUP_NAME_MAP: dict[str, LookupProtocol] = {
222+
LOOKUP_NAME_MAP: Mapping[str, LookupProtocol] = {
113223
"eq": lookup_exact,
114224
"exact": lookup_exact,
115225
"iexact": lookup_iexact,
@@ -127,7 +237,7 @@ def lookup_iregex(data, rhs):
127237

128238

129239
class QueryList(list[T]):
130-
"""Filter list of object/dicts. For small, local datasets. *Experimental, unstable*.
240+
"""Filter list of object/Mappings. For small, local datasets. *Experimental, unstable*.
131241
132242
>>> query = QueryList(
133243
... [
@@ -158,23 +268,33 @@ class QueryList(list[T]):
158268
"""
159269

160270
data: Sequence[T]
271+
pk_key: Optional[str]
161272

162-
def items(self):
273+
def items(self) -> list[T]:
163274
data: Sequence[T]
164275

165276
if self.pk_key is None:
166277
raise Exception("items() require a pk_key exists")
167278
return [(getattr(item, self.pk_key), item) for item in self]
168279

169-
def __eq__(self, other):
280+
def __eq__(
281+
self,
282+
other: object,
283+
# other: Union[
284+
# "QueryList[T]",
285+
# List[Mapping[str, str]],
286+
# List[Mapping[str, int]],
287+
# List[Mapping[str, Union[str, Mapping[str, Union[List[str], str]]]]],
288+
# ],
289+
) -> bool:
170290
data = other
171291

172292
if not isinstance(self, list) or not isinstance(data, list):
173293
return False
174294

175295
if len(self) == len(data):
176296
for (a, b) in zip(self, data):
177-
if isinstance(a, dict):
297+
if isinstance(a, Mapping):
178298
a_keys = a.keys()
179299
if a.keys == b.keys():
180300
for key in a_keys:
@@ -187,8 +307,10 @@ def __eq__(self, other):
187307
return True
188308
return False
189309

190-
def filter(self, matcher: Optional[Union[Callable[[T], bool], T]] = None, **kwargs):
191-
def filter_lookup(obj) -> bool:
310+
def filter(
311+
self, matcher: Optional[Union[Callable[[T], bool], T]] = None, **kwargs: Any
312+
) -> "QueryList[T]":
313+
def filter_lookup(obj: Any) -> bool:
192314
for path, v in kwargs.items():
193315
try:
194316
lhs, op = path.rsplit("__", 1)
@@ -203,7 +325,7 @@ def filter_lookup(obj) -> bool:
203325
path = lhs
204326
data = keygetter(obj, path)
205327

206-
if not LOOKUP_NAME_MAP[op](data, v):
328+
if data is None or not LOOKUP_NAME_MAP[op](data, v):
207329
return False
208330

209331
return True
@@ -212,7 +334,7 @@ def filter_lookup(obj) -> bool:
212334
_filter = matcher
213335
elif matcher is not None:
214336

215-
def val_match(obj):
337+
def val_match(obj: Union[str, list[Any]]) -> bool:
216338
if isinstance(matcher, list):
217339
return obj in matcher
218340
else:

0 commit comments

Comments
 (0)