Skip to content

Commit f7378d8

Browse files
Sebastian-LarssonjustinchubyCopilot
authored
feat(linter): Adapter for docformatter (#129)
Adapter for https://github.com/PyCQA/docformatter. --------- Co-authored-by: Justin Chu <justinchuby@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 4805e6a commit f7378d8

File tree

2 files changed

+211
-0
lines changed

2 files changed

+211
-0
lines changed
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# SPDX-FileCopyrightText: Copyright 2025 Arm Limited and/or its affiliates <open-source-office@arm.com>
2+
# SPDX-License-Identifier: MIT
3+
4+
[[linter]]
5+
code = 'DOCFORMATTER'
6+
is_formatter = true
7+
include_patterns = ['**/*.py']
8+
exclude_patterns = []
9+
command = [
10+
'python',
11+
'-m',
12+
'lintrunner_adapters',
13+
'run',
14+
'docformatter_linter',
15+
'--config=pyproject.toml',
16+
'--',
17+
'@{{PATHSFILE}}',
18+
]
19+
init_command = [
20+
'python',
21+
'-m',
22+
'lintrunner_adapters',
23+
'run',
24+
'pip_init',
25+
'--dry-run={{DRYRUN}}',
26+
'--requirement=requirements-lintrunner.txt',
27+
]
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
# SPDX-FileCopyrightText: Copyright 2025-2026 Arm Limited and/or its affiliates <open-source-office@arm.com>
2+
# SPDX-License-Identifier: MIT
3+
"""Adapter for https://github.com/PyCQA/docformatter"""
4+
from __future__ import annotations
5+
6+
import argparse
7+
import concurrent.futures
8+
import logging
9+
import os
10+
import subprocess
11+
import sys
12+
from pathlib import Path
13+
14+
from lintrunner_adapters import (
15+
LintMessage,
16+
LintSeverity,
17+
add_default_options,
18+
as_posix,
19+
run_command,
20+
)
21+
22+
LINTER_CODE = "DOCFORMATTER"
23+
24+
25+
def check_file(
26+
filename: str,
27+
retries: int,
28+
timeout: int,
29+
config: str | None,
30+
) -> list[LintMessage]:
31+
try:
32+
path = Path(filename)
33+
original = path.read_bytes()
34+
35+
args = ["docformatter"]
36+
if config is not None:
37+
args += ["--config", config]
38+
args.append("-")
39+
40+
proc = run_command(
41+
args,
42+
retries=retries,
43+
timeout=timeout,
44+
input=original,
45+
check=False,
46+
)
47+
48+
replacement = proc.stdout
49+
if not replacement:
50+
raise subprocess.CalledProcessError(
51+
proc.returncode,
52+
proc.args,
53+
output=proc.stdout,
54+
stderr=proc.stderr,
55+
)
56+
except subprocess.TimeoutExpired:
57+
return [
58+
LintMessage(
59+
path=filename,
60+
line=None,
61+
char=None,
62+
code=LINTER_CODE,
63+
severity=LintSeverity.ERROR,
64+
name="timeout",
65+
original=None,
66+
replacement=None,
67+
description=f"docformatter timed out while processing {filename}.",
68+
)
69+
]
70+
except (OSError, subprocess.CalledProcessError) as err:
71+
return [
72+
LintMessage(
73+
path=filename,
74+
line=None,
75+
char=None,
76+
code=LINTER_CODE,
77+
severity=LintSeverity.ADVICE,
78+
name="command-failed",
79+
original=None,
80+
replacement=None,
81+
description=(
82+
f"Failed due to {err.__class__.__name__}:\n{err}"
83+
if not isinstance(err, subprocess.CalledProcessError)
84+
else (
85+
"COMMAND (exit code {returncode})\n"
86+
"{command}\n\n"
87+
"STDERR\n{stderr}\n\n"
88+
"STDOUT\n{stdout}"
89+
).format(
90+
returncode=err.returncode,
91+
command=" ".join(as_posix(x) for x in err.cmd),
92+
stderr=err.stderr.decode("utf-8").strip() or "(empty)",
93+
stdout=err.stdout.decode("utf-8").strip() or "(empty)",
94+
)
95+
),
96+
)
97+
]
98+
99+
try:
100+
original_decoded = original.decode("utf-8")
101+
replacement_decoded = replacement.decode("utf-8")
102+
except UnicodeDecodeError as err:
103+
return [
104+
LintMessage(
105+
path=filename,
106+
line=None,
107+
char=None,
108+
code=LINTER_CODE,
109+
severity=LintSeverity.ERROR,
110+
name="decode-failed",
111+
original=None,
112+
replacement=None,
113+
description=f"utf-8 decoding failed due to {err.__class__.__name__}:\n{err}",
114+
)
115+
]
116+
117+
if original_decoded == replacement_decoded:
118+
return []
119+
120+
return [
121+
LintMessage(
122+
path=filename,
123+
line=None,
124+
char=None,
125+
code=LINTER_CODE,
126+
name="format",
127+
original=original_decoded,
128+
replacement=replacement_decoded,
129+
severity=LintSeverity.WARNING,
130+
description="Run `lintrunner -a` to apply this patch.",
131+
)
132+
]
133+
134+
135+
def main() -> None:
136+
parser = argparse.ArgumentParser(
137+
description=f"Docstring formatter. Linter code: {LINTER_CODE}.",
138+
fromfile_prefix_chars="@",
139+
)
140+
parser.add_argument(
141+
"--config",
142+
required=False,
143+
help="location of docformatter config",
144+
)
145+
parser.add_argument(
146+
"--timeout",
147+
default=90,
148+
type=int,
149+
help="seconds to wait for docformatter",
150+
)
151+
add_default_options(parser)
152+
args = parser.parse_args()
153+
154+
logging.basicConfig(
155+
format="<%(threadName)s:%(levelname)s> %(message)s",
156+
level=(
157+
logging.NOTSET
158+
if args.verbose
159+
else logging.DEBUG
160+
if len(args.filenames) < 1000
161+
else logging.INFO
162+
),
163+
stream=sys.stderr,
164+
)
165+
166+
with concurrent.futures.ThreadPoolExecutor(
167+
max_workers=os.cpu_count(),
168+
thread_name_prefix="Thread",
169+
) as executor:
170+
futures = {
171+
executor.submit(check_file, x, args.retries, args.timeout, args.config): x
172+
for x in args.filenames
173+
}
174+
for future in concurrent.futures.as_completed(futures):
175+
try:
176+
for lint_message in future.result():
177+
lint_message.display()
178+
except Exception:
179+
logging.critical('Failed at "%s".', futures[future])
180+
raise
181+
182+
183+
if __name__ == "__main__":
184+
main()

0 commit comments

Comments
 (0)