blob: 2fadcba6e0ae7405a9e00b38100e3dc87ca3fecc [file]
import argparse
import contextlib
import pathlib
import os
import shutil
import sys
import tempfile
from typing import Any, Generator, Optional
import mypy.api
import mypy.util
def _merge_upstream_caches(cache_dir: str, upstream_caches: list[str]) -> None:
current = pathlib.Path(cache_dir)
current.mkdir(parents=True, exist_ok=True)
for upstream_dir in upstream_caches:
upstream = pathlib.Path(upstream_dir)
# TODO(mark): maybe there's a more efficient way to synchronize the cache dirs?
for dirpath_str, _, filenames in os.walk(upstream.as_posix()):
dirpath = pathlib.Path(dirpath_str)
relative_dir = dirpath.relative_to(upstream)
for file in filenames:
upstream_path = dirpath / file
target_path = current / relative_dir / file
if not target_path.parent.exists():
target_path.parent.mkdir(parents=True)
if not target_path.exists():
shutil.copy(upstream_path, target_path)
# missing_stubs is mutable, so remove it
missing_stubs = current / "missing_stubs"
if missing_stubs.exists():
missing_stubs.unlink()
@contextlib.contextmanager
def managed_cache_dir(
cache_dir: Optional[str], upstream_caches: list[str]
) -> Generator[str, Any, Any]:
"""
Returns a managed cache directory.
When cache_dir exists, returns a merged view of cache_dir with upstream_caches.
Otherwise, returns a temporary directory that will be cleaned up when the resource
is released.
"""
if cache_dir:
_merge_upstream_caches(cache_dir, list(upstream_caches))
yield cache_dir
else:
tmpdir = tempfile.TemporaryDirectory()
yield tmpdir.name
tmpdir.cleanup()
def run_mypy(
mypy_ini: Optional[str], cache_dir: str, srcs: list[str]
) -> tuple[str, str, int]:
maybe_config = ["--config-file", mypy_ini] if mypy_ini else []
report, errors, status = mypy.api.run(
maybe_config
+ [
# do not check mtime in cache
"--skip-cache-mtime-checks",
# mypy defaults to incremental, but force it on anyway
"--incremental",
# use a known cache-dir
f"--cache-dir={cache_dir}",
# use current dir + MYPYPATH to resolve deps
"--explicit-package-bases",
# speedup
"--fast-module-lookup",
]
+ srcs
)
if status:
sys.stderr.write(errors)
sys.stderr.write(report)
return report, errors, status
def run(
output: Optional[str],
cache_dir: Optional[str],
upstream_caches: list[str],
mypy_ini: Optional[str],
srcs: list[str],
) -> None:
if len(srcs) > 0:
with managed_cache_dir(cache_dir, upstream_caches) as cache_dir:
report, errors, status = run_mypy(mypy_ini, cache_dir, srcs)
else:
report, errors, status = "", "", 0
if output:
with open(output, "w+") as file:
file.write(errors)
file.write(report)
# use mypy's hard_exit to exit without freeing objects, it can be meaningfully
# faster than an orderly shutdown
mypy.util.hard_exit(status)
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--output", required=False)
parser.add_argument("-c", "--cache-dir", required=False)
parser.add_argument("--upstream-cache", required=False, action="append")
parser.add_argument("--mypy-ini", required=False)
parser.add_argument("src", nargs="*")
args = parser.parse_args()
output: Optional[str] = args.output
cache_dir: Optional[str] = args.cache_dir
upstream_cache: list[str] = args.upstream_cache or []
mypy_ini: Optional[str] = args.mypy_ini
srcs: list[str] = args.src
run(output, cache_dir, upstream_cache, mypy_ini, srcs)
if __name__ == "__main__":
main()