|
| 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