Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
be26367
Update
ezyang May 10, 2026
cf52e4c
Update
ezyang May 10, 2026
bbbd7a9
Update
ezyang May 10, 2026
4303245
Update
ezyang May 10, 2026
d74cb3f
Update
ezyang May 10, 2026
e6a0be9
Update
ezyang May 10, 2026
e767cac
Update
ezyang May 10, 2026
91cee82
Update
ezyang May 10, 2026
764f541
Update
ezyang May 10, 2026
fb7bb57
Update
ezyang May 10, 2026
0227ca5
Update
ezyang May 10, 2026
8764ff3
Update
ezyang May 10, 2026
c80ecb9
Update
ezyang May 10, 2026
96831bb
Update
ezyang May 10, 2026
22d21a1
Update
ezyang May 10, 2026
328d8f4
Update
ezyang May 10, 2026
f048d0d
Update
ezyang May 10, 2026
67e1bd3
Update
ezyang May 10, 2026
5e71e3b
Update
ezyang May 10, 2026
4910212
Update
ezyang May 10, 2026
9585381
Update
ezyang May 10, 2026
68a19c7
Update
ezyang May 10, 2026
b3c2b98
Update
ezyang May 10, 2026
f72612d
Update
ezyang May 10, 2026
8e3d811
Update
ezyang May 10, 2026
8dbe0d8
Update
ezyang May 10, 2026
95e5abf
Update
ezyang May 10, 2026
68e981a
Update
ezyang May 10, 2026
bad7c32
Update
ezyang May 10, 2026
878222b
Update
ezyang May 10, 2026
bc05931
Update
ezyang May 10, 2026
235eacc
Update
ezyang May 10, 2026
c1de994
Update
ezyang May 10, 2026
268b468
Update
ezyang May 10, 2026
71b637b
Update
ezyang May 10, 2026
1fbeaa0
Update
ezyang May 10, 2026
c2ac3ca
Update
ezyang May 10, 2026
c29e973
Update
ezyang May 10, 2026
4dff331
Update
ezyang May 10, 2026
e418a5e
Update
ezyang May 10, 2026
e7e7f90
Update
ezyang May 10, 2026
322eef0
Update
ezyang May 10, 2026
3f068bc
Update
ezyang May 11, 2026
f2190c3
Update
ezyang May 11, 2026
c34aa7f
Update
ezyang May 11, 2026
96038db
Update
ezyang May 11, 2026
2898c1c
Update
ezyang May 11, 2026
d8b0eac
Update
ezyang May 11, 2026
593174d
Update
ezyang May 11, 2026
cca2d33
Update
ezyang May 11, 2026
73afb45
Update
ezyang May 11, 2026
4d71ede
Update
ezyang May 11, 2026
80d858b
Update
ezyang May 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions bench/bench_submit.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@
import subprocess
import sys
import tempfile
from typing import Dict, List, Tuple
from typing import Any, Dict, List, Tuple


def run(args: List[str], cwd: str, **kwargs) -> subprocess.CompletedProcess:
def run(args: List[str], cwd: str, **kwargs: Any) -> subprocess.CompletedProcess[str]:
return subprocess.run(args, cwd=cwd, capture_output=True, text=True, **kwargs)


Expand Down
1 change: 1 addition & 0 deletions mypy.ini
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ python_version = 3.9
strict_optional = True
show_column_numbers = True
show_error_codes = True
enable_error_code = unused-awaitable, unused-coroutine
warn_no_return = True
disallow_any_unimported = True

Expand Down
77 changes: 77 additions & 0 deletions test_mypy_linter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import json
import subprocess
import sys
from pathlib import Path
from typing import Any, Dict, List

import pytest

from tools.linter.adapters.mypy_linter import RESULTS_RE


def _check(paths: List[Path]) -> List[Dict[str, Any]]:
repo_root = Path(__file__).parent
proc = subprocess.run(
[
sys.executable,
"tools/linter/adapters/mypy_linter.py",
"--config=mypy.ini",
"--",
*[str(path) for path in paths],
],
capture_output=True,
check=True,
cwd=repo_root,
text=True,
)
return [json.loads(line) for line in proc.stdout.splitlines()]


def test_py_test_allows_top_level_await(tmp_path: Path) -> None:
test_file = tmp_path / "ok.py.test"
test_file.write_text(
"""\
async def commit(name: str) -> None:
pass

await commit("A")
"""
)

assert _check([test_file]) == []


@pytest.mark.skipif(
sys.platform == "win32",
reason="mypy does not report this top-level coroutine fixture on Windows",
)
def test_py_test_detects_unawaited_coroutine(tmp_path: Path) -> None:
test_file = tmp_path / "missing_await.py.test"
test_file.write_text(
"""\
async def commit(name: str) -> None:
pass

commit("A")
"""
)

lint_messages = _check([test_file])
assert [message["name"] for message in lint_messages] == ["[unused-coroutine]"]
assert lint_messages[0]["path"] == str(test_file)
assert lint_messages[0]["line"] == 4
assert lint_messages[0]["description"] == (
'Value of type "Coroutine[Any, Any, None]" must be used '
)


