from __future__ import annotations import os.path import re import shutil import subprocess import sys import tempfile # Examples: # Cannot find implementation or library stub for module named "codeowners" # Skipping analyzing "uwsgidecorators": module is installed, but missing library stubs or py.typed marker PAT = re.compile('(?:library stub for module named |Skipping analyzing )"([^"]+)"') def _format_mods(mods: list[str]) -> str: return "".join(f' "{mod}",\n' for mod in mods) def main() -> int: shutil.rmtree(".mypy_cache", ignore_errors=True) with open("pyproject.toml") as f: contents = f.read() msg_stubs = "missing 3rd party stubs" before, stubs_begin, rest = contents.partition(f"# begin: {msg_stubs}\n") _, stubs_end, rest = rest.partition(f"# end: {msg_stubs}\n") msg_ignore = "sentry modules with typing issues" between, ignore_begin, rest = rest.partition(f"# begin: {msg_ignore}\n") ignore, ignore_end, rest = rest.partition(f"# end: {msg_ignore}\n") with tempfile.TemporaryDirectory() as tmpdir: cfg = os.path.join(tmpdir, "mypy.toml") with open(cfg, "w") as f: f.write(before + stubs_begin + stubs_end + between + ignore_begin + ignore_end + rest) seen = set() out = subprocess.run(("mypy", "--config", cfg, *sys.argv[1:]), capture_output=True) for line in out.stdout.decode().splitlines(): match = PAT.search(line) if match is not None and match[1] not in seen: seen.add(match[1]) mods: list[str] = [] for mod in sorted(seen): if not mods or not mod.startswith(f"{mods[-1]}."): mods.append(mod) mods_s = "".join(f' "{mod}.*",\n' for mod in mods) stubs = ( f"# - add .pyi files to fixtures/stubs-for-mypy\n" f"# - or find a 3rd party stub\n" f"[[tool.mypy.overrides]]\n" f"module = [\n{mods_s}]\n" f"ignore_missing_imports = true\n" ) with open("pyproject.toml", "w") as f: f.write( before + stubs_begin + stubs + stubs_end + between + ignore_begin + ignore + ignore_end + rest ) return 0 if __name__ == "__main__": raise SystemExit(main())