115 lines
3.4 KiB
Python
Executable File
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()
|