Skip to content

Instantly share code, notes, and snippets.

@sharkdp
Last active December 27, 2024 12:40
Show Gist options
  • Save sharkdp/755bcde3f485084ca43bb9ad66b5d1bb to your computer and use it in GitHub Desktop.
Save sharkdp/755bcde3f485084ca43bb9ad66b5d1bb to your computer and use it in GitHub Desktop.
# /// 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