Last active
December 27, 2024 12:40
-
-
Save sharkdp/755bcde3f485084ca43bb9ad66b5d1bb to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# /// script | |
# requires-python = ">=3.10" | |
# dependencies = [ | |
# "click", | |
# ] | |
# /// | |
""" | |
Run a set of type checkers on a given folder. | |
Usage: | |
uv run -q typecheck.py <folder> | |
""" | |
import os | |
import subprocess | |
from concurrent.futures import ThreadPoolExecutor | |
from dataclasses import dataclass | |
from pathlib import Path | |
from typing import Callable | |
import click | |
@dataclass | |
class TypeChecker: | |
name: str | |
command: list[str] | |
cwd: str | None | |
filter: Callable[[str], bool] | None | |
MYPY = TypeChecker( | |
name="mypy", | |
command=[ | |
"mypy", | |
"--no-error-summary", | |
"--disable-error-code=empty-body", | |
"--warn-unreachable", | |
], | |
cwd=None, | |
filter=None, | |
) | |
PYRIGHT = TypeChecker(name="pyright", command=["pyright"], cwd=None, filter=None) | |
RED_KNOT_FEATURE = TypeChecker( | |
name="red knot 1 (feature branch)", | |
command=[ | |
"cargo", | |
"run", | |
"--quiet", | |
"--bin=red_knot", | |
"--", | |
"--python-version=3.13", | |
"--project", | |
], | |
cwd="/home/shark/ruff/", | |
filter=None, | |
) | |
RED_KNOT_MAIN = TypeChecker( | |
name="red knot 2 (main branch)", | |
command=[ | |
"cargo", | |
"run", | |
"--quiet", | |
"--bin=red_knot", | |
"--", | |
"--python-version=3.13", | |
"--project", | |
], | |
cwd="/home/shark/ruff2/", | |
filter=None, | |
) | |
@dataclass | |
class TypeCheckResult: | |
stdout: str | None | |
stderr: str | None | |
exit_code: int | |
def run_typechecker(typechecker: TypeChecker, folder: Path) -> TypeCheckResult: | |
env = os.environ.copy() | |
env["FORCE_COLOR"] = "3" # enable colorized output for pyright | |
result = subprocess.run( | |
[*typechecker.command, folder], | |
cwd=typechecker.cwd, | |
capture_output=True, | |
text=True, | |
env=env, | |
check=False, | |
) | |
return TypeCheckResult( | |
stdout=result.stdout, | |
stderr=result.stderr, | |
exit_code=result.returncode, | |
) | |
@click.command() | |
@click.argument("folder", type=click.Path(exists=True, file_okay=False, path_type=Path)) | |
@click.option("--no-main", is_flag=True) | |
def main(folder: Path, no_main: bool) -> None: | |
typecheckers: list[TypeChecker] = [] | |
typecheckers.append(MYPY) | |
typecheckers.append(PYRIGHT) | |
typecheckers.append(RED_KNOT_FEATURE) | |
if not no_main: | |
typecheckers.append(RED_KNOT_MAIN) | |
results = {} | |
with ThreadPoolExecutor() as executor: | |
futures = { | |
checker.name: executor.submit(run_typechecker, checker, folder) | |
for checker in typecheckers | |
} | |
for name, future in futures.items(): | |
results[name] = future.result() | |
for typechecker in typecheckers: | |
result = results[typechecker.name] | |
exit_code = result.exit_code | |
if exit_code == 0: | |
status = click.style(" [success]", fg="green", bold=True) | |
else: | |
status = click.style(f" [exit code {exit_code}]", fg="red", bold=True) | |
click.echo(click.style(typechecker.name, bold=True) + status) | |
click.secho("─" * 100, fg="black") | |
if result.stdout: | |
if typechecker.filter: | |
output = "" | |
for line in result.stdout.splitlines(): | |
if typechecker.filter(line): | |
output += line + "\n" | |
else: | |
output = result.stdout | |
click.secho(output) | |
if result.stderr: | |
if typechecker.filter: | |
output = "" | |
for line in result.stderr.splitlines(): | |
if typechecker.filter(line): | |
output += line + "\n" | |
else: | |
output = result.stderr | |
click.secho(output) | |
click.secho("\n") | |
if __name__ == "__main__": | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment