Skip to content

Commit fcdfa8f

Browse files
Add GitHub annotations format for --output
1 parent 26a77f9 commit fcdfa8f

File tree

5 files changed

+110
-7
lines changed

5 files changed

+110
-7
lines changed

docs/source/command_line.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ Optional arguments
9696

9797
Show program's version number and exit.
9898

99-
.. option:: -O FORMAT, --output FORMAT {json}
99+
.. option:: -O {json,github}, --output {json,github}
100100

101101
Set a custom output format.
102102

mypy/error_formatter.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,28 @@ def report_error(self, error: "MypyError") -> str:
3434
)
3535

3636

37-
OUTPUT_CHOICES = {"json": JSONFormatter()}
37+
class GitHubFormatter(ErrorFormatter):
38+
"""Formatter for GitHub Actions output format."""
39+
40+
def report_error(self, error: "MypyError") -> str:
41+
"""Prints out the errors as GitHub Actions annotations."""
42+
command = "error" if error.severity == "error" else "notice"
43+
title = f"Mypy ({error.errorcode.code})" if error.errorcode is not None else "Mypy"
44+
45+
message = f"{error.message}."
46+
47+
if error.hints:
48+
message += "%0A%0A"
49+
message += "%0A".join(error.hints)
50+
51+
return (
52+
f"::{command} "
53+
f"file={error.file_path},"
54+
f"line={error.line},"
55+
f"col={error.column},"
56+
f"title={title}"
57+
f"::{message}"
58+
)
59+
60+
61+
OUTPUT_CHOICES = {"json": JSONFormatter(), "github": GitHubFormatter()}

mypy/main.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -530,11 +530,7 @@ def add_invertible_flag(
530530
)
531531

532532
general_group.add_argument(
533-
"-O",
534-
"--output",
535-
metavar="FORMAT",
536-
help="Set a custom output format",
537-
choices=OUTPUT_CHOICES,
533+
"-O", "--output", help="Set a custom output format", choices=OUTPUT_CHOICES
538534
)
539535

540536
config_group = parser.add_argument_group(

mypy/test/testoutput.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,3 +56,42 @@ def test_output_json(testcase: DataDrivenTestCase) -> None:
5656
normalized_output = [line.replace(test_temp_dir + json_os_separator, "") for line in output]
5757

5858
assert normalized_output == testcase.output
59+
60+
61+
class OutputGitHubsuite(DataSuite):
62+
files = ["outputgithub.test"]
63+
64+
def run_case(self, testcase: DataDrivenTestCase) -> None:
65+
test_output_github(testcase)
66+
67+
68+
def test_output_github(testcase: DataDrivenTestCase) -> None:
69+
"""Run Mypy in a subprocess, and ensure that `--output=github` works as intended."""
70+
mypy_cmdline = ["--output=github"]
71+
mypy_cmdline.append(f"--python-version={'.'.join(map(str, PYTHON3_VERSION))}")
72+
73+
# Write the program to a file.
74+
program_path = os.path.join(test_temp_dir, "main")
75+
mypy_cmdline.append(program_path)
76+
with open(program_path, "w", encoding="utf8") as file:
77+
for s in testcase.input:
78+
file.write(f"{s}\n")
79+
80+
output = []
81+
# Type check the program.
82+
out, err, returncode = api.run(mypy_cmdline)
83+
# split lines, remove newlines, and remove directory of test case
84+
for line in (out + err).rstrip("\n").splitlines():
85+
if line.startswith(test_temp_dir + os.sep):
86+
output.append(line[len(test_temp_dir + os.sep) :].rstrip("\r\n"))
87+
else:
88+
output.append(line.rstrip("\r\n"))
89+
90+
if returncode > 1:
91+
output.append("!!! Mypy crashed !!!")
92+
93+
# Remove temp file.
94+
os.remove(program_path)
95+
96+
normalized_output = [line.replace(test_temp_dir + os.sep, "") for line in output]
97+
assert normalized_output == testcase.output

test-data/unit/outputgithub.test

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
-- Test cases for `--output=json`.
2+
-- These cannot be run by the usual unit test runner because of the backslashes
3+
-- in the output, which get normalized to forward slashes by the test suite on
4+
-- Windows.
5+
6+
[case testOutputGitHubNoIssues]
7+
# flags: --output=github
8+
def foo() -> None:
9+
pass
10+
11+
foo()
12+
[out]
13+
14+
[case testOutputGitHubSimple]
15+
# flags: --output=github
16+
def foo() -> None:
17+
pass
18+
19+
foo(1)
20+
[out]
21+
::error file=main,line=5,col=0,title=Mypy (call-arg)::Too many arguments for "foo".
22+
23+
[case testOutputGitHubWithHint]
24+
# flags: --output=github
25+
from typing import Optional, overload
26+
27+
@overload
28+
def foo() -> None: ...
29+
@overload
30+
def foo(x: int) -> None: ...
31+
32+
def foo(x: Optional[int] = None) -> None:
33+
...
34+
35+
reveal_type(foo)
36+
37+
foo('42')
38+
39+
def bar() -> None: ...
40+
bar('42')
41+
[out]
42+
::notice file=main,line=12,col=12,title=Mypy (misc)::Revealed type is "Overload(def (), def (x: builtins.int))".
43+
::error file=main,line=14,col=0,title=Mypy (call-overload)::No overload variant of "foo" matches argument type "str".%0A%0APossible overload variants:%0A def foo() -> None%0A def foo(x: int) -> None
44+
::error file=main,line=17,col=0,title=Mypy (call-arg)::Too many arguments for "bar".

0 commit comments

Comments
 (0)