enable mypy type checking & fixup some code Seems to be fast enough for this codebase, especially once cached. Change-Id: Id60f4c5fa7971e818f879fbe2548bd501e22e15e Reviewed-on: https://gerrit-review.googlesource.com/c/git-repohooks/+/574161 Reviewed-by: Raul Rangel <rrangel@google.com> Tested-by: Mike Frysinger <vapier@google.com> Commit-Queue: Mike Frysinger <vapier@google.com>
diff --git a/pre-upload.py b/pre-upload.py index cb0cf15..5c566c6 100755 --- a/pre-upload.py +++ b/pre-upload.py
@@ -26,7 +26,7 @@ from pathlib import Path import signal import sys -from typing import List, Optional +from typing import List, Optional, Sequence, Set, Tuple # Assert some minimum Python versions as we don't test or support any others. @@ -70,16 +70,16 @@ # How long a hook is allowed to run before we warn that it is "too slow". _SLOW_HOOK_DURATION = datetime.timedelta(seconds=30) - def __init__(self, project_name): + def __init__(self, project_name: str) -> None: """Create a new Output object for a specified project. Args: project_name: name of project. """ self.project_name = project_name - self.hooks = None - self.num_hooks = None - self.num_commits = None + self.hooks: Optional[Set[rh.hooks.CallableHook]] = None + self.num_hooks = 0 + self.num_commits = 0 self.commit_index = 0 self.success = True self.start_time = datetime.datetime.now() @@ -96,7 +96,12 @@ self.num_commits = num_commits self.commit_index = 1 - def commit_start(self, hooks, commit, commit_summary): + def commit_start( + self, + hooks: List[rh.hooks.CallableHook], + commit: str, + commit_summary: str, + ) -> None: """Emit status for new commit. Args: @@ -117,8 +122,9 @@ self.num_hooks = len(hooks) self.hook_banner() - def hook_banner(self): + def hook_banner(self) -> None: """Display the banner for current set of hooks.""" + assert self.hooks is not None, "Must call commit_start() first" pending = ", ".join(x.name for x in self.hooks) status_line = ( f"[{self.RUNNING} " @@ -130,8 +136,13 @@ status_line = status_line[0 : cols + self._banner_esc_chars] rh.terminal.print_status_line(status_line) - def hook_finish(self, hook, duration): + def hook_finish( + self, + hook: rh.hooks.CallableHook, + duration: datetime.timedelta, + ) -> None: """Finish processing any per-hook state.""" + assert self.hooks is not None, "Must call commit_start() first" self.hooks.remove(hook) if duration >= self._SLOW_HOOK_DURATION: d = rh.utils.timedelta_str(duration) @@ -146,7 +157,7 @@ if self.hooks: self.hook_banner() - def hook_error(self, hook, error): + def hook_error(self, hook: rh.hooks.CallableHook, error: str) -> None: """Print an error for a single hook. Args: @@ -155,7 +166,7 @@ """ self.error(f"{hook.name} hook", error) - def hook_warning(self, hook, warning): + def hook_warning(self, hook: rh.hooks.CallableHook, warning: str) -> None: """Print a warning for a single hook. Args: @@ -185,6 +196,8 @@ ) -> None: """Display summary of possible fixups for a single hook.""" for result in (x for x in hook_results if x.fixup_cmd): + # Workaround mypy unable to peer inside the loop generator above. + assert result.fixup_cmd cmd = result.fixup_cmd + list(result.files) for line in ( f"[{self.FIXUP}] {result.hook} has automated fixups available", @@ -193,7 +206,7 @@ ): rh.terminal.print_status_line(line, print_newline=True) - def finish(self): + def finish(self) -> None: """Print summary for all the hooks.""" header = self.PASSED if self.success else self.FAILED status = "passed" if self.success else "failed" @@ -244,7 +257,7 @@ ) -def _get_project_config(from_git=False): +def _get_project_config(from_git: bool = False) -> rh.config.PreUploadSettings: """Returns the configuration for a project. Expects to be called from within the project root. @@ -254,11 +267,11 @@ be used. """ if from_git: - global_paths = (rh.git.find_repo_root(),) + global_paths: Sequence[str] = (rh.git.find_repo_root(),) else: global_paths = ( # Load the global config found in the manifest repo. - (os.path.join(rh.git.find_repo_root(), ".repo", "manifests")), + os.path.join(rh.git.find_repo_root(), ".repo", "manifests"), # Load the global config found in the root of the repo checkout. rh.git.find_repo_root(), ) @@ -273,7 +286,7 @@ def _attempt_fixes(projects_results: List[rh.results.ProjectResults]) -> None: """Attempts to fix fixable results.""" # Filter out any result that has a fixup. - fixups = [] + fixups: List[Tuple[str, rh.results.HookResult]] = [] for project_results in projects_results: fixups.extend( (project_results.workdir, x) for x in project_results.fixups @@ -314,6 +327,10 @@ # Walk all the fixups and run them one-by-one. for workdir, result in fixups: + # ProjectResults.fixups only yields results that have a fixup command, + # but mypy is not able to see that extended logic. + assert result.fixup_cmd + if mode == "some": if not rh.terminal.boolean_prompt( f"Run {result.hook} fixup for {result.commit}" @@ -507,7 +524,9 @@ if not proj_dirs: print(f"{project_name} cannot be found.", file=sys.stderr) print("Please specify a valid project.", file=sys.stderr) - return False + return rh.results.ProjectResults( + project_name, "", [], internal_failure=True + ) if len(proj_dirs) > 1: print( f"{project_name} is associated with multiple directories.", @@ -517,7 +536,9 @@ "Please specify a directory to help disambiguate.", file=sys.stderr, ) - return False + return rh.results.ProjectResults( + project_name, "", [], internal_failure=True + ) proj_dir = proj_dirs[0] pwd = os.getcwd() @@ -608,7 +629,7 @@ sys.exit(1) -def _identify_project(path, from_git=False): +def _identify_project(path: str, from_git: bool = False) -> str: """Identify the repo project associated with the given path. Returns: @@ -642,7 +663,7 @@ return rh.utils.run(cmd, capture_output=True, cwd=path).stdout.strip() -def direct_main(argv): +def direct_main(argv: List[str]) -> int: """Run hooks directly (outside of the context of repo). Args:
diff --git a/rh/config.py b/rh/config.py index c421485..10686da 100644 --- a/rh/config.py +++ b/rh/config.py
@@ -14,6 +14,8 @@ """Manage various config files.""" +from __future__ import annotations + import configparser import functools import itertools @@ -21,6 +23,7 @@ from pathlib import Path import shlex import sys +from typing import Dict, Iterable, Iterator, List, Optional, Sequence THIS_FILE = Path(__file__).resolve() @@ -98,7 +101,11 @@ OPTION_IGNORE_MERGED_COMMITS = "ignore_merged_commits" VALID_OPTIONS = {OPTION_IGNORE_MERGED_COMMITS} - def __init__(self, config=None, source=None): + def __init__( + self, + config: Optional[RawConfigParser] = None, + source: Optional[str] = None, + ) -> None: """Initialize. Args: @@ -112,26 +119,26 @@ self._validate() @property - def custom_hooks(self): + def custom_hooks(self) -> List[str]: """List of custom hooks to run (their keys/names).""" return self.config.options(self.CUSTOM_HOOKS_SECTION, []) - def custom_hook(self, hook): + def custom_hook(self, hook: str) -> List[str]: """The command to execute for |hook|.""" return shlex.split( self.config.get(self.CUSTOM_HOOKS_SECTION, hook, fallback="") ) @property - def builtin_hooks(self): + def builtin_hooks(self) -> List[str]: """List of all enabled builtin hooks (their keys/names).""" return [ k for k, v in self.config.items(self.BUILTIN_HOOKS_SECTION, ()) - if rh.shell.boolean_shell_value(v, None) + if rh.shell.boolean_shell_value(v, False) ] - def builtin_hook_option(self, hook): + def builtin_hook_option(self, hook: str) -> List[str]: """The options to pass to |hook|.""" return shlex.split( self.config.get( @@ -139,7 +146,7 @@ ) ) - def builtin_hook_exclude_paths(self, hook): + def builtin_hook_exclude_paths(self, hook: str) -> List[str]: """List of paths for which |hook| should not be executed.""" return shlex.split( self.config.get( @@ -148,11 +155,11 @@ ) @property - def tool_paths(self): + def tool_paths(self) -> Dict[str, str]: """List of all tool paths.""" return dict(self.config.items(self.TOOL_PATHS_SECTION, ())) - def callable_custom_hooks(self): + def callable_custom_hooks(self) -> Iterator[rh.hooks.CallableHook]: """Yield a CallableHook for each hook to be executed.""" scope = rh.hooks.ExclusionScope([]) for hook in self.custom_hooks: @@ -162,7 +169,7 @@ func = functools.partial(rh.hooks.check_custom, options=options) yield rh.hooks.CallableHook(hook, func, scope) - def callable_builtin_hooks(self): + def callable_builtin_hooks(self) -> Iterator[rh.hooks.CallableHook]: """Yield a CallableHook for each hook to be executed.""" scope = rh.hooks.ExclusionScope([]) for hook in self.builtin_hooks: @@ -178,7 +185,7 @@ yield rh.hooks.CallableHook(hook, func, scope) @property - def ignore_merged_commits(self): + def ignore_merged_commits(self) -> bool: """Whether to skip hooks for merged commits.""" return rh.shell.boolean_shell_value( self.config.get( @@ -193,7 +200,7 @@ """Merge settings from |preupload_config| into ourself.""" self.config.read_dict(preupload_config.config) - def _validate(self): + def _validate(self) -> None: """Run consistency checks on the config settings.""" config = self.config @@ -282,9 +289,9 @@ path: The path of the file. """ - FILENAME = None + FILENAME: str - def __init__(self, path): + def __init__(self, path: str) -> None: """Initialize. Args: @@ -301,7 +308,7 @@ self._validate() @classmethod - def from_paths(cls, paths): + def from_paths(cls, paths: Iterable[str]) -> Iterator["PreUploadFile"]: """Search for files within paths that matches the class FILENAME. Args: @@ -321,7 +328,7 @@ FILENAME = "PREUPLOAD.cfg" - def _validate(self): + def _validate(self) -> None: super()._validate() # Reject Exclude Paths section for local config. @@ -345,7 +352,11 @@ settings for a particular project. """ - def __init__(self, paths=("",), global_paths=()): + def __init__( + self, + paths: Sequence[str] = ("",), + global_paths: Sequence[str] = (), + ) -> None: """Initialize. All the config files found will be merged together in order.
diff --git a/rh/git.py b/rh/git.py index f6fde73..072bbb6 100644 --- a/rh/git.py +++ b/rh/git.py
@@ -18,6 +18,7 @@ from pathlib import Path import re import sys +from typing import List, Optional, Union THIS_FILE = Path(__file__).resolve() @@ -28,7 +29,7 @@ import rh.utils -def get_upstream_remote(): +def get_upstream_remote() -> str: """Returns the current upstream remote name.""" # First get the current branch name. cmd = ["git", "rev-parse", "--abbrev-ref", "HEAD"] @@ -41,7 +42,7 @@ return result.stdout.strip() -def get_upstream_branch(): +def get_upstream_branch() -> str: """Returns the upstream tracking branch of the current branch. Raises: @@ -69,14 +70,14 @@ return full_upstream.replace("heads", "remotes/" + remote) -def get_commit_for_ref(ref): +def get_commit_for_ref(ref: str) -> str: """Returns the latest commit for this ref.""" cmd = ["git", "rev-parse", ref] result = rh.utils.run(cmd, capture_output=True) return result.stdout.strip() -def get_remote_revision(ref, remote): +def get_remote_revision(ref: str, remote: str) -> str: """Returns the remote revision for this ref.""" prefix = f"refs/remotes/{remote}/" if ref.startswith(prefix): @@ -84,13 +85,13 @@ return ref -def get_patch(commit): +def get_patch(commit: str) -> str: """Returns the patch for this commit.""" cmd = ["git", "format-patch", "--stdout", "-1", commit] return rh.utils.run(cmd, capture_output=True).stdout -def get_file_content(commit, path): +def get_file_content(commit: str, path: str) -> str: """Returns the content of a file at a specific commit. We can't rely on the file as it exists in the filesystem as people might be @@ -110,16 +111,16 @@ # pylint: disable=redefined-builtin def __init__( self, - src_mode=0, - dst_mode=0, - src_sha=None, - dst_sha=None, - status=None, - score=None, - src_file=None, - dst_file=None, - file=None, - ): + src_mode: Union[str, int] = 0, + dst_mode: Union[str, int] = 0, + src_sha: Optional[str] = None, + dst_sha: Optional[str] = None, + status: Optional[str] = None, + score: Optional[str] = None, + src_file: Optional[str] = None, + dst_file: Optional[str] = None, + file: Optional[str] = None, + ) -> None: self.src_mode = src_mode self.dst_mode = dst_mode self.src_sha = src_sha @@ -140,7 +141,7 @@ ) -def raw_diff(path, target): +def raw_diff(path: str, target: str) -> List[RawDiffEntry]: """Return the parsed raw format diff of target Args: @@ -170,7 +171,7 @@ return entries -def get_affected_files(commit): +def get_affected_files(commit: str) -> List[RawDiffEntry]: """Returns list of file paths that were modified/added. Returns: @@ -179,7 +180,7 @@ return raw_diff(os.getcwd(), f"{commit}^-") -def get_commits(ignore_merged_commits=False): +def get_commits(ignore_merged_commits=False) -> List[str]: """Returns a list of commits for this review.""" cmd = ["git", "rev-list", f"{get_upstream_branch()}.."] if ignore_merged_commits: @@ -187,16 +188,17 @@ return rh.utils.run(cmd, capture_output=True).stdout.split() -def get_commit_desc(commit): +def get_commit_desc(commit: str) -> str: """Returns the full commit message of a commit.""" cmd = ["git", "diff-tree", "-s", "--always", "--format=%B", commit] return rh.utils.run(cmd, capture_output=True).stdout -def find_repo_root(path=None, outer=False): +def find_repo_root(path: Optional[str] = None, outer: bool = False) -> str: """Locate the top level of this repo checkout starting at |path|. Args: + path: Path under repo checkout to search; defaults to the cwd. outer: Whether to find the outermost manifest, or the sub-manifest. """ if path is None: @@ -235,7 +237,7 @@ return path -def is_git_repository(path): +def is_git_repository(path: str) -> bool: """Returns True if the path is a valid git repository.""" cmd = ["git", "rev-parse", "--resolve-git-dir", os.path.join(path, ".git")] result = rh.utils.run(cmd, capture_output=True, check=False)
diff --git a/rh/results.py b/rh/results.py index a3b95d2..75a6316 100644 --- a/rh/results.py +++ b/rh/results.py
@@ -16,27 +16,30 @@ from pathlib import Path import sys -from typing import List, NamedTuple, Optional +from typing import Iterable, Iterator, List, NamedTuple, Optional THIS_FILE = Path(__file__).resolve() THIS_DIR = THIS_FILE.parent sys.path.insert(0, str(THIS_DIR.parent)) +# pylint: disable=wrong-import-position +import rh.utils + class HookResult(object): """A single hook result.""" def __init__( self, - hook, - project, - commit, - error, + hook: str, + project: str, + commit: str, + error: str, warning: bool = False, - files=(), + files: Iterable[str] = (), fixup_cmd: Optional[List[str]] = None, - ): + ) -> None: """Initialize. Args: @@ -59,11 +62,11 @@ self.files = files self.fixup_cmd = fixup_cmd - def __bool__(self): + def __bool__(self) -> bool: """Whether this result is an error.""" return bool(self.error) and not self.is_warning() - def is_warning(self): + def is_warning(self) -> bool: """Whether this result is a non-fatal warning.""" return self._warning @@ -73,14 +76,14 @@ def __init__( self, - hook, - project, - commit, - result, + hook: str, + project: str, + commit: str, + result: rh.utils.CompletedProcess, warning: bool = False, - files=(), + files: Iterable[str] = (), fixup_cmd: Optional[List[str]] = None, - ): + ) -> None: HookResult.__init__( self, hook, @@ -93,11 +96,11 @@ ) self.result = result - def __bool__(self): + def __bool__(self) -> bool: """Whether this result is an error.""" return not self.is_warning() and self.result.returncode not in (None, 0) - def is_warning(self): + def is_warning(self) -> bool: """Whether this result is a non-fatal warning.""" return self._warning or self.result.returncode == 77 @@ -121,12 +124,12 @@ self.results.extend(results) @property - def fixups(self): + def fixups(self) -> Iterator[HookResult]: """Yield results that have a fixup available.""" yield from ( x for x in self.results if (x or x.is_warning()) and x.fixup_cmd ) - def __bool__(self): + def __bool__(self) -> bool: """Whether there are any errors in this set of results.""" return self.internal_failure or any(self.results)
diff --git a/rh/shell.py b/rh/shell.py index 1d51197..cccac74 100644 --- a/rh/shell.py +++ b/rh/shell.py
@@ -17,6 +17,7 @@ import pathlib from pathlib import Path import sys +from typing import Iterable, Optional, Union THIS_FILE = Path(__file__).resolve() @@ -39,7 +40,10 @@ _SHELL_ESCAPE_CHARS = r"\"`$" -def quote(s): +_SHELL_QUOTABLE_T = Union[bytes, str, Path] + + +def quote(s: _SHELL_QUOTABLE_T) -> str: """Quote |s| in a way that is safe for use in a shell. We aim to be safe, but also to produce "nice" output. That means we don't @@ -68,7 +72,7 @@ """ # If callers pass down bad types, don't blow up. if isinstance(s, bytes): - s = s.encode("utf-8") + s = s.decode("utf-8") elif isinstance(s, pathlib.PurePath): return str(s) elif not isinstance(s, str): @@ -93,7 +97,7 @@ return f'"{s}"' -def unquote(s): +def unquote(s: str) -> str: """Do the opposite of ShellQuote. This function assumes that the input is a valid escaped string. @@ -126,7 +130,7 @@ return output + s[i] if i < len(s) else output -def cmd_to_str(cmd): +def cmd_to_str(cmd: Iterable[_SHELL_QUOTABLE_T]) -> str: """Translate a command list into a space-separated string. The resulting string should be suitable for logging messages and for @@ -151,7 +155,7 @@ return " ".join(quote(arg) for arg in cmd) -def boolean_shell_value(sval, default): +def boolean_shell_value(sval: Optional[str], default: bool) -> bool: """See if |sval| is a value users typically consider as boolean.""" if sval is None: return default
diff --git a/rh/signals.py b/rh/signals.py index a7df902..40ec2ba 100644 --- a/rh/signals.py +++ b/rh/signals.py
@@ -17,6 +17,7 @@ from pathlib import Path import signal import sys +from typing import Any, Callable, Union THIS_FILE = Path(__file__).resolve() @@ -24,18 +25,24 @@ sys.path.insert(0, str(THIS_DIR.parent)) -def relay_signal(handler, signum, frame): +def relay_signal( + handler: Union[Callable[[int, Any], Any], int, None], + signum: int, + frame: Any, +) -> bool: """Notify a listener returned from getsignal of receipt of a signal. Returns: True if it was relayed to the target, False otherwise. False in particular occurs if the target isn't relayable. """ - if handler in (None, signal.SIG_IGN): + if handler is None or handler == signal.SIG_IGN: return True - if handler == signal.SIG_DFL: + elif handler == signal.SIG_DFL: # This scenario is a fairly painful to handle fully, thus we just # state we couldn't handle it and leave it to client code. return False + elif isinstance(handler, int): + raise ValueError(f"Unknown handler '{handler}'") handler(signum, frame) return True
diff --git a/rh/terminal.py b/rh/terminal.py index 105aec6..2c4d574 100644 --- a/rh/terminal.py +++ b/rh/terminal.py
@@ -20,7 +20,7 @@ import os from pathlib import Path import sys -from typing import List, Optional +from typing import Iterable, Optional THIS_FILE = Path(__file__).resolve() @@ -46,7 +46,7 @@ BOLD_START = "\033[1m" RESET = "\033[m" - def __init__(self, enabled=None): + def __init__(self, enabled: Optional[bool] = None) -> None: """Create a new Color object, optionally disabling color output. Args: @@ -55,7 +55,7 @@ """ self._enabled = enabled - def start(self, color): + def start(self, color: int) -> str: """Returns a start color code. Args: @@ -69,7 +69,7 @@ return self.COLOR_START % (color + 30) return "" - def stop(self): + def stop(self) -> str: """Returns a stop color code. Returns: @@ -80,7 +80,7 @@ return self.RESET return "" - def color(self, color, text): + def color(self, color: int, text: str) -> str: """Returns text with conditionally added color escape sequences. Args: @@ -102,7 +102,7 @@ return start + text + self.RESET @property - def enabled(self): + def enabled(self) -> bool: """See if the colorization is enabled.""" if self._enabled is None: if "NOCOLOR" in os.environ: @@ -114,7 +114,7 @@ return self._enabled -def print_status_line(line, print_newline=False): +def print_status_line(line: str, print_newline: bool = False) -> None: """Clears the current terminal line, and prints |line|. Args: @@ -134,14 +134,15 @@ def str_prompt( prompt: str, - choices: List[str], + choices: Iterable[str], lower: bool = True, ) -> Optional[str]: """Helper function for processing user input. Args: - prompt: The question to present to the user. - lower: Whether to lowercase the response. + prompt: The question to present to the user. + choices: What choices to provide to the user. + lower: Whether to lowercase the response. Returns: The string the user entered, or None if EOF (e.g. Ctrl+D). @@ -161,12 +162,12 @@ def boolean_prompt( - prompt="Do you want to continue?", - default=True, - true_value="yes", - false_value="no", - prolog=None, -): + prompt: str = "Do you want to continue?", + default: bool = True, + true_value: str = "yes", + false_value: str = "no", + prolog: Optional[str] = None, +) -> bool: """Helper function for processing boolean choice prompts. Args:
diff --git a/rh/utils.py b/rh/utils.py index fc20301..25a0e75 100644 --- a/rh/utils.py +++ b/rh/utils.py
@@ -23,6 +23,7 @@ import sys import tempfile import time +from typing import IO, Optional, Sequence, Union THIS_FILE = Path(__file__).resolve() @@ -255,7 +256,7 @@ # We use the keyword arg |input| which trips up pylint checks. # pylint: disable=redefined-builtin def run( - cmd, + cmd: Sequence[Union[str, Path]], redirect_stdout=False, redirect_stderr=False, cwd=None, @@ -306,8 +307,8 @@ redirect_stdout, redirect_stderr = True, True # Set default for variables. - popen_stdout = None - popen_stderr = None + popen_stdout: Optional[IO[bytes]] = None + popen_stderr: Optional[Union[int, IO[bytes]]] = None stdin = None result = CompletedProcess() @@ -315,7 +316,7 @@ # a self-explanatory exception will be thrown. kill_timeout = float(kill_timeout) - def _get_tempfile(): + def _get_tempfile() -> IO[bytes]: try: return tempfile.TemporaryFile(buffering=0) except EnvironmentError as e: @@ -431,14 +432,12 @@ if popen_stdout: # The linter is confused by how stdout is a file & an int. - # pylint: disable=maybe-no-member,no-member popen_stdout.seek(0) result.stdout = popen_stdout.read() popen_stdout.close() - if popen_stderr and popen_stderr != subprocess.STDOUT: + if popen_stderr and not isinstance(popen_stderr, int): # The linter is confused by how stderr is a file & an int. - # pylint: disable=maybe-no-member,no-member popen_stderr.seek(0) result.stderr = popen_stderr.read() popen_stderr.close()
diff --git a/run_tests b/run_tests index 8749c2f..91e59f6 100755 --- a/run_tests +++ b/run_tests
@@ -67,7 +67,7 @@ "run_tests", "tools/cpplint.py-update", ] - argv = ["--diff", "--check", ROOT_DIR] + extra_programs + argv = ["--diff", "--check", str(ROOT_DIR)] + extra_programs log_cmd("black", argv) return subprocess.run( [sys.executable, "-m", "black"] + argv, @@ -102,7 +102,7 @@ ).returncode -def run_isort(): +def run_isort() -> int: """Returns the exit code from isort.""" argv = ["--version-number"] log_cmd("isort", argv) @@ -112,7 +112,7 @@ cwd=ROOT_DIR, ) - argv = ["--check", ROOT_DIR] + argv = ["--check", str(ROOT_DIR)] log_cmd("isort", argv) return subprocess.run( [sys.executable, "-m", "isort"] + argv, @@ -121,13 +121,33 @@ ).returncode -def main(argv): +def run_mypy() -> int: + """Returns the exit code from mypy.""" + argv = ["--version"] + log_cmd("mypy", argv) + subprocess.run( + [sys.executable, "-m", "mypy"] + argv, + check=True, + cwd=ROOT_DIR, + ) + + argv = ["rh", "pre-upload.py", "run_tests"] + log_cmd("mypy", argv) + return subprocess.run( + [sys.executable, "-m", "mypy"] + argv, + check=False, + cwd=ROOT_DIR, + ).returncode + + +def main(argv: List[str]) -> int: """The main entry.""" checks = ( - functools.partial(run_pytest, argv), + lambda: run_pytest(argv), run_black, run_pylint, run_isort, + run_mypy, ) # Run all the tests all the time to get full feedback. Don't exit on the # first error as that makes it more difficult to iterate in the CQ.
diff --git a/run_tests.vpython3 b/run_tests.vpython3 index dd3f4df..5bc3942 100644 --- a/run_tests.vpython3 +++ b/run_tests.vpython3
@@ -62,7 +62,7 @@ # Required by black==25.1.0 wheel: < name: "infra/python/wheels/mypy-extensions-py3" - version: "version:0.4.3" + version: "version:1.0.0" > # Required by black==25.1.0 @@ -128,3 +128,8 @@ name: "infra/python/wheels/dill-py3" version: "version:0.3.7" > + +wheel: < + name: "infra/python/wheels/mypy-py3" + version: "version:1.2.0" +>