Skip to content

Commit 20fb4d4

Browse files
authored
[Lint] Add a formatter GitHub Action for ufmt and clang-format (#4977)
* add lint workflow * add line number * address comments * update package versions * remove autopep8 config
1 parent 774d575 commit 20fb4d4

File tree

6 files changed

+587
-19
lines changed

6 files changed

+587
-19
lines changed

.github/workflows/lint.yml

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
name: Lint
2+
3+
on: [pull_request]
4+
5+
jobs:
6+
lintrunner:
7+
runs-on: ubuntu-latest
8+
steps:
9+
- name: Pull DGL
10+
uses: actions/checkout@v3
11+
with:
12+
fetch-depth: 0
13+
14+
- name: Checkout master and HEAD
15+
run: |
16+
git checkout -t origin/master
17+
git checkout ${{ github.event.pull_request.head.sha }}
18+
19+
- name: Setup Python
20+
uses: actions/setup-python@v4
21+
with:
22+
python-version: '3.8'
23+
24+
- name: Install requirements
25+
run: |
26+
python -m pip install --upgrade pip
27+
pip install lintrunner --user
28+
29+
- name: Initialize lint dependencies
30+
run: lintrunner init
31+
32+
- name: Run lintrunner on all changed files
33+
run: |
34+
set +e
35+
if ! lintrunner --force-color -m master --tee-json=lint.json; then
36+
echo ""
37+
echo -e "\e[1m\e[36mYou can reproduce these results locally by using \`lintrunner\`.\e[0m"
38+
echo -e "\e[1m\e[36mSee https://github.com/pytorch/pytorch/wiki/lintrunner for setup instructions.\e[0m"
39+
exit 1
40+
fi
41+
42+
- name: Store annotations
43+
if: always() && github.event_name == 'pull_request'
44+
# Don't show this as an error; the above step will have already failed.
45+
continue-on-error: true
46+
run: |
47+
# Use jq to massage the JSON lint output into GitHub Actions workflow commands.
48+
jq --raw-output \
49+
'"::\(if .severity == "advice" or .severity == "disabled" then "warning" else .severity end) file=\(.path),line=\(.line),col=\(.char),title=\(.code) \(.name)::" + (.description | gsub("\\n"; "%0A"))' \
50+
lint.json
51+
52+
concurrency:
53+
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }}-${{ github.event_name == 'workflow_dispatch' }}
54+
cancel-in-progress: true

.lintrunner.toml

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# Black + usort
2+
[[linter]]
3+
code = 'UFMT'
4+
include_patterns = [
5+
'**/*.py',
6+
]
7+
command = [
8+
'python3',
9+
'tests/lint/ufmt_linter.py',
10+
'--',
11+
'@{{PATHSFILE}}'
12+
]
13+
exclude_patterns = [
14+
'.github/*',
15+
'build/*',
16+
'cmake/*',
17+
'conda/*',
18+
'docker/*',
19+
'third_party/*',
20+
]
21+
init_command = [
22+
'python3',
23+
'tests/lint/pip_init.py',
24+
'--dry-run={{DRYRUN}}',
25+
'black==22.10.0',
26+
'ufmt==2.0.1',
27+
'usort==1.0.5',
28+
]
29+
is_formatter = true
30+
31+
[[linter]]
32+
code = 'CLANGFORMAT'
33+
include_patterns = [
34+
'**/*.h',
35+
'**/*.c',
36+
'**/*.cc',
37+
'**/*.cpp',
38+
'**/*.cuh',
39+
'**/*.cu',
40+
]
41+
exclude_patterns = [
42+
]
43+
init_command = [
44+
'python3',
45+
'tests/lint/pip_init.py',
46+
'--dry-run={{DRYRUN}}',
47+
'clang-format==15.0.4',
48+
]
49+
command = [
50+
'python3',
51+
'tests/lint/clangformat_linter.py',
52+
'--binary=clang-format',
53+
'--',
54+
'@{{PATHSFILE}}'
55+
]
56+
is_formatter = true

