Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 1decae6

Browse files
committed
Add script for identifying missing/broken type hints
Could use some front end work to make it easier to run and supply options/configuration Does not do set checks for class methods, as there are far too many false positives
1 parent e4cedb1 commit 1decae6

File tree

1 file changed

+257
-0
lines changed

1 file changed

+257
-0
lines changed

tools/check_typehints.py

Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
#!/usr/bin/env python
2+
3+
import ast
4+
import pathlib
5+
import sys
6+
7+
MISSING_STUB = 1
8+
MISSING_IMPL = 2
9+
POS_ARGS = 4
10+
ARGS = 8
11+
VARARG = 16
12+
KWARGS = 32
13+
VARKWARG = 64
14+
15+
16+
def check_file(path, ignore=0):
17+
stubpath = path.with_suffix(".pyi")
18+
ret = 0
19+
if not stubpath.exists():
20+
return 0, 0
21+
tree = ast.parse(path.read_text())
22+
stubtree = ast.parse(stubpath.read_text())
23+
return check_namespace(tree, stubtree, path, ignore)
24+
25+
26+
def check_namespace(tree, stubtree, path, ignore=0):
27+
ret = 0
28+
count = 0
29+
tree_items = set(
30+
i.name for i in tree.body if hasattr(i, "name") and not i.name.startswith("_")
31+
)
32+
stubtree_items = set(
33+
i.name
34+
for i in stubtree.body
35+
if hasattr(i, "name") and not i.name.startswith("_")
36+
)
37+
38+
for item in tree.body:
39+
if isinstance(item, ast.Assign):
40+
tree_items |= set(
41+
i.id
42+
for i in item.targets
43+
if hasattr(i, "id") and not i.id.startswith("_")
44+
)
45+
for target in item.targets:
46+
if isinstance(target, ast.Tuple):
47+
tree_items |= set(i.id for i in target.elts)
48+
elif isinstance(item, ast.AnnAssign):
49+
tree_items |= {item.target.id}
50+
for item in stubtree.body:
51+
if isinstance(item, ast.Assign):
52+
stubtree_items |= set(
53+
i.id
54+
for i in item.targets
55+
if hasattr(i, "id") and not i.id.startswith("_")
56+
)
57+
for target in item.targets:
58+
if isinstance(target, ast.Tuple):
59+
stubtree_items |= set(i.id for i in target.elts)
60+
elif isinstance(item, ast.AnnAssign):
61+
stubtree_items |= {item.target.id}
62+
63+
try:
64+
all_ = ast.literal_eval(ast.unparse(get_subtree(tree, "__all__").value))
65+
except ValueError:
66+
all_ = []
67+
68+
if all_:
69+
missing = (tree_items - stubtree_items) & set(all_)
70+
else:
71+
missing = tree_items - stubtree_items
72+
73+
deprecated = set()
74+
for item_name in missing:
75+
item = get_subtree(tree, item_name)
76+
if hasattr(item, "decorator_list"):
77+
if "deprecated" in [
78+
i.func.attr
79+
for i in item.decorator_list
80+
if hasattr(i, "func") and hasattr(i.func, "attr")
81+
]:
82+
deprecated |= {item_name}
83+
84+
if missing - deprecated and ~ignore & MISSING_STUB:
85+
print(f"{path}: {missing - deprecated} missing from stubs")
86+
ret |= MISSING_STUB
87+
count += 1
88+
89+
non_class_or_func = set()
90+
for item_name in stubtree_items - tree_items:
91+
try:
92+
get_subtree(tree, item_name)
93+
except ValueError:
94+
pass
95+
else:
96+
non_class_or_func |= {item_name}
97+
98+
missing_implementation = stubtree_items - tree_items - non_class_or_func
99+
if missing_implementation and ~ignore & MISSING_IMPL:
100+
print(
101+
f"{path}: {missing_implementation} in stubs and not source"
102+
)
103+
ret |= MISSING_IMPL
104+
count += 1
105+
106+
for item_name in tree_items & stubtree_items:
107+
item = get_subtree(tree, item_name)
108+
stubitem = get_subtree(stubtree, item_name)
109+
if isinstance(item, ast.FunctionDef) and isinstance(stubitem, ast.FunctionDef):
110+
err, c = check_function(item, stubitem, f"{path}::{item_name}", ignore)
111+
ret |= err
112+
count += c
113+
if isinstance(item, ast.ClassDef):
114+
# Ignore set differences for classes... while it would be nice to have
115+
# inheritance and attributes set in init/methods make both presence and
116+
# absence of nodes spurious
117+
err, c = check_namespace(
118+
item,
119+
stubitem,
120+
f"{path}::{item_name}",
121+
ignore | MISSING_STUB | MISSING_IMPL,
122+
)
123+
ret |= err
124+
count += c
125+
126+
return ret, count
127+
128+
129+
def check_function(item, stubitem, path, ignore):
130+
ret = 0
131+
count = 0
132+
133+
# if the stub calls overload, assume it knows what its doing
134+
overloaded = "overload" in [
135+
i.id for i in stubitem.decorator_list if hasattr(i, "id")
136+
]
137+
if overloaded:
138+
return 0, 0
139+
140+
item_posargs = [a.arg for a in item.args.posonlyargs]
141+
stubitem_posargs = [a.arg for a in stubitem.args.posonlyargs]
142+
if item_posargs != stubitem_posargs and ~ignore & POS_ARGS:
143+
print(
144+
f"{path} {item.name} posargs differ: {item_posargs} vs {stubitem_posargs}"
145+
)
146+
ret |= POS_ARGS
147+
count += 1
148+
149+
item_args = [a.arg for a in item.args.args]
150+
stubitem_args = [a.arg for a in stubitem.args.args]
151+
if item_args != stubitem_args and ~ignore & ARGS:
152+
print(f"{path} args differ for {item.name}: {item_args} vs {stubitem_args}")
153+
ret |= ARGS
154+
count += 1
155+
156+
item_vararg = item.args.vararg
157+
stubitem_vararg = stubitem.args.vararg
158+
if ~ignore & VARARG:
159+
if (item_vararg is None) ^ (stubitem_vararg is None):
160+
if item_vararg:
161+
print(
162+
f"{path} {item.name} vararg differ: "
163+
f"{item_vararg.arg} vs {stubitem_vararg}"
164+
)
165+
else:
166+
print(
167+
f"{path} {item.name} vararg differ: "
168+
f"{item_vararg} vs {stubitem_vararg.arg}"
169+
)
170+
ret |= VARARG
171+
count += 1
172+
elif item_vararg is None:
173+
pass
174+
elif item_vararg.arg != stubitem_vararg.arg:
175+
print(
176+
f"{path} {item.name} vararg differ: "
177+
f"{item_vararg.arg} vs {stubitem_vararg.arg}"
178+
)
179+
ret |= VARARG
180+
count += 1
181+
182+
item_kwonlyargs = [a.arg for a in item.args.kwonlyargs]
183+
stubitem_kwonlyargs = [a.arg for a in stubitem.args.kwonlyargs]
184+
if item_kwonlyargs != stubitem_kwonlyargs and ~ignore & KWARGS:
185+
print(
186+
f"{path} {item.name} kwonlyargs differ: "
187+
f"{item_kwonlyargs} vs {stubitem_kwonlyargs}"
188+
)
189+
ret |= KWARGS
190+
count += 1
191+
192+
item_kwarg = item.args.kwarg
193+
stubitem_kwarg = stubitem.args.kwarg
194+
if ~ignore & VARKWARG:
195+
if (item_kwarg is None) ^ (stubitem_kwarg is None):
196+
if item_kwarg:
197+
print(
198+
f"{path} {item.name} kwarg differ: "
199+
f"{item_kwarg.arg} vs {stubitem_kwarg}"
200+
)
201+
else:
202+
print(
203+
f"{path} {item.name} kwarg differ: "
204+
f"{item_kwarg} vs {stubitem_kwarg.arg}"
205+
)
206+
ret |= VARKWARG
207+
count += 1
208+
elif item_kwarg is None:
209+
pass
210+
elif item_kwarg.arg != stubitem_kwarg.arg:
211+
print(
212+
f"{path} {item.name} kwarg differ: "
213+
f"{item_kwarg.arg} vs {stubitem_kwarg.arg}"
214+
)
215+
ret |= VARKWARG
216+
count += 1
217+
218+
return ret, count
219+
220+
221+
def get_subtree(tree, name):
222+
for item in tree.body:
223+
if isinstance(item, ast.Assign):
224+
if name in [i.id for i in item.targets if hasattr(i, "id")]:
225+
return item
226+
for target in item.targets:
227+
if isinstance(target, ast.Tuple):
228+
if name in [i.id for i in target.elts]:
229+
return item
230+
if isinstance(item, ast.AnnAssign):
231+
if name == item.target.id:
232+
return item
233+
if not hasattr(item, "name"):
234+
continue
235+
if item.name == name:
236+
return item
237+
raise ValueError(f"no such item {name} in tree")
238+
239+
240+
if __name__ == "__main__":
241+
out = 0
242+
count = 0
243+
basedir = pathlib.Path("lib/matplotlib")
244+
per_file_ignore = {
245+
# Edge cases for items set via `get_attr`, etc
246+
basedir / "__init__.py": MISSING_IMPL,
247+
# Base class has **kwargs, subclasses have more specific
248+
basedir / "ticker.py": VARKWARG,
249+
basedir / "layout_engine.py": VARKWARG,
250+
}
251+
for f in basedir.rglob("**/*.py"):
252+
err, c = check_file(f, ignore=0 | per_file_ignore.get(f, 0))
253+
out |= err
254+
count += c
255+
print("\n")
256+
print(f"{count} total errors found")
257+
sys.exit(out)

0 commit comments

Comments
 (0)