| import argparse |
| import json |
| import logging |
| import os |
| import re |
| import subprocess |
| import sys |
| import time |
| from enum import Enum |
| from pathlib import Path |
| from typing import Any, Dict, List, NamedTuple, Optional, Pattern |
| |
| |
| IS_WINDOWS: bool = os.name == "nt" |
| |
| |
| def eprint(*args: Any, **kwargs: Any) -> None: |
| print(*args, file=sys.stderr, flush=True, **kwargs) |
| |
| |
| class LintSeverity(str, Enum): |
| ERROR = "error" |
| WARNING = "warning" |
| ADVICE = "advice" |
| DISABLED = "disabled" |
| |
| |
| class LintMessage(NamedTuple): |
| path: Optional[str] |
| line: Optional[int] |
| char: Optional[int] |
| code: str |
| severity: LintSeverity |
| name: str |
| original: Optional[str] |
| replacement: Optional[str] |
| description: Optional[str] |
| |
| |
| def as_posix(name: str) -> str: |
| return name.replace("\\", "/") if IS_WINDOWS else name |
| |
| |
| # tools/linter/flake8_linter.py:15:13: error: Incompatibl...int") [assignment] |
| RESULTS_RE: Pattern[str] = re.compile( |
| r"""(?mx) |
| ^ |
| (?P<file>.*?): |
| (?P<line>\d+): |
| (?:(?P<column>-?\d+):)? |
| \s(?P<severity>\S+?):? |
| \s(?P<message>.*) |
| \s(?P<code>\[.*\]) |
| $ |
| """ |
| ) |
| |
| |
| def run_command( |
| args: List[str], |
| *, |
| extra_env: Optional[Dict[str, str]], |
| retries: int, |
| ) -> "subprocess.CompletedProcess[bytes]": |
| logging.debug("$ %s", " ".join(args)) |
| start_time = time.monotonic() |
| try: |
| return subprocess.run( |
| args, |
| stdout=subprocess.PIPE, |
| stderr=subprocess.PIPE, |
| ) |
| finally: |
| end_time = time.monotonic() |
| logging.debug("took %dms", (end_time - start_time) * 1000) |
| |
| |
| # Severity is either "error" or "note": |
| # https://github.com/python/mypy/blob/8b47a032e1317fb8e3f9a818005a6b63e9bf0311/mypy/errors.py#L46-L47 |
| severities = { |
| "error": LintSeverity.ERROR, |
| "note": LintSeverity.ADVICE, |
| } |
| |
| |
| def check_files( |
| filenames: List[str], |
| config: str, |
| retries: int, |
| ) -> List[LintMessage]: |
| try: |
| proc = run_command( |
| [sys.executable, "-mmypy", f"--config={config}"] + filenames, |
| extra_env={}, |
| retries=retries, |
| ) |
| except OSError as err: |
| return [ |
| LintMessage( |
| path=None, |
| line=None, |
| char=None, |
| code="MYPY", |
| severity=LintSeverity.ERROR, |
| name="command-failed", |
| original=None, |
| replacement=None, |
| description=(f"Failed due to {err.__class__.__name__}:\n{err}"), |
| ) |
| ] |
| stdout = str(proc.stdout, "utf-8").strip() |
| return [ |
| LintMessage( |
| path=match["file"], |
| name=match["code"], |
| description=match["message"], |
| line=int(match["line"]), |
| char=int(match["column"]) |
| if match["column"] is not None and not match["column"].startswith("-") |
| else None, |
| code="MYPY", |
| severity=severities.get(match["severity"], LintSeverity.ERROR), |
| original=None, |
| replacement=None, |
| ) |
| for match in RESULTS_RE.finditer(stdout) |
| ] |
| |
| |
| def main() -> None: |
| parser = argparse.ArgumentParser( |
| description="mypy wrapper linter.", |
| fromfile_prefix_chars="@", |
| ) |
| parser.add_argument( |
| "--retries", |
| default=3, |
| type=int, |
| help="times to retry timed out mypy", |
| ) |
| parser.add_argument( |
| "--config", |
| required=True, |
| help="path to an mypy .ini config file", |
| ) |
| parser.add_argument( |
| "--verbose", |
| action="store_true", |
| help="verbose logging", |
| ) |
| parser.add_argument( |
| "filenames", |
| nargs="+", |
| help="paths to lint", |
| ) |
| args = parser.parse_args() |
| |
| logging.basicConfig( |
| format="<%(threadName)s:%(levelname)s> %(message)s", |
| level=logging.NOTSET |
| if args.verbose |
| else logging.DEBUG |
| if len(args.filenames) < 1000 |
| else logging.INFO, |
| stream=sys.stderr, |
| ) |
| |
| # Use a dictionary here to preserve order. mypy cares about order, |
| # tragically, e.g. https://github.com/python/mypy/issues/2015 |
| filenames: Dict[str, bool] = {} |
| |
| # If a stub file exists, have mypy check it instead of the original file, in |
| # accordance with PEP-484 (see https://www.python.org/dev/peps/pep-0484/#stub-files) |
| for filename in args.filenames: |
| if filename.endswith(".pyi"): |
| filenames[filename] = True |
| continue |
| |
| stub_filename = filename.replace(".py", ".pyi") |
| if Path(stub_filename).exists(): |
| filenames[stub_filename] = True |
| else: |
| filenames[filename] = True |
| |
| lint_messages = check_files(list(filenames), args.config, args.retries) |
| for lint_message in lint_messages: |
| print(json.dumps(lint_message._asdict()), flush=True) |
| |
| |
| if __name__ == "__main__": |
| main() |