Skip to content

Commit bfddfde

Browse files
zklauspytorchmergebot
authored andcommitted
Add basic spin config and linting commands (pytorch#167226)
This PR adds a basic spin configuration to allow for linting. It is designed as a drop-in replacement for the current Makefile based solution, i.e. it sets up and updates lintrunner based on the hashes of certain configuration files. Lintrunner is called via Uv's `uvx` command, separating its environment from the general development environment in an effort to reduce instances of competing requirements breaking environments. Pull Request resolved: pytorch#167226 Approved by: https://github.com/atalman, https://github.com/albanD
1 parent b657061 commit bfddfde

File tree

3 files changed

+347
-0
lines changed

3 files changed

+347
-0
lines changed

.spin/cmds.py

Lines changed: 330 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,330 @@
1+
import hashlib
2+
import subprocess
3+
import sys
4+
from pathlib import Path
5+
6+
import click
7+
import spin
8+
9+
10+
def file_digest(file, algorithm: str):
11+
try:
12+
return hashlib.file_digest(file, algorithm)
13+
except AttributeError:
14+
pass # Fallback to manual implementation below
15+
hash = hashlib.new(algorithm)
16+
while chunk := file.read(8192):
17+
hash.update(chunk)
18+
return hash
19+
20+
21+
def _hash_file(file):
22+
with open(file, "rb") as f:
23+
hash = file_digest(f, "sha256")
24+
return hash.hexdigest()
25+
26+
27+
def _hash_files(files):
28+
hashes = {file: _hash_file(file) for file in files}
29+
return hashes
30+
31+
32+
def _read_hashes(hash_file: Path):
33+
if not hash_file.exists():
34+
return {}
35+
with hash_file.open("r") as f:
36+
lines = f.readlines()
37+
hashes = {}
38+
for line in lines:
39+
hash = line[:64]
40+
file = line[66:].strip()
41+
hashes[file] = hash
42+
return hashes
43+
44+
45+
def _updated_hashes(hash_file, files_to_hash):
46+
old_hashes = _read_hashes(hash_file)
47+
new_hashes = _hash_files(files_to_hash)
48+
if new_hashes != old_hashes:
49+
return new_hashes
50+
return None
51+
52+
53+
@click.command()
54+
def regenerate_version():
55+
"""Regenerate version.py."""
56+
cmd = [
57+
sys.executable,
58+
"-m",
59+
"tools.generate_torch_version",
60+
"--is-debug=false",
61+
]
62+
spin.util.run(cmd)
63+
64+
65+
TYPE_STUBS = [
66+
(
67+
"Pytorch type stubs",
68+
Path(".lintbin/.pytorch-type-stubs.sha256"),
69+
[
70+
"aten/src/ATen/native/native_functions.yaml",
71+
"aten/src/ATen/native/tags.yaml",
72+
"tools/autograd/deprecated.yaml",
73+
],
74+
[
75+
sys.executable,
76+
"-m",
77+
"tools.pyi.gen_pyi",
78+
"--native-functions-path",
79+
"aten/src/ATen/native/native_functions.yaml",
80+
"--tags-path",
81+
"aten/src/ATen/native/tags.yaml",
82+
"--deprecated-functions-path",
83+
"tools/autograd/deprecated.yaml",
84+
],
85+
),
86+
(
87+
"Datapipes type stubs",
88+
None,
89+
[],
90+
[
91+
sys.executable,
92+
"torch/utils/data/datapipes/gen_pyi.py",
93+
],
94+
),
95+
]
96+
97+
98+
@click.command()
99+
def regenerate_type_stubs():
100+
"""Regenerate type stubs."""
101+
for name, hash_file, files_to_hash, cmd in TYPE_STUBS:
102+
if hash_file:
103+
if hashes := _updated_hashes(hash_file, files_to_hash):
104+
click.echo(
105+
f"Changes detected in type stub files for {name}. Regenerating..."
106+
)
107+
spin.util.run(cmd)
108+
hash_file.parent.mkdir(parents=True, exist_ok=True)
109+
with hash_file.open("w") as f:
110+
for file, hash in hashes.items():
111+
f.write(f"{hash} {file}\n")
112+
click.echo("Type stubs and hashes updated.")
113+
else:
114+
click.echo(f"No changes detected in type stub files for {name}.")
115+
else:
116+
click.echo(f"No hash file for {name}. Regenerating...")
117+
spin.util.run(cmd)
118+
click.echo("Type stubs regenerated.")
119+
120+
121+
@click.command()
122+
def regenerate_clangtidy_files():
123+
"""Regenerate clang-tidy files."""
124+
cmd = [
125+
sys.executable,
126+
"-m",
127+
"tools.linter.clang_tidy.generate_build_files",
128+
]
129+
spin.util.run(cmd)
130+
131+
132+
#: These linters are expected to need less than 3s cpu time total
133+
VERY_FAST_LINTERS = {
134+
"ATEN_CPU_GPU_AGNOSTIC",
135+
"BAZEL_LINTER",
136+
"C10_NODISCARD",
137+
"C10_UNUSED",
138+
"CALL_ONCE",
139+
"CMAKE_MINIMUM_REQUIRED",
140+
"CONTEXT_DECORATOR",
141+
"COPYRIGHT",
142+
"CUBINCLUDE",
143+
"DEPLOY_DETECTION",
144+
"ERROR_PRONE_ISINSTANCE",
145+
"EXEC",
146+
"HEADER_ONLY_LINTER",
147+
"IMPORT_LINTER",
148+
"INCLUDE",
149+
"LINTRUNNER_VERSION",
150+
"MERGE_CONFLICTLESS_CSV",
151+
"META_NO_CREATE_UNBACKED",
152+
"NEWLINE",
153+
"NOQA",
154+
"NO_WORKFLOWS_ON_FORK",
155+
"ONCE_FLAG",
156+
"PYBIND11_INCLUDE",
157+
"PYBIND11_SPECIALIZATION",
158+
"PYPIDEP",
159+
"PYPROJECT",
160+
"RAWCUDA",
161+
"RAWCUDADEVICE",
162+
"ROOT_LOGGING",
163+
"TABS",
164+
"TESTOWNERS",
165+
"TYPEIGNORE",
166+
"TYPENOSKIP",
167+
"WORKFLOWSYNC",
168+
}
169+
170+
171+
#: These linters are expected to take a few seconds, but less than 10s cpu time total
172+
FAST_LINTERS = {
173+
"CMAKE",
174+
"DOCSTRING_LINTER",
175+
"GHA",
176+
"NATIVEFUNCTIONS",
177+
"RUFF",
178+
"SET_LINTER",
179+
"SHELLCHECK",
180+
"SPACES",
181+
}
182+
183+
184+
#: These linters are expected to take more than 10s cpu time total;
185+
#: some need more than 1 hour.
186+
SLOW_LINTERS = {
187+
"ACTIONLINT",
188+
"CLANGFORMAT",
189+
"CLANGTIDY",
190+
"CODESPELL",
191+
"FLAKE8",
192+
"GB_REGISTRY",
193+
"PYFMT",
194+
"PYREFLY",
195+
"TEST_DEVICE_BIAS",
196+
"TEST_HAS_MAIN",
197+
}
198+
199+
200+
ALL_LINTERS = VERY_FAST_LINTERS | FAST_LINTERS | SLOW_LINTERS
201+
202+
203+
LINTRUNNER_CACHE_INFO = (
204+
Path(".lintbin/.lintrunner.sha256"),
205+
[
206+
"requirements.txt",
207+
"pyproject.toml",
208+
".lintrunner.toml",
209+
],
210+
)
211+
212+
213+
LINTRUNNER_BASE_CMD = [
214+
"uvx",
215+
"--python",
216+
"3.10",
217+
218+
]
219+
220+
221+
@click.command()
222+
def setup_lint():
223+
"""Set up lintrunner with current CI version."""
224+
cmd = LINTRUNNER_BASE_CMD + ["init"]
225+
subprocess.run(cmd, check=True, capture_output=True, text=True)
226+
227+
228+
def _check_linters():
229+
cmd = LINTRUNNER_BASE_CMD + ["list"]
230+
ret = spin.util.run(cmd, output=False, stderr=subprocess.PIPE)
231+
linters = {l.strip() for l in ret.stdout.decode().strip().split("\n")[1:]}
232+
unknown_linters = linters - ALL_LINTERS
233+
missing_linters = ALL_LINTERS - linters
234+
if unknown_linters:
235+
click.secho(
236+
f"Unknown linters found; please add them to the correct category "
237+
f"in .spin/cmds.py: {', '.join(unknown_linters)}",
238+
fg="yellow",
239+
)
240+
if missing_linters:
241+
click.secho(
242+
f"Missing linters found; please update the corresponding category "
243+
f"in .spin/cmds.py: {', '.join(missing_linters)}",
244+
fg="yellow",
245+
)
246+
return unknown_linters, missing_linters
247+
248+
249+
@spin.util.extend_command(
250+
setup_lint,
251+
doc=f"""
252+
If configuration has changed, update lintrunner.
253+
254+
Compares the stored old hashes of configuration files with new ones and
255+
performs setup via setup-lint if the hashes have changed.
256+
Hashes are stored in {LINTRUNNER_CACHE_INFO[0]}; the following files are
257+
considered: {", ".join(LINTRUNNER_CACHE_INFO[1])}.
258+
""",
259+
)
260+
@click.pass_context
261+
def lazy_setup_lint(ctx, parent_callback, **kwargs):
262+
if hashes := _updated_hashes(*LINTRUNNER_CACHE_INFO):
263+
click.echo(
264+
"Changes detected in lint configuration files. Setting up linting tools..."
265+
)
266+
parent_callback(**kwargs)
267+
hash_file = LINTRUNNER_CACHE_INFO[0]
268+
hash_file.parent.mkdir(parents=True, exist_ok=True)
269+
with hash_file.open("w") as f:
270+
for file, hash in hashes.items():
271+
f.write(f"{hash} {file}\n")
272+
click.echo("Linting tools set up and hashes updated.")
273+
else:
274+
click.echo("No changes detected in lint configuration files. Skipping setup.")
275+
click.echo("Regenerating version...")
276+
ctx.invoke(regenerate_version)
277+
click.echo("Regenerating type stubs...")
278+
ctx.invoke(regenerate_type_stubs)
279+
click.echo("Done.")
280+
_check_linters()
281+
282+
283+
@click.command()
284+
@click.option("-a", "--apply-patches", is_flag=True)
285+
@click.pass_context
286+
def lint(ctx, apply_patches, **kwargs):
287+
"""Lint all files."""
288+
ctx.invoke(lazy_setup_lint)
289+
all_files_linters = VERY_FAST_LINTERS | FAST_LINTERS
290+
changed_files_linters = SLOW_LINTERS
291+
cmd = LINTRUNNER_BASE_CMD
292+
if apply_patches:
293+
cmd += ["--apply-patches"]
294+
all_files_cmd = cmd + [
295+
"--take",
296+
",".join(all_files_linters),
297+
"--all-files",
298+
]
299+
spin.util.run(all_files_cmd)
300+
changed_files_cmd = cmd + [
301+
"--take",
302+
",".join(changed_files_linters),
303+
]
304+
spin.util.run(changed_files_cmd)
305+
306+
307+
@click.command()
308+
@click.pass_context
309+
def fixlint(ctx, **kwargs):
310+
"""Autofix all files."""
311+
ctx.invoke(lint, apply_patches=True)
312+
313+
314+
@click.command()
315+
@click.option("-a", "--apply-patches", is_flag=True)
316+
@click.pass_context
317+
def quicklint(ctx, apply_patches, **kwargs):
318+
"""Lint changed files."""
319+
ctx.invoke(lazy_setup_lint)
320+
cmd = LINTRUNNER_BASE_CMD
321+
if apply_patches:
322+
cmd += ["--apply-patches"]
323+
spin.util.run(cmd)
324+
325+
326+
@click.command()
327+
@click.pass_context
328+
def quickfix(ctx, **kwargs):
329+
"""Autofix changed files."""
330+
ctx.invoke(quicklint, apply_patches=True)

pyproject.toml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,3 +376,19 @@ keep-runtime-typing = true
376376

377377
[tool.codespell]
378378
ignore-words = "tools/linter/dictionary.txt"
379+
380+
[tool.spin]
381+
package = 'torch'
382+
383+
[tool.spin.commands]
384+
"Build" = [
385+
".spin/cmds.py:lint",
386+
".spin/cmds.py:fixlint",
387+
".spin/cmds.py:quicklint",
388+
".spin/cmds.py:quickfix",
389+
]
390+
"Regenerate" = [
391+
".spin/cmds.py:regenerate_version",
392+
".spin/cmds.py:regenerate_type_stubs",
393+
".spin/cmds.py:regenerate_clangtidy_files",
394+
]

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ lintrunner ; platform_machine != "s390x" and platform_machine != "riscv64"
1414
networkx>=2.5.1
1515
optree>=0.13.0
1616
psutil
17+
spin
1718
sympy>=1.13.3
1819
typing-extensions>=4.13.2
1920
wheel

0 commit comments

Comments
 (0)