From 4e05056b9b5a3cec646c3d00769728d1b4529dbd Mon Sep 17 00:00:00 2001 From: Charlie Denton Date: Tue, 29 Nov 2022 14:34:50 +0000 Subject: [PATCH 1/9] Split logic from entrypoint --- mypy_json_report.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/mypy_json_report.py b/mypy_json_report.py index 1509526..8277439 100644 --- a/mypy_json_report.py +++ b/mypy_json_report.py @@ -20,6 +20,10 @@ def main() -> None: + report_errors() + + +def report_errors() -> None: print(produce_errors_report(sys.stdin)) From 62998ad71cde44e84213063bd8f283e6f9d0e823 Mon Sep 17 00:00:00 2001 From: Charlie Denton Date: Tue, 29 Nov 2022 13:45:40 +0000 Subject: [PATCH 2/9] Break calculation into separate statement --- mypy_json_report.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mypy_json_report.py b/mypy_json_report.py index 8277439..ea4c128 100644 --- a/mypy_json_report.py +++ b/mypy_json_report.py @@ -24,7 +24,8 @@ def main() -> None: def report_errors() -> None: - print(produce_errors_report(sys.stdin)) + errors = produce_errors_report(sys.stdin) + print(errors) @dataclass(frozen=True) From 8c42db0f4a8513bf7d46f30cd4252cddec11e9bc Mon Sep 17 00:00:00 2001 From: Charlie Denton Date: Tue, 29 Nov 2022 13:49:23 +0000 Subject: [PATCH 3/9] Isolate calculation from serialization --- mypy_json_report.py | 7 ++++--- tests/test_mypy_json_report.py | 3 +-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/mypy_json_report.py b/mypy_json_report.py index ea4c128..4d8bb12 100644 --- a/mypy_json_report.py +++ b/mypy_json_report.py @@ -25,7 +25,8 @@ def main() -> None: def report_errors() -> None: errors = produce_errors_report(sys.stdin) - print(errors) + error_json = json.dumps(errors, sort_keys=True, indent=2) + print(error_json) @dataclass(frozen=True) @@ -34,12 +35,12 @@ class MypyError: message: str -def produce_errors_report(input_lines: Iterator[str]) -> str: +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]: diff --git a/tests/test_mypy_json_report.py b/tests/test_mypy_json_report.py index 99f3dbf..427de8e 100644 --- a/tests/test_mypy_json_report.py +++ b/tests/test_mypy_json_report.py @@ -1,4 +1,3 @@ -import json from io import StringIO from mypy_json_report import produce_errors_report @@ -14,7 +13,7 @@ def test_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, From 605c28f4a07ad4a1ac50f6bc60ac747b43c53b67 Mon Sep 17 00:00:00 2001 From: Charlie Denton Date: Tue, 31 Jan 2023 13:47:13 +0000 Subject: [PATCH 4/9] Rename produce_errors_report->parse_errors_report "Parse" is a better reflection of what this function does. --- mypy_json_report.py | 4 ++-- tests/test_mypy_json_report.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/mypy_json_report.py b/mypy_json_report.py index 4d8bb12..a00ede9 100644 --- a/mypy_json_report.py +++ b/mypy_json_report.py @@ -24,7 +24,7 @@ def main() -> None: def report_errors() -> None: - errors = produce_errors_report(sys.stdin) + errors = parse_errors_report(sys.stdin) error_json = json.dumps(errors, sort_keys=True, indent=2) print(error_json) @@ -35,7 +35,7 @@ class MypyError: message: str -def produce_errors_report(input_lines: Iterator[str]) -> Dict[str, Dict[str, int]]: +def parse_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) diff --git a/tests/test_mypy_json_report.py b/tests/test_mypy_json_report.py index 427de8e..aa1b827 100644 --- a/tests/test_mypy_json_report.py +++ b/tests/test_mypy_json_report.py @@ -1,6 +1,6 @@ from io import StringIO -from mypy_json_report import produce_errors_report +from mypy_json_report import parse_errors_report EXAMPLE_MYPY_STDOUT = """\ @@ -10,8 +10,8 @@ Found 2 errors in 1 file (checked 3 source files)""" -def test_report() -> None: - report = produce_errors_report(StringIO(EXAMPLE_MYPY_STDOUT)) +def test_parse_errors_report() -> None: + report = parse_errors_report(StringIO(EXAMPLE_MYPY_STDOUT)) assert report == { "mypy_json_report.py": { From 74319ef9d7a4535a28d2a15b8674bd5f985c6619 Mon Sep 17 00:00:00 2001 From: Charlie Denton Date: Tue, 31 Jan 2023 15:27:34 +0000 Subject: [PATCH 5/9] Require "parse" subcommand when calling We intend to add more functionality to this tool, so it no longer makes sense to have the error parsing functionality at the top level when calling the script. This change introduces (and requires) a new "parse" subcommand, under which the existing parsing function is moved. Because we're using argparse to do this, we also get a new "--help" flag on the command. --- CHANGELOG.md | 2 ++ README.md | 4 ++-- mypy_json_report.py | 40 ++++++++++++++++++++++++++++++++++++++-- tox.ini | 2 +- 4 files changed, 43 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 61f1b05..afd3606 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- *Action required:* Move existing behaviour under "parse" subcommand. + Invocations of `mypy-json-report` should now be replaced with `mypy-json-report parse`. - Use GA version of Python 3.11 in test matrix. ## v0.1.3 [2022-09-07] diff --git a/README.md b/README.md index 502abca..87283bf 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Pipe the output of mypy through the `mypy-json-report` CLI app. Store the output to a file, and commit it to your git repo. ``` -mypy . --strict | mypy-json-report > known-mypy-errors.json +mypy . --strict | mypy-json-report parse > known-mypy-errors.json git add known-mypy-errors.json git commit -m "Add mypy errors lockfile" ``` @@ -80,7 +80,7 @@ jobs: - name: Run mypy run: | - mypy . --strict | mypy-json-report > known-mypy-errors.json + mypy . --strict | mypy-json-report parse > known-mypy-errors.json - name: Check for mypy changes run: | diff --git a/mypy_json_report.py b/mypy_json_report.py index a00ede9..f7db976 100644 --- a/mypy_json_report.py +++ b/mypy_json_report.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +import argparse +import enum import json import sys from collections import Counter, defaultdict @@ -19,16 +21,50 @@ from typing import Counter as CounterType, Dict, Iterator +class ErrorCodes(enum.IntEnum): + DEPRECATED = 1 + + def main() -> None: - report_errors() + """ + The primary entrypoint of the program. + + Parses the CLI flags, and delegates to other functions as appropriate. + For details of how to invoke the program, call it with `--help`. + """ + parser = argparse.ArgumentParser() + subparsers = parser.add_subparsers(title="subcommand") + + parser.set_defaults(func=_no_command) + + parse_parser = subparsers.add_parser( + "parse", help="Transform Mypy output into JSON." + ) + parse_parser.set_defaults(func=_parse_command) -def report_errors() -> None: + args = sys.argv[1:] + parsed = parser.parse_args(args) + parsed.func(parsed) + + +def _parse_command(args: object) -> None: + """Handle the `parse` command.""" errors = parse_errors_report(sys.stdin) error_json = json.dumps(errors, sort_keys=True, indent=2) print(error_json) +def _no_command(args: object) -> None: + """ + Handle the lack of an explicit command. + + This will be hit when the program is called without arguments. + """ + print("A subcommand is required. Pass --help for usage info.") + sys.exit(ErrorCodes.DEPRECATED) + + @dataclass(frozen=True) class MypyError: filename: str diff --git a/tox.ini b/tox.ini index d211354..9ab2b06 100644 --- a/tox.ini +++ b/tox.ini @@ -39,5 +39,5 @@ allowlist_externals = commands_pre = poetry install commands = - poetry run bash -c "mypy . --strict | mypy-json-report > known-mypy-errors.json" + poetry run bash -c "mypy . --strict | mypy-json-report parse > known-mypy-errors.json" git diff --exit-code known-mypy-errors.json From 88e3d91079efbcd52656b64bb9525b1834ce86a7 Mon Sep 17 00:00:00 2001 From: Charlie Denton Date: Tue, 31 Jan 2023 22:23:05 +0000 Subject: [PATCH 6/9] Use better typing for command handlers Co-authored-by: Jeppe Fihl-Pearson --- mypy_json_report.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mypy_json_report.py b/mypy_json_report.py index f7db976..bf7509e 100644 --- a/mypy_json_report.py +++ b/mypy_json_report.py @@ -48,14 +48,14 @@ def main() -> None: parsed.func(parsed) -def _parse_command(args: object) -> None: +def _parse_command(args: argparse.Namespace) -> None: """Handle the `parse` command.""" errors = parse_errors_report(sys.stdin) error_json = json.dumps(errors, sort_keys=True, indent=2) print(error_json) -def _no_command(args: object) -> None: +def _no_command(args: argparse.Namespace) -> None: """ Handle the lack of an explicit command. From ea09facd80557be03cbd211953b1826a8a80e819 Mon Sep 17 00:00:00 2001 From: Charlie Denton Date: Tue, 31 Jan 2023 22:29:03 +0000 Subject: [PATCH 7/9] Update docstring The summary returned is no longer JSON. Co-authored-by: Jeppe Fihl-Pearson --- mypy_json_report.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy_json_report.py b/mypy_json_report.py index bf7509e..c68fa54 100644 --- a/mypy_json_report.py +++ b/mypy_json_report.py @@ -72,7 +72,7 @@ class MypyError: def parse_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.""" + """Given lines from mypy's output, return a summary of error frequencies by file.""" errors = _extract_errors(input_lines) error_frequencies = _count_errors(errors) structured_errors = _structure_errors(error_frequencies) From eeaccc4a97d351011682f668480427108e61b985 Mon Sep 17 00:00:00 2001 From: Charlie Denton Date: Tue, 31 Jan 2023 22:31:47 +0000 Subject: [PATCH 8/9] Let argparse source the CLI arguments There's no point in passing in the defaults. Co-authored-by: Jeppe Fihl-Pearson --- mypy_json_report.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/mypy_json_report.py b/mypy_json_report.py index c68fa54..849f68a 100644 --- a/mypy_json_report.py +++ b/mypy_json_report.py @@ -43,8 +43,7 @@ def main() -> None: parse_parser.set_defaults(func=_parse_command) - args = sys.argv[1:] - parsed = parser.parse_args(args) + parsed = parser.parse_args() parsed.func(parsed) From 2ddb4b88bda46c7092d2c0e8b14e81c0f78c0d22 Mon Sep 17 00:00:00 2001 From: Charlie Denton Date: Tue, 31 Jan 2023 22:52:43 +0000 Subject: [PATCH 9/9] Add --indentation to vary spacing in JSON report Some environments may want control over this to make working with existing style-guides easier. Co-authored-by: Jeppe Fihl-Pearson --- CHANGELOG.md | 1 + mypy_json_report.py | 9 ++++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index afd3606..dee1f52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - *Action required:* Move existing behaviour under "parse" subcommand. Invocations of `mypy-json-report` should now be replaced with `mypy-json-report parse`. +- Add `parse --indentation` flag to grant control over how much indentation is used in the JSON report. - Use GA version of Python 3.11 in test matrix. ## v0.1.3 [2022-09-07] diff --git a/mypy_json_report.py b/mypy_json_report.py index 849f68a..8b6c2f5 100644 --- a/mypy_json_report.py +++ b/mypy_json_report.py @@ -40,6 +40,13 @@ def main() -> None: parse_parser = subparsers.add_parser( "parse", help="Transform Mypy output into JSON." ) + parse_parser.add_argument( + "-i", + "--indentation", + type=int, + default=2, + help="Number of spaces to indent JSON output.", + ) parse_parser.set_defaults(func=_parse_command) @@ -50,7 +57,7 @@ def main() -> None: def _parse_command(args: argparse.Namespace) -> None: """Handle the `parse` command.""" errors = parse_errors_report(sys.stdin) - error_json = json.dumps(errors, sort_keys=True, indent=2) + error_json = json.dumps(errors, sort_keys=True, indent=args.indentation) print(error_json)