def test_results_re_parses_windows_drive_paths() -> None:
match = RESULTS_RE.match(
r'C:\tmp\py_test_0.py:4:1: error: Value of type "Coroutine[Any, Any, None]" must be used [unused-coroutine]'
)
assert match is not None
assert match["file"] == r"C:\tmp\py_test_0.py"
assert match["line"] == "4"
assert match["column"] == "1"
assert match["severity"] == "error"
assert match["code"] == "[unused-coroutine]"
70 changes: 61 additions & 9 deletions tools/linter/adapters/mypy_linter.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@
import re
import subprocess
import sys
import tempfile
import time
from enum import Enum
from pathlib import Path
from typing import Any, Dict, List, NamedTuple, Optional, Pattern
from typing import Any, Dict, List, NamedTuple, Optional, Pattern, Tuple


IS_WINDOWS: bool = os.name == "nt"
Expand Down Expand Up @@ -41,11 +42,15 @@ def as_posix(name: str) -> str:
return name.replace("\\", "/") if IS_WINDOWS else name


def _path_key(name: str) -> str:
return as_posix(os.path.abspath(name))


# tools/linter/flake8_linter.py:15:13: error: Incompatibl...int") [assignment]
RESULTS_RE: Pattern[str] = re.compile(
r"""(?mx)
^
(?P<file>.*?):
(?P<file>(?:[A-Za-z]:)?.*?):
(?P<line>\d+):
(?:(?P<column>-?\d+):)?
\s(?P<severity>\S+?):?
Expand All @@ -59,7 +64,7 @@ def as_posix(name: str) -> str:
INTERNAL_ERROR_RE: Pattern[str] = re.compile(
r"""(?mx)
^
(?P<file>.*?):
(?P<file>(?:[A-Za-z]:)?.*?):
(?P<line>\d+):
\s(?P<severity>\S+?):?
\s(?P<message>INTERNAL\sERROR.*)
Expand Down Expand Up @@ -121,10 +126,14 @@ def check_files(
config: str,
retries: int,
code: str,
extra_mypy_args: Optional[List[str]] = None,
path_map: Optional[Dict[str, str]] = None,
) -> List[LintMessage]:
try:
proc = run_command(
[sys.executable, "-mmypy", f"--config={config}"] + filenames,
[sys.executable, "-mmypy", f"--config={config}"]
+ (extra_mypy_args or [])
+ filenames,
extra_env={},
retries=retries,
)
Expand All @@ -144,9 +153,15 @@ def check_files(
]
stdout = str(proc.stdout, "utf-8").strip()
stderr = str(proc.stderr, "utf-8").strip()

def report_path(path: str) -> str:
if path_map is None:
return path
return path_map.get(_path_key(path), path)

rc = [
LintMessage(
path=match["file"],
path=report_path(match["file"]),
name=match["code"],
description=match["message"],
line=int(match["line"]),
Expand All @@ -163,7 +178,7 @@ def check_files(
for match in RESULTS_RE.finditer(stdout)
] + [
LintMessage(
path=match["file"],
path=report_path(match["file"]),
name="INTERNAL ERROR",
description=match["message"],
line=int(match["line"]),
Expand All @@ -178,6 +193,20 @@ def check_files(
return rc


def make_py_test_mypy_inputs(
filenames: List[str],
tmpdir: str,
) -> Tuple[List[str], Dict[str, str]]:
mypy_filenames: List[str] = []
path_map: Dict[str, str] = {}
for i, filename in enumerate(filenames):
mypy_filename = os.path.join(tmpdir, f"py_test_{i}.py")
Path(mypy_filename).write_text(Path(filename).read_text())
mypy_filenames.append(mypy_filename)
path_map[_path_key(mypy_filename)] = filename
return mypy_filenames, path_map


def main() -> None:
parser = argparse.ArgumentParser(
description="mypy wrapper linter.",
Expand Down Expand Up @@ -238,9 +267,32 @@ def main() -> None:
else:
filenames[filename] = True

lint_messages = check_mypy_installed(args.code) + check_files(
list(filenames), args.config, args.retries, args.code
)
py_filenames = [
filename for filename in filenames if not filename.endswith(".py.test")
]
py_test_filenames = [
filename for filename in filenames if filename.endswith(".py.test")
]

lint_messages = check_mypy_installed(args.code)
if py_filenames:
lint_messages += check_files(py_filenames, args.config, args.retries, args.code)
if py_test_filenames:
with tempfile.TemporaryDirectory() as tmpdir:
mypy_filenames, path_map = make_py_test_mypy_inputs(
py_test_filenames, tmpdir
)
lint_messages += check_files(
mypy_filenames,
args.config,
args.retries,
args.code,
extra_mypy_args=[
"--no-incremental",
"--disable-error-code=top-level-await",
],
path_map=path_map,
)
for lint_message in lint_messages:
print(json.dumps(lint_message._asdict()), flush=True)

Expand Down
Loading