Skip to content

Commit c1c1083

Browse files
Run mypy via run_mypy.py script
This gives easier access to mypy error messages that would otherwise be suppressed by settings from `setup.cfg`.
1 parent 96060bf commit c1c1083

File tree

3 files changed

+208
-185
lines changed

3 files changed

+208
-185
lines changed

.pre-commit-config.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,13 @@ repos:
5050
rev: v0.991
5151
hooks:
5252
- id: mypy
53+
language: python
54+
entry: python ./scripts/run_mypy.py --verbose
5355
additional_dependencies:
5456
- numpy>=1.20
57+
- pandas
5558
- types-filelock
5659
- types-setuptools
60+
always_run: true
61+
require_serial: true
62+
pass_filenames: false

scripts/run_mypy.py

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
"""
2+
Invokes mypy and compare the reults with files in /pytensor
3+
and a list of files that are known to fail.
4+
5+
Exit code 0 indicates that there are no unexpected results.
6+
7+
Usage
8+
-----
9+
python scripts/run_mypy.py [--verbose]
10+
"""
11+
import argparse
12+
import importlib
13+
import os
14+
import pathlib
15+
import subprocess
16+
import sys
17+
from typing import Iterator
18+
19+
import pandas
20+
21+
22+
DP_ROOT = pathlib.Path(__file__).absolute().parent.parent
23+
FAILING = """
24+
pytensor/compile/builders.py
25+
pytensor/compile/compilelock.py
26+
pytensor/compile/debugmode.py
27+
pytensor/compile/function/pfunc.py
28+
pytensor/compile/function/types.py
29+
pytensor/compile/mode.py
30+
pytensor/compile/sharedvalue.py
31+
pytensor/graph/rewriting/basic.py
32+
pytensor/ifelse.py
33+
pytensor/link/basic.py
34+
pytensor/link/numba/dispatch/elemwise.py
35+
pytensor/link/numba/dispatch/random.py
36+
pytensor/link/numba/dispatch/scan.py
37+
pytensor/printing.py
38+
pytensor/raise_op.py
39+
pytensor/sandbox/rng_mrg.py
40+
pytensor/scalar/basic.py
41+
pytensor/sparse/basic.py
42+
pytensor/sparse/type.py
43+
pytensor/tensor/basic.py
44+
pytensor/tensor/blas.py
45+
pytensor/tensor/blas_c.py
46+
pytensor/tensor/blas_headers.py
47+
pytensor/tensor/elemwise.py
48+
pytensor/tensor/extra_ops.py
49+
pytensor/tensor/math.py
50+
pytensor/tensor/nnet/abstract_conv.py
51+
pytensor/tensor/nnet/ctc.py
52+
pytensor/tensor/nnet/neighbours.py
53+
pytensor/tensor/random/basic.py
54+
pytensor/tensor/random/op.py
55+
pytensor/tensor/random/utils.py
56+
pytensor/tensor/rewriting/basic.py
57+
pytensor/tensor/rewriting/elemwise.py
58+
pytensor/tensor/shape.py
59+
pytensor/tensor/slinalg.py
60+
pytensor/tensor/subtensor.py
61+
pytensor/tensor/type.py
62+
pytensor/tensor/type_other.py
63+
pytensor/tensor/var.py
64+
pytensor/typed_list/basic.py
65+
"""
66+
67+
68+
def enforce_pep561(module_name):
69+
try:
70+
module = importlib.import_module(module_name)
71+
fp = pathlib.Path(module.__path__[0], "py.typed")
72+
if not fp.exists():
73+
fp.touch()
74+
except ModuleNotFoundError:
75+
print(f"Can't enforce PEP 561 for {module_name} because it is not installed.")
76+
return
77+
78+
79+
def mypy_to_pandas(input_lines: Iterator[str]) -> pandas.DataFrame:
80+
"""Reformats mypy output with error codes to a DataFrame.
81+
82+
Adapted from: https://gist.github.com/michaelosthege/24d0703e5f37850c9e5679f69598930a
83+
"""
84+
current_section = None
85+
data = {
86+
"file": [],
87+
"line": [],
88+
"type": [],
89+
"errorcode": [],
90+
"message": [],
91+
}
92+
for line in input_lines:
93+
line = line.strip()
94+
elems = line.split(":")
95+
if len(elems) < 3:
96+
continue
97+
try:
98+
file, lineno, message_type, *_ = elems[0:3]
99+
message_type = message_type.strip()
100+
if message_type == "error":
101+
current_section = line.split(" [")[-1][:-1]
102+
message = line.replace(f"{file}:{lineno}: {message_type}: ", "").replace(
103+
f" [{current_section}]", ""
104+
)
105+
data["file"].append(file)
106+
data["line"].append(lineno)
107+
data["type"].append(message_type)
108+
data["errorcode"].append(current_section)
109+
data["message"].append(message)
110+
except Exception as ex:
111+
print(elems)
112+
print(ex)
113+
return pandas.DataFrame(data=data).set_index(["file", "line"])
114+
115+
116+
def check_no_unexpected_results(mypy_lines: Iterator[str]):
117+
"""Compares mypy results with list of known FAILING files.
118+
119+
Exits the process with non-zero exit code upon unexpected results.
120+
"""
121+
df = mypy_to_pandas(mypy_lines)
122+
123+
all_files = {
124+
str(fp).replace(str(DP_ROOT), "").strip(os.sep).replace(os.sep, "/")
125+
for fp in DP_ROOT.glob("pytensor/**/*.py")
126+
}
127+
failing = set(df.reset_index().file.str.replace(os.sep, "/", regex=False))
128+
if not failing.issubset(all_files):
129+
raise Exception(
130+
"Mypy should have ignored these files:\n"
131+
+ "\n".join(sorted(map(str, failing - all_files)))
132+
)
133+
passing = all_files - failing
134+
expected_failing = set(FAILING.strip().split("\n")) - {""}
135+
unexpected_failing = failing - expected_failing
136+
unexpected_passing = passing.intersection(expected_failing)
137+
138+
if not unexpected_failing:
139+
print(f"{len(passing)}/{len(all_files)} files pass as expected.")
140+
else:
141+
print("!!!!!!!!!")
142+
print(f"{len(unexpected_failing)} files unexpectedly failed.")
143+
print("\n".join(sorted(map(str, unexpected_failing))))
144+
print(
145+
"These files did not fail before, so please check the above output"
146+
f" for errors in {unexpected_failing} and fix them."
147+
)
148+
print(
149+
"You can run `python scripts/run_mypy.py --verbose` to reproduce this test locally."
150+
)
151+
sys.exit(1)
152+
153+
if unexpected_passing:
154+
print("!!!!!!!!!")
155+
print(f"{len(unexpected_passing)} files unexpectedly passed the type checks:")
156+
print("\n".join(sorted(map(str, unexpected_passing))))
157+
print(
158+
"This is good news! Go to scripts/run_mypy.py and remove them from the `FAILING` list."
159+
)
160+
if all_files.issubset(passing):
161+
print("WOW! All files are passing the mypy type checks!")
162+
print("scripts\\run_mypy.py may no longer be needed.")
163+
print("!!!!!!!!!")
164+
sys.exit(1)
165+
return
166+
167+
168+
if __name__ == "__main__":
169+
parser = argparse.ArgumentParser(
170+
description="Run mypy type checks on PyTensor codebase."
171+
)
172+
parser.add_argument(
173+
"--verbose", action="count", default=0, help="Pass this to print mypy output."
174+
)
175+
parser.add_argument(
176+
"--groupby",
177+
default="file",
178+
help="How to group verbose output. One of {file|errorcode|message}.",
179+
)
180+
args, _ = parser.parse_known_args()
181+
182+
cp = subprocess.run(
183+
["mypy", "--show-error-codes", "pytensor"],
184+
capture_output=True,
185+
)
186+
output = cp.stdout.decode()
187+
if args.verbose:
188+
df = mypy_to_pandas(output.split("\n"))
189+
for section, sdf in df.reset_index().groupby(args.groupby):
190+
print(f"\n\n[{section}]")
191+
for row in sdf.itertuples():
192+
print(f"{row.file}:{row.line}: {row.type}: {row.message}")
193+
print()
194+
else:
195+
print(
196+
"Mypy output hidden."
197+
" Run `python run_mypy.py --verbose` to see the full output,"
198+
" or `python run_mypy.py --help` for other options."
199+
)
200+
201+
check_no_unexpected_results(output.split("\n"))
202+
sys.exit(0)

0 commit comments

Comments
 (0)