-
-
Notifications
You must be signed in to change notification settings - Fork 7.8k
/
Copy pathstubtest.py
123 lines (108 loc) · 4.32 KB
/
stubtest.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
import ast
import os
import pathlib
import re
import subprocess
import sys
import tempfile
root = pathlib.Path(__file__).parent.parent
lib = root / "lib"
mpl = lib / "matplotlib"
class Visitor(ast.NodeVisitor):
def __init__(self, filepath, output, existing_allowed):
self.filepath = filepath
self.context = list(filepath.with_suffix("").relative_to(lib).parts)
self.output = output
self.existing_allowed = existing_allowed
def _is_already_allowed(self, parts):
# Skip outputting a path if it's already allowed before.
candidates = ['.'.join(parts[:s]) for s in range(1, len(parts))]
for allow in self.existing_allowed:
if any(allow.fullmatch(path) for path in candidates):
return True
return False
def visit_FunctionDef(self, node):
# delete_parameter adds a private sentinel value that leaks
# we do not want that sentinel value in the type hints but it breaks typing
# Does not apply to variadic arguments (args/kwargs)
for dec in node.decorator_list:
if "delete_parameter" in ast.unparse(dec):
deprecated_arg = dec.args[1].value
if (
node.args.vararg is not None
and node.args.vararg.arg == deprecated_arg
):
continue
if (
node.args.kwarg is not None
and node.args.kwarg.arg == deprecated_arg
):
continue
parents = []
if hasattr(node, "parent"):
parent = node.parent
while hasattr(parent, "parent") and not isinstance(
parent, ast.Module
):
parents.insert(0, parent.name)
parent = parent.parent
parts = [*self.context, *parents, node.name]
if not self._is_already_allowed(parts):
self.output.write("\\.".join(parts) + "\n")
break
def visit_ClassDef(self, node):
for dec in node.decorator_list:
if "define_aliases" in ast.unparse(dec):
parents = []
if hasattr(node, "parent"):
parent = node.parent
while hasattr(parent, "parent") and not isinstance(
parent, ast.Module
):
parents.insert(0, parent.name)
parent = parent.parent
aliases = ast.literal_eval(dec.args[0])
# Written as a regex rather than two lines to avoid unused entries
# for setters on items with only a getter
for substitutions in aliases.values():
parts = self.context + parents + [node.name]
for a in substitutions:
if not (self._is_already_allowed([*parts, f"get_{a}"]) and
self._is_already_allowed([*parts, f"set_{a}"])):
self.output.write("\\.".join([*parts, f"[gs]et_{a}\n"]))
for child in ast.iter_child_nodes(node):
self.visit(child)
existing_allowed = []
with (root / 'ci/mypy-stubtest-allowlist.txt').open() as f:
for line in f:
line, _, _ = line.partition('#')
line = line.strip()
if line:
existing_allowed.append(re.compile(line))
with tempfile.TemporaryDirectory() as d:
p = pathlib.Path(d) / "allowlist.txt"
with p.open("wt") as f:
for path in mpl.glob("**/*.py"):
v = Visitor(path, f, existing_allowed)
tree = ast.parse(path.read_text())
# Assign parents to tree so they can be backtraced
for node in ast.walk(tree):
for child in ast.iter_child_nodes(node):
child.parent = node
v.visit(tree)
proc = subprocess.run(
[
"stubtest",
"--mypy-config-file=pyproject.toml",
"--allowlist=ci/mypy-stubtest-allowlist.txt",
f"--allowlist={p}",
"matplotlib",
],
cwd=root,
env=os.environ | {"MPLBACKEND": "agg"},
)
try:
os.unlink(f.name)
except OSError:
pass
sys.exit(proc.returncode)