pyproject.toml

-19
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,3 @@
11
[tool.black]
22

33
line-length = 80
4-
5-
6-
[tool.autopep8]
7-
8-
max_line_length = 80
9-
in-place = true
10-
aggressive = 3
11-
# Add the path to here if you want to exclude them from autopep8 auto reformat.
12-
# When a directory or multiple files are passed to autopep8, it will ignore the
13-
# following directory and files. It is not recommended to pass a directory to
14-
# autopep8.
15-
exclude = '''
16-
.github/*,
17-
build/*,
18-
cmake/*,
19-
conda/*,
20-
docker/*,
21-
third_party/*,
22-
'''

tests/lint/clangformat_linter.py

+242
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
"""Borrowed from github.com/pytorch/pytorch/tools/linter/adapters/clangformat_linter.py"""
2+
import argparse
3+
import concurrent.futures
4+
import json
5+
import logging
6+
import os
7+
import subprocess
8+
import sys
9+
import time
10+
from enum import Enum
11+
from pathlib import Path
12+
from typing import Any, List, NamedTuple, Optional
13+
14+
15+
IS_WINDOWS: bool = os.name == "nt"
16+
17+
18+
def eprint(*args: Any, **kwargs: Any) -> None:
19+
print(*args, file=sys.stderr, flush=True, **kwargs)
20+
21+
22+
class LintSeverity(str, Enum):
23+
ERROR = "error"
24+
WARNING = "warning"
25+
ADVICE = "advice"
26+
DISABLED = "disabled"
27+
28+
29+
class LintMessage(NamedTuple):
30+
path: Optional[str]
31+
line: Optional[int]
32+
char: Optional[int]
33+
code: str
34+
severity: LintSeverity
35+
name: str
36+
original: Optional[str]
37+
replacement: Optional[str]
38+
description: Optional[str]
39+
40+
41+
def as_posix(name: str) -> str:
42+
return name.replace("\\", "/") if IS_WINDOWS else name
43+
44+
45+
def _run_command(
46+
args: List[str],
47+
*,
48+
timeout: int,
49+
) -> "subprocess.CompletedProcess[bytes]":
50+
logging.debug("$ %s", " ".join(args))
51+
start_time = time.monotonic()
52+
try:
53+
return subprocess.run(
54+
args,
55+
stdout=subprocess.PIPE,
56+
stderr=subprocess.PIPE,
57+
shell=IS_WINDOWS, # So batch scripts are found.
58+
timeout=timeout,
59+
check=True,
60+
)
61+
finally:
62+
end_time = time.monotonic()
63+
logging.debug("took %dms", (end_time - start_time) * 1000)
64+
65+
66+
def run_command(
67+
args: List[str],
68+
*,
69+
retries: int,
70+
timeout: int,
71+
) -> "subprocess.CompletedProcess[bytes]":
72+
remaining_retries = retries
73+
while True:
74+
try:
75+
return _run_command(args, timeout=timeout)
76+
except subprocess.TimeoutExpired as err:
77+
if remaining_retries == 0:
78+
raise err
79+
remaining_retries -= 1
80+
logging.warning(
81+
"(%s/%s) Retrying because command failed with: %r",
82+
retries - remaining_retries,
83+
retries,
84+
err,
85+
)
86+
time.sleep(1)
87+
88+
89+
def check_file(
90+
filename: str,
91+
binary: str,
92+
retries: int,
93+
timeout: int,
94+
) -> List[LintMessage]:
95+
try:
96+
with open(filename, "rb") as f:
97+
original = f.read()
98+
proc = run_command(
99+
[binary, filename],
100+
retries=retries,
101+
timeout=timeout,
102+
)
103+
except subprocess.TimeoutExpired:
104+
return [
105+
LintMessage(
106+
path=filename,
107+
line=None,
108+
char=None,
109+
code="CLANGFORMAT",
110+
severity=LintSeverity.ERROR,
111+
name="timeout",
112+
original=None,
113+
replacement=None,
114+
description=(
115+
"clang-format timed out while trying to process a file. "
116+
"Please report an issue in pytorch/pytorch with the "
117+
"label 'module: lint'"
118+
),
119+
)
120+
]
121+
except (OSError, subprocess.CalledProcessError) as err:
122+
return [
123+
LintMessage(
124+
path=filename,
125+
line=None,
126+
char=None,
127+
code="CLANGFORMAT",
128+
severity=LintSeverity.ADVICE,
129+
name="command-failed",
130+
original=None,
131+
replacement=None,
132+
description=(
133+
f"Failed due to {err.__class__.__name__}:\n{err}"
134+
if not isinstance(err, subprocess.CalledProcessError)
135+
else (
136+
"COMMAND (exit code {returncode})\n"
137+
"{command}\n\n"
138+
"STDERR\n{stderr}\n\n"
139+
"STDOUT\n{stdout}"
140+
).format(
141+
returncode=err.returncode,
142+
command=" ".join(as_posix(x) for x in err.cmd),
143+
stderr=err.stderr.decode("utf-8").strip() or "(empty)",
144+
stdout=err.stdout.decode("utf-8").strip() or "(empty)",
145+
)
146+
),
147+
)
148+
]
149+
150+
replacement = proc.stdout
151+
if original == replacement:
152+
return []
153+
154+
line = 0
155+
original = original.decode("utf-8")
156+
replacement = replacement.decode("utf-8")
157+
for line, (i, j) in enumerate(
158+
zip(original.split("\n"), replacement.split("\n"))
159+
):
160+
if i != j:
161+
break
162+
163+
return [
164+
LintMessage(
165+
path=filename,
166+
line=line,
167+
char=None,
168+
code="CLANGFORMAT",
169+
severity=LintSeverity.WARNING,
170+
name="format",
171+
original=original,
172+
replacement=replacement,
173+
description="See https://clang.llvm.org/docs/ClangFormat.html.\nRun `lintrunner -a` to apply this patch.",
174+
)
175+
]
176+
177+
178+
def main() -> None:
179+
parser = argparse.ArgumentParser(
180+
description="Format files with clang-format.",
181+
fromfile_prefix_chars="@",
182+
)
183+
parser.add_argument(
184+
"--binary",
185+
required=True,
186+
help="clang-format binary path",
187+
)
188+
parser.add_argument(
189+
"--retries",
190+
default=3,
191+
type=int,
192+
help="times to retry timed out clang-format",
193+
)
194+
parser.add_argument(
195+
"--timeout",
196+
default=90,
197+
type=int,
198+
help="seconds to wait for clang-format",
199+
)
200+
parser.add_argument(
201+
"--verbose",
202+
action="store_true",
203+
help="verbose logging",
204+
)
205+
parser.add_argument(
206+
"filenames",
207+
nargs="+",
208+
help="paths to lint",
209+
)
210+
args = parser.parse_args()
211+
212+
logging.basicConfig(
213+
format="<%(threadName)s:%(levelname)s> %(message)s",
214+
level=logging.NOTSET
215+
if args.verbose
216+
else logging.DEBUG
217+
if len(args.filenames) < 1000
218+
else logging.INFO,
219+
stream=sys.stderr,
220+
)
221+
222+
with concurrent.futures.ThreadPoolExecutor(
223+
max_workers=os.cpu_count(),
224+
thread_name_prefix="Thread",
225+
) as executor:
226+
futures = {
227+
executor.submit(
228+
check_file, x, args.binary, args.retries, args.timeout
229+
): x
230+
for x in args.filenames
231+
}
232+
for future in concurrent.futures.as_completed(futures):
233+
try:
234+
for lint_message in future.result():
235+
print(json.dumps(lint_message._asdict()), flush=True)
236+
except Exception:
237+
logging.critical('Failed at "%s".', futures[future])
238+
raise
239+
240+
241+
if __name__ == "__main__":
242+
main()

0 commit comments

Comments
 (0)