Skip to content

Commit

Permalink
Add GitHub annotations format for --output
Browse files Browse the repository at this point in the history
  • Loading branch information
edgarrmondragon committed Sep 27, 2024
1 parent 26a77f9 commit e03e7f1
Show file tree
Hide file tree
Showing 5 changed files with 110 additions and 3 deletions.
2 changes: 1 addition & 1 deletion docs/source/command_line.rst
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ Optional arguments

Show program's version number and exit.

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

Set a custom output format.

Expand Down
27 changes: 26 additions & 1 deletion mypy/error_formatter.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Defines the different custom formats in which mypy can output."""

import json
import urllib.parse
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING

Expand Down Expand Up @@ -34,4 +35,28 @@ def report_error(self, error: "MypyError") -> str:
)


OUTPUT_CHOICES = {"json": JSONFormatter()}
class GitHubFormatter(ErrorFormatter):
"""Formatter for GitHub Actions output format."""

def report_error(self, error: "MypyError") -> str:
"""Prints out the errors as GitHub Actions annotations."""
command = "error" if error.severity == "error" else "notice"
title = f"Mypy ({error.errorcode.code})" if error.errorcode is not None else "Mypy"

message = error.message

if error.hints:
message += "\n\n"
message += "\n\n".join(error.hints)

return (
f"::{command} "
f"file={error.file_path},"
f"line={error.line},"
f"col={error.column},"
f"title={title}"
f"::{urllib.parse.quote(message)}"
)


OUTPUT_CHOICES = {"json": JSONFormatter(), "github": GitHubFormatter()}
1 change: 0 additions & 1 deletion mypy/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -532,7 +532,6 @@ def add_invertible_flag(
general_group.add_argument(
"-O",
"--output",
metavar="FORMAT",
help="Set a custom output format",
choices=OUTPUT_CHOICES,
)
Expand Down
39 changes: 39 additions & 0 deletions mypy/test/testoutput.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,42 @@ def test_output_json(testcase: DataDrivenTestCase) -> None:
normalized_output = [line.replace(test_temp_dir + json_os_separator, "") for line in output]

assert normalized_output == testcase.output


class OutputGitHubsuite(DataSuite):
files = ["outputgithub.test"]

def run_case(self, testcase: DataDrivenTestCase) -> None:
test_output_github(testcase)


def test_output_github(testcase: DataDrivenTestCase) -> None:
"""Run Mypy in a subprocess, and ensure that `--output=github` works as intended."""
mypy_cmdline = ["--output=github"]
mypy_cmdline.append(f"--python-version={'.'.join(map(str, PYTHON3_VERSION))}")

# Write the program to a file.
program_path = os.path.join(test_temp_dir, "main")
mypy_cmdline.append(program_path)
with open(program_path, "w", encoding="utf8") as file:
for s in testcase.input:
file.write(f"{s}\n")

output = []
# Type check the program.
out, err, returncode = api.run(mypy_cmdline)
# split lines, remove newlines, and remove directory of test case
for line in (out + err).rstrip("\n").splitlines():
if line.startswith(test_temp_dir + os.sep):
output.append(line[len(test_temp_dir + os.sep) :].rstrip("\r\n"))
else:
output.append(line.rstrip("\r\n"))

if returncode > 1:
output.append("!!! Mypy crashed !!!")

# Remove temp file.
os.remove(program_path)

normalized_output = [line.replace(test_temp_dir + os.sep, "") for line in output]
assert normalized_output == testcase.output
44 changes: 44 additions & 0 deletions test-data/unit/outputgithub.test
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
-- Test cases for `--output=json`.
-- These cannot be run by the usual unit test runner because of the backslashes
-- in the output, which get normalized to forward slashes by the test suite on
-- Windows.

[case testOutputGitHubNoIssues]
# flags: --output=github
def foo() -> None:
pass

foo()
[out]

[case testOutputGitHubSimple]
# flags: --output=github
def foo() -> None:
pass

foo(1)
[out]
::error file=main,line=5,col=0,title=Mypy (call-arg)::Too%20many%20arguments%20for%20%22foo%22

[case testOutputGitHubWithHint]
# flags: --output=github
from typing import Optional, overload

@overload
def foo() -> None: ...
@overload
def foo(x: int) -> None: ...

def foo(x: Optional[int] = None) -> None:
...

reveal_type(foo)

foo('42')

def bar() -> None: ...
bar('42')
[out]
::notice file=main,line=12,col=12,title=Mypy (misc)::Revealed%20type%20is%20%22Overload%28def%20%28%29%2C%20def%20%28x%3A%20builtins.int%29%29%22
::error file=main,line=14,col=0,title=Mypy (call-overload)::No%20overload%20variant%20of%20%22foo%22%20matches%20argument%20type%20%22str%22%0A%0APossible%20overload%20variants%3A%0A%0A%20%20%20%20def%20foo%28%29%20-%3E%20None%0A%0A%20%20%20%20def%20foo%28x%3A%20int%29%20-%3E%20None
::error file=main,line=17,col=0,title=Mypy (call-arg)::Too%20many%20arguments%20for%20%22bar%22

0 comments on commit e03e7f1

Please sign in to comment.