| 1 | # SPDX-License-Identifier: Apache-2.0 |
| 2 | # SPDX-FileCopyrightText: Copyright contributors to the vLLM project |
| 3 | """Run mypy on changed files. |
| 4 | |
| 5 | This script is designed to be used as a pre-commit hook. It runs mypy |
| 6 | on files that have been changed. It groups files into different mypy calls |
| 7 | based on their directory to avoid import following issues. |
| 8 | |
| 9 | Usage: |
| 10 | python tools/pre_commit/mypy.py <python_version> <changed_files...> |
| 11 | |
| 12 | Args: |
| 13 | python_version: Python version to use (e.g., "3.10") or "local" to use |
| 14 | the local Python version. |
| 15 | changed_files: List of changed files to check. |
| 16 | |
| 17 | """ |
| 18 | |
| 19 | import subprocess |
| 20 | import sys |
| 21 | |
| 22 | import regex as re |
| 23 | |
| 24 | # Paths verified clean under follow_imports="silent". Matched before |
| 25 | # SEPARATE_GROUPS, so these files join the default group and are checked at the |
| 26 | # stricter setting even while a parent directory remains in SEPARATE_GROUPS. |
| 27 | # |
| 28 | # Fixing a directory means moving it from SEPARATE_GROUPS to here. Without that |
| 29 | # move the fixes are not enforced because "tests" claims every file below it. |
| 30 | SILENT_GROUPS = [ |
| 31 | "tests/benchmarks", |
| 32 | "tests/compile/correctness_e2e", |
| 33 | "tests/compile/fullgraph", |
| 34 | "tests/compile/fusions_e2e", |
| 35 | "tests/compile/passes", |
| 36 | "tests/config", |
| 37 | "tests/entrypoints/anthropic", |
| 38 | "tests/entrypoints/generate", |
| 39 | "tests/entrypoints/multimodal", |
| 40 | "tests/entrypoints/pooling", |
| 41 | "tests/entrypoints/serve", |
| 42 | "tests/entrypoints/tool_parsers", |
| 43 | "tests/entrypoints/unit_tests", |
| 44 | "tests/entrypoints/weight_transfer", |
| 45 | "tests/kernels/attention", |
| 46 | "tests/kernels/core", |
| 47 | "tests/kernels/helion", |
| 48 | "tests/kernels/mamba", |
| 49 | "tests/kernels/quantization", |
| 50 | "tests/model_executor/layers", |
| 51 | "tests/models/language", |
| 52 | "tests/models/quantization", |
| 53 | "tests/multimodal", |
| 54 | "tests/plugins/bge_m3_sparse_plugin", |
| 55 | "tests/plugins/prithvi_io_processor_plugin", |
| 56 | "tests/plugins/vllm_add_dummy_platform", |
| 57 | "tests/plugins/vllm_add_dummy_stat_logger", |
| 58 | "tests/plugins_tests", |
| 59 | "tests/quantization", |
| 60 | "tests/reasoning", |
| 61 | "tests/samplers", |
| 62 | "tests/spec_decode", |
| 63 | "tests/tokenizers_", |
| 64 | "tests/tool_use", |
| 65 | "tests/transformers_utils", |
| 66 | "tests/utils_", |
| 67 | "tests/v1/cudagraph", |
| 68 | "tests/v1/determinism", |
| 69 | "tests/v1/distributed", |
| 70 | "tests/v1/executor", |
| 71 | "tests/v1/logits_processors", |
| 72 | "tests/v1/sample", |
| 73 | "tests/v1/shutdown", |
| 74 | "tests/v1/simple_kv_offload", |
| 75 | "tests/v1/spec_decode", |
| 76 | "tests/v1/structured_output", |
| 77 | ] |
| 78 | |
| 79 | # After fixing errors resulting from changing follow_imports |
| 80 | # from "skip" to "silent", move its directory to SILENT_GROUPS. |
| 81 | SEPARATE_GROUPS = [ |
| 82 | "tests", |
| 83 | "tests/compile", |
| 84 | "tests/distributed", |
| 85 | "tests/entrypoints/llm", |
| 86 | "tests/entrypoints/openai", |
| 87 | "tests/entrypoints/speech_to_text", |
| 88 | "tests/kernels", |
| 89 | "tests/kernels/moe", |
| 90 | "tests/lora", |
| 91 | "tests/model_executor", |
| 92 | "tests/model_executor/model_loader", |
| 93 | "tests/models", |
| 94 | "tests/models/test_initialization.py", |
| 95 | "tests/models/multimodal", |
| 96 | "tests/parser", |
| 97 | "tests/renderers", |
| 98 | "tests/tool_parsers", |
| 99 | "tests/v1", |
| 100 | "tests/v1/attention", |
| 101 | "tests/v1/core", |
| 102 | "tests/v1/e2e", |
| 103 | "tests/v1/ec_connector", |
| 104 | "tests/v1/engine", |
| 105 | "tests/v1/kv_connector", |
| 106 | "tests/v1/kv_offload", |
| 107 | "tests/v1/metrics", |
| 108 | "tests/v1/streaming_input", |
| 109 | "tests/v1/worker", |
| 110 | ] |
| 111 | |
| 112 | |
| 113 | def group_files(changed_files: list[str]) -> dict[str, list[str]]: |
| 114 | """Group changed files into different mypy calls. |
| 115 | |
| 116 | Args: |
| 117 | changed_files: List of changed files. |
| 118 | |
| 119 | Returns: |
| 120 | A dictionary mapping file group names to lists of changed files. |
| 121 | |
| 122 | """ |
| 123 | silent_pattern = re.compile(f"^({'|'.join(SILENT_GROUPS)}).*") |
| 124 | file_groups: dict[str, list[str]] = {"": []} |
| 125 | file_groups.update({k: [] for k in SEPARATE_GROUPS}) |
| 126 | # Longest path first so a sub-directory is not shadowed by its parent |
| 127 | separate_groups = sorted(SEPARATE_GROUPS, key=len, reverse=True) |
| 128 | for changed_file in changed_files: |
| 129 | # Already-fixed paths go in the default group, which runs at the |
| 130 | # stricter follow_imports setting from pyproject.toml |
| 131 | if silent_pattern.match(changed_file): |
| 132 | file_groups[""].append(changed_file) |
| 133 | continue |
| 134 | # Group files by mypy call |
| 135 | for directory in separate_groups: |
| 136 | if re.match(f"^{directory}.*", changed_file): |
| 137 | file_groups[directory].append(changed_file) |
| 138 | break |
| 139 | else: |
| 140 | if changed_file.startswith(("vllm/", "tests/")): |
| 141 | file_groups[""].append(changed_file) |
| 142 | return file_groups |
| 143 | |
| 144 | |
| 145 | def mypy( |
| 146 | targets: list[str], |
| 147 | python_version: str | None, |
| 148 | follow_imports: str | None, |
| 149 | file_group: str, |
| 150 | ) -> int: |
| 151 | """Run mypy on the given targets. |
| 152 | |
| 153 | Args: |
| 154 | targets: List of files or directories to check. |
| 155 | python_version: Python version to use (e.g., "3.10") or None to use |
| 156 | the default mypy version. |
| 157 | follow_imports: Value for the --follow-imports option or None to use |
| 158 | the default mypy behavior. |
| 159 | file_group: The file group name for logging purposes. |
| 160 | |
| 161 | Returns: |
| 162 | The return code from mypy. |
| 163 | |
| 164 | """ |
| 165 | args = ["mypy"] |
| 166 | if python_version is not None: |
| 167 | args += ["--python-version", python_version] |
| 168 | if follow_imports is not None: |
| 169 | args += ["--follow-imports", follow_imports] |
| 170 | print(f"$ {' '.join(args)} {file_group}") |
| 171 | return subprocess.run(args + targets, check=False).returncode |
| 172 | |
| 173 | |
| 174 | def main(): |
| 175 | python_version = sys.argv[1] |
| 176 | file_groups = group_files(sys.argv[2:]) |
| 177 | |
| 178 | if python_version == "local": |
| 179 | python_version = f"{sys.version_info.major}.{sys.version_info.minor}" |
| 180 | |
| 181 | returncode = 0 |
| 182 | for file_group, changed_files in file_groups.items(): |
| 183 | follow_imports = None if file_group == "" else "skip" |
| 184 | if changed_files: |
| 185 | returncode |= mypy( |
| 186 | changed_files, python_version, follow_imports, file_group |
| 187 | ) |
| 188 | return returncode |
| 189 | |
| 190 | |
| 191 | if __name__ == "__main__": |
| 192 | sys.exit(main()) |