Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add totals report #18

Draft
wants to merge 12 commits into
base: main
Choose a base branch
from
8 changes: 8 additions & 0 deletions .flake8
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# This file is not used by anything in the repo.
# It is here as a convenience to developers who use flake8 locally.

[flake8]
ignore =
E203 # whitespace before ':' - conflicts with black
E501 # line too long - black takes care of this for us
W503 # line break before binary operator - this is what black does
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## Unreleased

- Add `help` command to print out usage information.
- Add `totals` command to quantify the errors recorded in a file.
- Use GA version of Python 3.11 in test matrix.

## v0.1.3 [2022-09-07]
Expand Down
64 changes: 59 additions & 5 deletions mypy_json_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,64 @@
# See the License for the specific language governing permissions and
# limitations under the License.

import argparse
import json
import pathlib
import sys
from collections import Counter, defaultdict
from dataclasses import dataclass
from typing import Counter as CounterType
from typing import Dict, Iterator
from typing import Counter as CounterType, Dict, Iterator

from typing_extensions import Protocol


def parser_command(args: object) -> None:
report_errors()


class TotalsArgs(Protocol):
filepath: str


def totals_command(args: TotalsArgs) -> None:
summarize_errors(filepath=args.filepath)


def main() -> None:
print(produce_errors_report(sys.stdin))
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(title="command")
# If no command is defined, default to the parser command.
# This preserves legacy behavior.
parser.set_defaults(func=parser_command)

parse_parser = subparsers.add_parser(
"parse", help="Transform Mypy output into JSON."
)
parse_parser.set_defaults(func=parser_command)

totals_parser = subparsers.add_parser(
"totals", help="Summarizes the errors in _file_ in JSON format."
)
totals_parser.add_argument("filepath", type=pathlib.Path)
totals_parser.set_defaults(func=totals_command)

args = sys.argv[1:]
parsed = parser.parse_args(args)
parsed.func(parsed)


def report_errors() -> None:
errors = produce_errors_report(sys.stdin)
error_json = json.dumps(errors, sort_keys=True, indent=2)
print(error_json)


def summarize_errors(*, filepath: str) -> None:
errors_json = pathlib.Path(filepath).read_text()
errors = json.loads(errors_json)
summary = produce_errors_summary(errors)
summary_json = json.dumps(summary, sort_keys=True)
print(summary_json)


@dataclass(frozen=True)
Expand All @@ -30,12 +78,18 @@ class MypyError:
message: str


def produce_errors_report(input_lines: Iterator[str]) -> str:
def produce_errors_summary(errors: Dict[str, Dict[str, int]]) -> Dict[str, int]:
total_errors = sum(sum(file_errors.values()) for file_errors in errors.values())
files_with_errors = len(errors.keys())
return {"files_with_errors": files_with_errors, "total_errors": total_errors}


def produce_errors_report(input_lines: Iterator[str]) -> Dict[str, Dict[str, int]]:
"""Given lines from mypy's output, return a JSON summary of error frequencies by file."""
errors = _extract_errors(input_lines)
error_frequencies = _count_errors(errors)
structured_errors = _structure_errors(error_frequencies)
return json.dumps(structured_errors, sort_keys=True, indent=2)
return structured_errors


def _extract_errors(lines: Iterator[str]) -> Iterator[MypyError]:
Expand Down
6 changes: 6 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,9 @@ mypy-json-report = "mypy_json_report:main"
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"

[tool.isort]
combine_as_imports = true
known_first_party = ["mypy_json_report"]
lines_after_imports = 2
profile = "black"
25 changes: 21 additions & 4 deletions tests/test_mypy_json_report.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import json
from io import StringIO

from mypy_json_report import produce_errors_report
from mypy_json_report import produce_errors_report, produce_errors_summary


EXAMPLE_MYPY_STDOUT = """\
mypy_json_report.py:8: error: Function is missing a return type annotation
Expand All @@ -10,12 +10,29 @@
Found 2 errors in 1 file (checked 3 source files)"""


def test_report() -> None:
def test_errors_report() -> None:
report = produce_errors_report(StringIO(EXAMPLE_MYPY_STDOUT))

assert json.loads(report) == {
assert report == {
"mypy_json_report.py": {
'Call to untyped function "main" in typed context': 1,
"Function is missing a return type annotation": 1,
}
}


def test_errors_summary() -> None:
errors = {
"file.py": {
'Call to untyped function "main" in typed context': 1,
"Function is missing a return type annotation": 2,
},
"another_file.py": {
'Call to untyped function "test" in typed context': 1,
"Function is missing a return type annotation": 1,
},
}

totals = produce_errors_summary(errors)

assert totals == {"files_with_errors": 2, "total_errors": 5}