1+ #!/usr/bin/env python
12"""
2- Invokes mypy and compare the reults with files in /pytensor
3- and a list of files that are known to fail.
3+ Invoke mypy and compare the reults with files in /pymc.
4+
5+ Excludes tests and a list of files that are known to fail.
46
57Exit code 0 indicates that there are no unexpected results.
68
1113
1214import argparse
1315import importlib
16+ import io
17+ import os
1418import subprocess
1519import sys
16- from collections .abc import Iterable
1720from pathlib import Path
1821
1922import pandas as pd
2023
2124
2225DP_ROOT = Path (__file__ ).absolute ().parent .parent
2326FAILING = [
24- Path ( line .strip ()). absolute ()
27+ line .strip ()
2528 for line in (DP_ROOT / "scripts" / "mypy-failing.txt" ).read_text ().splitlines ()
2629]
2730
@@ -37,52 +40,25 @@ def enforce_pep561(module_name):
3740 return
3841
3942
40- def mypy_to_pandas (input_lines : Iterable [ str ] ) -> pd .DataFrame :
43+ def mypy_to_pandas (mypy_result : str ) -> pd .DataFrame :
4144 """Reformats mypy output with error codes to a DataFrame.
4245
4346 Adapted from: https://gist.github.com/michaelosthege/24d0703e5f37850c9e5679f69598930a
4447 """
45- current_section = None
46- data : dict [str , list ] = {
47- "file" : [],
48- "line" : [],
49- "type" : [],
50- "errorcode" : [],
51- "message" : [],
52- }
53- for line in input_lines :
54- line = line .strip ()
55- elems = line .split (":" )
56- if len (elems ) < 3 :
57- continue
58- try :
59- file , lineno , message_type , * _ = elems [0 :3 ]
60- message_type = message_type .strip ()
61- if message_type == "error" :
62- current_section = line .split (" [" )[- 1 ][:- 1 ]
63- message = line .replace (f"{ file } :{ lineno } : { message_type } : " , "" ).replace (
64- f" [{ current_section } ]" , ""
65- )
66- data ["file" ].append (Path (file ))
67- data ["line" ].append (lineno )
68- data ["type" ].append (message_type )
69- data ["errorcode" ].append (current_section )
70- data ["message" ].append (message )
71- except Exception as ex :
72- print (elems )
73- print (ex )
74- return pd .DataFrame (data = data ).set_index (["file" , "line" ])
48+ return pd .read_json (io .StringIO (mypy_result ), lines = True )
7549
7650
77- def check_no_unexpected_results (mypy_lines : Iterable [ str ] ):
78- """Compares mypy results with list of known FAILING files.
51+ def check_no_unexpected_results (mypy_df : pd . DataFrame , show_expected : bool ):
52+ """Compare mypy results with list of known FAILING files.
7953
8054 Exits the process with non-zero exit code upon unexpected results.
8155 """
82- df = mypy_to_pandas (mypy_lines )
83-
84- all_files = {fp .absolute () for fp in DP_ROOT .glob ("pytensor/**/*.py" )}
85- failing = {f .absolute () for f in df .reset_index ().file }
56+ all_files = {
57+ str (fp ).replace (str (DP_ROOT ), "" ).strip (os .sep ).replace (os .sep , "/" )
58+ for fp in DP_ROOT .glob ("pytensor/**/*.py" )
59+ if "tests" not in str (fp )
60+ }
61+ failing = set (mypy_df .file .str .replace (os .sep , "/" , regex = False ))
8662 if not failing .issubset (all_files ):
8763 raise Exception (
8864 "Mypy should have ignored these files:\n "
@@ -97,15 +73,28 @@ def check_no_unexpected_results(mypy_lines: Iterable[str]):
9773 print (f"{ len (passing )} /{ len (all_files )} files pass as expected." )
9874 else :
9975 print ("!!!!!!!!!" )
100- print (f"{ len (unexpected_failing )} files unexpectedly failed. " )
76+ print (f"{ len (unexpected_failing )} files unexpectedly failed: " )
10177 print ("\n " .join (sorted (map (str , unexpected_failing ))))
78+
79+ if show_expected :
80+ print (
81+ "\n These files did not fail before, so please check the above output"
82+ f" for errors in { unexpected_failing } and fix them."
83+ )
84+ else :
85+ print (
86+ "\n These files did not fail before. Fix all errors reported in the output above."
87+ )
88+ print (
89+ f"\n Note: In addition to these errors, { len (failing .intersection (expected_failing ))} errors in files "
90+ f'marked as "expected failures" were also found. To see these failures, run: '
91+ f"`python scripts/run_mypy.py --show-expected`"
92+ )
93+
10294 print (
103- "These files did not fail before, so please check the above output"
104- f" for errors in { unexpected_failing } and fix them."
105- )
106- print (
107- "You can run `python scripts/run_mypy.py --verbose` to reproduce this test locally."
95+ "You can run `python scripts/run_mypy.py` to reproduce this test locally."
10896 )
97+
10998 sys .exit (1 )
11099
111100 if unexpected_passing :
@@ -125,40 +114,50 @@ def check_no_unexpected_results(mypy_lines: Iterable[str]):
125114
126115if __name__ == "__main__" :
127116 parser = argparse .ArgumentParser (
128- description = "Run mypy type checks on PyTensor codebase."
117+ description = "Run mypy type checks on PyMC codebase."
129118 )
130119 parser .add_argument (
131- "--verbose" , action = "count" , default = 0 , help = "Pass this to print mypy output."
120+ "--verbose" , action = "count" , default = 1 , help = "Pass this to print mypy output."
121+ )
122+ parser .add_argument (
123+ "--show-expected" ,
124+ action = "store_true" ,
125+ help = "Also show expected failures in verbose output." ,
132126 )
133127 parser .add_argument (
134128 "--groupby" ,
135129 default = "file" ,
136130 help = "How to group verbose output. One of {file|errorcode|message}." ,
137131 )
138132 args , _ = parser .parse_known_args ()
139- missing = [path for path in FAILING if not path .exists ()]
140- if missing :
141- print ("These files are missing but still kept in FAILING" )
142- print (* missing , sep = "\n " )
143- sys .exit (1 )
133+
144134 cp = subprocess .run (
145135 [
146136 "mypy" ,
147- "--show-error-codes" ,
137+ "--output" ,
138+ "json" ,
148139 "--disable-error-code" ,
149140 "annotation-unchecked" ,
150141 "pytensor" ,
151142 ],
152143 capture_output = True ,
153144 )
154- output = cp .stdout .decode ()
145+
146+ output = cp .stdout .decode ("utf-8" )
147+ df = mypy_to_pandas (output )
148+
155149 if args .verbose :
156- df = mypy_to_pandas (output .split ("\n " ))
157- for section , sdf in df .reset_index ().groupby (args .groupby ):
150+ if not args .show_expected :
151+ expected_failing = set (FAILING )
152+ filtered_df = df .query ("file not in @expected_failing" )
153+ else :
154+ filtered_df = df
155+
156+ for section , sdf in filtered_df .groupby (args .groupby ):
158157 print (f"\n \n [{ section } ]" )
159- for row in sdf .itertuples ():
158+ for idx , row in sdf .iterrows ():
160159 print (
161- f"{ row .file } :{ row .line } : { row .type } [{ row .errorcode } ]: { row .message } "
160+ f"{ row .file } :{ row .line } : { row .code } [{ row .severity } ]: { row .message } "
162161 )
163162 print ()
164163 else :
@@ -168,5 +167,6 @@ def check_no_unexpected_results(mypy_lines: Iterable[str]):
168167 " or `python run_mypy.py --help` for other options."
169168 )
170169
171- check_no_unexpected_results (output .split ("\n " ))
170+ check_no_unexpected_results (df , show_expected = args .show_expected )
171+
172172 sys .exit (0 )
0 commit comments