Files
2026-07-13 13:39:38 +08:00

115 lines
3.4 KiB
Python
Executable File

#!/usr/bin/env python3
"""Run mypy type checking on all livekit packages.
Auto-discovers all plugin packages in livekit-plugins/ and runs mypy on them.
Uses mypy's incremental mode (.mypy_cache) for fast re-checks after the first run.
Passes given arguments to mypy. Arguments after `--` passed to `uv run`.
Third-party type stubs are declared in the `typing` dependency group in
pyproject.toml and locked in uv.lock. We intentionally do NOT use
`mypy --install-types`: it requires pip, installs unpinned stubs,
and requires a forward pass in most cases.
When a dependency introduces a stub not declared yet, mypy records
the complete set of stub packages it wants in `.mypy_cache/missing_stubs`,
the same list `--install-types` consumes.
See https://github.com/python/mypy/issues/10600#issuecomment-2481074163.
We read that file for the full set, then fail with the
exact `uv add` command to declare and lock them. The script never installs stubs
itself, so every run is a single deterministic pass..
"""
import subprocess
import sys
from pathlib import Path
# Plugins to exclude from type checking
EXCLUDED_PLUGINS = [
"browser",
"nvidia",
"rtzr",
]
# mypy records the full set of stub packages it wants here, one per line.
_MISSING_STUBS = ".mypy_cache/missing_stubs"
INSTALL_STUBS_MESSAGE = """
check_types: mypy needs type stubs that aren't currently installed.
Make sure to add them to the `typing` group and lock them with:
uv add --group typing {}
"""
def get_packages(repo_root: Path) -> list[str]:
"""Return all packages to type-check."""
packages = ["livekit.agents"]
plugins_dir = repo_root / "livekit-plugins"
for plugin_dir in sorted(plugins_dir.glob("livekit-plugins-*")):
if plugin_dir.is_dir():
# livekit-plugins-openai -> openai
# livekit-plugins-turn-detector -> turn_detector
plugin_name = plugin_dir.name.replace("livekit-plugins-", "").replace("-", "_")
if plugin_name in EXCLUDED_PLUGINS:
continue
packages.append(f"livekit.plugins.{plugin_name}")
return packages
def read_missing_stubs(repo_root: Path) -> list[str]:
"""The full stub-package list mypy recorded in .mypy_cache/missing_stubs."""
marker = repo_root / _MISSING_STUBS
if not marker.exists():
return []
return sorted(set(map(str.strip, marker.read_text().splitlines())))
def main() -> None:
"""
Command:
`python scripts/check_types.py --verbose -- --no-sync`
Translates to:
`uv run --no-sync mypy ... --verbose`
"""
repo_root = Path(__file__).parent.parent
packages = get_packages(repo_root)
try:
mypy_args_end = sys.argv.index("--")
except ValueError:
mypy_args, uv_run_args = sys.argv[1:], []
else:
mypy_args, uv_run_args = sys.argv[1:mypy_args_end], sys.argv[mypy_args_end + 1 :]
pkg_args: list[str] = []
for pkg in packages:
pkg_args.extend(["-p", pkg])
command = [
"uv",
"run",
"--group",
"typing",
*uv_run_args,
"mypy",
"--untyped-calls-exclude=smithy_aws_core",
*pkg_args,
*mypy_args,
]
print(*command, "\n")
returncode = subprocess.run(command, cwd=repo_root).returncode
if returncode and (missing := read_missing_stubs(repo_root)):
print(INSTALL_STUBS_MESSAGE.format(" ".join(missing)), file=sys.stderr)
sys.exit(returncode)
if __name__ == "__main__":
main()