From 448dbbb200694356129b01c45aefcef2b791c20f Mon Sep 17 00:00:00 2001 From: Javier Buzzi Date: Thu, 15 May 2025 12:36:28 +0200 Subject: [PATCH 01/39] Adds ruff check --- diff_cover/diff_quality_tool.py | 2 ++ .../violationsreporters/violations_reporter.py | 14 ++++++++++++++ 2 files changed, 16 insertions(+) diff --git a/diff_cover/diff_quality_tool.py b/diff_cover/diff_quality_tool.py index 9400ca0c..9ebf5da5 100644 --- a/diff_cover/diff_quality_tool.py +++ b/diff_cover/diff_quality_tool.py @@ -55,6 +55,7 @@ pydocstyle_driver, pyflakes_driver, shellcheck_driver, + ruff_check_driver, ) QUALITY_DRIVERS = { @@ -62,6 +63,7 @@ "pycodestyle": pycodestyle_driver, "pyflakes": pyflakes_driver, "pylint": PylintDriver(), + "ruff.check": ruff_check_driver, "flake8": flake8_driver, "jshint": jshint_driver, "eslint": EslintDriver(), diff --git a/diff_cover/violationsreporters/violations_reporter.py b/diff_cover/violationsreporters/violations_reporter.py index a06efe01..446fa179 100644 --- a/diff_cover/violationsreporters/violations_reporter.py +++ b/diff_cover/violationsreporters/violations_reporter.py @@ -476,6 +476,20 @@ def measured_lines(self, src_path): exit_codes=[0, 1], ) +ruff_check_driver = RegexBasedDriver( + name="ruff.check", + supported_extensions=["py"], + command=["ruff", "check"], + # Match lines of the form: + # path/to/file.py:328:27 F541 [*] f-string without any placeholders + # path/to/file.py:418:26 F841 [*] Local variable `e` is assigned to but never used + expression=r"^([^:]+):(\d+):\d*:? (.*)$", + command_to_check_install=["ruff", "--version"], + # ruff exit code is 1 if there are violations + # https://docs.astral.sh/ruff/linter/#exit-codes + exit_codes=[0, 1], +) + """ Report Flake8 violations. """ From fe444b52b5d4da0ce442bb8fcb82ae90edf4d8b2 Mon Sep 17 00:00:00 2001 From: Javier Buzzi Date: Thu, 15 May 2025 12:43:56 +0200 Subject: [PATCH 02/39] Fixes isort --- diff_cover/diff_quality_tool.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/diff_cover/diff_quality_tool.py b/diff_cover/diff_quality_tool.py index 9ebf5da5..1b389013 100644 --- a/diff_cover/diff_quality_tool.py +++ b/diff_cover/diff_quality_tool.py @@ -54,8 +54,8 @@ pycodestyle_driver, pydocstyle_driver, pyflakes_driver, - shellcheck_driver, ruff_check_driver, + shellcheck_driver, ) QUALITY_DRIVERS = { From cedcff16efaa236814ed6a4e1d9dcf1252ddde02 Mon Sep 17 00:00:00 2001 From: Javier Buzzi Date: Thu, 15 May 2025 13:33:19 +0200 Subject: [PATCH 03/39] Adds tests --- tests/test_violations_reporter.py | 55 +++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/tests/test_violations_reporter.py b/tests/test_violations_reporter.py index 2c4be0ad..4be8a546 100644 --- a/tests/test_violations_reporter.py +++ b/tests/test_violations_reporter.py @@ -28,6 +28,7 @@ pycodestyle_driver, pydocstyle_driver, pyflakes_driver, + ruff_check_driver, shellcheck_driver, ) @@ -2088,3 +2089,57 @@ def test_parse_report(self): assert len(actual_violations) == len(expected_violations) for expected in expected_violations: assert expected in actual_violations + + +class TestRuffCheckQualityDriverTest: + """Tests for ruff check quality driver.""" + + def test_quality(self, process_patcher): + """Integration test.""" + process_patcher( + ( + dedent( + """ + foo/bar/path/to/file.py:244:26: F541 [*] f-string without any placeholders + | + 242 | ] + 243 | if len(xml_roots) > 0 and len(lcov_roots) > 0: + 244 | raise ValueError(f"Mixing LCov and XML reports is not supported yet") + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ F541 + 245 | elif len(xml_roots) > 0: + 246 | coverage = XmlCoverageReporter(xml_roots, src_roots, expand_coverage_report) + | + = help: Remove extraneous `f` prefix + + foo/bar/path/to/file.py:132:27: F841 [*] Local variable `e` is assigned to but never used + | + 130 | with open(self.diff_file_path, "r") as file: + 131 | return file.read() + 132 | except IOError as e: + | ^ F841 + 133 | raise ValueError( + 134 | dedent( + | + = help: Remove assignment to unused variable `e` + """ + ) + .strip() + .encode("ascii"), + "", + ) + ) + + expected_violations = [ + Violation(line=244, message="F541 [*] f-string without any placeholders"), + Violation( + line=132, + message="F841 [*] Local variable `e` is assigned to but never used", + ), + ] + + quality = QualityReporter(ruff_check_driver) + + assert quality.name() == "ruff.check" + assert quality.measured_lines("foo/bar/path/to/file.sh") is None + actual_violations = quality.violations("foo/bar/path/to/file.py") + assert actual_violations == expected_violations From d0336e3a8c7c67207b1661d66556a5c9b163ea12 Mon Sep 17 00:00:00 2001 From: Javier Buzzi Date: Thu, 15 May 2025 13:34:52 +0200 Subject: [PATCH 04/39] . --- tests/test_violations_reporter.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/test_violations_reporter.py b/tests/test_violations_reporter.py index 4be8a546..74e884b9 100644 --- a/tests/test_violations_reporter.py +++ b/tests/test_violations_reporter.py @@ -2138,8 +2138,7 @@ def test_quality(self, process_patcher): ] quality = QualityReporter(ruff_check_driver) + actual_violations = quality.violations("foo/bar/path/to/file.py") assert quality.name() == "ruff.check" - assert quality.measured_lines("foo/bar/path/to/file.sh") is None - actual_violations = quality.violations("foo/bar/path/to/file.py") assert actual_violations == expected_violations From 588255b4d9149e71bd0e390901d1164ae09fc2cc Mon Sep 17 00:00:00 2001 From: Javier Buzzi Date: Thu, 15 May 2025 13:58:09 +0200 Subject: [PATCH 05/39] Adds docs --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index afcb27b1..acb141cf 100644 --- a/README.rst +++ b/README.rst @@ -128,7 +128,7 @@ You can use diff-cover to see quality reports on the diff as well by running diff-quality --violations= Where ``tool`` is the quality checker to use. Currently ``pycodestyle``, ``pyflakes``, -``flake8``, ``pylint``, ``checkstyle``, ``checkstylexml`` are supported, but more +``flake8``, ``pylint``, ``checkstyle``, ``checkstylexml``, ``ruff check`` are supported, but more checkers can (and should!) be supported. See the section "Adding `diff-quality`` Support for a New Quality Checker". From 3b100b0b9cec29ab2c88d50f1227604d723f0e46 Mon Sep 17 00:00:00 2001 From: kingbuzzman Date: Fri, 16 May 2025 03:30:40 -0400 Subject: [PATCH 06/39] oops --- poetry.lock | 30 +++++++++++++++++++++++++++++- pyproject.toml | 1 + 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/poetry.lock b/poetry.lock index acde1684..8bbf2979 100644 --- a/poetry.lock +++ b/poetry.lock @@ -707,6 +707,34 @@ files = [ [package.dependencies] docutils = ">=0.11,<1.0" +[[package]] +name = "ruff" +version = "0.11.10" +description = "An extremely fast Python linter and code formatter, written in Rust." +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "ruff-0.11.10-py3-none-linux_armv6l.whl", hash = "sha256:859a7bfa7bc8888abbea31ef8a2b411714e6a80f0d173c2a82f9041ed6b50f58"}, + {file = "ruff-0.11.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:968220a57e09ea5e4fd48ed1c646419961a0570727c7e069842edd018ee8afed"}, + {file = "ruff-0.11.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1067245bad978e7aa7b22f67113ecc6eb241dca0d9b696144256c3a879663bca"}, + {file = "ruff-0.11.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4854fd09c7aed5b1590e996a81aeff0c9ff51378b084eb5a0b9cd9518e6cff2"}, + {file = "ruff-0.11.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8b4564e9f99168c0f9195a0fd5fa5928004b33b377137f978055e40008a082c5"}, + {file = "ruff-0.11.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b6a9cc5b62c03cc1fea0044ed8576379dbaf751d5503d718c973d5418483641"}, + {file = "ruff-0.11.10-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:607ecbb6f03e44c9e0a93aedacb17b4eb4f3563d00e8b474298a201622677947"}, + {file = "ruff-0.11.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7b3a522fa389402cd2137df9ddefe848f727250535c70dafa840badffb56b7a4"}, + {file = "ruff-0.11.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2f071b0deed7e9245d5820dac235cbdd4ef99d7b12ff04c330a241ad3534319f"}, + {file = "ruff-0.11.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a60e3a0a617eafba1f2e4186d827759d65348fa53708ca547e384db28406a0b"}, + {file = "ruff-0.11.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:da8ec977eaa4b7bf75470fb575bea2cb41a0e07c7ea9d5a0a97d13dbca697bf2"}, + {file = "ruff-0.11.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ddf8967e08227d1bd95cc0851ef80d2ad9c7c0c5aab1eba31db49cf0a7b99523"}, + {file = "ruff-0.11.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5a94acf798a82db188f6f36575d80609072b032105d114b0f98661e1679c9125"}, + {file = "ruff-0.11.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:3afead355f1d16d95630df28d4ba17fb2cb9c8dfac8d21ced14984121f639bad"}, + {file = "ruff-0.11.10-py3-none-win32.whl", hash = "sha256:dc061a98d32a97211af7e7f3fa1d4ca2fcf919fb96c28f39551f35fc55bdbc19"}, + {file = "ruff-0.11.10-py3-none-win_amd64.whl", hash = "sha256:5cc725fbb4d25b0f185cb42df07ab6b76c4489b4bfb740a175f3a59c70e8a224"}, + {file = "ruff-0.11.10-py3-none-win_arm64.whl", hash = "sha256:ef69637b35fb8b210743926778d0e45e1bffa850a7c61e428c6b971549b5f5d1"}, + {file = "ruff-0.11.10.tar.gz", hash = "sha256:d522fb204b4959909ecac47da02830daec102eeb100fb50ea9554818d47a5fa6"}, +] + [[package]] name = "setuptools" version = "80.1.0" @@ -830,4 +858,4 @@ toml = ["tomli"] [metadata] lock-version = "2.1" python-versions = "^3.9.17" -content-hash = "5ef6a26ec8409fc44c74bced6235b8a2a69d4a9440d91242dc4353ec711c1a1f" +content-hash = "0c2d55a34ab373120f336773707f1d99ca290909a4803f44902b74a8e2512e5a" diff --git a/pyproject.toml b/pyproject.toml index e0f79610..c7945293 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -62,6 +62,7 @@ pydocstyle = "^6.1.1" black = "^25.1.0" isort = "^6.0.1" doc8 = "1.1.2" +ruff = "^0.11.10" [tool.poetry.extras] toml = ["tomli"] From 439a6cfdef211df4d112f35e9d8cf87fcbec29be Mon Sep 17 00:00:00 2001 From: kingbuzzman Date: Fri, 16 May 2025 03:53:01 -0400 Subject: [PATCH 07/39] Adds ruff linting and formatting --- .flake8 | 18 ------------------ pyproject.toml | 32 ++++++++++++++++++++++++++++++++ verify.sh | 4 ++-- 3 files changed, 34 insertions(+), 20 deletions(-) delete mode 100644 .flake8 diff --git a/.flake8 b/.flake8 deleted file mode 100644 index 3d94dbae..00000000 --- a/.flake8 +++ /dev/null @@ -1,18 +0,0 @@ -[flake8] -max-line-length=100 -select= - # flake8-mccabe - C901, - # flake8-pycodestyle - E, - # flake8-pyflakes - F, - # flake8-pycodestyle - W, -ignore = - # conflict with black formatter - W503,E203, -per-file-ignores = - # supression for __init__ - diff_cover/tests/*: E501 - diff_cover/tests/fixtures/*: E,F,W \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index c7945293..7ff254f5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -150,3 +150,35 @@ markers = [ [tool.doc8] max_line_length = 120 + +[tool.ruff] +line-length = 100 +target-version = "py39" +exclude = [ + "tests/fixtures/snippet_8859.py", + "tests/fixtures/snippet_unicode.py", + "tests/fixtures/snippet_src.py", +] + +[tool.ruff.format] +quote-style = "double" +indent-style = "space" +line-ending = "auto" +skip-magic-trailing-comma = false + +[tool.ruff.lint] +select = [ + # McCabe complexity + "C901", + # pycodestyle + "E", + # pyflakes + "F", + # pycodestyle warnings + "W", +] +ignore = ["E203"] # Whitespace before ':' + +[tool.ruff.lint.per-file-ignores] +"diff_cover/tests/*" = ["E501"] +"diff_cover/tests/fixtures/*" = ["E", "F", "W"] diff --git a/verify.sh b/verify.sh index 99996d59..55a1816f 100755 --- a/verify.sh +++ b/verify.sh @@ -2,8 +2,8 @@ set -euo pipefail IFS=$'\n\t' -black diff_cover tests --check -isort diff_cover tests --check +ruff format --check . +ruff check . python -m pytest -n auto --cov-context test --cov --cov-report=xml tests git fetch origin main:refs/remotes/origin/main diff-cover --version From d1cbd0bbfd80e19f7ab6231251389fadfd4b9b22 Mon Sep 17 00:00:00 2001 From: kingbuzzman Date: Fri, 16 May 2025 04:11:49 -0400 Subject: [PATCH 08/39] format --- diff_cover/command_runner.py | 10 +-- diff_cover/diff_cover_tool.py | 28 ++----- diff_cover/diff_quality_tool.py | 44 +++-------- diff_cover/diff_reporter.py | 4 +- diff_cover/git_diff.py | 10 +-- diff_cover/report_generator.py | 22 ++---- diff_cover/snippets.py | 16 +--- diff_cover/violationsreporters/base.py | 4 +- .../java_violations_reporter.py | 4 +- .../violations_reporter.py | 56 ++++---------- tests/fixtures/hello.py | 2 +- tests/fixtures/hi.py | 2 +- tests/fixtures/violations_test_file.py | 8 +- tests/test_clover_violations_reporter.py | 4 +- tests/test_config_parser.py | 4 +- tests/test_diff_quality_main.py | 4 +- tests/test_diff_reporter.py | 66 +++++------------ tests/test_git_diff.py | 16 +--- tests/test_git_diff_file.py | 5 +- tests/test_git_path.py | 4 +- tests/test_integration.py | 6 +- tests/test_java_violations_reporter.py | 4 +- tests/test_report_generator.py | 8 +- tests/test_snippets.py | 12 +-- tests/test_violations_reporter.py | 74 ++++++------------- 25 files changed, 109 insertions(+), 308 deletions(-) diff --git a/diff_cover/command_runner.py b/diff_cover/command_runner.py index 0761dc1b..31a1c995 100644 --- a/diff_cover/command_runner.py +++ b/diff_cover/command_runner.py @@ -31,11 +31,7 @@ def execute(command, exit_codes=None): sys.stderr.write( " ".join( [ - ( - cmd.decode(sys.getfilesystemencoding()) - if isinstance(cmd, bytes) - else cmd - ) + (cmd.decode(sys.getfilesystemencoding()) if isinstance(cmd, bytes) else cmd) for cmd in command ] ) @@ -54,9 +50,7 @@ def run_command_for_code(command): Returns command's exit code. """ try: - process = subprocess.Popen( - command, stdout=subprocess.PIPE, stderr=subprocess.PIPE - ) + process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE) process.communicate() except FileNotFoundError: return 1 diff --git a/diff_cover/diff_cover_tool.py b/diff_cover/diff_cover_tool.py index 782415a8..d2af8876 100644 --- a/diff_cover/diff_cover_tool.py +++ b/diff_cover/diff_cover_tool.py @@ -26,9 +26,7 @@ MARKDOWN_REPORT_HELP = "Diff coverage Markdown output" COMPARE_BRANCH_HELP = "Branch to compare" CSS_FILE_HELP = "Write CSS into an external file" -FAIL_UNDER_HELP = ( - "Returns an error code if coverage or quality score is below this value" -) +FAIL_UNDER_HELP = "Returns an error code if coverage or quality score is below this value" IGNORE_STAGED_HELP = "Ignores staged changes" IGNORE_UNSTAGED_HELP = "Ignores unstaged changes" IGNORE_WHITESPACE = "When getting a diff ignore any and all whitespace" @@ -92,9 +90,7 @@ def parse_coverage_args(argv): help=MARKDOWN_REPORT_HELP, ) - parser.add_argument( - "--show-uncovered", action="store_true", default=None, help=SHOW_UNCOVERED - ) + parser.add_argument("--show-uncovered", action="store_true", default=None, help=SHOW_UNCOVERED) parser.add_argument( "--expand-coverage-report", @@ -139,13 +135,9 @@ def parse_coverage_args(argv): help=INCLUDE_UNTRACKED_HELP, ) - parser.add_argument( - "--exclude", metavar="EXCLUDE", type=str, nargs="+", help=EXCLUDE_HELP - ) + parser.add_argument("--exclude", metavar="EXCLUDE", type=str, nargs="+", help=EXCLUDE_HELP) - parser.add_argument( - "--include", metavar="INCLUDE", type=str, nargs="+", help=INCLUDE_HELP - ) + parser.add_argument("--include", metavar="INCLUDE", type=str, nargs="+", help=INCLUDE_HELP) parser.add_argument( "--src-roots", @@ -172,13 +164,9 @@ def parse_coverage_args(argv): help=IGNORE_WHITESPACE, ) - parser.add_argument( - "-q", "--quiet", action="store_true", default=None, help=QUIET_HELP - ) + parser.add_argument("-q", "--quiet", action="store_true", default=None, help=QUIET_HELP) - parser.add_argument( - "-c", "--config-file", help=CONFIG_FILE_HELP, metavar="CONFIG_FILE" - ) + parser.add_argument("-c", "--config-file", help=CONFIG_FILE_HELP, metavar="CONFIG_FILE") parser.add_argument("--diff-file", type=str, default=None, help=DIFF_FILE_HELP) @@ -298,9 +286,7 @@ def main(argv=None, directory=None): diff_tool = None if not arg_dict["diff_file"]: - diff_tool = GitDiffTool( - arg_dict["diff_range_notation"], arg_dict["ignore_whitespace"] - ) + diff_tool = GitDiffTool(arg_dict["diff_range_notation"], arg_dict["ignore_whitespace"]) else: diff_tool = GitDiffFileTool(arg_dict["diff_file"]) diff --git a/diff_cover/diff_quality_tool.py b/diff_cover/diff_quality_tool.py index 1b389013..3277d98e 100644 --- a/diff_cover/diff_quality_tool.py +++ b/diff_cover/diff_quality_tool.py @@ -75,9 +75,7 @@ "shellcheck": shellcheck_driver, } -VIOLATION_CMD_HELP = "Which code quality tool to use (%s)" % "/".join( - sorted(QUALITY_DRIVERS) -) +VIOLATION_CMD_HELP = "Which code quality tool to use (%s)" % "/".join(sorted(QUALITY_DRIVERS)) INPUT_REPORTS_HELP = "Which violations reports to use" OPTIONS_HELP = "Options to be passed to the violations tool" INCLUDE_HELP = "Files to include (glob pattern)" @@ -145,9 +143,7 @@ def parse_quality_args(argv): parser.add_argument("--options", type=str, nargs="?", help=OPTIONS_HELP) - parser.add_argument( - "--fail-under", metavar="SCORE", type=float, help=FAIL_UNDER_HELP - ) + parser.add_argument("--fail-under", metavar="SCORE", type=float, help=FAIL_UNDER_HELP) parser.add_argument( "--ignore-staged", action="store_true", default=None, help=IGNORE_STAGED_HELP @@ -167,13 +163,9 @@ def parse_quality_args(argv): help=INCLUDE_UNTRACKED_HELP, ) - parser.add_argument( - "--exclude", metavar="EXCLUDE", type=str, nargs="+", help=EXCLUDE_HELP - ) + parser.add_argument("--exclude", metavar="EXCLUDE", type=str, nargs="+", help=EXCLUDE_HELP) - parser.add_argument( - "--include", metavar="INCLUDE", nargs="+", type=str, help=INCLUDE_HELP - ) + parser.add_argument("--include", metavar="INCLUDE", nargs="+", type=str, help=INCLUDE_HELP) parser.add_argument( "--diff-range-notation", @@ -194,17 +186,11 @@ def parse_quality_args(argv): help=IGNORE_WHITESPACE, ) - parser.add_argument( - "-q", "--quiet", action="store_true", default=None, help=QUIET_HELP - ) + parser.add_argument("-q", "--quiet", action="store_true", default=None, help=QUIET_HELP) - parser.add_argument( - "-c", "--config-file", help=CONFIG_FILE_HELP, metavar="CONFIG_FILE" - ) + parser.add_argument("-c", "--config-file", help=CONFIG_FILE_HELP, metavar="CONFIG_FILE") - parser.add_argument( - "--report-root-path", help=REPORT_ROOT_PATH_HELP, metavar="ROOT_PATH" - ) + parser.add_argument("--report-root-path", help=REPORT_ROOT_PATH_HELP, metavar="ROOT_PATH") defaults = { "ignore_whitespace": False, @@ -218,9 +204,7 @@ def parse_quality_args(argv): "quiet": False, } - return get_config( - parser=parser, argv=argv, defaults=defaults, tool=Tool.DIFF_QUALITY - ) + return get_config(parser=parser, argv=argv, defaults=defaults, tool=Tool.DIFF_QUALITY) def generate_quality_report( @@ -341,22 +325,16 @@ def main(argv=None, directory=None): # If we've been given pre-generated reports, # try to open the files if arg_dict["report_root_path"]: - driver.add_driver_args( - report_root_path=arg_dict["report_root_path"] - ) + driver.add_driver_args(report_root_path=arg_dict["report_root_path"]) reporter = QualityReporter(driver, input_reports, user_options) elif reporter_factory_fn: - reporter = reporter_factory_fn( - reports=input_reports, options=user_options - ) + reporter = reporter_factory_fn(reports=input_reports, options=user_options) percent_passing = generate_quality_report( reporter, arg_dict["compare_branch"], - GitDiffTool( - arg_dict["diff_range_notation"], arg_dict["ignore_whitespace"] - ), + GitDiffTool(arg_dict["diff_range_notation"], arg_dict["ignore_whitespace"]), html_report=arg_dict["html_report"], json_report=arg_dict["json_report"], markdown_report=arg_dict["markdown_report"], diff --git a/diff_cover/diff_reporter.py b/diff_cover/diff_reporter.py index 8f404b95..a7a387c2 100644 --- a/diff_cover/diff_reporter.py +++ b/diff_cover/diff_reporter.py @@ -243,9 +243,7 @@ def _git_diff(self): # Remove any lines from the dict that have been deleted # Include any lines that have been added result_dict[src_path] = [ - line - for line in result_dict.get(src_path, []) - if line not in deleted_lines + line for line in result_dict.get(src_path, []) if line not in deleted_lines ] + added_lines # Eliminate repeats and order line numbers diff --git a/diff_cover/git_diff.py b/diff_cover/git_diff.py index 683b8a6b..4bf20535 100644 --- a/diff_cover/git_diff.py +++ b/diff_cover/git_diff.py @@ -68,9 +68,7 @@ def diff_committed(self, compare_branch="origin/main"): branch=compare_branch, notation=self.range_notation ) try: - return execute( - self._default_git_args + self._default_diff_args + [diff_range] - )[0] + return execute(self._default_git_args + self._default_diff_args + [diff_range])[0] except CommandError as e: if "unknown revision" in str(e): raise ValueError( @@ -101,9 +99,7 @@ def diff_staged(self): Raises a `GitDiffError` if `git diff` outputs anything to stderr. """ - return execute(self._default_git_args + self._default_diff_args + ["--cached"])[ - 0 - ] + return execute(self._default_git_args + self._default_diff_args + ["--cached"])[0] def untracked(self): """Return the untracked files.""" @@ -114,9 +110,7 @@ def untracked(self): class GitDiffFileTool(GitDiffTool): - def __init__(self, diff_file_path): - self.diff_file_path = diff_file_path super().__init__("...", False) diff --git a/diff_cover/report_generator.py b/diff_cover/report_generator.py index 132f9d93..4efcec2a 100644 --- a/diff_cover/report_generator.py +++ b/diff_cover/report_generator.py @@ -19,13 +19,9 @@ class DiffViolations: """ def __init__(self, violations, measured_lines, diff_lines): - self.lines = {violation.line for violation in violations}.intersection( - diff_lines - ) + self.lines = {violation.line for violation in violations}.intersection(diff_lines) - self.violations = { - violation for violation in violations if violation.line in self.lines - } + self.violations = {violation for violation in violations if violation.line in self.lines} # By convention, a violation reporter # can return `None` to indicate that all lines are "measured" @@ -121,9 +117,7 @@ def covered_lines(self, src_path): return [] return sorted( - set(diff_violations.measured_lines).difference( - set(self.violation_lines(src_path)) - ) + set(diff_violations.measured_lines).difference(set(self.violation_lines(src_path))) ) def violation_lines(self, src_path): @@ -148,12 +142,7 @@ def total_num_lines(self): which we have coverage info. """ - return sum( - [ - len(summary.measured_lines) - for summary in self._diff_violations().values() - ] - ) + return sum([len(summary.measured_lines) for summary in self._diff_violations().values()]) def total_num_violations(self): """ @@ -179,8 +168,7 @@ def total_percent_covered(self): def num_changed_lines(self): """Returns the number of changed lines.""" return sum( - len(self._diff.lines_changed(src_path)) - for src_path in self._diff.src_paths_changed() + len(self._diff.lines_changed(src_path)) for src_path in self._diff.src_paths_changed() ) def _diff_violations(self): diff --git a/diff_cover/snippets.py b/diff_cover/snippets.py index 11524148..65aa9112 100644 --- a/diff_cover/snippets.py +++ b/diff_cover/snippets.py @@ -134,11 +134,7 @@ def markdown(self): header = "Lines %d-%d\n\n" % (self._start_line, self._last_line) if self._lexer_name in self.LEXER_TO_MARKDOWN_CODE_HINT: return header + ( - "```" - + self.LEXER_TO_MARKDOWN_CODE_HINT[self._lexer_name] - + "\n" - + text - + "\n```\n" + "```" + self.LEXER_TO_MARKDOWN_CODE_HINT[self._lexer_name] + "\n" + text + "\n```\n" ) # unknown programming language, return a non-decorated fenced code block: @@ -383,9 +379,7 @@ def _snippet_ranges(cls, num_src_lines, violation_lines): elif lines_since_last_violation > cls.MAX_GAP_IN_SNIPPET: # Expand to include extra context, but not after last line snippet_end = line_num - lines_since_last_violation - snippet_end = min( - num_src_lines, snippet_end + cls.NUM_CONTEXT_LINES - ) + snippet_end = min(num_src_lines, snippet_end + cls.NUM_CONTEXT_LINES) current_range = (current_range[0], snippet_end) # Store the snippet and start looking for the next one @@ -414,8 +408,4 @@ def _shift_lines(line_num_list, start_line): than or equal to `start_line`; otherwise, they will be excluded from the list. """ - return [ - line_num - start_line + 1 - for line_num in line_num_list - if line_num >= start_line - ] + return [line_num - start_line + 1 for line_num in line_num_list if line_num >= start_line] diff --git a/diff_cover/violationsreporters/base.py b/diff_cover/violationsreporters/base.py index 3f1a6ca0..550b20c9 100644 --- a/diff_cover/violationsreporters/base.py +++ b/diff_cover/violationsreporters/base.py @@ -74,9 +74,7 @@ def name(self): class QualityDriver(ABC): - def __init__( - self, name, supported_extensions, command, exit_codes=None, output_stderr=False - ): + def __init__(self, name, supported_extensions, command, exit_codes=None, output_stderr=False): """ Args: name: (str) name of the driver diff --git a/diff_cover/violationsreporters/java_violations_reporter.py b/diff_cover/violationsreporters/java_violations_reporter.py index 2ace2a79..bc25f3e9 100644 --- a/diff_cover/violationsreporters/java_violations_reporter.py +++ b/diff_cover/violationsreporters/java_violations_reporter.py @@ -72,9 +72,7 @@ def parse_reports(self, reports): for file_tree in files: for error in file_tree.findall("error"): line_number = error.get("line") - error_str = "{}: {}".format( - error.get("severity"), error.get("message") - ) + error_str = "{}: {}".format(error.get("severity"), error.get("message")) violation = Violation(int(line_number), error_str) filename = GitPathTool.relative_path(file_tree.get("name")) violations_dict[filename].append(violation) diff --git a/diff_cover/violationsreporters/violations_reporter.py b/diff_cover/violationsreporters/violations_reporter.py index 446fa179..0e8b3147 100644 --- a/diff_cover/violationsreporters/violations_reporter.py +++ b/diff_cover/violationsreporters/violations_reporter.py @@ -97,9 +97,7 @@ def _get_classes(self, index, xml_document, src_path): if not self._xml_cache[index]: self._xml_cache[index] = self._get_xml_classes(xml_document) - return self._xml_cache[index].get(src_abs_path) or self._xml_cache[index].get( - src_rel_path - ) + return self._xml_cache[index].get(src_abs_path) or self._xml_cache[index].get(src_rel_path) def get_src_path_line_nodes_cobertura(self, index, xml_document, src_path): classes = self._get_classes(index, xml_document, src_path) @@ -140,9 +138,7 @@ def _measured_source_path_matches(self, package_name, file_name, src_path): for root in self._src_roots: if ( os.path.normcase( - GitPathTool.relative_path( - os.path.join(root, package_name, file_name) - ) + GitPathTool.relative_path(os.path.join(root, package_name, file_name)) ) == norm_src_path ): @@ -163,9 +159,7 @@ def get_src_path_line_nodes_jacoco(self, xml_document, src_path): _files = [ _file for _file in pkg.findall("sourcefile") - if self._measured_source_path_matches( - pkg.get("name"), _file.get("name"), src_path - ) + if self._measured_source_path_matches(pkg.get("name"), _file.get("name"), src_path) ] files.extend(_files) @@ -195,23 +189,17 @@ def _cache_file(self, src_path): for i, xml_document in enumerate(self._xml_roots): if xml_document.findall(".[@clover]"): # see etc/schema/clover.xsd at https://bitbucket.org/atlassian/clover/src - line_nodes = self.get_src_path_line_nodes_clover( - xml_document, src_path - ) + line_nodes = self.get_src_path_line_nodes_clover(xml_document, src_path) _number = "num" _hits = "count" elif xml_document.findall(".[@name]"): # https://github.com/jacoco/jacoco/blob/master/org.jacoco.report/src/org/jacoco/report/xml/report.dtd - line_nodes = self.get_src_path_line_nodes_jacoco( - xml_document, src_path - ) + line_nodes = self.get_src_path_line_nodes_jacoco(xml_document, src_path) _number = "nr" _hits = "ci" else: # https://github.com/cobertura/web/blob/master/htdocs/xml/coverage-04.dtd - line_nodes = self.get_src_path_line_nodes_cobertura( - i, xml_document, src_path - ) + line_nodes = self.get_src_path_line_nodes_cobertura(i, xml_document, src_path) _number = "number" _hits = "hits" if line_nodes is None: @@ -221,9 +209,7 @@ def _cache_file(self, src_path): if self._expand_coverage_report: reported_line_hits = {} for line in line_nodes: - reported_line_hits[int(line.get(_number))] = int( - line.get(_hits, 0) - ) + reported_line_hits[int(line.get(_number))] = int(line.get(_hits, 0)) if reported_line_hits: last_hit_number = 0 for line_number in range( @@ -235,9 +221,7 @@ def _cache_file(self, src_path): else: # This is an unreported line. # We add it with the previous line hit score - line_nodes.append( - {_hits: last_hit_number, _number: line_number} - ) + line_nodes.append({_hits: last_hit_number, _number: line_number}) # First case, need to define violations initially if violations is None: @@ -330,9 +314,7 @@ def parse(lcov_file): line_no = int(args[0]) num_executions = int(args[1]) if source_file is None: - raise ValueError( - f"No source file specified for line coverage: {line}" - ) + raise ValueError(f"No source file specified for line coverage: {line}") if line_no not in lcov_report[source_file]: lcov_report[source_file][line_no] = 0 lcov_report[source_file][line_no] += num_executions @@ -399,9 +381,7 @@ def _cache_file(self, src_path): if violations is None: violations = { Violation(int(line_no), None) - for line_no, num_executions in lcov_document[ - src_search_path - ].items() + for line_no, num_executions in lcov_document[src_search_path].items() if int(num_executions) == 0 } @@ -411,9 +391,7 @@ def _cache_file(self, src_path): else: violations = violations & { Violation(int(line_no), None) - for line_no, num_executions in lcov_document[ - src_search_path - ].items() + for line_no, num_executions in lcov_document[src_search_path].items() if int(num_executions) == 0 } @@ -421,9 +399,7 @@ def _cache_file(self, src_path): # measured = measured | {int(line.get(_number)) for line in line_nodes} measured = measured | { int(line_no) - for line_no, num_executions in lcov_document[ - src_search_path - ].items() + for line_no, num_executions in lcov_document[src_search_path].items() } # If we don't have any information about the source file, @@ -615,9 +591,7 @@ def __init__(self): 2 | 4 | 8 | 16, ], ) - self.pylint_expression = re.compile( - r"^([^:]+):(\d+): \[(\w+),? ?([^\]]*)] (.*)$" - ) + self.pylint_expression = re.compile(r"^([^:]+):(\d+): \[(\w+),? ?([^\]]*)] (.*)$") self.dupe_code_violation = "R0801" self.command_to_check_install = ["pylint", "--version"] @@ -681,9 +655,7 @@ def parse_reports(self, reports): # If we're looking for a particular source file, # ignore any other source files. if function_name: - error_str = "{}: {}: {}".format( - pylint_code, function_name, message - ) + error_str = "{}: {}: {}".format(pylint_code, function_name, message) else: error_str = f"{pylint_code}: {message}" diff --git a/tests/fixtures/hello.py b/tests/fixtures/hello.py index e2974381..ed14ac1a 100644 --- a/tests/fixtures/hello.py +++ b/tests/fixtures/hello.py @@ -1,2 +1,2 @@ print("hello") -print(unknown_var) \ No newline at end of file +print(unknown_var) diff --git a/tests/fixtures/hi.py b/tests/fixtures/hi.py index e2974381..ed14ac1a 100644 --- a/tests/fixtures/hi.py +++ b/tests/fixtures/hi.py @@ -1,2 +1,2 @@ print("hello") -print(unknown_var) \ No newline at end of file +print(unknown_var) diff --git a/tests/fixtures/violations_test_file.py b/tests/fixtures/violations_test_file.py index 08991bc7..18160a90 100644 --- a/tests/fixtures/violations_test_file.py +++ b/tests/fixtures/violations_test_file.py @@ -1,12 +1,14 @@ def func_1(apple, my_list): - if apple<10: - # Do something + if apple < 10: + # Do something my_list.append(apple) return my_list[1:] + + def func_2(spongebob, squarepants): """A less messy function""" for char in spongebob: if char in squarepants: return char - unused=1 + unused = 1 return None diff --git a/tests/test_clover_violations_reporter.py b/tests/test_clover_violations_reporter.py index e79df37a..7af07ce0 100644 --- a/tests/test_clover_violations_reporter.py +++ b/tests/test_clover_violations_reporter.py @@ -13,7 +13,5 @@ def test_get_src_path_clover(datadir): GitPathTool._cwd = "/" GitPathTool._root = "/" clover_report = etree.parse(str(datadir / "test.xml")) - result = XmlCoverageReporter.get_src_path_line_nodes_clover( - clover_report, "isLucky.js" - ) + result = XmlCoverageReporter.get_src_path_line_nodes_clover(clover_report, "isLucky.js") assert sorted([int(line.attrib["num"]) for line in result]) == [2, 3, 5, 6, 8, 12] diff --git a/tests/test_config_parser.py b/tests/test_config_parser.py index eb0e300f..99de5c8a 100644 --- a/tests/test_config_parser.py +++ b/tests/test_config_parser.py @@ -85,9 +85,7 @@ def test_get_config_unrecognized_file(mocker, tool): ), ], ) -def test_get_config( - mocker, tmp_path, tool, cli_config, defaults, file_content, expected -): +def test_get_config(mocker, tmp_path, tool, cli_config, defaults, file_content, expected): if file_content: toml_file = tmp_path / "foo.toml" toml_file.write_text(file_content) diff --git a/tests/test_diff_quality_main.py b/tests/test_diff_quality_main.py index a201b9a1..76f9217c 100644 --- a/tests/test_diff_quality_main.py +++ b/tests/test_diff_quality_main.py @@ -114,9 +114,7 @@ def patch_git_patch(mocker): @pytest.fixture def report_mock(mocker): - return mocker.patch( - "diff_cover.diff_quality_tool.generate_quality_report", return_value=100 - ) + return mocker.patch("diff_cover.diff_quality_tool.generate_quality_report", return_value=100) def test_parse_options(report_mock): diff --git a/tests/test_diff_reporter.py b/tests/test_diff_reporter.py index 0d61b4e7..e22c7a22 100644 --- a/tests/test_diff_reporter.py +++ b/tests/test_diff_reporter.py @@ -59,9 +59,7 @@ def test_name_ignore_unstaged(git_diff): def test_name_ignore_staged_and_unstaged(git_diff): # Override the default branch assert ( - GitDiffReporter( - git_diff=git_diff, ignore_staged=True, ignore_unstaged=True - ).name() + GitDiffReporter(git_diff=git_diff, ignore_staged=True, ignore_unstaged=True).name() == "origin/main...HEAD" ) @@ -120,9 +118,7 @@ def test_git_path_selection(diff, git_diff, include, exclude, expected): _set_git_diff_output( diff, git_diff, - git_diff_output( - {"subdir1/file1.py": line_numbers(3, 10) + line_numbers(34, 47)} - ), + git_diff_output({"subdir1/file1.py": line_numbers(3, 10) + line_numbers(34, 47)}), git_diff_output({"subdir2/file2.py": line_numbers(3, 10), "file3.py": [0]}), git_diff_output(dict(), deleted_files=["README.md"]), ) @@ -144,9 +140,7 @@ def test_git_source_paths(diff, git_diff): _set_git_diff_output( diff, git_diff, - git_diff_output( - {"subdir/file1.py": line_numbers(3, 10) + line_numbers(34, 47)} - ), + git_diff_output({"subdir/file1.py": line_numbers(3, 10) + line_numbers(34, 47)}), git_diff_output({"subdir/file2.py": line_numbers(3, 10), "file3.py": [0]}), git_diff_output(dict(), deleted_files=["README.md"]), ) @@ -178,9 +172,7 @@ def test_git_source_paths_with_space(diff, git_diff): def test_duplicate_source_paths(diff, git_diff): # Duplicate the output for committed, staged, and unstaged changes - diff_output = git_diff_output( - {"subdir/file1.py": line_numbers(3, 10) + line_numbers(34, 47)} - ) + diff_output = git_diff_output({"subdir/file1.py": line_numbers(3, 10) + line_numbers(34, 47)}) _set_git_diff_output(diff, git_diff, diff_output, diff_output, diff_output) # Get the source paths in the diff @@ -196,9 +188,7 @@ def test_git_source_paths_with_supported_extensions(diff, git_diff): _set_git_diff_output( diff, git_diff, - git_diff_output( - {"subdir/file1.py": line_numbers(3, 10) + line_numbers(34, 47)} - ), + git_diff_output({"subdir/file1.py": line_numbers(3, 10) + line_numbers(34, 47)}), git_diff_output({"subdir/file2.py": line_numbers(3, 10), "file3.py": [0]}), git_diff_output({"README.md": line_numbers(3, 10)}), ) @@ -221,9 +211,7 @@ def test_git_lines_changed(diff, git_diff): _set_git_diff_output( diff, git_diff, - git_diff_output( - {"subdir/file1.py": line_numbers(3, 10) + line_numbers(34, 47)} - ), + git_diff_output({"subdir/file1.py": line_numbers(3, 10) + line_numbers(34, 47)}), git_diff_output({"subdir/file2.py": line_numbers(3, 10), "file3.py": [0]}), git_diff_output(dict(), deleted_files=["README.md"]), ) @@ -282,9 +270,7 @@ def test_git_deleted_lines(diff, git_diff): _set_git_diff_output( diff, git_diff, - git_diff_output( - {"subdir/file1.py": line_numbers(3, 10) + line_numbers(34, 47)} - ), + git_diff_output({"subdir/file1.py": line_numbers(3, 10) + line_numbers(34, 47)}), git_diff_output({"subdir/file2.py": line_numbers(3, 10), "file3.py": [0]}), git_diff_output(dict(), deleted_files=["README.md"]), ) @@ -323,9 +309,7 @@ def test_git_unicode_filename(diff, git_diff): def test_git_repeat_lines(diff, git_diff): # Same committed, staged, and unstaged lines - diff_output = git_diff_output( - {"subdir/file1.py": line_numbers(3, 10) + line_numbers(34, 47)} - ) + diff_output = git_diff_output({"subdir/file1.py": line_numbers(3, 10) + line_numbers(34, 47)}) _set_git_diff_output(diff, git_diff, diff_output, diff_output, diff_output) # Get the lines changed in the diff @@ -336,9 +320,7 @@ def test_git_repeat_lines(diff, git_diff): def test_git_overlapping_lines(diff, git_diff): - main_diff = git_diff_output( - {"subdir/file1.py": line_numbers(3, 10) + line_numbers(34, 47)} - ) + main_diff = git_diff_output({"subdir/file1.py": line_numbers(3, 10) + line_numbers(34, 47)}) # Overlap, extending the end of the hunk (lines 3 to 10) overlap_1 = git_diff_output({"subdir/file1.py": line_numbers(5, 14)}) @@ -357,9 +339,7 @@ def test_git_overlapping_lines(diff, git_diff): def test_git_line_within_hunk(diff, git_diff): - main_diff = git_diff_output( - {"subdir/file1.py": line_numbers(3, 10) + line_numbers(34, 47)} - ) + main_diff = git_diff_output({"subdir/file1.py": line_numbers(3, 10) + line_numbers(34, 47)}) # Surround hunk in main (lines 3 to 10) surround = git_diff_output({"subdir/file1.py": line_numbers(2, 11)}) @@ -413,9 +393,7 @@ def test_inter_diff_conflict(diff, git_diff): def test_git_no_such_file(diff, git_diff): - diff_output = git_diff_output( - {"subdir/file1.py": [1], "subdir/file2.py": [2], "file3.py": [3]} - ) + diff_output = git_diff_output({"subdir/file1.py": [1], "subdir/file2.py": [2], "file3.py": [3]}) # Configure the git diff output _set_git_diff_output(diff, git_diff, diff_output, "", "") @@ -478,7 +456,7 @@ def test_git_diff_error( # Expect that both methods that access git diff raise an error with pytest.raises(GitDiffError): - print("src_paths_changed() " "should fail for {}".format(diff_str)) + print("src_paths_changed() should fail for {}".format(diff_str)) diff.src_paths_changed() with pytest.raises(GitDiffError): @@ -563,9 +541,7 @@ def test_inclusion_list(diff, git_diff): def test_ignore_staged_inclusion(git_diff): reporter = GitDiffReporter(git_diff=git_diff, ignore_staged=True) - staged_input = git_diff_output( - {"subdir/file1.py": line_numbers(3, 10) + line_numbers(34, 47)} - ) + staged_input = git_diff_output({"subdir/file1.py": line_numbers(3, 10) + line_numbers(34, 47)}) _set_git_diff_output(reporter, git_diff, "", staged_input, "") assert reporter._get_included_diff_results() == ["", ""] @@ -583,13 +559,9 @@ def test_ignore_unstaged_inclusion(git_diff): def test_ignore_staged_and_unstaged_inclusion(git_diff): - reporter = GitDiffReporter( - git_diff=git_diff, ignore_staged=True, ignore_unstaged=True - ) + reporter = GitDiffReporter(git_diff=git_diff, ignore_staged=True, ignore_unstaged=True) - staged_input = git_diff_output( - {"subdir/file1.py": line_numbers(3, 10) + line_numbers(34, 47)} - ) + staged_input = git_diff_output({"subdir/file1.py": line_numbers(3, 10) + line_numbers(34, 47)}) unstaged_input = git_diff_output( {"subdir/file2.py": line_numbers(3, 10) + line_numbers(34, 47)} ) @@ -614,9 +586,7 @@ def test_fnmatch_returns_the_default_with_empty_default(diff): def test_include_untracked(mocker, git_diff): reporter = GitDiffReporter(git_diff=git_diff, include_untracked=True) - diff_output = git_diff_output( - {"subdir/file1.py": line_numbers(3, 10) + line_numbers(34, 47)} - ) + diff_output = git_diff_output({"subdir/file1.py": line_numbers(3, 10) + line_numbers(34, 47)}) _set_git_diff_output( reporter, git_diff, @@ -662,9 +632,7 @@ def test_include_untracked__not_valid_path__not_include_it( supported_extensions=supported_extensions, exclude=excluded, ) - diff_output = git_diff_output( - {"subdir/file1.py": line_numbers(3, 10) + line_numbers(34, 47)} - ) + diff_output = git_diff_output({"subdir/file1.py": line_numbers(3, 10) + line_numbers(34, 47)}) _set_git_diff_output( reporter, git_diff, diff --git a/tests/test_git_diff.py b/tests/test_git_diff.py index 56a1ff47..0cb95cbf 100644 --- a/tests/test_git_diff.py +++ b/tests/test_git_diff.py @@ -39,9 +39,7 @@ def _inner(stdout, stderr, returncode=0): @pytest.fixture def check_diff_committed(subprocess, set_git_diff_output): def _inner(diff_range_notation, ignore_whitespace): - tool_ = GitDiffTool( - range_notation=diff_range_notation, ignore_whitespace=ignore_whitespace - ) + tool_ = GitDiffTool(range_notation=diff_range_notation, ignore_whitespace=ignore_whitespace) set_git_diff_output("test output", "") output = tool_.diff_committed() @@ -98,9 +96,7 @@ def test_diff_unstaged(set_git_diff_output, tool, subprocess): "--no-ext-diff", "-U0", ] - subprocess.Popen.assert_called_with( - expected, stdout=subprocess.PIPE, stderr=subprocess.PIPE - ) + subprocess.Popen.assert_called_with(expected, stdout=subprocess.PIPE, stderr=subprocess.PIPE) def test_diff_staged(tool, subprocess, set_git_diff_output): @@ -123,9 +119,7 @@ def test_diff_staged(tool, subprocess, set_git_diff_output): "-U0", "--cached", ] - subprocess.Popen.assert_called_with( - expected, stdout=subprocess.PIPE, stderr=subprocess.PIPE - ) + subprocess.Popen.assert_called_with(expected, stdout=subprocess.PIPE, stderr=subprocess.PIPE) def test_diff_missing_branch_error(set_git_diff_output, tool, subprocess): @@ -165,9 +159,7 @@ def test_diff_committed_compare_branch(set_git_diff_output, tool, subprocess): "-U0", "release...HEAD", ] - subprocess.Popen.assert_called_with( - expected, stdout=subprocess.PIPE, stderr=subprocess.PIPE - ) + subprocess.Popen.assert_called_with(expected, stdout=subprocess.PIPE, stderr=subprocess.PIPE) def test_errors(set_git_diff_output, tool): diff --git a/tests/test_git_diff_file.py b/tests/test_git_diff_file.py index 2e1d25a7..9e674896 100644 --- a/tests/test_git_diff_file.py +++ b/tests/test_git_diff_file.py @@ -32,9 +32,8 @@ def test_diff_file_not_found(mocker, diff_tool): with pytest.raises(ValueError) as excinfo: _diff_tool.diff_committed() - assert ( - f"Could not read the diff file. Make sure '{_diff_tool.diff_file_path}' exists?" - in str(excinfo.value) + assert f"Could not read the diff file. Make sure '{_diff_tool.diff_file_path}' exists?" in str( + excinfo.value ) assert _diff_tool.diff_file_path == "non_existent_diff_file.txt" diff --git a/tests/test_git_path.py b/tests/test_git_path.py index abd6a9f4..5941534a 100644 --- a/tests/test_git_path.py +++ b/tests/test_git_path.py @@ -34,9 +34,7 @@ def test_project_root_command(process, subprocess): # Expect that the correct command was executed expected = ["git", "rev-parse", "--show-toplevel", "--encoding=utf-8"] - subprocess.Popen.assert_called_with( - expected, stdout=subprocess.PIPE, stderr=subprocess.PIPE - ) + subprocess.Popen.assert_called_with(expected, stdout=subprocess.PIPE, stderr=subprocess.PIPE) def test_relative_path(process): diff --git a/tests/test_integration.py b/tests/test_integration.py index 94f74f92..789c84fc 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -604,11 +604,7 @@ def test_tool_not_installed(self): # Pretend we support a tool named not_installed self.mocker.patch.dict( diff_quality_tool.QUALITY_DRIVERS, - { - "not_installed": DoNothingDriver( - "not_installed", ["txt"], ["not_installed"] - ) - }, + {"not_installed": DoNothingDriver("not_installed", ["txt"], ["not_installed"])}, ) self._call_quality_expecting_error( diff --git a/tests/test_java_violations_reporter.py b/tests/test_java_violations_reporter.py index dd15eb51..388bfd2b 100644 --- a/tests/test_java_violations_reporter.py +++ b/tests/test_java_violations_reporter.py @@ -258,9 +258,7 @@ def test_quality_pregenerated_report(self): # Expect that we get the right violations expected_violations = [ Violation(1, "error: Missing docstring"), - Violation( - 57, "warning: TODO the name of this method is a little bit confusing" - ), + Violation(57, "warning: TODO the name of this method is a little bit confusing"), Violation( 183, "error: Invalid name '' for type argument (should match [a-z_][a-z0-9_]{2,30}$)", diff --git a/tests/test_report_generator.py b/tests/test_report_generator.py index 926d82bb..b7b19958 100644 --- a/tests/test_report_generator.py +++ b/tests/test_report_generator.py @@ -239,9 +239,7 @@ class TestTemplateReportGenerator(BaseReportGeneratorTest): def _test_input_expected_output(self, input_with_expected_output): for test_input, expected_output in input_with_expected_output: - assert expected_output == TemplateReportGenerator.combine_adjacent_lines( - test_input - ) + assert expected_output == TemplateReportGenerator.combine_adjacent_lines(test_input) def test_combine_adjacent_lines_no_adjacent(self): in_out = [([1, 3], ["1", "3"]), ([1, 5, 7, 10], ["1", "5", "7", "10"])] @@ -551,9 +549,7 @@ def setup(self): self.use_default_values() # Have violations_batch() return the violations. self.coverage.violations_batch.side_effect = None - self.coverage.violations_batch.return_value = copy.deepcopy( - self._violations_dict - ) + self.coverage.violations_batch.return_value = copy.deepcopy(self._violations_dict) # Have violations() return an empty list to ensure violations_batch() # is used. for src in self.SRC_PATHS: diff --git a/tests/test_snippets.py b/tests/test_snippets.py index 4f285f2a..99378302 100644 --- a/tests/test_snippets.py +++ b/tests/test_snippets.py @@ -72,9 +72,7 @@ def _src_lines(start_line, end_line): Test lines to write to the source file (Line 1, Line 2, ...). """ - return "\n".join( - [f"Line {line_num}" for line_num in range(start_line, end_line + 1)] - ) + return "\n".join([f"Line {line_num}" for line_num in range(start_line, end_line + 1)]) def _assert_format( @@ -85,9 +83,7 @@ def _assert_format( violation_lines, expected_fixture, ): - snippet = Snippet( - src_tokens, src_filename, start_line, last_line, violation_lines, None - ) + snippet = Snippet(src_tokens, src_filename, start_line, last_line, violation_lines, None) result = snippet.html() expected_str = load_fixture(expected_fixture, encoding="utf-8") @@ -290,9 +286,7 @@ def test_load_utf8_snippets(): @pytest.mark.usefixtures("switch_to_fixture_dir") def test_load_declared_arabic(): - _compare_snippets_output( - "html", "snippet_8859.py", [7], "snippet_arabic_output.html" - ) + _compare_snippets_output("html", "snippet_8859.py", [7], "snippet_arabic_output.html") def test_latin_one_undeclared(tmp_path): diff --git a/tests/test_violations_reporter.py b/tests/test_violations_reporter.py index 74e884b9..df4f3113 100644 --- a/tests/test_violations_reporter.py +++ b/tests/test_violations_reporter.py @@ -134,9 +134,7 @@ def test_non_python_violations(self): violations = self.MANY_VIOLATIONS measured = self.FEW_MEASURED - xml = self._coverage_xml( - file_paths, violations, measured, source_paths=source_paths - ) + xml = self._coverage_xml(file_paths, violations, measured, source_paths=source_paths) coverage = XmlCoverageReporter([xml]) assert violations == coverage.violations(f"{fancy_path}/{file_paths[0]}") @@ -227,9 +225,7 @@ def test_three_inputs(self): # By construction, each file has the same set # of covered/uncovered lines - assert violations1 & violations2 & violations3 == coverage.violations( - "file1.py" - ) + assert violations1 & violations2 & violations3 == coverage.violations("file1.py") assert measured1 | measured2 | measured3 == coverage.measured_lines("file1.py") @@ -237,9 +233,7 @@ def test_different_files_in_inputs(self): # Construct the XML report xml_roots = [ self._coverage_xml(["file.py"], self.MANY_VIOLATIONS, self.FEW_MEASURED), - self._coverage_xml( - ["other_file.py"], self.FEW_VIOLATIONS, self.MANY_MEASURED - ), + self._coverage_xml(["other_file.py"], self.FEW_VIOLATIONS, self.MANY_MEASURED), ] # Parse the report @@ -300,9 +294,7 @@ def test_expand_unreported_lines_when_configured(self): # By construction, each file has the same set # of covered/uncovered lines - assert self.MANY_VIOLATIONS_EXPANDED_MANY_MEASURED == coverage.violations( - "file1.java" - ) + assert self.MANY_VIOLATIONS_EXPANDED_MANY_MEASURED == coverage.violations("file1.java") def test_expand_unreported_lines_without_violations(self): # Construct the XML report @@ -490,20 +482,14 @@ def test_three_inputs(self): # By construction, each file has the same set # of covered/uncovered lines - assert violations1 & violations2 & violations3 == coverage.violations( - "file1.java" - ) - assert measured1 | measured2 | measured3 == coverage.measured_lines( - "file1.java" - ) + assert violations1 & violations2 & violations3 == coverage.violations("file1.java") + assert measured1 | measured2 | measured3 == coverage.measured_lines("file1.java") def test_different_files_in_inputs(self): # Construct the XML report xml_roots = [ self._coverage_xml(["file.java"], self.MANY_VIOLATIONS, self.FEW_MEASURED), - self._coverage_xml( - ["other_file.java"], self.FEW_VIOLATIONS, self.MANY_MEASURED - ), + self._coverage_xml(["other_file.java"], self.FEW_VIOLATIONS, self.MANY_MEASURED), ] # Parse the report @@ -700,21 +686,15 @@ def test_three_inputs(self): # By construction, each file has the same set # of covered/uncovered lines - assert violations1 & violations2 & violations3 == coverage.violations( - "file1.java" - ) + assert violations1 & violations2 & violations3 == coverage.violations("file1.java") - assert measured1 | measured2 | measured3 == coverage.measured_lines( - "file1.java" - ) + assert measured1 | measured2 | measured3 == coverage.measured_lines("file1.java") def test_different_files_in_inputs(self): # Construct the XML report xml_roots = [ self._coverage_xml(["file.java"], self.MANY_VIOLATIONS, self.FEW_MEASURED), - self._coverage_xml( - ["other_file.java"], self.FEW_VIOLATIONS, self.MANY_MEASURED - ), + self._coverage_xml(["other_file.java"], self.FEW_VIOLATIONS, self.MANY_MEASURED), ] # Parse the report @@ -912,21 +892,15 @@ def test_three_inputs(self): # By construction, each file has the same set # of covered/uncovered lines - assert violations1 & violations2 & violations3 == coverage.violations( - "file1.java" - ) + assert violations1 & violations2 & violations3 == coverage.violations("file1.java") - assert measured1 | measured2 | measured3 == coverage.measured_lines( - "file1.java" - ) + assert measured1 | measured2 | measured3 == coverage.measured_lines("file1.java") def test_different_files_in_inputs(self): # Construct the LCOV report lcov_repots = [ self._coverage_lcov(["file.java"], self.MANY_VIOLATIONS, self.FEW_MEASURED), - self._coverage_lcov( - ["other_file.java"], self.FEW_VIOLATIONS, self.MANY_MEASURED - ), + self._coverage_lcov(["other_file.java"], self.FEW_VIOLATIONS, self.MANY_MEASURED), ] # Parse the report @@ -982,9 +956,7 @@ def _coverage_lcov(self, file_paths, violations, measured): for file_path in file_paths: f.write(f"SF:{file_path}\n") for line_num in measured: - f.write( - f"DA:{line_num},{0 if line_num in violation_lines else 1}\n" - ) + f.write(f"DA:{line_num},{0 if line_num in violation_lines else 1}\n") f.write("end_of_record\n") try: return LcovCoverageReporter.parse(f.name) @@ -1592,12 +1564,10 @@ def test_unicode(self, process_patcher): ] violations = quality.violations("file.py") - assert violations == [ - Violation(2, "W0612: cls_name.func_\u9492: Unused variable '\u2920'") - ] + assert violations == [Violation(2, "W0612: cls_name.func_\u9492: Unused variable '\u2920'")] def test_unicode_continuation_char(self, process_patcher): - process_patcher((b"file.py:2: [W1401]" b" Invalid char '\xc3'", ""), 0) + process_patcher((b"file.py:2: [W1401] Invalid char '\xc3'", ""), 0) # Since we are replacing characters we can't interpet, this should # return a valid string with the char replaced with '?' quality = QualityReporter(PylintDriver()) @@ -1642,9 +1612,7 @@ def test_quality_deprecation_warning(self, process_patcher): def test_quality_error(self, mocker, process_patcher): # Patch the output stderr/stdout and returncode of `pylint` - process_patcher( - (b"file1.py:1: [C0111] Missing docstring", b"oops"), status_code=1 - ) + process_patcher((b"file1.py:1: [C0111] Missing docstring", b"oops"), status_code=1) # Parse the report code = mocker.patch( @@ -1710,9 +1678,7 @@ def test_quality_pregenerated_report(self): # Expect that we get the right violations expected_violations = [ Violation(1, "C0111: Missing docstring"), - Violation( - 57, "W0511: TODO the name of this method is a little bit confusing" - ), + Violation(57, "W0511: TODO the name of this method is a little bit confusing"), Violation( 183, 'C0103: Foo.bar.gettag: Invalid name "\u3240" for type argument (should match [a-z_][a-z0-9_]{2,30}$)', @@ -2082,7 +2048,9 @@ def test_parse_report(self): "(error) Array 'yolo[4]' accessed at index 4, which is out of bounds.", ), } - report = "[src/foo.c:123]: (error) Array 'yolo[4]' accessed at index 4, which is out of bounds." + report = ( + "[src/foo.c:123]: (error) Array 'yolo[4]' accessed at index 4, which is out of bounds." + ) driver = CppcheckDriver() actual_violations = driver.parse_reports([report]) From 686e18f0e6e75339d4e8fab853768ce3a2c6351a Mon Sep 17 00:00:00 2001 From: kingbuzzman Date: Fri, 16 May 2025 04:18:02 -0400 Subject: [PATCH 09/39] . --- .git-blame-ignore-revs | 5 +++++ pyproject.toml | 10 ++-------- tests/fixtures/hello.py | 2 +- tests/fixtures/hi.py | 2 +- tests/fixtures/violations_test_file.py | 8 +++----- verify.sh | 4 +--- 6 files changed, 13 insertions(+), 18 deletions(-) diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index cd2af3d4..9c5f97ff 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -1,2 +1,7 @@ # Migrate code style to Black e1db83a447700237a96be7a9db4413fe5024d328 +# Migrate code style to Ruff +# ######################################################################## +# TODO: IMPORTANT replace this with the real commit once it's merged +# ######################################################################## +d1cbd0bbfd80e19f7ab6231251389fadfd4b9b22 \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 7ff254f5..bb8dca9b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -155,9 +155,7 @@ max_line_length = 120 line-length = 100 target-version = "py39" exclude = [ - "tests/fixtures/snippet_8859.py", - "tests/fixtures/snippet_unicode.py", - "tests/fixtures/snippet_src.py", + "tests/fixtures/*", ] [tool.ruff.format] @@ -177,8 +175,4 @@ select = [ # pycodestyle warnings "W", ] -ignore = ["E203"] # Whitespace before ':' - -[tool.ruff.lint.per-file-ignores] -"diff_cover/tests/*" = ["E501"] -"diff_cover/tests/fixtures/*" = ["E", "F", "W"] +ignore = ["E203"] # Whitespace before ':' \ No newline at end of file diff --git a/tests/fixtures/hello.py b/tests/fixtures/hello.py index ed14ac1a..e2974381 100644 --- a/tests/fixtures/hello.py +++ b/tests/fixtures/hello.py @@ -1,2 +1,2 @@ print("hello") -print(unknown_var) +print(unknown_var) \ No newline at end of file diff --git a/tests/fixtures/hi.py b/tests/fixtures/hi.py index ed14ac1a..e2974381 100644 --- a/tests/fixtures/hi.py +++ b/tests/fixtures/hi.py @@ -1,2 +1,2 @@ print("hello") -print(unknown_var) +print(unknown_var) \ No newline at end of file diff --git a/tests/fixtures/violations_test_file.py b/tests/fixtures/violations_test_file.py index 18160a90..08991bc7 100644 --- a/tests/fixtures/violations_test_file.py +++ b/tests/fixtures/violations_test_file.py @@ -1,14 +1,12 @@ def func_1(apple, my_list): - if apple < 10: - # Do something + if apple<10: + # Do something my_list.append(apple) return my_list[1:] - - def func_2(spongebob, squarepants): """A less messy function""" for char in spongebob: if char in squarepants: return char - unused = 1 + unused=1 return None diff --git a/verify.sh b/verify.sh index 55a1816f..33a43252 100755 --- a/verify.sh +++ b/verify.sh @@ -3,13 +3,11 @@ set -euo pipefail IFS=$'\n\t' ruff format --check . -ruff check . python -m pytest -n auto --cov-context test --cov --cov-report=xml tests git fetch origin main:refs/remotes/origin/main diff-cover --version diff-quality --version diff-cover coverage.xml --include-untracked -diff-quality --violations flake8 --include-untracked -diff-quality --violations pylint --include-untracked +diff-quality --violations ruff.check --include-untracked doc8 README.rst --ignore D001 From a63341255a69fb7fa9718b9b1bb1795f98b74a4d Mon Sep 17 00:00:00 2001 From: Javier Buzzi Date: Fri, 16 May 2025 08:22:30 -0400 Subject: [PATCH 10/39] Update .git-blame-ignore-revs --- .git-blame-ignore-revs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index 9c5f97ff..a1070e14 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -4,4 +4,4 @@ e1db83a447700237a96be7a9db4413fe5024d328 # ######################################################################## # TODO: IMPORTANT replace this with the real commit once it's merged # ######################################################################## -d1cbd0bbfd80e19f7ab6231251389fadfd4b9b22 \ No newline at end of file +d1cbd0bbfd80e19f7ab6231251389fadfd4b9b22 From 614465150846069295559c1c269e47152718e9ec Mon Sep 17 00:00:00 2001 From: kingbuzzman Date: Fri, 16 May 2025 10:18:30 -0400 Subject: [PATCH 11/39] putting it all back --- diff_cover/command_runner.py | 10 +- diff_cover/diff_cover_tool.py | 28 +++- diff_cover/diff_quality_tool.py | 44 ++++-- diff_cover/diff_reporter.py | 4 +- diff_cover/git_diff.py | 8 +- diff_cover/report_generator.py | 22 ++- diff_cover/snippets.py | 16 ++- diff_cover/violationsreporters/base.py | 4 +- .../java_violations_reporter.py | 4 +- .../violations_reporter.py | 56 ++++++-- pyproject.toml | 130 ++++++++---------- tests/test_clover_violations_reporter.py | 4 +- tests/test_config_parser.py | 4 +- tests/test_diff_quality_main.py | 4 +- tests/test_diff_reporter.py | 64 ++++++--- tests/test_git_diff.py | 16 ++- tests/test_git_diff_file.py | 5 +- tests/test_git_path.py | 4 +- tests/test_integration.py | 6 +- tests/test_java_violations_reporter.py | 4 +- tests/test_report_generator.py | 8 +- tests/test_snippets.py | 12 +- tests/test_violations_reporter.py | 72 +++++++--- verify.sh | 3 +- 24 files changed, 360 insertions(+), 172 deletions(-) diff --git a/diff_cover/command_runner.py b/diff_cover/command_runner.py index 31a1c995..0761dc1b 100644 --- a/diff_cover/command_runner.py +++ b/diff_cover/command_runner.py @@ -31,7 +31,11 @@ def execute(command, exit_codes=None): sys.stderr.write( " ".join( [ - (cmd.decode(sys.getfilesystemencoding()) if isinstance(cmd, bytes) else cmd) + ( + cmd.decode(sys.getfilesystemencoding()) + if isinstance(cmd, bytes) + else cmd + ) for cmd in command ] ) @@ -50,7 +54,9 @@ def run_command_for_code(command): Returns command's exit code. """ try: - process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + process = subprocess.Popen( + command, stdout=subprocess.PIPE, stderr=subprocess.PIPE + ) process.communicate() except FileNotFoundError: return 1 diff --git a/diff_cover/diff_cover_tool.py b/diff_cover/diff_cover_tool.py index d2af8876..782415a8 100644 --- a/diff_cover/diff_cover_tool.py +++ b/diff_cover/diff_cover_tool.py @@ -26,7 +26,9 @@ MARKDOWN_REPORT_HELP = "Diff coverage Markdown output" COMPARE_BRANCH_HELP = "Branch to compare" CSS_FILE_HELP = "Write CSS into an external file" -FAIL_UNDER_HELP = "Returns an error code if coverage or quality score is below this value" +FAIL_UNDER_HELP = ( + "Returns an error code if coverage or quality score is below this value" +) IGNORE_STAGED_HELP = "Ignores staged changes" IGNORE_UNSTAGED_HELP = "Ignores unstaged changes" IGNORE_WHITESPACE = "When getting a diff ignore any and all whitespace" @@ -90,7 +92,9 @@ def parse_coverage_args(argv): help=MARKDOWN_REPORT_HELP, ) - parser.add_argument("--show-uncovered", action="store_true", default=None, help=SHOW_UNCOVERED) + parser.add_argument( + "--show-uncovered", action="store_true", default=None, help=SHOW_UNCOVERED + ) parser.add_argument( "--expand-coverage-report", @@ -135,9 +139,13 @@ def parse_coverage_args(argv): help=INCLUDE_UNTRACKED_HELP, ) - parser.add_argument("--exclude", metavar="EXCLUDE", type=str, nargs="+", help=EXCLUDE_HELP) + parser.add_argument( + "--exclude", metavar="EXCLUDE", type=str, nargs="+", help=EXCLUDE_HELP + ) - parser.add_argument("--include", metavar="INCLUDE", type=str, nargs="+", help=INCLUDE_HELP) + parser.add_argument( + "--include", metavar="INCLUDE", type=str, nargs="+", help=INCLUDE_HELP + ) parser.add_argument( "--src-roots", @@ -164,9 +172,13 @@ def parse_coverage_args(argv): help=IGNORE_WHITESPACE, ) - parser.add_argument("-q", "--quiet", action="store_true", default=None, help=QUIET_HELP) + parser.add_argument( + "-q", "--quiet", action="store_true", default=None, help=QUIET_HELP + ) - parser.add_argument("-c", "--config-file", help=CONFIG_FILE_HELP, metavar="CONFIG_FILE") + parser.add_argument( + "-c", "--config-file", help=CONFIG_FILE_HELP, metavar="CONFIG_FILE" + ) parser.add_argument("--diff-file", type=str, default=None, help=DIFF_FILE_HELP) @@ -286,7 +298,9 @@ def main(argv=None, directory=None): diff_tool = None if not arg_dict["diff_file"]: - diff_tool = GitDiffTool(arg_dict["diff_range_notation"], arg_dict["ignore_whitespace"]) + diff_tool = GitDiffTool( + arg_dict["diff_range_notation"], arg_dict["ignore_whitespace"] + ) else: diff_tool = GitDiffFileTool(arg_dict["diff_file"]) diff --git a/diff_cover/diff_quality_tool.py b/diff_cover/diff_quality_tool.py index 3277d98e..1b389013 100644 --- a/diff_cover/diff_quality_tool.py +++ b/diff_cover/diff_quality_tool.py @@ -75,7 +75,9 @@ "shellcheck": shellcheck_driver, } -VIOLATION_CMD_HELP = "Which code quality tool to use (%s)" % "/".join(sorted(QUALITY_DRIVERS)) +VIOLATION_CMD_HELP = "Which code quality tool to use (%s)" % "/".join( + sorted(QUALITY_DRIVERS) +) INPUT_REPORTS_HELP = "Which violations reports to use" OPTIONS_HELP = "Options to be passed to the violations tool" INCLUDE_HELP = "Files to include (glob pattern)" @@ -143,7 +145,9 @@ def parse_quality_args(argv): parser.add_argument("--options", type=str, nargs="?", help=OPTIONS_HELP) - parser.add_argument("--fail-under", metavar="SCORE", type=float, help=FAIL_UNDER_HELP) + parser.add_argument( + "--fail-under", metavar="SCORE", type=float, help=FAIL_UNDER_HELP + ) parser.add_argument( "--ignore-staged", action="store_true", default=None, help=IGNORE_STAGED_HELP @@ -163,9 +167,13 @@ def parse_quality_args(argv): help=INCLUDE_UNTRACKED_HELP, ) - parser.add_argument("--exclude", metavar="EXCLUDE", type=str, nargs="+", help=EXCLUDE_HELP) + parser.add_argument( + "--exclude", metavar="EXCLUDE", type=str, nargs="+", help=EXCLUDE_HELP + ) - parser.add_argument("--include", metavar="INCLUDE", nargs="+", type=str, help=INCLUDE_HELP) + parser.add_argument( + "--include", metavar="INCLUDE", nargs="+", type=str, help=INCLUDE_HELP + ) parser.add_argument( "--diff-range-notation", @@ -186,11 +194,17 @@ def parse_quality_args(argv): help=IGNORE_WHITESPACE, ) - parser.add_argument("-q", "--quiet", action="store_true", default=None, help=QUIET_HELP) + parser.add_argument( + "-q", "--quiet", action="store_true", default=None, help=QUIET_HELP + ) - parser.add_argument("-c", "--config-file", help=CONFIG_FILE_HELP, metavar="CONFIG_FILE") + parser.add_argument( + "-c", "--config-file", help=CONFIG_FILE_HELP, metavar="CONFIG_FILE" + ) - parser.add_argument("--report-root-path", help=REPORT_ROOT_PATH_HELP, metavar="ROOT_PATH") + parser.add_argument( + "--report-root-path", help=REPORT_ROOT_PATH_HELP, metavar="ROOT_PATH" + ) defaults = { "ignore_whitespace": False, @@ -204,7 +218,9 @@ def parse_quality_args(argv): "quiet": False, } - return get_config(parser=parser, argv=argv, defaults=defaults, tool=Tool.DIFF_QUALITY) + return get_config( + parser=parser, argv=argv, defaults=defaults, tool=Tool.DIFF_QUALITY + ) def generate_quality_report( @@ -325,16 +341,22 @@ def main(argv=None, directory=None): # If we've been given pre-generated reports, # try to open the files if arg_dict["report_root_path"]: - driver.add_driver_args(report_root_path=arg_dict["report_root_path"]) + driver.add_driver_args( + report_root_path=arg_dict["report_root_path"] + ) reporter = QualityReporter(driver, input_reports, user_options) elif reporter_factory_fn: - reporter = reporter_factory_fn(reports=input_reports, options=user_options) + reporter = reporter_factory_fn( + reports=input_reports, options=user_options + ) percent_passing = generate_quality_report( reporter, arg_dict["compare_branch"], - GitDiffTool(arg_dict["diff_range_notation"], arg_dict["ignore_whitespace"]), + GitDiffTool( + arg_dict["diff_range_notation"], arg_dict["ignore_whitespace"] + ), html_report=arg_dict["html_report"], json_report=arg_dict["json_report"], markdown_report=arg_dict["markdown_report"], diff --git a/diff_cover/diff_reporter.py b/diff_cover/diff_reporter.py index a7a387c2..8f404b95 100644 --- a/diff_cover/diff_reporter.py +++ b/diff_cover/diff_reporter.py @@ -243,7 +243,9 @@ def _git_diff(self): # Remove any lines from the dict that have been deleted # Include any lines that have been added result_dict[src_path] = [ - line for line in result_dict.get(src_path, []) if line not in deleted_lines + line + for line in result_dict.get(src_path, []) + if line not in deleted_lines ] + added_lines # Eliminate repeats and order line numbers diff --git a/diff_cover/git_diff.py b/diff_cover/git_diff.py index 4bf20535..e3649318 100644 --- a/diff_cover/git_diff.py +++ b/diff_cover/git_diff.py @@ -68,7 +68,9 @@ def diff_committed(self, compare_branch="origin/main"): branch=compare_branch, notation=self.range_notation ) try: - return execute(self._default_git_args + self._default_diff_args + [diff_range])[0] + return execute( + self._default_git_args + self._default_diff_args + [diff_range] + )[0] except CommandError as e: if "unknown revision" in str(e): raise ValueError( @@ -99,7 +101,9 @@ def diff_staged(self): Raises a `GitDiffError` if `git diff` outputs anything to stderr. """ - return execute(self._default_git_args + self._default_diff_args + ["--cached"])[0] + return execute(self._default_git_args + self._default_diff_args + ["--cached"])[ + 0 + ] def untracked(self): """Return the untracked files.""" diff --git a/diff_cover/report_generator.py b/diff_cover/report_generator.py index 4efcec2a..132f9d93 100644 --- a/diff_cover/report_generator.py +++ b/diff_cover/report_generator.py @@ -19,9 +19,13 @@ class DiffViolations: """ def __init__(self, violations, measured_lines, diff_lines): - self.lines = {violation.line for violation in violations}.intersection(diff_lines) + self.lines = {violation.line for violation in violations}.intersection( + diff_lines + ) - self.violations = {violation for violation in violations if violation.line in self.lines} + self.violations = { + violation for violation in violations if violation.line in self.lines + } # By convention, a violation reporter # can return `None` to indicate that all lines are "measured" @@ -117,7 +121,9 @@ def covered_lines(self, src_path): return [] return sorted( - set(diff_violations.measured_lines).difference(set(self.violation_lines(src_path))) + set(diff_violations.measured_lines).difference( + set(self.violation_lines(src_path)) + ) ) def violation_lines(self, src_path): @@ -142,7 +148,12 @@ def total_num_lines(self): which we have coverage info. """ - return sum([len(summary.measured_lines) for summary in self._diff_violations().values()]) + return sum( + [ + len(summary.measured_lines) + for summary in self._diff_violations().values() + ] + ) def total_num_violations(self): """ @@ -168,7 +179,8 @@ def total_percent_covered(self): def num_changed_lines(self): """Returns the number of changed lines.""" return sum( - len(self._diff.lines_changed(src_path)) for src_path in self._diff.src_paths_changed() + len(self._diff.lines_changed(src_path)) + for src_path in self._diff.src_paths_changed() ) def _diff_violations(self): diff --git a/diff_cover/snippets.py b/diff_cover/snippets.py index 65aa9112..11524148 100644 --- a/diff_cover/snippets.py +++ b/diff_cover/snippets.py @@ -134,7 +134,11 @@ def markdown(self): header = "Lines %d-%d\n\n" % (self._start_line, self._last_line) if self._lexer_name in self.LEXER_TO_MARKDOWN_CODE_HINT: return header + ( - "```" + self.LEXER_TO_MARKDOWN_CODE_HINT[self._lexer_name] + "\n" + text + "\n```\n" + "```" + + self.LEXER_TO_MARKDOWN_CODE_HINT[self._lexer_name] + + "\n" + + text + + "\n```\n" ) # unknown programming language, return a non-decorated fenced code block: @@ -379,7 +383,9 @@ def _snippet_ranges(cls, num_src_lines, violation_lines): elif lines_since_last_violation > cls.MAX_GAP_IN_SNIPPET: # Expand to include extra context, but not after last line snippet_end = line_num - lines_since_last_violation - snippet_end = min(num_src_lines, snippet_end + cls.NUM_CONTEXT_LINES) + snippet_end = min( + num_src_lines, snippet_end + cls.NUM_CONTEXT_LINES + ) current_range = (current_range[0], snippet_end) # Store the snippet and start looking for the next one @@ -408,4 +414,8 @@ def _shift_lines(line_num_list, start_line): than or equal to `start_line`; otherwise, they will be excluded from the list. """ - return [line_num - start_line + 1 for line_num in line_num_list if line_num >= start_line] + return [ + line_num - start_line + 1 + for line_num in line_num_list + if line_num >= start_line + ] diff --git a/diff_cover/violationsreporters/base.py b/diff_cover/violationsreporters/base.py index 550b20c9..3f1a6ca0 100644 --- a/diff_cover/violationsreporters/base.py +++ b/diff_cover/violationsreporters/base.py @@ -74,7 +74,9 @@ def name(self): class QualityDriver(ABC): - def __init__(self, name, supported_extensions, command, exit_codes=None, output_stderr=False): + def __init__( + self, name, supported_extensions, command, exit_codes=None, output_stderr=False + ): """ Args: name: (str) name of the driver diff --git a/diff_cover/violationsreporters/java_violations_reporter.py b/diff_cover/violationsreporters/java_violations_reporter.py index bc25f3e9..2ace2a79 100644 --- a/diff_cover/violationsreporters/java_violations_reporter.py +++ b/diff_cover/violationsreporters/java_violations_reporter.py @@ -72,7 +72,9 @@ def parse_reports(self, reports): for file_tree in files: for error in file_tree.findall("error"): line_number = error.get("line") - error_str = "{}: {}".format(error.get("severity"), error.get("message")) + error_str = "{}: {}".format( + error.get("severity"), error.get("message") + ) violation = Violation(int(line_number), error_str) filename = GitPathTool.relative_path(file_tree.get("name")) violations_dict[filename].append(violation) diff --git a/diff_cover/violationsreporters/violations_reporter.py b/diff_cover/violationsreporters/violations_reporter.py index 0e8b3147..446fa179 100644 --- a/diff_cover/violationsreporters/violations_reporter.py +++ b/diff_cover/violationsreporters/violations_reporter.py @@ -97,7 +97,9 @@ def _get_classes(self, index, xml_document, src_path): if not self._xml_cache[index]: self._xml_cache[index] = self._get_xml_classes(xml_document) - return self._xml_cache[index].get(src_abs_path) or self._xml_cache[index].get(src_rel_path) + return self._xml_cache[index].get(src_abs_path) or self._xml_cache[index].get( + src_rel_path + ) def get_src_path_line_nodes_cobertura(self, index, xml_document, src_path): classes = self._get_classes(index, xml_document, src_path) @@ -138,7 +140,9 @@ def _measured_source_path_matches(self, package_name, file_name, src_path): for root in self._src_roots: if ( os.path.normcase( - GitPathTool.relative_path(os.path.join(root, package_name, file_name)) + GitPathTool.relative_path( + os.path.join(root, package_name, file_name) + ) ) == norm_src_path ): @@ -159,7 +163,9 @@ def get_src_path_line_nodes_jacoco(self, xml_document, src_path): _files = [ _file for _file in pkg.findall("sourcefile") - if self._measured_source_path_matches(pkg.get("name"), _file.get("name"), src_path) + if self._measured_source_path_matches( + pkg.get("name"), _file.get("name"), src_path + ) ] files.extend(_files) @@ -189,17 +195,23 @@ def _cache_file(self, src_path): for i, xml_document in enumerate(self._xml_roots): if xml_document.findall(".[@clover]"): # see etc/schema/clover.xsd at https://bitbucket.org/atlassian/clover/src - line_nodes = self.get_src_path_line_nodes_clover(xml_document, src_path) + line_nodes = self.get_src_path_line_nodes_clover( + xml_document, src_path + ) _number = "num" _hits = "count" elif xml_document.findall(".[@name]"): # https://github.com/jacoco/jacoco/blob/master/org.jacoco.report/src/org/jacoco/report/xml/report.dtd - line_nodes = self.get_src_path_line_nodes_jacoco(xml_document, src_path) + line_nodes = self.get_src_path_line_nodes_jacoco( + xml_document, src_path + ) _number = "nr" _hits = "ci" else: # https://github.com/cobertura/web/blob/master/htdocs/xml/coverage-04.dtd - line_nodes = self.get_src_path_line_nodes_cobertura(i, xml_document, src_path) + line_nodes = self.get_src_path_line_nodes_cobertura( + i, xml_document, src_path + ) _number = "number" _hits = "hits" if line_nodes is None: @@ -209,7 +221,9 @@ def _cache_file(self, src_path): if self._expand_coverage_report: reported_line_hits = {} for line in line_nodes: - reported_line_hits[int(line.get(_number))] = int(line.get(_hits, 0)) + reported_line_hits[int(line.get(_number))] = int( + line.get(_hits, 0) + ) if reported_line_hits: last_hit_number = 0 for line_number in range( @@ -221,7 +235,9 @@ def _cache_file(self, src_path): else: # This is an unreported line. # We add it with the previous line hit score - line_nodes.append({_hits: last_hit_number, _number: line_number}) + line_nodes.append( + {_hits: last_hit_number, _number: line_number} + ) # First case, need to define violations initially if violations is None: @@ -314,7 +330,9 @@ def parse(lcov_file): line_no = int(args[0]) num_executions = int(args[1]) if source_file is None: - raise ValueError(f"No source file specified for line coverage: {line}") + raise ValueError( + f"No source file specified for line coverage: {line}" + ) if line_no not in lcov_report[source_file]: lcov_report[source_file][line_no] = 0 lcov_report[source_file][line_no] += num_executions @@ -381,7 +399,9 @@ def _cache_file(self, src_path): if violations is None: violations = { Violation(int(line_no), None) - for line_no, num_executions in lcov_document[src_search_path].items() + for line_no, num_executions in lcov_document[ + src_search_path + ].items() if int(num_executions) == 0 } @@ -391,7 +411,9 @@ def _cache_file(self, src_path): else: violations = violations & { Violation(int(line_no), None) - for line_no, num_executions in lcov_document[src_search_path].items() + for line_no, num_executions in lcov_document[ + src_search_path + ].items() if int(num_executions) == 0 } @@ -399,7 +421,9 @@ def _cache_file(self, src_path): # measured = measured | {int(line.get(_number)) for line in line_nodes} measured = measured | { int(line_no) - for line_no, num_executions in lcov_document[src_search_path].items() + for line_no, num_executions in lcov_document[ + src_search_path + ].items() } # If we don't have any information about the source file, @@ -591,7 +615,9 @@ def __init__(self): 2 | 4 | 8 | 16, ], ) - self.pylint_expression = re.compile(r"^([^:]+):(\d+): \[(\w+),? ?([^\]]*)] (.*)$") + self.pylint_expression = re.compile( + r"^([^:]+):(\d+): \[(\w+),? ?([^\]]*)] (.*)$" + ) self.dupe_code_violation = "R0801" self.command_to_check_install = ["pylint", "--version"] @@ -655,7 +681,9 @@ def parse_reports(self, reports): # If we're looking for a particular source file, # ignore any other source files. if function_name: - error_str = "{}: {}: {}".format(pylint_code, function_name, message) + error_str = "{}: {}: {}".format( + pylint_code, function_name, message + ) else: error_str = f"{pylint_code}: {message}" diff --git a/pyproject.toml b/pyproject.toml index bb8dca9b..7a3b7c0e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -71,22 +71,6 @@ toml = ["tomli"] requires = ["poetry-core>=1.0.7"] build-backend = "poetry.core.masonry.api" -[tool.black] -line-length = 88 -target-version = ['py310'] -include = '\.pyi?$' -exclude = "tests/fixtures/*" - -[tool.isort] -profile = "black" -extend_skip = "tests/fixtures/" - -[tool.pylint.master] -max-line-length = 100 -load-plugins = [ - "pylint_pytest", -] - [tool.coverage.run] branch = true relative_files = true @@ -102,45 +86,6 @@ show_missing = true show_contexts = true skip_covered = false -[tool.pylint."messages control"] -enable = ["all"] -disable = [ - # allow TODO comments - "fixme", - # allow disables - "locally-disabled", - "suppressed-message", - # covered by isort - "ungrouped-imports", - # allow classes and functions w/o docstring - "missing-docstring", - # hard number checks can be ignored, because they are covered in code reviews - "too-many-instance-attributes", - "too-many-arguments", - "too-many-locals", - "too-many-branches", - "too-few-public-methods", - "too-many-nested-blocks", - "too-many-public-methods", - # allow methods not to use self - "no-self-use", - # currently some code seems duplicated for pylint - "duplicate-code", - # we are a command line tool and don't want to show all internals - "raise-missing-from", -] - -[tool.pylint.basic] -good-names = [ - "_", - "i", - "setUp", - "tearDown", - "e", - "ex", -] -no-docstring-rgx = "^_" - [tool.pytest.ini_options] addopts = "--strict-markers" xfail_strict = true @@ -152,27 +97,70 @@ markers = [ max_line_length = 120 [tool.ruff] -line-length = 100 -target-version = "py39" -exclude = [ - "tests/fixtures/*", -] +line-length = 88 +target-version = "py310" +src = ["diff_cover", "tests"] +exclude = ["tests/fixtures/*"] [tool.ruff.format] quote-style = "double" indent-style = "space" -line-ending = "auto" skip-magic-trailing-comma = false +line-ending = "auto" [tool.ruff.lint] -select = [ - # McCabe complexity - "C901", - # pycodestyle - "E", - # pyflakes - "F", - # pycodestyle warnings - "W", +select = ["ALL"] +ignore = [ + # allow TODO comments (equivalent to "fixme") + "FIX", + + # allow disables (equivalent to "locally-disabled", "suppressed-message") + "RUF100", + + # covered by isort (equivalent to "ungrouped-imports") + "I", + + # allow classes and functions w/o docstring (equivalent to "missing-docstring") + "D1", + + # hard number checks (equivalent to "too-many-*" rules) + "C901", # complexity + "PLR0912", # too many branches + "PLR0913", # too many arguments + "PLR0914", # too many locals + "PLR0915", # too many statements + "PLR0916", # too many nested blocks + "PLR0904", # too many public methods + + # duplicate code detection (equivalent to "duplicate-code") + "CPY", + + # we are a command line tool (equivalent to "raise-missing-from") + "B904", + + # Resolve incompatible docstring rules + "D203", # Keep D211 (no-blank-line-before-class) + "D213", # Keep D212 (multi-line-summary-first-line) + + # Avoid formatter conflicts + "COM812", ] -ignore = ["E203"] # Whitespace before ':' \ No newline at end of file + +# The equivalent of pylint's good-names +allowed-confusables = ["_"] + +[tool.ruff.lint.pep8-naming] +# Allow specific names that might otherwise violate naming conventions +ignore-names = ["i", "e", "ex", "setUp", "tearDown"] + +[tool.ruff.lint.pydocstyle] +# Equivalent to pylint's no-docstring-rgx +ignore-decorators = ["^_"] + +[tool.ruff.lint.isort] +known-first-party = ["diff_cover", "tests"] +section-order = ["future", "standard-library", "third-party", "first-party", "local-folder"] +lines-between-types = 0 +lines-after-imports = 2 +combine-as-imports = true +split-on-trailing-comma = false \ No newline at end of file diff --git a/tests/test_clover_violations_reporter.py b/tests/test_clover_violations_reporter.py index 7af07ce0..e79df37a 100644 --- a/tests/test_clover_violations_reporter.py +++ b/tests/test_clover_violations_reporter.py @@ -13,5 +13,7 @@ def test_get_src_path_clover(datadir): GitPathTool._cwd = "/" GitPathTool._root = "/" clover_report = etree.parse(str(datadir / "test.xml")) - result = XmlCoverageReporter.get_src_path_line_nodes_clover(clover_report, "isLucky.js") + result = XmlCoverageReporter.get_src_path_line_nodes_clover( + clover_report, "isLucky.js" + ) assert sorted([int(line.attrib["num"]) for line in result]) == [2, 3, 5, 6, 8, 12] diff --git a/tests/test_config_parser.py b/tests/test_config_parser.py index 99de5c8a..eb0e300f 100644 --- a/tests/test_config_parser.py +++ b/tests/test_config_parser.py @@ -85,7 +85,9 @@ def test_get_config_unrecognized_file(mocker, tool): ), ], ) -def test_get_config(mocker, tmp_path, tool, cli_config, defaults, file_content, expected): +def test_get_config( + mocker, tmp_path, tool, cli_config, defaults, file_content, expected +): if file_content: toml_file = tmp_path / "foo.toml" toml_file.write_text(file_content) diff --git a/tests/test_diff_quality_main.py b/tests/test_diff_quality_main.py index 76f9217c..a201b9a1 100644 --- a/tests/test_diff_quality_main.py +++ b/tests/test_diff_quality_main.py @@ -114,7 +114,9 @@ def patch_git_patch(mocker): @pytest.fixture def report_mock(mocker): - return mocker.patch("diff_cover.diff_quality_tool.generate_quality_report", return_value=100) + return mocker.patch( + "diff_cover.diff_quality_tool.generate_quality_report", return_value=100 + ) def test_parse_options(report_mock): diff --git a/tests/test_diff_reporter.py b/tests/test_diff_reporter.py index e22c7a22..09dc7b10 100644 --- a/tests/test_diff_reporter.py +++ b/tests/test_diff_reporter.py @@ -59,7 +59,9 @@ def test_name_ignore_unstaged(git_diff): def test_name_ignore_staged_and_unstaged(git_diff): # Override the default branch assert ( - GitDiffReporter(git_diff=git_diff, ignore_staged=True, ignore_unstaged=True).name() + GitDiffReporter( + git_diff=git_diff, ignore_staged=True, ignore_unstaged=True + ).name() == "origin/main...HEAD" ) @@ -118,7 +120,9 @@ def test_git_path_selection(diff, git_diff, include, exclude, expected): _set_git_diff_output( diff, git_diff, - git_diff_output({"subdir1/file1.py": line_numbers(3, 10) + line_numbers(34, 47)}), + git_diff_output( + {"subdir1/file1.py": line_numbers(3, 10) + line_numbers(34, 47)} + ), git_diff_output({"subdir2/file2.py": line_numbers(3, 10), "file3.py": [0]}), git_diff_output(dict(), deleted_files=["README.md"]), ) @@ -140,7 +144,9 @@ def test_git_source_paths(diff, git_diff): _set_git_diff_output( diff, git_diff, - git_diff_output({"subdir/file1.py": line_numbers(3, 10) + line_numbers(34, 47)}), + git_diff_output( + {"subdir/file1.py": line_numbers(3, 10) + line_numbers(34, 47)} + ), git_diff_output({"subdir/file2.py": line_numbers(3, 10), "file3.py": [0]}), git_diff_output(dict(), deleted_files=["README.md"]), ) @@ -172,7 +178,9 @@ def test_git_source_paths_with_space(diff, git_diff): def test_duplicate_source_paths(diff, git_diff): # Duplicate the output for committed, staged, and unstaged changes - diff_output = git_diff_output({"subdir/file1.py": line_numbers(3, 10) + line_numbers(34, 47)}) + diff_output = git_diff_output( + {"subdir/file1.py": line_numbers(3, 10) + line_numbers(34, 47)} + ) _set_git_diff_output(diff, git_diff, diff_output, diff_output, diff_output) # Get the source paths in the diff @@ -188,7 +196,9 @@ def test_git_source_paths_with_supported_extensions(diff, git_diff): _set_git_diff_output( diff, git_diff, - git_diff_output({"subdir/file1.py": line_numbers(3, 10) + line_numbers(34, 47)}), + git_diff_output( + {"subdir/file1.py": line_numbers(3, 10) + line_numbers(34, 47)} + ), git_diff_output({"subdir/file2.py": line_numbers(3, 10), "file3.py": [0]}), git_diff_output({"README.md": line_numbers(3, 10)}), ) @@ -211,7 +221,9 @@ def test_git_lines_changed(diff, git_diff): _set_git_diff_output( diff, git_diff, - git_diff_output({"subdir/file1.py": line_numbers(3, 10) + line_numbers(34, 47)}), + git_diff_output( + {"subdir/file1.py": line_numbers(3, 10) + line_numbers(34, 47)} + ), git_diff_output({"subdir/file2.py": line_numbers(3, 10), "file3.py": [0]}), git_diff_output(dict(), deleted_files=["README.md"]), ) @@ -270,7 +282,9 @@ def test_git_deleted_lines(diff, git_diff): _set_git_diff_output( diff, git_diff, - git_diff_output({"subdir/file1.py": line_numbers(3, 10) + line_numbers(34, 47)}), + git_diff_output( + {"subdir/file1.py": line_numbers(3, 10) + line_numbers(34, 47)} + ), git_diff_output({"subdir/file2.py": line_numbers(3, 10), "file3.py": [0]}), git_diff_output(dict(), deleted_files=["README.md"]), ) @@ -309,7 +323,9 @@ def test_git_unicode_filename(diff, git_diff): def test_git_repeat_lines(diff, git_diff): # Same committed, staged, and unstaged lines - diff_output = git_diff_output({"subdir/file1.py": line_numbers(3, 10) + line_numbers(34, 47)}) + diff_output = git_diff_output( + {"subdir/file1.py": line_numbers(3, 10) + line_numbers(34, 47)} + ) _set_git_diff_output(diff, git_diff, diff_output, diff_output, diff_output) # Get the lines changed in the diff @@ -320,7 +336,9 @@ def test_git_repeat_lines(diff, git_diff): def test_git_overlapping_lines(diff, git_diff): - main_diff = git_diff_output({"subdir/file1.py": line_numbers(3, 10) + line_numbers(34, 47)}) + main_diff = git_diff_output( + {"subdir/file1.py": line_numbers(3, 10) + line_numbers(34, 47)} + ) # Overlap, extending the end of the hunk (lines 3 to 10) overlap_1 = git_diff_output({"subdir/file1.py": line_numbers(5, 14)}) @@ -339,7 +357,9 @@ def test_git_overlapping_lines(diff, git_diff): def test_git_line_within_hunk(diff, git_diff): - main_diff = git_diff_output({"subdir/file1.py": line_numbers(3, 10) + line_numbers(34, 47)}) + main_diff = git_diff_output( + {"subdir/file1.py": line_numbers(3, 10) + line_numbers(34, 47)} + ) # Surround hunk in main (lines 3 to 10) surround = git_diff_output({"subdir/file1.py": line_numbers(2, 11)}) @@ -393,7 +413,9 @@ def test_inter_diff_conflict(diff, git_diff): def test_git_no_such_file(diff, git_diff): - diff_output = git_diff_output({"subdir/file1.py": [1], "subdir/file2.py": [2], "file3.py": [3]}) + diff_output = git_diff_output( + {"subdir/file1.py": [1], "subdir/file2.py": [2], "file3.py": [3]} + ) # Configure the git diff output _set_git_diff_output(diff, git_diff, diff_output, "", "") @@ -541,7 +563,9 @@ def test_inclusion_list(diff, git_diff): def test_ignore_staged_inclusion(git_diff): reporter = GitDiffReporter(git_diff=git_diff, ignore_staged=True) - staged_input = git_diff_output({"subdir/file1.py": line_numbers(3, 10) + line_numbers(34, 47)}) + staged_input = git_diff_output( + {"subdir/file1.py": line_numbers(3, 10) + line_numbers(34, 47)} + ) _set_git_diff_output(reporter, git_diff, "", staged_input, "") assert reporter._get_included_diff_results() == ["", ""] @@ -559,9 +583,13 @@ def test_ignore_unstaged_inclusion(git_diff): def test_ignore_staged_and_unstaged_inclusion(git_diff): - reporter = GitDiffReporter(git_diff=git_diff, ignore_staged=True, ignore_unstaged=True) + reporter = GitDiffReporter( + git_diff=git_diff, ignore_staged=True, ignore_unstaged=True + ) - staged_input = git_diff_output({"subdir/file1.py": line_numbers(3, 10) + line_numbers(34, 47)}) + staged_input = git_diff_output( + {"subdir/file1.py": line_numbers(3, 10) + line_numbers(34, 47)} + ) unstaged_input = git_diff_output( {"subdir/file2.py": line_numbers(3, 10) + line_numbers(34, 47)} ) @@ -586,7 +614,9 @@ def test_fnmatch_returns_the_default_with_empty_default(diff): def test_include_untracked(mocker, git_diff): reporter = GitDiffReporter(git_diff=git_diff, include_untracked=True) - diff_output = git_diff_output({"subdir/file1.py": line_numbers(3, 10) + line_numbers(34, 47)}) + diff_output = git_diff_output( + {"subdir/file1.py": line_numbers(3, 10) + line_numbers(34, 47)} + ) _set_git_diff_output( reporter, git_diff, @@ -632,7 +662,9 @@ def test_include_untracked__not_valid_path__not_include_it( supported_extensions=supported_extensions, exclude=excluded, ) - diff_output = git_diff_output({"subdir/file1.py": line_numbers(3, 10) + line_numbers(34, 47)}) + diff_output = git_diff_output( + {"subdir/file1.py": line_numbers(3, 10) + line_numbers(34, 47)} + ) _set_git_diff_output( reporter, git_diff, diff --git a/tests/test_git_diff.py b/tests/test_git_diff.py index 0cb95cbf..56a1ff47 100644 --- a/tests/test_git_diff.py +++ b/tests/test_git_diff.py @@ -39,7 +39,9 @@ def _inner(stdout, stderr, returncode=0): @pytest.fixture def check_diff_committed(subprocess, set_git_diff_output): def _inner(diff_range_notation, ignore_whitespace): - tool_ = GitDiffTool(range_notation=diff_range_notation, ignore_whitespace=ignore_whitespace) + tool_ = GitDiffTool( + range_notation=diff_range_notation, ignore_whitespace=ignore_whitespace + ) set_git_diff_output("test output", "") output = tool_.diff_committed() @@ -96,7 +98,9 @@ def test_diff_unstaged(set_git_diff_output, tool, subprocess): "--no-ext-diff", "-U0", ] - subprocess.Popen.assert_called_with(expected, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + subprocess.Popen.assert_called_with( + expected, stdout=subprocess.PIPE, stderr=subprocess.PIPE + ) def test_diff_staged(tool, subprocess, set_git_diff_output): @@ -119,7 +123,9 @@ def test_diff_staged(tool, subprocess, set_git_diff_output): "-U0", "--cached", ] - subprocess.Popen.assert_called_with(expected, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + subprocess.Popen.assert_called_with( + expected, stdout=subprocess.PIPE, stderr=subprocess.PIPE + ) def test_diff_missing_branch_error(set_git_diff_output, tool, subprocess): @@ -159,7 +165,9 @@ def test_diff_committed_compare_branch(set_git_diff_output, tool, subprocess): "-U0", "release...HEAD", ] - subprocess.Popen.assert_called_with(expected, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + subprocess.Popen.assert_called_with( + expected, stdout=subprocess.PIPE, stderr=subprocess.PIPE + ) def test_errors(set_git_diff_output, tool): diff --git a/tests/test_git_diff_file.py b/tests/test_git_diff_file.py index 9e674896..2e1d25a7 100644 --- a/tests/test_git_diff_file.py +++ b/tests/test_git_diff_file.py @@ -32,8 +32,9 @@ def test_diff_file_not_found(mocker, diff_tool): with pytest.raises(ValueError) as excinfo: _diff_tool.diff_committed() - assert f"Could not read the diff file. Make sure '{_diff_tool.diff_file_path}' exists?" in str( - excinfo.value + assert ( + f"Could not read the diff file. Make sure '{_diff_tool.diff_file_path}' exists?" + in str(excinfo.value) ) assert _diff_tool.diff_file_path == "non_existent_diff_file.txt" diff --git a/tests/test_git_path.py b/tests/test_git_path.py index 5941534a..abd6a9f4 100644 --- a/tests/test_git_path.py +++ b/tests/test_git_path.py @@ -34,7 +34,9 @@ def test_project_root_command(process, subprocess): # Expect that the correct command was executed expected = ["git", "rev-parse", "--show-toplevel", "--encoding=utf-8"] - subprocess.Popen.assert_called_with(expected, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + subprocess.Popen.assert_called_with( + expected, stdout=subprocess.PIPE, stderr=subprocess.PIPE + ) def test_relative_path(process): diff --git a/tests/test_integration.py b/tests/test_integration.py index 789c84fc..94f74f92 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -604,7 +604,11 @@ def test_tool_not_installed(self): # Pretend we support a tool named not_installed self.mocker.patch.dict( diff_quality_tool.QUALITY_DRIVERS, - {"not_installed": DoNothingDriver("not_installed", ["txt"], ["not_installed"])}, + { + "not_installed": DoNothingDriver( + "not_installed", ["txt"], ["not_installed"] + ) + }, ) self._call_quality_expecting_error( diff --git a/tests/test_java_violations_reporter.py b/tests/test_java_violations_reporter.py index 388bfd2b..dd15eb51 100644 --- a/tests/test_java_violations_reporter.py +++ b/tests/test_java_violations_reporter.py @@ -258,7 +258,9 @@ def test_quality_pregenerated_report(self): # Expect that we get the right violations expected_violations = [ Violation(1, "error: Missing docstring"), - Violation(57, "warning: TODO the name of this method is a little bit confusing"), + Violation( + 57, "warning: TODO the name of this method is a little bit confusing" + ), Violation( 183, "error: Invalid name '' for type argument (should match [a-z_][a-z0-9_]{2,30}$)", diff --git a/tests/test_report_generator.py b/tests/test_report_generator.py index b7b19958..926d82bb 100644 --- a/tests/test_report_generator.py +++ b/tests/test_report_generator.py @@ -239,7 +239,9 @@ class TestTemplateReportGenerator(BaseReportGeneratorTest): def _test_input_expected_output(self, input_with_expected_output): for test_input, expected_output in input_with_expected_output: - assert expected_output == TemplateReportGenerator.combine_adjacent_lines(test_input) + assert expected_output == TemplateReportGenerator.combine_adjacent_lines( + test_input + ) def test_combine_adjacent_lines_no_adjacent(self): in_out = [([1, 3], ["1", "3"]), ([1, 5, 7, 10], ["1", "5", "7", "10"])] @@ -549,7 +551,9 @@ def setup(self): self.use_default_values() # Have violations_batch() return the violations. self.coverage.violations_batch.side_effect = None - self.coverage.violations_batch.return_value = copy.deepcopy(self._violations_dict) + self.coverage.violations_batch.return_value = copy.deepcopy( + self._violations_dict + ) # Have violations() return an empty list to ensure violations_batch() # is used. for src in self.SRC_PATHS: diff --git a/tests/test_snippets.py b/tests/test_snippets.py index 99378302..4f285f2a 100644 --- a/tests/test_snippets.py +++ b/tests/test_snippets.py @@ -72,7 +72,9 @@ def _src_lines(start_line, end_line): Test lines to write to the source file (Line 1, Line 2, ...). """ - return "\n".join([f"Line {line_num}" for line_num in range(start_line, end_line + 1)]) + return "\n".join( + [f"Line {line_num}" for line_num in range(start_line, end_line + 1)] + ) def _assert_format( @@ -83,7 +85,9 @@ def _assert_format( violation_lines, expected_fixture, ): - snippet = Snippet(src_tokens, src_filename, start_line, last_line, violation_lines, None) + snippet = Snippet( + src_tokens, src_filename, start_line, last_line, violation_lines, None + ) result = snippet.html() expected_str = load_fixture(expected_fixture, encoding="utf-8") @@ -286,7 +290,9 @@ def test_load_utf8_snippets(): @pytest.mark.usefixtures("switch_to_fixture_dir") def test_load_declared_arabic(): - _compare_snippets_output("html", "snippet_8859.py", [7], "snippet_arabic_output.html") + _compare_snippets_output( + "html", "snippet_8859.py", [7], "snippet_arabic_output.html" + ) def test_latin_one_undeclared(tmp_path): diff --git a/tests/test_violations_reporter.py b/tests/test_violations_reporter.py index df4f3113..a457f571 100644 --- a/tests/test_violations_reporter.py +++ b/tests/test_violations_reporter.py @@ -134,7 +134,9 @@ def test_non_python_violations(self): violations = self.MANY_VIOLATIONS measured = self.FEW_MEASURED - xml = self._coverage_xml(file_paths, violations, measured, source_paths=source_paths) + xml = self._coverage_xml( + file_paths, violations, measured, source_paths=source_paths + ) coverage = XmlCoverageReporter([xml]) assert violations == coverage.violations(f"{fancy_path}/{file_paths[0]}") @@ -225,7 +227,9 @@ def test_three_inputs(self): # By construction, each file has the same set # of covered/uncovered lines - assert violations1 & violations2 & violations3 == coverage.violations("file1.py") + assert violations1 & violations2 & violations3 == coverage.violations( + "file1.py" + ) assert measured1 | measured2 | measured3 == coverage.measured_lines("file1.py") @@ -233,7 +237,9 @@ def test_different_files_in_inputs(self): # Construct the XML report xml_roots = [ self._coverage_xml(["file.py"], self.MANY_VIOLATIONS, self.FEW_MEASURED), - self._coverage_xml(["other_file.py"], self.FEW_VIOLATIONS, self.MANY_MEASURED), + self._coverage_xml( + ["other_file.py"], self.FEW_VIOLATIONS, self.MANY_MEASURED + ), ] # Parse the report @@ -294,7 +300,9 @@ def test_expand_unreported_lines_when_configured(self): # By construction, each file has the same set # of covered/uncovered lines - assert self.MANY_VIOLATIONS_EXPANDED_MANY_MEASURED == coverage.violations("file1.java") + assert self.MANY_VIOLATIONS_EXPANDED_MANY_MEASURED == coverage.violations( + "file1.java" + ) def test_expand_unreported_lines_without_violations(self): # Construct the XML report @@ -482,14 +490,20 @@ def test_three_inputs(self): # By construction, each file has the same set # of covered/uncovered lines - assert violations1 & violations2 & violations3 == coverage.violations("file1.java") - assert measured1 | measured2 | measured3 == coverage.measured_lines("file1.java") + assert violations1 & violations2 & violations3 == coverage.violations( + "file1.java" + ) + assert measured1 | measured2 | measured3 == coverage.measured_lines( + "file1.java" + ) def test_different_files_in_inputs(self): # Construct the XML report xml_roots = [ self._coverage_xml(["file.java"], self.MANY_VIOLATIONS, self.FEW_MEASURED), - self._coverage_xml(["other_file.java"], self.FEW_VIOLATIONS, self.MANY_MEASURED), + self._coverage_xml( + ["other_file.java"], self.FEW_VIOLATIONS, self.MANY_MEASURED + ), ] # Parse the report @@ -686,15 +700,21 @@ def test_three_inputs(self): # By construction, each file has the same set # of covered/uncovered lines - assert violations1 & violations2 & violations3 == coverage.violations("file1.java") + assert violations1 & violations2 & violations3 == coverage.violations( + "file1.java" + ) - assert measured1 | measured2 | measured3 == coverage.measured_lines("file1.java") + assert measured1 | measured2 | measured3 == coverage.measured_lines( + "file1.java" + ) def test_different_files_in_inputs(self): # Construct the XML report xml_roots = [ self._coverage_xml(["file.java"], self.MANY_VIOLATIONS, self.FEW_MEASURED), - self._coverage_xml(["other_file.java"], self.FEW_VIOLATIONS, self.MANY_MEASURED), + self._coverage_xml( + ["other_file.java"], self.FEW_VIOLATIONS, self.MANY_MEASURED + ), ] # Parse the report @@ -892,15 +912,21 @@ def test_three_inputs(self): # By construction, each file has the same set # of covered/uncovered lines - assert violations1 & violations2 & violations3 == coverage.violations("file1.java") + assert violations1 & violations2 & violations3 == coverage.violations( + "file1.java" + ) - assert measured1 | measured2 | measured3 == coverage.measured_lines("file1.java") + assert measured1 | measured2 | measured3 == coverage.measured_lines( + "file1.java" + ) def test_different_files_in_inputs(self): # Construct the LCOV report lcov_repots = [ self._coverage_lcov(["file.java"], self.MANY_VIOLATIONS, self.FEW_MEASURED), - self._coverage_lcov(["other_file.java"], self.FEW_VIOLATIONS, self.MANY_MEASURED), + self._coverage_lcov( + ["other_file.java"], self.FEW_VIOLATIONS, self.MANY_MEASURED + ), ] # Parse the report @@ -956,7 +982,9 @@ def _coverage_lcov(self, file_paths, violations, measured): for file_path in file_paths: f.write(f"SF:{file_path}\n") for line_num in measured: - f.write(f"DA:{line_num},{0 if line_num in violation_lines else 1}\n") + f.write( + f"DA:{line_num},{0 if line_num in violation_lines else 1}\n" + ) f.write("end_of_record\n") try: return LcovCoverageReporter.parse(f.name) @@ -1564,7 +1592,9 @@ def test_unicode(self, process_patcher): ] violations = quality.violations("file.py") - assert violations == [Violation(2, "W0612: cls_name.func_\u9492: Unused variable '\u2920'")] + assert violations == [ + Violation(2, "W0612: cls_name.func_\u9492: Unused variable '\u2920'") + ] def test_unicode_continuation_char(self, process_patcher): process_patcher((b"file.py:2: [W1401] Invalid char '\xc3'", ""), 0) @@ -1612,7 +1642,9 @@ def test_quality_deprecation_warning(self, process_patcher): def test_quality_error(self, mocker, process_patcher): # Patch the output stderr/stdout and returncode of `pylint` - process_patcher((b"file1.py:1: [C0111] Missing docstring", b"oops"), status_code=1) + process_patcher( + (b"file1.py:1: [C0111] Missing docstring", b"oops"), status_code=1 + ) # Parse the report code = mocker.patch( @@ -1678,7 +1710,9 @@ def test_quality_pregenerated_report(self): # Expect that we get the right violations expected_violations = [ Violation(1, "C0111: Missing docstring"), - Violation(57, "W0511: TODO the name of this method is a little bit confusing"), + Violation( + 57, "W0511: TODO the name of this method is a little bit confusing" + ), Violation( 183, 'C0103: Foo.bar.gettag: Invalid name "\u3240" for type argument (should match [a-z_][a-z0-9_]{2,30}$)', @@ -2048,9 +2082,7 @@ def test_parse_report(self): "(error) Array 'yolo[4]' accessed at index 4, which is out of bounds.", ), } - report = ( - "[src/foo.c:123]: (error) Array 'yolo[4]' accessed at index 4, which is out of bounds." - ) + report = "[src/foo.c:123]: (error) Array 'yolo[4]' accessed at index 4, which is out of bounds." driver = CppcheckDriver() actual_violations = driver.parse_reports([report]) diff --git a/verify.sh b/verify.sh index 33a43252..175f188d 100755 --- a/verify.sh +++ b/verify.sh @@ -2,7 +2,8 @@ set -euo pipefail IFS=$'\n\t' -ruff format --check . +ruff format --check +ruff check --select I python -m pytest -n auto --cov-context test --cov --cov-report=xml tests git fetch origin main:refs/remotes/origin/main diff-cover --version From 191134e128a77c3c6ea7712ee84990035f3219e2 Mon Sep 17 00:00:00 2001 From: kingbuzzman Date: Fri, 16 May 2025 10:19:17 -0400 Subject: [PATCH 12/39] isort --- diff_cover/config_parser.py | 1 + diff_cover/diff_cover_tool.py | 1 + diff_cover/diff_quality_tool.py | 1 + diff_cover/hook.py | 1 + diff_cover/hookspecs.py | 1 + diff_cover/violationsreporters/base.py | 1 + diff_cover/violationsreporters/java_violations_reporter.py | 2 ++ tests/helpers.py | 1 + tests/test_config_parser.py | 1 + tests/test_snippets.py | 1 + 10 files changed, 11 insertions(+) diff --git a/diff_cover/config_parser.py b/diff_cover/config_parser.py index e9281eb4..da455986 100644 --- a/diff_cover/config_parser.py +++ b/diff_cover/config_parser.py @@ -1,6 +1,7 @@ import abc import enum + try: import tomli as toml diff --git a/diff_cover/diff_cover_tool.py b/diff_cover/diff_cover_tool.py index 782415a8..49b46dee 100644 --- a/diff_cover/diff_cover_tool.py +++ b/diff_cover/diff_cover_tool.py @@ -21,6 +21,7 @@ XmlCoverageReporter, ) + HTML_REPORT_HELP = "Diff coverage HTML output" JSON_REPORT_HELP = "Diff coverage JSON output" MARKDOWN_REPORT_HELP = "Diff coverage Markdown output" diff --git a/diff_cover/diff_quality_tool.py b/diff_cover/diff_quality_tool.py index 1b389013..f9e51c77 100644 --- a/diff_cover/diff_quality_tool.py +++ b/diff_cover/diff_quality_tool.py @@ -58,6 +58,7 @@ shellcheck_driver, ) + QUALITY_DRIVERS = { "cppcheck": CppcheckDriver(), "pycodestyle": pycodestyle_driver, diff --git a/diff_cover/hook.py b/diff_cover/hook.py index 1d7dfb39..fdd7cc34 100644 --- a/diff_cover/hook.py +++ b/diff_cover/hook.py @@ -1,4 +1,5 @@ import pluggy + # Other packages that implement diff_cover plugins use this. hookimpl = pluggy.HookimplMarker("diff_cover") diff --git a/diff_cover/hookspecs.py b/diff_cover/hookspecs.py index 3a79cc66..e66ef3ad 100644 --- a/diff_cover/hookspecs.py +++ b/diff_cover/hookspecs.py @@ -1,5 +1,6 @@ import pluggy + hookspec = pluggy.HookspecMarker("diff_cover") diff --git a/diff_cover/violationsreporters/base.py b/diff_cover/violationsreporters/base.py index 3f1a6ca0..3595f67c 100644 --- a/diff_cover/violationsreporters/base.py +++ b/diff_cover/violationsreporters/base.py @@ -7,6 +7,7 @@ from diff_cover.command_runner import execute, run_command_for_code + Violation = namedtuple("Violation", "line, message") diff --git a/diff_cover/violationsreporters/java_violations_reporter.py b/diff_cover/violationsreporters/java_violations_reporter.py index 2ace2a79..a3097f8a 100644 --- a/diff_cover/violationsreporters/java_violations_reporter.py +++ b/diff_cover/violationsreporters/java_violations_reporter.py @@ -5,6 +5,7 @@ import os from collections import defaultdict + try: # Needed for Python < 3.3, works up to 3.8 import xml.etree.ElementTree as etree @@ -20,6 +21,7 @@ Violation, ) + # Report checkstyle violations. # http://checkstyle.sourceforge.net/apidocs/com/puppycrawl/tools/checkstyle/DefaultLogger.html # https://github.com/checkstyle/checkstyle/blob/master/src/main/java/com/puppycrawl/tools/checkstyle/AuditEventDefaultFormatter.java diff --git a/tests/helpers.py b/tests/helpers.py index 69211477..28210ba4 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -5,6 +5,7 @@ import os.path import random + HUNK_BUFFER = 2 MAX_LINE_LENGTH = 300 LINE_STRINGS = ["test", "+ has a plus sign", "- has a minus sign"] diff --git a/tests/test_config_parser.py b/tests/test_config_parser.py index eb0e300f..f7ec8988 100644 --- a/tests/test_config_parser.py +++ b/tests/test_config_parser.py @@ -3,6 +3,7 @@ from diff_cover import config_parser from diff_cover.config_parser import ParserError, TOMLParser, Tool, get_config + tools = pytest.mark.parametrize("tool", list(Tool)) diff --git a/tests/test_snippets.py b/tests/test_snippets.py index 4f285f2a..a21bb84c 100644 --- a/tests/test_snippets.py +++ b/tests/test_snippets.py @@ -11,6 +11,7 @@ from diff_cover.snippets import Snippet from tests.helpers import fixture_path, load_fixture + SRC_TOKENS = [ (Token.Comment, "# Test source"), (Token.Text, "\n"), From 8171a16d6dd9de862413d6e88dc30de797fcb754 Mon Sep 17 00:00:00 2001 From: kingbuzzman Date: Fri, 16 May 2025 10:22:02 -0400 Subject: [PATCH 13/39] . --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 7a3b7c0e..d83c89d1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -163,4 +163,4 @@ section-order = ["future", "standard-library", "third-party", "first-party", "lo lines-between-types = 0 lines-after-imports = 2 combine-as-imports = true -split-on-trailing-comma = false \ No newline at end of file +split-on-trailing-comma = false From 50f1db70b35e83962cc2a32e98af31e7bc60f869 Mon Sep 17 00:00:00 2001 From: kingbuzzman Date: Fri, 16 May 2025 10:41:45 -0400 Subject: [PATCH 14/39] . --- pyproject.toml | 17 +++++++++++++++++ verify.sh | 1 - 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index d83c89d1..88a1e8eb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -111,8 +111,16 @@ line-ending = "auto" [tool.ruff.lint] select = ["ALL"] ignore = [ + # Disable all annotations + "ANN", # allow TODO comments (equivalent to "fixme") "FIX", + + "D200", # Disables One-line docstring should fit on one line + "D205", # Disables 1 blank line required between summary line and description + "D212", # Disables Multi-line docstring summary should start at the first line + "D400", # Disables First line should end with a period + "D415", # Disables First line should end with a period, question mark, or exclamation point # allow disables (equivalent to "locally-disabled", "suppressed-message") "RUF100", @@ -153,6 +161,15 @@ allowed-confusables = ["_"] # Allow specific names that might otherwise violate naming conventions ignore-names = ["i", "e", "ex", "setUp", "tearDown"] +[tool.ruff.lint.per-file-ignores] +# Allow assert statements in test files +"tests/*" = [ + "S101", # Disables Assert statements + "PLR2004", # Disables Magic value + "PT011", # Disables Exception is too broad + "RUF012", # Disables Mutable class attributes should be annotated with typing.ClassVar +] + [tool.ruff.lint.pydocstyle] # Equivalent to pylint's no-docstring-rgx ignore-decorators = ["^_"] diff --git a/verify.sh b/verify.sh index 175f188d..01a155b2 100755 --- a/verify.sh +++ b/verify.sh @@ -11,4 +11,3 @@ diff-quality --version diff-cover coverage.xml --include-untracked diff-quality --violations ruff.check --include-untracked doc8 README.rst --ignore D001 - From 76dd7155483ae3ed88baee47bdc045015d0ed48b Mon Sep 17 00:00:00 2001 From: kingbuzzman Date: Fri, 16 May 2025 11:13:06 -0400 Subject: [PATCH 15/39] . --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 88a1e8eb..e7e8cf51 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -162,7 +162,6 @@ allowed-confusables = ["_"] ignore-names = ["i", "e", "ex", "setUp", "tearDown"] [tool.ruff.lint.per-file-ignores] -# Allow assert statements in test files "tests/*" = [ "S101", # Disables Assert statements "PLR2004", # Disables Magic value From d5df82a9593df4daae98796539018cfec96d11b9 Mon Sep 17 00:00:00 2001 From: kingbuzzman Date: Sun, 18 May 2025 01:45:47 -0400 Subject: [PATCH 16/39] Min version should be our least required --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index e8f3b164..f18b9aeb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -97,7 +97,7 @@ max_line_length = 120 [tool.ruff] line-length = 88 -target-version = "py310" +target-version = "py39" src = ["diff_cover", "tests"] exclude = ["tests/fixtures/*"] From 29abb57e0e139a6cc487ffb03c5d10009b24441c Mon Sep 17 00:00:00 2001 From: kingbuzzman Date: Sun, 18 May 2025 14:16:24 -0400 Subject: [PATCH 17/39] . --- diff_cover/command_runner.py | 2 + diff_cover/diff_cover_tool.py | 4 +- diff_cover/diff_quality_tool.py | 1 - diff_cover/diff_reporter.py | 39 +++++++------------ diff_cover/git_diff.py | 6 +-- diff_cover/report_generator.py | 6 --- diff_cover/snippets.py | 4 -- diff_cover/violationsreporters/base.py | 12 ++++-- .../java_violations_reporter.py | 7 ++++ .../violations_reporter.py | 18 ++++----- tests/helpers.py | 6 +-- tests/test_diff_reporter.py | 2 +- tests/test_integration.py | 2 +- tests/test_report_generator.py | 6 +-- tests/test_violations_reporter.py | 5 +-- 15 files changed, 52 insertions(+), 68 deletions(-) diff --git a/diff_cover/command_runner.py b/diff_cover/command_runner.py index 6165ec1c..19e4f31d 100644 --- a/diff_cover/command_runner.py +++ b/diff_cover/command_runner.py @@ -57,8 +57,10 @@ def _ensure_unicode(text): Ensures the text passed in becomes unicode Args: text (str|unicode) + Returns: unicode + """ if isinstance(text, bytes): return text.decode(sys.getfilesystemencoding(), "replace") diff --git a/diff_cover/diff_cover_tool.py b/diff_cover/diff_cover_tool.py index 9808ddce..71a31bb0 100644 --- a/diff_cover/diff_cover_tool.py +++ b/diff_cover/diff_cover_tool.py @@ -243,8 +243,8 @@ def generate_coverage_report( if not coverage_file.endswith(".xml") ] if len(xml_roots) > 0 and len(lcov_roots) > 0: - raise ValueError(f"Mixing LCov and XML reports is not supported yet") - elif len(xml_roots) > 0: + raise ValueError("Mixing LCov and XML reports is not supported yet") + if len(xml_roots) > 0: coverage = XmlCoverageReporter(xml_roots, src_roots, expand_coverage_report) else: coverage = LcovCoverageReporter(lcov_roots, src_roots) diff --git a/diff_cover/diff_quality_tool.py b/diff_cover/diff_quality_tool.py index f9e51c77..3c39f2c8 100644 --- a/diff_cover/diff_quality_tool.py +++ b/diff_cover/diff_quality_tool.py @@ -293,7 +293,6 @@ def main(argv=None, directory=None): 1 is an error 0 is successful run """ - argv = argv or sys.argv arg_dict = parse_quality_args(argv[1:]) diff --git a/diff_cover/diff_reporter.py b/diff_cover/diff_reporter.py index 411586e3..4b50c8f7 100644 --- a/diff_cover/diff_reporter.py +++ b/diff_cover/diff_reporter.py @@ -162,7 +162,6 @@ def src_paths_changed(self): """ See base class docstring. """ - # Get the diff dictionary diff_dict = self._git_diff() # include untracked files @@ -183,7 +182,6 @@ def _get_file_lines(path): """ Return the number of lines in a file. """ - try: with open(path, encoding="utf-8") as file_handle: return len(file_handle.readlines()) @@ -194,7 +192,6 @@ def lines_changed(self, src_path): """ See base class docstring. """ - # Get the diff dictionary (cached) diff_dict = self._git_diff() @@ -227,7 +224,6 @@ def _git_diff(self): Raises a GitDiffError if `git diff` has an error. """ - # If we do not have a cached result, execute `git diff` if self._diff_dict is None: result_dict = {} @@ -268,7 +264,6 @@ def _validate_path_to_diff(self, src_path: str) -> bool: - If the path is excluded - If the path has an extension that is not supported """ - if self._is_path_excluded(src_path): return False @@ -297,7 +292,6 @@ def _parse_diff_str(self, diff_str): If the output could not be parsed, raises a GitDiffError. """ - # Create a dict to hold results diff_dict = {} @@ -320,7 +314,6 @@ def _parse_source_sections(self, diff_str): Raises a `GitDiffError` if `diff_str` is in an invalid format. """ - # Create a dict to map source files to lines in the diff output source_dict = {} @@ -349,22 +342,20 @@ def _parse_source_sections(self, diff_str): # Every other line is stored in the dictionary for this source file # once we find a hunk section - else: - # Only add lines if we're in a hunk section - # (ignore index and files changed lines) - if found_hunk or line.startswith("@@"): - # Remember that we found a hunk - found_hunk = True - - if src_path is not None: - source_dict[src_path].append(line) - - else: - # We tolerate other information before we have - # a source file defined, unless it's a hunk line - if line.startswith("@@"): - msg = f"Hunk has no source file: '{line}'" - raise GitDiffError(msg) + # Only add lines if we're in a hunk section + # (ignore index and files changed lines) + elif found_hunk or line.startswith("@@"): + # Remember that we found a hunk + found_hunk = True + + if src_path is not None: + source_dict[src_path].append(line) + + # We tolerate other information before we have + # a source file defined, unless it's a hunk line + elif line.startswith("@@"): + msg = f"Hunk has no source file: '{line}'" + raise GitDiffError(msg) return source_dict @@ -378,7 +369,6 @@ def _parse_lines(self, diff_lines): Raises a `GitDiffError` if the diff lines are in an invalid format. """ - added_lines = [] deleted_lines = [] @@ -502,7 +492,6 @@ def _unique_ordered_lines(line_numbers): Given a list of line numbers, return a list in which each line number is included once and the lines are ordered sequentially. """ - if len(line_numbers) == 0: return [] diff --git a/diff_cover/git_diff.py b/diff_cover/git_diff.py index fe837bd2..f46b2478 100644 --- a/diff_cover/git_diff.py +++ b/diff_cover/git_diff.py @@ -65,9 +65,7 @@ def diff_committed(self, compare_branch="origin/main"): Raises a `GitDiffError` if `git diff` outputs anything to stderr. """ - diff_range = "{branch}{notation}HEAD".format( - branch=compare_branch, notation=self.range_notation - ) + diff_range = f"{compare_branch}{self.range_notation}HEAD" try: return execute( self._default_git_args + self._default_diff_args + [diff_range] @@ -129,7 +127,7 @@ def diff_committed(self, compare_branch="origin/main"): Raises a `GitDiffError` if the file cannot be read. """ try: - with open(self.diff_file_path, "r") as file: + with open(self.diff_file_path) as file: return file.read() except OSError as e: error_message = ( diff --git a/diff_cover/report_generator.py b/diff_cover/report_generator.py index 132f9d93..d3d96b67 100644 --- a/diff_cover/report_generator.py +++ b/diff_cover/report_generator.py @@ -134,7 +134,6 @@ def violation_lines(self, src_path): If we have no coverage information for `src_path`, returns an empty list. """ - diff_violations = self._diff_violations().get(src_path) if diff_violations is None: @@ -147,7 +146,6 @@ def total_num_lines(self): Return the total number of lines in the diff for which we have coverage info. """ - return sum( [ len(summary.measured_lines) @@ -160,7 +158,6 @@ def total_num_violations(self): Returns the total number of lines in the diff that are in violation. """ - return sum(len(summary.lines) for summary in self._diff_violations().values()) def total_percent_covered(self): @@ -233,7 +230,6 @@ def _src_path_stats(self, src_path): """ Return a dict of statistics for the source file at `src_path`. """ - covered_lines = self.covered_lines(src_path) # Find violation lines @@ -296,7 +292,6 @@ def generate_report(self, output_file): See base class. output_file must be a file handler that takes in bytes! """ - if self.template_path is not None: template = TEMPLATE_ENV.get_template(self.template_path) report = template.render(self._context()) @@ -340,7 +335,6 @@ def _context(self): 'total_percent_covered': TOTAL_PERCENT_COVERED } """ - # Include snippet style info if we're displaying # source code snippets if self.include_snippets: diff --git a/diff_cover/snippets.py b/diff_cover/snippets.py index 11524148..28af38ad 100644 --- a/diff_cover/snippets.py +++ b/diff_cover/snippets.py @@ -116,7 +116,6 @@ def markdown(self): Return a Markdown representation of the snippet using Markdown fenced code blocks. See https://github.github.com/gfm/#fenced-code-blocks. """ - line_number_length = len(str(self._last_line)) text = "" @@ -188,7 +187,6 @@ def load_formatted_snippets(cls, src_path, violation_lines): See `load_snippets()` for details. """ - # load once... snippet_list = cls.load_snippets(src_path, violation_lines) @@ -266,7 +264,6 @@ def _parse_src(cls, src_contents, src_filename): Uses `src_filename` to guess the type of file so it can highlight syntax correctly. """ - # Parse the source into tokens try: lexer = guess_lexer_for_filename(src_filename, src_contents) @@ -302,7 +299,6 @@ def _group_tokens(cls, token_stream, range_list): The algorithm is slightly complicated because a single token can contain multiple line breaks. """ - # Create a map from ranges (start/end tuples) to tokens token_map = {rng: [] for rng in range_list} diff --git a/diff_cover/violationsreporters/base.py b/diff_cover/violationsreporters/base.py index 3595f67c..8a371a53 100644 --- a/diff_cover/violationsreporters/base.py +++ b/diff_cover/violationsreporters/base.py @@ -61,7 +61,7 @@ def measured_lines(self, src_path): """ # An existing quality plugin "sqlfluff" depends on this # being not abstract and returning None - return None + return def name(self): """ @@ -87,6 +87,7 @@ def __init__( to create a report exit_codes: (list[int]) list of exit codes that do not indicate a command error output_stderr: (bool) use stderr instead of stdout from the invoked command + """ self.name = name self.supported_extensions = supported_extensions @@ -102,6 +103,7 @@ def parse_reports(self, reports): Return: A dict[Str:Violation] Violation is a simple named tuple Defined above + """ @abstractmethod @@ -126,6 +128,7 @@ def __init__(self, driver, reports=None, options=None): driver (QualityDriver) object that works with the underlying quality tool reports (list[file]) pre-generated reports. If not provided the tool will be run instead options (str) options to be passed into the command + """ super().__init__(driver.name) self.reports = self._load_reports(reports) if reports else None @@ -138,6 +141,7 @@ def _load_reports(self, report_files): """ Args: report_files: list[file] reports to read in + """ contents = [] for file_handle in report_files: @@ -179,7 +183,7 @@ def measured_lines(self, src_path): """ Quality Reports Consider all lines measured """ - return None + return def name(self): """ @@ -204,13 +208,14 @@ def __init__( exit_codes=None, ): """ - args: + Args: expression: regex used to parse report, will be fed lines singly unless flags contain re.MULTILINE flags: such as re.MULTILINE See super for other args command_to_check_install: (list[str]) command to run to see if the tool is installed + """ super().__init__(name, supported_extensions, command, exit_codes) self.expression = re.compile(expression, flags) @@ -224,6 +229,7 @@ def parse_reports(self, reports): Return: A dict[Str:Violation] Violation is a simple named tuple Defined above + """ violations_dict = defaultdict(list) for report in reports: diff --git a/diff_cover/violationsreporters/java_violations_reporter.py b/diff_cover/violationsreporters/java_violations_reporter.py index 8faf7b28..653fcfc9 100644 --- a/diff_cover/violationsreporters/java_violations_reporter.py +++ b/diff_cover/violationsreporters/java_violations_reporter.py @@ -59,6 +59,7 @@ def parse_reports(self, reports): Return: A dict[Str:Violation] Violation is a simple named tuple Defined above + """ violations_dict = defaultdict(list) for report in reports: @@ -97,6 +98,7 @@ def parse_reports(self, reports): Return: A dict[Str:Violation] Violation is a simple named tuple Defined above + """ violations_dict = defaultdict(list) for report in reports: @@ -121,9 +123,11 @@ def parse_reports(self, reports): def installed(self): """ Method checks if the provided tool is installed. + Returns: boolean False: As findbugs analyses bytecode, it would be hard to run it from outside the build framework. + """ return False @@ -142,6 +146,7 @@ def parse_reports(self, reports): Return: A dict[Str:Violation] Violation is a simple named tuple Defined above + """ violations_dict = defaultdict(list) for report in reports: @@ -161,8 +166,10 @@ def parse_reports(self, reports): def installed(self): """ Method checks if the provided tool is installed. + Returns: boolean False: As findbugs analyses bytecode, it would be hard to run it from outside the build framework. + """ return False diff --git a/diff_cover/violationsreporters/violations_reporter.py b/diff_cover/violationsreporters/violations_reporter.py index 446fa179..a91497e4 100644 --- a/diff_cover/violationsreporters/violations_reporter.py +++ b/diff_cover/violationsreporters/violations_reporter.py @@ -117,7 +117,6 @@ def get_src_path_line_nodes_clover(xml_document, src_path): If file is not present in `xml_document`, return None """ - files = [ file_tree for file_tree in xml_document.findall(".//file") @@ -156,7 +155,6 @@ def get_src_path_line_nodes_jacoco(self, xml_document, src_path): If file is not present in `xml_document`, return None """ - files = [] packages = list(xml_document.findall(".//package")) for pkg in packages: @@ -271,7 +269,6 @@ def violations(self, src_path): """ See base class comments. """ - self._cache_file(src_path) # Yield all lines not covered @@ -322,7 +319,7 @@ def parse(lcov_file): # SF: source_file = util.to_unix_path(GitPathTool.relative_path(content)) continue - elif directive == "DA": + if directive == "DA": # DA:,[,] args = content.split(",") if len(args) < 2 or len(args) > 3: @@ -437,7 +434,6 @@ def violations(self, src_path): """ See base class comments. """ - self._cache_file(src_path) # Yield all lines not covered @@ -580,9 +576,10 @@ def parse_reports(self, reports): class PylintDriver(QualityDriver): def __init__(self): """ - args: + Args: expression: regex used to parse report See super for other args + """ super().__init__( "pylint", @@ -649,6 +646,7 @@ def parse_reports(self, reports): Return: A dict[Str:Violation] Violation is a simple named tuple Defined above + """ violations_dict = defaultdict(list) for report in reports: @@ -681,9 +679,7 @@ def parse_reports(self, reports): # If we're looking for a particular source file, # ignore any other source files. if function_name: - error_str = "{}: {}: {}".format( - pylint_code, function_name, message - ) + error_str = f"{pylint_code}: {function_name}: {message}" else: error_str = f"{pylint_code}: {message}" @@ -707,9 +703,10 @@ class CppcheckDriver(QualityDriver): def __init__(self): """ - args: + Args: expression: regex used to parse report See super for other args + """ super().__init__( "cppcheck", @@ -731,6 +728,7 @@ def parse_reports(self, reports): Return: A dict[Str:Violation] Violation is a simple named tuple Defined above + """ violations_dict = defaultdict(list) for report in reports: diff --git a/tests/helpers.py b/tests/helpers.py index 28210ba4..a7e0259f 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -64,7 +64,6 @@ def git_diff_output(diff_dict, deleted_files=None): Returns a byte string. """ - output = [] # Entries for deleted files @@ -86,7 +85,6 @@ def _deleted_file_entries(deleted_files): Returns a list of lines in the diff output. """ - output = [] if deleted_files is not None: @@ -116,7 +114,6 @@ def _source_file_entry(src_file, modified_lines): Returns a list of lines in the diff output. """ - output = [] # Line for the file names @@ -158,7 +155,7 @@ def _hunk_entry(start, end, modified_lines): # for before/after the change, but since we're only interested # in after the change, we use the same numbers for both. length = end - start - output.append("@@ -{0},{1} +{0},{1} @@".format(start, length)) + output.append(f"@@ -{start},{length} +{start},{length} @@") # Output line modifications for line_number in range(start, end + 1): @@ -183,7 +180,6 @@ def _hunks(modified_lines): Given a list of line numbers, return a list of hunks represented as `(start, end)` tuples. """ - # Identify contiguous lines as hunks hunks = [] last_line = None diff --git a/tests/test_diff_reporter.py b/tests/test_diff_reporter.py index 09dc7b10..fc99fd38 100644 --- a/tests/test_diff_reporter.py +++ b/tests/test_diff_reporter.py @@ -478,7 +478,7 @@ def test_git_diff_error( # Expect that both methods that access git diff raise an error with pytest.raises(GitDiffError): - print("src_paths_changed() should fail for {}".format(diff_str)) + print(f"src_paths_changed() should fail for {diff_str}") diff.src_paths_changed() with pytest.raises(GitDiffError): diff --git a/tests/test_integration.py b/tests/test_integration.py index c56b6c59..b8a49ec9 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -146,7 +146,7 @@ def test_added_file_console_lcov(self, runbin, patch_git_command, capsys): def test_lua_coverage(self, runbin, patch_git_command, capsys): """ - coverage report shows that diff-cover needs to normalize + Coverage report shows that diff-cover needs to normalize paths read in """ patch_git_command.set_stdout("git_diff_lua.txt") diff --git a/tests/test_report_generator.py b/tests/test_report_generator.py index 8cbd36b0..c1de0c47 100644 --- a/tests/test_report_generator.py +++ b/tests/test_report_generator.py @@ -304,7 +304,7 @@ def test_generate_report(self): def test_hundred_percent(self): # Have the dependencies return an empty report self.set_src_paths_changed(["file.py"]) - self.set_lines_changed("file.py", list(range(0, 100))) + self.set_lines_changed("file.py", list(range(100))) self.set_violations("file.py", []) self.set_measured("file.py", [2]) @@ -377,7 +377,7 @@ def test_generate_report(self): def test_hundred_percent(self): # Have the dependencies return an empty report self.set_src_paths_changed(["file.py"]) - self.set_lines_changed("file.py", list(range(0, 100))) + self.set_lines_changed("file.py", list(range(100))) self.set_violations("file.py", []) self.set_measured("file.py", [2]) @@ -484,7 +484,7 @@ def test_generate_report(self): def test_hundred_percent(self): # Have the dependencies return an empty report self.set_src_paths_changed(["file.py"]) - self.set_lines_changed("file.py", list(range(0, 100))) + self.set_lines_changed("file.py", list(range(100))) self.set_violations("file.py", []) self.set_measured("file.py", [2]) diff --git a/tests/test_violations_reporter.py b/tests/test_violations_reporter.py index 05e32302..4114cd5f 100644 --- a/tests/test_violations_reporter.py +++ b/tests/test_violations_reporter.py @@ -976,7 +976,6 @@ def _coverage_lcov(self, file_paths, violations, measured): """ Build an LCOV document based on the provided arguments. """ - violation_lines = {violation.line for violation in violations} with tempfile.NamedTemporaryFile("w", delete=False) as f: @@ -1768,9 +1767,9 @@ def patcher(self, mocker): def _get_out(self): """ - get Object Under Test + Get Object Under Test """ - return None # pragma: no cover + return # pragma: no cover def test_quality(self): """ From eb13b7d1925286ab41df7b7a4483af4338ffb4ff Mon Sep 17 00:00:00 2001 From: kingbuzzman Date: Sun, 18 May 2025 14:18:36 -0400 Subject: [PATCH 18/39] . --- tests/test_integration.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_integration.py b/tests/test_integration.py index b8a49ec9..e6ec5c3f 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -125,6 +125,7 @@ class TestDiffCoverIntegration: @pytest.fixture def runbin(self, cwd): + del cwd return lambda x: diff_cover_tool.main(["diff-cover", *x]) def test_added_file_html(self, runbin, patch_git_command): @@ -217,7 +218,7 @@ def test_changed_file_console(self, runbin, patch_git_command, capsys): assert runbin(["coverage.xml"]) == 0 compare_console("changed_console_report.txt", capsys.readouterr().out) - def test_moved_file_html(self, runbin, patch_git_command, capsys): + def test_moved_file_html(self, runbin, patch_git_command): patch_git_command.set_stdout("git_diff_moved.txt") assert ( runbin(["moved_coverage.xml", "--html-report", "dummy/diff_coverage.html"]) @@ -358,6 +359,7 @@ class TestDiffQualityIntegration: @pytest.fixture def runbin(self, cwd): + del cwd return lambda x: diff_quality_tool.main(["diff-quality", *x]) def test_git_diff_error_diff_quality(self, runbin, patch_git_command): From 047a9a72d40c8c074acc95021a2b78bfc3286873 Mon Sep 17 00:00:00 2001 From: kingbuzzman Date: Sun, 18 May 2025 14:24:22 -0400 Subject: [PATCH 19/39] Improvements --- diff_cover/git_diff.py | 2 -- diff_cover/violationsreporters/base.py | 2 +- pyproject.toml | 8 ++++++++ 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/diff_cover/git_diff.py b/diff_cover/git_diff.py index f46b2478..7a626400 100644 --- a/diff_cover/git_diff.py +++ b/diff_cover/git_diff.py @@ -110,8 +110,6 @@ def untracked(self): return self._untracked_cache output = execute(["git", "ls-files", "--exclude-standard", "--others"])[0] - if not output: - return [] return [to_unescaped_filename(line) for line in output.splitlines() if line] diff --git a/diff_cover/violationsreporters/base.py b/diff_cover/violationsreporters/base.py index 8a371a53..5f0412a4 100644 --- a/diff_cover/violationsreporters/base.py +++ b/diff_cover/violationsreporters/base.py @@ -61,7 +61,7 @@ def measured_lines(self, src_path): """ # An existing quality plugin "sqlfluff" depends on this # being not abstract and returning None - return + raise NotImplementedError def name(self): """ diff --git a/pyproject.toml b/pyproject.toml index f18b9aeb..adc85d56 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -80,6 +80,14 @@ omit = ["./tests/*"] [tool.coverage.report] show_missing = true +exclude_also = [ + "if typing.TYPE_CHECKING:", + "if TYPE_CHECKING:", + # Don't complain if tests don't hit defensive assertion code: + "raise NotImplementedError", + "raise AssertionError", + "pass", +] [tool.coverage.html] show_contexts = true From 99f587d6f29394efcbddd36e1bbc40ef182128fc Mon Sep 17 00:00:00 2001 From: kingbuzzman Date: Sun, 18 May 2025 14:34:40 -0400 Subject: [PATCH 20/39] More replacements.. --- tests/test_diff_quality_main.py | 1 - tests/test_diff_reporter.py | 2 -- 2 files changed, 3 deletions(-) diff --git a/tests/test_diff_quality_main.py b/tests/test_diff_quality_main.py index a201b9a1..6abfedfe 100644 --- a/tests/test_diff_quality_main.py +++ b/tests/test_diff_quality_main.py @@ -69,7 +69,6 @@ def test_parse_invalid_arg(): for argv in invalid_argv: with pytest.raises(SystemExit): - print(f"args = {argv}") parse_quality_args(argv) diff --git a/tests/test_diff_reporter.py b/tests/test_diff_reporter.py index fc99fd38..08e5367f 100644 --- a/tests/test_diff_reporter.py +++ b/tests/test_diff_reporter.py @@ -478,11 +478,9 @@ def test_git_diff_error( # Expect that both methods that access git diff raise an error with pytest.raises(GitDiffError): - print(f"src_paths_changed() should fail for {diff_str}") diff.src_paths_changed() with pytest.raises(GitDiffError): - print(f"lines_changed() should fail for {diff_str}") diff.lines_changed("subdir/file1.py") From 1a3d77eed44fd48061460fc2842dbf945b890a1f Mon Sep 17 00:00:00 2001 From: kingbuzzman Date: Sun, 18 May 2025 17:53:05 -0400 Subject: [PATCH 21/39] . --- .../violations_reporter.py | 94 +++++++++---------- pyproject.toml | 1 + verify.sh | 1 + 3 files changed, 49 insertions(+), 47 deletions(-) diff --git a/diff_cover/violationsreporters/violations_reporter.py b/diff_cover/violationsreporters/violations_reporter.py index a91497e4..4b6e511f 100644 --- a/diff_cover/violationsreporters/violations_reporter.py +++ b/diff_cover/violationsreporters/violations_reporter.py @@ -308,52 +308,53 @@ def parse(lcov_file): File format: https://ltp.sourceforge.net/coverage/lcov/geninfo.1.php """ lcov_report = defaultdict(dict) - lcov = open(lcov_file) - while True: - line = lcov.readline() - if not line: - break - directive, _, content = line.strip().partition(":") - # we're only interested in file name and line coverage - if directive == "SF": - # SF: - source_file = util.to_unix_path(GitPathTool.relative_path(content)) - continue - if directive == "DA": - # DA:,[,] - args = content.split(",") - if len(args) < 2 or len(args) > 3: - raise ValueError(f"Unknown syntax in lcov report: {line}") - line_no = int(args[0]) - num_executions = int(args[1]) - if source_file is None: - raise ValueError( - f"No source file specified for line coverage: {line}" - ) - if line_no not in lcov_report[source_file]: - lcov_report[source_file][line_no] = 0 - lcov_report[source_file][line_no] += num_executions - elif directive in [ - "TN", - "FNF", - "FNH", - "FN", - "FNDA", - "LH", - "LF", - "BRF", - "BRH", - "BRDA", - "VER", - ]: - # these are valid lines, but not we don't need them - continue - elif directive == "end_of_record": - source_file = None - else: - raise ValueError(f"Unknown syntax in lcov report: {line}") + skippable_directives = [ + "TN", + "FNF", + "FNH", + "FN", + "FNDA", + "LH", + "LF", + "BRF", + "BRH", + "BRDA", + "VER", + ] + with open(lcov_file) as lcov: + while True: + line = lcov.readline() + if not line: + break + directive, _, content = line.strip().partition(":") + if directive in skippable_directives: + # these are valid lines, but not we don't need them + continue + # we're only interested in file name and line coverage + if directive == "SF": + # SF: + source_file = util.to_unix_path(GitPathTool.relative_path(content)) + continue + if directive == "DA": + # DA:,[,] + args = content.split(",") + if len(args) < 2 or len(args) > 3: + msg = f"Unknown syntax in lcov report: {line}" + raise ValueError(msg) + line_no = int(args[0]) + num_executions = int(args[1]) + if source_file is None: + msg = f"No source file specified for line coverage: {line}" + raise ValueError(msg) + if line_no not in lcov_report[source_file]: + lcov_report[source_file][line_no] = 0 + lcov_report[source_file][line_no] += num_executions + elif directive == "end_of_record": + source_file = None + else: + msg = f"Unknown syntax in lcov report: {line}" + raise ValueError(msg) - lcov.close() return lcov_report def _cache_file(self, src_path): @@ -415,7 +416,6 @@ def _cache_file(self, src_path): } # Measured is the union of itself and the new measured - # measured = measured | {int(line.get(_number)) for line in line_nodes} measured = measured | { int(line_no) for line_no, num_executions in lcov_document[ @@ -636,7 +636,7 @@ def _process_dupe_code_violation(self, lines, current_line, message): current_line += 1 match = self.multi_line_violation_regex.match(lines[current_line]) src_path, l_number = match.groups() - src_paths.append(("%s.py" % src_path, l_number)) + src_paths.append((f"{src_path}.py", l_number)) return src_paths def parse_reports(self, reports): diff --git a/pyproject.toml b/pyproject.toml index adc85d56..6b4f979d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -127,6 +127,7 @@ ignore = [ "D205", # Disables 1 blank line required between summary line and description "D212", # Disables Multi-line docstring summary should start at the first line "D400", # Disables First line should end with a period + "D401", # Disables First line of docstring should be in imperative mood "D415", # Disables First line should end with a period, question mark, or exclamation point # allow disables (equivalent to "locally-disabled", "suppressed-message") diff --git a/verify.sh b/verify.sh index fb39e259..249d7f62 100755 --- a/verify.sh +++ b/verify.sh @@ -1,6 +1,7 @@ #!/bin/bash set -euo pipefail IFS=$'\n\t' +COMPARE_BRANCH=${COMPARE_BRANCH:-origin/main} ruff format --check ruff check --select I From dffcb7bf1743864e265b90d1ec6a89ac59904def Mon Sep 17 00:00:00 2001 From: kingbuzzman Date: Mon, 19 May 2025 08:16:41 -0400 Subject: [PATCH 22/39] wip --- diff_cover/diff_cover_tool.py | 7 +- diff_cover/snippets.py | 3 +- diff_cover/violationsreporters/base.py | 17 ++-- .../violations_reporter.py | 94 +++++++++---------- pyproject.toml | 1 + 5 files changed, 58 insertions(+), 64 deletions(-) diff --git a/diff_cover/diff_cover_tool.py b/diff_cover/diff_cover_tool.py index 4e621b74..473b74b4 100644 --- a/diff_cover/diff_cover_tool.py +++ b/diff_cover/diff_cover_tool.py @@ -241,9 +241,10 @@ def generate_coverage_report( for coverage_file in coverage_files if not coverage_file.endswith(".xml") ] - if len(xml_roots) > 0 and len(lcov_roots) > 0: - raise ValueError("Mixing LCov and XML reports is not supported yet") - if len(xml_roots) > 0: + if xml_roots and lcov_roots: + msg = "Mixing LCov and XML reports is not supported yet" + raise ValueError(msg) + if xml_roots: coverage = XmlCoverageReporter(xml_roots, src_roots, expand_coverage_report) else: coverage = LcovCoverageReporter(lcov_roots, src_roots) diff --git a/diff_cover/snippets.py b/diff_cover/snippets.py index 28af38ad..613d7c08 100644 --- a/diff_cover/snippets.py +++ b/diff_cover/snippets.py @@ -78,7 +78,8 @@ def __init__( Raises a `ValueError` if `start_line` is less than 1 """ if start_line < 1: - raise ValueError("Start line must be >= 1") + msg = "Start line must be >= 1" + raise ValueError(msg) self._src_tokens = src_tokens self._src_filename = src_filename diff --git a/diff_cover/violationsreporters/base.py b/diff_cover/violationsreporters/base.py index 5f0412a4..e7c52132 100644 --- a/diff_cover/violationsreporters/base.py +++ b/diff_cover/violationsreporters/base.py @@ -118,7 +118,8 @@ def add_driver_args(self, **kwargs): A driver can override the method. By default an exception is raised. """ - raise ValueError(f"Unsupported argument(s) {kwargs.keys()}") + msg = f"Unsupported argument(s) {kwargs.keys()}" + raise ValueError(msg) class QualityReporter(BaseViolationReporter): @@ -162,7 +163,8 @@ def violations(self, src_path): if self.driver_tool_installed is None: self.driver_tool_installed = self.driver.installed() if not self.driver_tool_installed: - raise OSError(f"{self.driver.name} is not installed") + msg = f"{self.driver.name} is not installed" + raise OSError(msg) command = copy.deepcopy(self.driver.command) if self.options: for arg in self.options.split(): @@ -170,12 +172,9 @@ def violations(self, src_path): if os.path.exists(src_path): command.append(src_path.encode(sys.getfilesystemencoding())) - output = execute(command, self.driver.exit_codes) - if self.driver.output_stderr: - output = output[1] - else: - output = output[0] - self.violations_dict.update(self.driver.parse_reports([output])) + stdout, stderr = execute(command, self.driver.exit_codes) + output = stderr if self.driver.output_stderr else stdout + self.violations_dict.update(self.driver.parse_reports([output])) return self.violations_dict[src_path] @@ -183,7 +182,7 @@ def measured_lines(self, src_path): """ Quality Reports Consider all lines measured """ - return + del src_path def name(self): """ diff --git a/diff_cover/violationsreporters/violations_reporter.py b/diff_cover/violationsreporters/violations_reporter.py index 4b6e511f..c31c5005 100644 --- a/diff_cover/violationsreporters/violations_reporter.py +++ b/diff_cover/violationsreporters/violations_reporter.py @@ -305,10 +305,11 @@ def __init__(self, lcov_roots, src_roots=None): def parse(lcov_file): """ Parse a single LCov coverage report - File format: https://ltp.sourceforge.net/coverage/lcov/geninfo.1.php + File format: https://linux.die.net/man/1/geninfo """ lcov_report = defaultdict(dict) - skippable_directives = [ + source_file = None + skippable = { "TN", "FNF", "FNH", @@ -320,35 +321,27 @@ def parse(lcov_file): "BRH", "BRDA", "VER", - ] + } + with open(lcov_file) as lcov: - while True: - line = lcov.readline() - if not line: - break - directive, _, content = line.strip().partition(":") - if directive in skippable_directives: - # these are valid lines, but not we don't need them + for line in (line for line in (line.strip() for line in lcov) if line): + directive, _, content = line.partition(":") + + if directive in skippable: continue - # we're only interested in file name and line coverage + if directive == "SF": - # SF: source_file = util.to_unix_path(GitPathTool.relative_path(content)) - continue - if directive == "DA": - # DA:,[,] - args = content.split(",") - if len(args) < 2 or len(args) > 3: - msg = f"Unknown syntax in lcov report: {line}" - raise ValueError(msg) - line_no = int(args[0]) - num_executions = int(args[1]) + elif directive == "DA": + line_no, hits, *_ = content.split(",") if source_file is None: msg = f"No source file specified for line coverage: {line}" raise ValueError(msg) - if line_no not in lcov_report[source_file]: - lcov_report[source_file][line_no] = 0 - lcov_report[source_file][line_no] += num_executions + line_no = int(line_no) + hits = int(hits) + lcov_report[source_file][line_no] = ( + lcov_report[source_file].get(line_no, 0) + hits + ) elif directive == "end_of_record": source_file = None else: @@ -657,34 +650,33 @@ def parse_reports(self, reports): # Ignore any line that isn't matched # (for example, snippets from the source code) - if match is not None: - ( - pylint_src_path, - line_number, - pylint_code, - function_name, - message, - ) = match.groups() - if pylint_code == self.dupe_code_violation: - files_involved = self._process_dupe_code_violation( - output_lines, output_line_number, message - ) + if match is None: + continue + + ( + pylint_src_path, + line_number, + pylint_code, + function_name, + message, + ) = match.groups() + files_involved = [(pylint_src_path, line_number)] + if pylint_code == self.dupe_code_violation: + files_involved = self._process_dupe_code_violation( + output_lines, output_line_number, message + ) + + for pylint_src_path, line_number in files_involved: + # If we're looking for a particular source file, + # ignore any other source files. + if function_name: + error_str = f"{pylint_code}: {function_name}: {message}" else: - files_involved = [(pylint_src_path, line_number)] - - for violation in files_involved: - pylint_src_path, line_number = violation - # pylint might uses windows paths - pylint_src_path = util.to_unix_path(pylint_src_path) - # If we're looking for a particular source file, - # ignore any other source files. - if function_name: - error_str = f"{pylint_code}: {function_name}: {message}" - else: - error_str = f"{pylint_code}: {message}" - - violation = Violation(int(line_number), error_str) - violations_dict[pylint_src_path].append(violation) + error_str = f"{pylint_code}: {message}" + + clean_path = util.to_unix_path(pylint_src_path) + violation = Violation(int(line_number), error_str) + violations_dict[clean_path].append(violation) return violations_dict diff --git a/pyproject.toml b/pyproject.toml index 908716cf..e4c70189 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -128,6 +128,7 @@ ignore = [ "D400", # Disables First line should end with a period "D401", # Disables First line of docstring should be in imperative mood "D415", # Disables First line should end with a period, question mark, or exclamation point + "D417", # Disables Missing argument descriptions in the docstring # allow disables (equivalent to "locally-disabled", "suppressed-message") "RUF100", From 3db272253728e0b4a88a450cfc816fd62b64ceff Mon Sep 17 00:00:00 2001 From: Javier Buzzi Date: Tue, 3 Jun 2025 15:29:59 -0400 Subject: [PATCH 23/39] Update diff_cover/git_diff.py --- diff_cover/git_diff.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/diff_cover/git_diff.py b/diff_cover/git_diff.py index 7a626400..8c8da5f2 100644 --- a/diff_cover/git_diff.py +++ b/diff_cover/git_diff.py @@ -110,7 +110,8 @@ def untracked(self): return self._untracked_cache output = execute(["git", "ls-files", "--exclude-standard", "--others"])[0] - return [to_unescaped_filename(line) for line in output.splitlines() if line] + self._untracked_cache =[to_unescaped_filename(line) for line in output.splitlines() if line] + return self._untracked_cache class GitDiffFileTool(GitDiffTool): From ba03bd6e91c6e7b0935c74205b75ecee044917bc Mon Sep 17 00:00:00 2001 From: Javier Buzzi Date: Tue, 3 Jun 2025 15:33:52 -0400 Subject: [PATCH 24/39] Update diff_cover/diff_cover_tool.py --- diff_cover/diff_cover_tool.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/diff_cover/diff_cover_tool.py b/diff_cover/diff_cover_tool.py index e3c3a83b..73053a22 100644 --- a/diff_cover/diff_cover_tool.py +++ b/diff_cover/diff_cover_tool.py @@ -22,10 +22,10 @@ XmlCoverageReporter, ) - -HTML_REPORT_HELP = "Diff coverage HTML output" -JSON_REPORT_HELP = "Diff coverage JSON output" -MARKDOWN_REPORT_HELP = "Diff coverage Markdown output" +FORMAT_HELP = "Format to use" +HTML_REPORT_DEFAULT_PATH = "diff-cover.html" +JSON_REPORT_DEFAULT_PATH = "diff-cover.json" +MARKDOWN_REPORT_DEFAULT_PATH = "diff-cover.md" COMPARE_BRANCH_HELP = "Branch to compare" CSS_FILE_HELP = "Write CSS into an external file" FAIL_UNDER_HELP = ( From bdb3018f6a3b6ce6e9e2f778c844966a47a66cca Mon Sep 17 00:00:00 2001 From: kingbuzzman Date: Tue, 3 Jun 2025 15:37:45 -0400 Subject: [PATCH 25/39] . --- diff_cover/git_diff.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/diff_cover/git_diff.py b/diff_cover/git_diff.py index 8c8da5f2..0185afd1 100644 --- a/diff_cover/git_diff.py +++ b/diff_cover/git_diff.py @@ -110,7 +110,9 @@ def untracked(self): return self._untracked_cache output = execute(["git", "ls-files", "--exclude-standard", "--others"])[0] - self._untracked_cache =[to_unescaped_filename(line) for line in output.splitlines() if line] + self._untracked_cache = [ + to_unescaped_filename(line) for line in output.splitlines() if line + ] return self._untracked_cache From 25a12726e61a912a4dbc946f90d9385b96311311 Mon Sep 17 00:00:00 2001 From: kingbuzzman Date: Tue, 3 Jun 2025 15:52:47 -0400 Subject: [PATCH 26/39] . --- diff_cover/diff_cover_tool.py | 1 + 1 file changed, 1 insertion(+) diff --git a/diff_cover/diff_cover_tool.py b/diff_cover/diff_cover_tool.py index 73053a22..83f6d8be 100644 --- a/diff_cover/diff_cover_tool.py +++ b/diff_cover/diff_cover_tool.py @@ -22,6 +22,7 @@ XmlCoverageReporter, ) + FORMAT_HELP = "Format to use" HTML_REPORT_DEFAULT_PATH = "diff-cover.html" JSON_REPORT_DEFAULT_PATH = "diff-cover.json" From 39de6079211f1379ead4bf8bec98cd3b080fea00 Mon Sep 17 00:00:00 2001 From: kingbuzzman Date: Tue, 3 Jun 2025 16:15:49 -0400 Subject: [PATCH 27/39] . --- diff_cover/git_diff.py | 2 +- diff_cover/violationsreporters/violations_reporter.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/diff_cover/git_diff.py b/diff_cover/git_diff.py index 0185afd1..49020b9b 100644 --- a/diff_cover/git_diff.py +++ b/diff_cover/git_diff.py @@ -128,7 +128,7 @@ def diff_committed(self, compare_branch="origin/main"): Raises a `GitDiffError` if the file cannot be read. """ try: - with open(self.diff_file_path) as file: + with open(self.diff_file_path, encoding="utf-8") as file: return file.read() except OSError as e: error_message = ( diff --git a/diff_cover/violationsreporters/violations_reporter.py b/diff_cover/violationsreporters/violations_reporter.py index c31c5005..057f13dc 100644 --- a/diff_cover/violationsreporters/violations_reporter.py +++ b/diff_cover/violationsreporters/violations_reporter.py @@ -323,7 +323,7 @@ def parse(lcov_file): "VER", } - with open(lcov_file) as lcov: + with open(lcov_file, encoding="utf-8") as lcov: for line in (line for line in (line.strip() for line in lcov) if line): directive, _, content = line.partition(":") From 124a1fbabc97266eb204bba6e40f19c909562424 Mon Sep 17 00:00:00 2001 From: kingbuzzman Date: Sun, 22 Jun 2025 02:38:43 -0400 Subject: [PATCH 28/39] Update --- diff_cover/diff_cover_tool.py | 8 +- diff_cover/diff_quality_tool.py | 108 ++++++++---------- diff_cover/git_diff.py | 1 + .../java_violations_reporter.py | 8 +- tests/fixtures/pylint_violations_report.txt | 6 +- tests/test_clover_violations_reporter.py | 4 +- tests/test_integration.py | 8 +- tests/test_report_generator.py | 7 -- tests/test_util.py | 7 +- tests/test_violations_reporter.py | 42 +++---- 10 files changed, 93 insertions(+), 106 deletions(-) diff --git a/diff_cover/diff_cover_tool.py b/diff_cover/diff_cover_tool.py index 27fdd3c7..e5e0d382 100644 --- a/diff_cover/diff_cover_tool.py +++ b/diff_cover/diff_cover_tool.py @@ -4,7 +4,7 @@ import os import sys import warnings -import xml.etree.ElementTree as etree +import xml.etree.ElementTree as ET from diff_cover import DESCRIPTION, VERSION from diff_cover.config_parser import Tool, get_config @@ -52,7 +52,7 @@ CONFIG_FILE_HELP = "The configuration file to use" DIFF_FILE_HELP = "The diff file to use" -LOGGER = logging.getLogger(__name__) +logger = logging.getLogger(__name__) def format_type(value): @@ -229,7 +229,7 @@ def generate_coverage_report( ) xml_roots = [ - etree.parse(coverage_file) + ET.parse(coverage_file) for coverage_file in coverage_files if coverage_file.endswith(".xml") ] @@ -372,7 +372,7 @@ def main(argv=None, directory=None): if percent_covered >= fail_under: return 0 - LOGGER.error("Failure. Coverage is below %i%%.", fail_under) + logger.error("Failure. Coverage is below %i%%.", fail_under) return 1 diff --git a/diff_cover/diff_quality_tool.py b/diff_cover/diff_quality_tool.py index 5b170953..b6191f4a 100644 --- a/diff_cover/diff_quality_tool.py +++ b/diff_cover/diff_quality_tool.py @@ -6,6 +6,7 @@ import io import logging import os +import pathlib import sys import pluggy @@ -89,7 +90,7 @@ REPORT_ROOT_PATH_HELP = "The root path used to generate a report" -LOGGER = logging.getLogger(__name__) +logger = logging.getLogger(__name__) def parse_quality_args(argv): @@ -321,65 +322,56 @@ def main(argv=None, directory=None): reporter_factory_fn = hookimpl.function break - if reporter or driver or reporter_factory_fn: - input_reports = [] - try: - for path in arg_dict["input_reports"]: - try: - input_reports.append(open(path, "rb")) - except OSError: - LOGGER.error("Could not load report '%s'", path) - return 1 - if driver is not None: - # If we've been given pre-generated reports, - # try to open the files - if arg_dict["report_root_path"]: - driver.add_driver_args( - report_root_path=arg_dict["report_root_path"] - ) - - reporter = QualityReporter(driver, input_reports, user_options) - elif reporter_factory_fn: - reporter = reporter_factory_fn( - reports=input_reports, options=user_options - ) - - percent_passing = generate_quality_report( - reporter, - arg_dict["compare_branch"], - GitDiffTool( - arg_dict["diff_range_notation"], arg_dict["ignore_whitespace"] - ), - report_formats=arg_dict["format"], - css_file=arg_dict["external_css_file"], - ignore_staged=arg_dict["ignore_staged"], - ignore_unstaged=arg_dict["ignore_unstaged"], - include_untracked=arg_dict["include_untracked"], - exclude=arg_dict["exclude"], - include=arg_dict["include"], - quiet=quiet, - ) - if percent_passing >= fail_under: - return 0 - - LOGGER.error("Failure. Quality is below %i.", fail_under) - return 1 - - except ImportError: - LOGGER.error("Quality tool not installed: '%s'", tool) - return 1 - except OSError as exc: - LOGGER.error("Failure: '%s'", str(exc)) - return 1 - # Close any reports we opened - finally: - for file_handle in input_reports: - file_handle.close() - - else: - LOGGER.error("Quality tool not recognized: '%s'", tool) + if not (reporter or driver or reporter_factory_fn): + logger.error("Quality tool not recognized: '%s'", tool) return 1 + input_reports = [] + try: + for path in arg_dict["input_reports"]: + if not pathlib.Path(path).exists(): + logger.exception("Could not load report '%s'", path) + return 1 + if driver is not None: + # If we've been given pre-generated reports, + # try to open the files + if arg_dict["report_root_path"]: + driver.add_driver_args(report_root_path=arg_dict["report_root_path"]) + + reporter = QualityReporter(driver, input_reports, user_options) + elif reporter_factory_fn: + reporter = reporter_factory_fn(reports=input_reports, options=user_options) + + percent_passing = generate_quality_report( + reporter, + arg_dict["compare_branch"], + GitDiffTool(arg_dict["diff_range_notation"], arg_dict["ignore_whitespace"]), + report_formats=arg_dict["format"], + css_file=arg_dict["external_css_file"], + ignore_staged=arg_dict["ignore_staged"], + ignore_unstaged=arg_dict["ignore_unstaged"], + include_untracked=arg_dict["include_untracked"], + exclude=arg_dict["exclude"], + include=arg_dict["include"], + quiet=quiet, + ) + if percent_passing >= fail_under: + return 0 + + logger.error("Failure. Quality is below %i.", fail_under) + return 1 + + except ImportError: + logger.exception("Quality tool not installed: '%s'", tool) + return 1 + except OSError as exc: + logger.exception("Failure: '%s'", str(exc)) + return 1 + # Close any reports we opened + finally: + for file_handle in input_reports: + file_handle.close() + if __name__ == "__main__": sys.exit(main()) diff --git a/diff_cover/git_diff.py b/diff_cover/git_diff.py index 49020b9b..745cceca 100644 --- a/diff_cover/git_diff.py +++ b/diff_cover/git_diff.py @@ -127,6 +127,7 @@ def diff_committed(self, compare_branch="origin/main"): Raises a `GitDiffError` if the file cannot be read. """ + del compare_branch try: with open(self.diff_file_path, encoding="utf-8") as file: return file.read() diff --git a/diff_cover/violationsreporters/java_violations_reporter.py b/diff_cover/violationsreporters/java_violations_reporter.py index 653fcfc9..d8cfb330 100644 --- a/diff_cover/violationsreporters/java_violations_reporter.py +++ b/diff_cover/violationsreporters/java_violations_reporter.py @@ -3,7 +3,7 @@ """ import os -import xml.etree.ElementTree as etree +import xml.etree.ElementTree as ET from collections import defaultdict from diff_cover.command_runner import run_command_for_code @@ -63,7 +63,7 @@ def parse_reports(self, reports): """ violations_dict = defaultdict(list) for report in reports: - xml_document = etree.fromstring("".join(report)) + xml_document = ET.fromstring("".join(report)) files = xml_document.findall(".//file") for file_tree in files: for error in file_tree.findall("error"): @@ -102,7 +102,7 @@ def parse_reports(self, reports): """ violations_dict = defaultdict(list) for report in reports: - xml_document = etree.fromstring("".join(report)) + xml_document = ET.fromstring("".join(report)) bugs = xml_document.findall(".//BugInstance") for bug in bugs: category = bug.get("category") @@ -150,7 +150,7 @@ def parse_reports(self, reports): """ violations_dict = defaultdict(list) for report in reports: - xml_document = etree.fromstring("".join(report)) + xml_document = ET.fromstring("".join(report)) node_files = xml_document.findall(".//file") for node_file in node_files: for error in node_file.findall("violation"): diff --git a/tests/fixtures/pylint_violations_report.txt b/tests/fixtures/pylint_violations_report.txt index e8c59933..c0a65a9a 100644 --- a/tests/fixtures/pylint_violations_report.txt +++ b/tests/fixtures/pylint_violations_report.txt @@ -4,9 +4,9 @@ Quality Report: pylint Diff: origin/main...HEAD, staged and unstaged changes ------------- violations_test_file.py (77.8%): -violations_test_file.py:1: C0111: Missing docstring -violations_test_file.py:1: C0111: func_1: Missing docstring -violations_test_file.py:2: C0322: func_1: Operator not preceded by a space +violations_test_file.py:1: C0114: (missing-module-docstring), : Missing module docstring +violations_test_file.py:1: C0116: (missing-function-docstring), func_1: Missing function or method docstring +violations_test_file.py:11: W0612: (unused-variable), func_2: Unused variable 'unused' ------------- Total: 9 lines Violations: 2 lines diff --git a/tests/test_clover_violations_reporter.py b/tests/test_clover_violations_reporter.py index e79df37a..59fb549f 100644 --- a/tests/test_clover_violations_reporter.py +++ b/tests/test_clover_violations_reporter.py @@ -2,7 +2,7 @@ """Test for diff_cover.violationsreporters - clover""" -import xml.etree.ElementTree as etree +import xml.etree.ElementTree as ET from diff_cover.git_path import GitPathTool from diff_cover.violationsreporters.violations_reporter import XmlCoverageReporter @@ -12,7 +12,7 @@ def test_get_src_path_clover(datadir): GitPathTool._cwd = "/" GitPathTool._root = "/" - clover_report = etree.parse(str(datadir / "test.xml")) + clover_report = ET.parse(str(datadir / "test.xml")) result = XmlCoverageReporter.get_src_path_line_nodes_clover( clover_report, "isLucky.js" ) diff --git a/tests/test_integration.py b/tests/test_integration.py index 1298abb4..e15888f2 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -376,7 +376,7 @@ def test_show_uncovered_lines_console(self, runbin, patch_git_command, capsys): assert runbin(["--show-uncovered", "coverage.xml"]) == 0 compare_console("show_uncovered_lines_console.txt", capsys.readouterr().out) - def test_multiple_lcov_xml_reports(self, runbin, patch_git_command, capsys): + def test_multiple_lcov_xml_reports(self, runbin, patch_git_command): patch_git_command.set_stdout("git_diff_add.txt") with pytest.raises( ValueError, match="Mixing LCov and XML reports is not supported yet" @@ -594,7 +594,7 @@ def test_pylint_report_with_dup_code_violation( def test_tool_not_recognized(self, runbin, patch_git_command, mocker): patch_git_command.set_stdout("git_diff_violations.txt") - logger = mocker.patch("diff_cover.diff_quality_tool.LOGGER") + logger = mocker.patch("diff_cover.diff_quality_tool.logger") assert runbin(["--violations=garbage", "pylint_report.txt"]) == 1 logger.error.assert_called_with("Quality tool not recognized: '%s'", "garbage") @@ -609,9 +609,9 @@ def test_tool_not_installed(self, mocker, runbin, patch_git_command): }, ) patch_git_command.set_stdout("git_diff_add.txt") - logger = mocker.patch("diff_cover.diff_quality_tool.LOGGER") + logger = mocker.patch("diff_cover.diff_quality_tool.logger") assert runbin(["--violations=not_installed"]) == 1 - logger.error.assert_called_with( + logger.exception.assert_called_with( "Failure: '%s'", "not_installed is not installed" ) diff --git a/tests/test_report_generator.py b/tests/test_report_generator.py index 0c56dcdb..1c527682 100644 --- a/tests/test_report_generator.py +++ b/tests/test_report_generator.py @@ -1,6 +1,5 @@ # pylint: disable=attribute-defined-outside-init,not-callable -import copy import json from io import BytesIO from textwrap import dedent @@ -214,7 +213,6 @@ def test_total_percent_covered(self): class TestTemplateReportGenerator(BaseReportGeneratorTest): - @pytest.fixture def report(self, coverage, diff): # Create a concrete instance of a report generator @@ -246,7 +244,6 @@ def test_one_number(self): class TestJsonReportGenerator(BaseReportGeneratorTest): - @pytest.fixture def report(self, coverage, diff): # Create a concrete instance of a report generator @@ -336,7 +333,6 @@ def test_empty_report(self): class TestStringReportGenerator(BaseReportGeneratorTest): - @pytest.fixture def report(self, coverage, diff): # Create a concrete instance of a report generator @@ -408,7 +404,6 @@ def test_empty_report(self): class TestHtmlReportGenerator(BaseReportGeneratorTest): - @pytest.fixture def report(self, coverage, diff): # Create a concrete instance of a report generator @@ -449,7 +444,6 @@ def test_multiple_snippets(self): class TestMarkdownReportGenerator(BaseReportGeneratorTest): - @pytest.fixture def report(self, coverage, diff): # Create a concrete instance of a report generator @@ -541,7 +535,6 @@ def test_multiple_snippets(self): class TestSimpleReportGeneratorWithBatchViolationReporter(BaseReportGeneratorTest): - @pytest.fixture def report(self, coverage, diff): # Create a concrete instance of a report generator diff --git a/tests/test_util.py b/tests/test_util.py index b5185e24..a9f11119 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -60,9 +60,10 @@ def test_open_file_encoding(tmp_path): assert f.encoding == "utf-16" assert f.read() == "café naïve résumé" - with pytest.raises(UnicodeDecodeError): - with util.open_file(tmp_path / "some_file.txt", "r", encoding="utf-8") as f: - f.read() + raise_error = pytest.raises(UnicodeDecodeError) + open_file = util.open_file(tmp_path / "some_file.txt", "r", encoding="utf-8") + with raise_error, open_file as f: + f.read() def test_open_file_encoding_binary(tmp_path): diff --git a/tests/test_violations_reporter.py b/tests/test_violations_reporter.py index 4114cd5f..bd5ae957 100644 --- a/tests/test_violations_reporter.py +++ b/tests/test_violations_reporter.py @@ -6,7 +6,7 @@ import os import subprocess import tempfile -import xml.etree.ElementTree as etree +import xml.etree.ElementTree as ET from io import BytesIO, StringIO from subprocess import Popen from textwrap import dedent @@ -147,7 +147,7 @@ def test_non_python_violations_empty_path(self): In the wild empty sources can happen. See https://github.com/Bachmann1234/diff-cover/issues/88 Best I can tell its mostly irrelevant but I mostly don't want it crashing """ - xml = etree.fromstring( + xml = ET.fromstring( """ @@ -345,29 +345,29 @@ def _coverage_xml(self, file_paths, violations, measured, source_paths=None): This leaves out some attributes of the Cobertura format, but includes all the elements. """ - root = etree.Element("coverage") + root = ET.Element("coverage") if source_paths: - sources = etree.SubElement(root, "sources") + sources = ET.SubElement(root, "sources") for path in source_paths: - source = etree.SubElement(sources, "source") + source = ET.SubElement(sources, "source") source.text = path - packages = etree.SubElement(root, "packages") - classes = etree.SubElement(packages, "classes") + packages = ET.SubElement(root, "packages") + classes = ET.SubElement(packages, "classes") violation_lines = {violation.line for violation in violations} for path in file_paths: - src_node = etree.SubElement(classes, "class") + src_node = ET.SubElement(classes, "class") src_node.set("filename", path) - etree.SubElement(src_node, "methods") - lines_node = etree.SubElement(src_node, "lines") + ET.SubElement(src_node, "methods") + lines_node = ET.SubElement(src_node, "lines") # Create a node for each line in measured for line_num in measured: is_covered = line_num not in violation_lines - line = etree.SubElement(lines_node, "line") + line = ET.SubElement(lines_node, "line") hits = 1 if is_covered else 0 line.set("hits", str(hits)) @@ -563,21 +563,21 @@ def _coverage_xml(self, file_paths, violations, measured): This leaves out some attributes of the Cobertura format, but includes all the elements. """ - root = etree.Element("coverage") + root = ET.Element("coverage") root.set("clover", "4.2.0") - project = etree.SubElement(root, "project") - package = etree.SubElement(project, "package") + project = ET.SubElement(root, "project") + package = ET.SubElement(project, "package") violation_lines = {violation.line for violation in violations} for path in file_paths: - src_node = etree.SubElement(package, "file") + src_node = ET.SubElement(package, "file") src_node.set("path", path) # Create a node for each line in measured for line_num in measured: is_covered = line_num not in violation_lines - line = etree.SubElement(src_node, "line") + line = ET.SubElement(src_node, "line") hits = 1 if is_covered else 0 line.set("count", str(hits)) @@ -774,23 +774,23 @@ def _coverage_xml(self, file_paths, violations, measured): This leaves out some attributes of the Cobertura format, but includes all the elements. """ - root = etree.Element("report") + root = ET.Element("report") root.set("name", "diff-cover") - sessioninfo = etree.SubElement(root, "sessioninfo") + sessioninfo = ET.SubElement(root, "sessioninfo") sessioninfo.set("id", "C13WQ1WFHTEE-83e2bc9b") violation_lines = {violation.line for violation in violations} for path in file_paths: - package = etree.SubElement(root, "package") + package = ET.SubElement(root, "package") package.set("name", os.path.dirname(path)) - src_node = etree.SubElement(package, "sourcefile") + src_node = ET.SubElement(package, "sourcefile") src_node.set("name", os.path.basename(path)) # Create a node for each line in measured for line_num in measured: is_covered = line_num not in violation_lines - line = etree.SubElement(src_node, "line") + line = ET.SubElement(src_node, "line") hits = 1 if is_covered else 0 line.set("ci", str(hits)) From 46c46c220c9e41e7075e371e3a7601aac63c9414 Mon Sep 17 00:00:00 2001 From: kingbuzzman Date: Sun, 22 Jun 2025 02:46:08 -0400 Subject: [PATCH 29/39] FINALLY fixes isort issue that was driving me up a tree --- diff_cover/__init__.py | 1 - diff_cover/config_parser.py | 1 - diff_cover/diff_cover_tool.py | 1 - diff_cover/diff_quality_tool.py | 1 - diff_cover/hook.py | 1 - diff_cover/hookspecs.py | 1 - diff_cover/violationsreporters/base.py | 1 - diff_cover/violationsreporters/java_violations_reporter.py | 1 - pyproject.toml | 6 +++--- tests/helpers.py | 1 - tests/test_config_parser.py | 1 - tests/test_snippets.py | 1 - 12 files changed, 3 insertions(+), 14 deletions(-) diff --git a/diff_cover/__init__.py b/diff_cover/__init__.py index 35c26432..e050c7de 100644 --- a/diff_cover/__init__.py +++ b/diff_cover/__init__.py @@ -1,6 +1,5 @@ from importlib.metadata import version - VERSION = version("diff_cover") DESCRIPTION = "Automatically find diff lines that need test coverage." QUALITY_DESCRIPTION = "Automatically find diff lines with quality violations." diff --git a/diff_cover/config_parser.py b/diff_cover/config_parser.py index da455986..e9281eb4 100644 --- a/diff_cover/config_parser.py +++ b/diff_cover/config_parser.py @@ -1,7 +1,6 @@ import abc import enum - try: import tomli as toml diff --git a/diff_cover/diff_cover_tool.py b/diff_cover/diff_cover_tool.py index e5e0d382..d77e699b 100644 --- a/diff_cover/diff_cover_tool.py +++ b/diff_cover/diff_cover_tool.py @@ -23,7 +23,6 @@ XmlCoverageReporter, ) - FORMAT_HELP = "Format to use" HTML_REPORT_DEFAULT_PATH = "diff-cover.html" JSON_REPORT_DEFAULT_PATH = "diff-cover.json" diff --git a/diff_cover/diff_quality_tool.py b/diff_cover/diff_quality_tool.py index b6191f4a..48c59173 100644 --- a/diff_cover/diff_quality_tool.py +++ b/diff_cover/diff_quality_tool.py @@ -63,7 +63,6 @@ shellcheck_driver, ) - QUALITY_DRIVERS = { "cppcheck": CppcheckDriver(), "pycodestyle": pycodestyle_driver, diff --git a/diff_cover/hook.py b/diff_cover/hook.py index fdd7cc34..1d7dfb39 100644 --- a/diff_cover/hook.py +++ b/diff_cover/hook.py @@ -1,5 +1,4 @@ import pluggy - # Other packages that implement diff_cover plugins use this. hookimpl = pluggy.HookimplMarker("diff_cover") diff --git a/diff_cover/hookspecs.py b/diff_cover/hookspecs.py index e66ef3ad..3a79cc66 100644 --- a/diff_cover/hookspecs.py +++ b/diff_cover/hookspecs.py @@ -1,6 +1,5 @@ import pluggy - hookspec = pluggy.HookspecMarker("diff_cover") diff --git a/diff_cover/violationsreporters/base.py b/diff_cover/violationsreporters/base.py index 1e0d4d55..99dc34a4 100644 --- a/diff_cover/violationsreporters/base.py +++ b/diff_cover/violationsreporters/base.py @@ -7,7 +7,6 @@ from diff_cover.command_runner import execute, run_command_for_code - Violation = namedtuple("Violation", "line, message") diff --git a/diff_cover/violationsreporters/java_violations_reporter.py b/diff_cover/violationsreporters/java_violations_reporter.py index d8cfb330..26f2538f 100644 --- a/diff_cover/violationsreporters/java_violations_reporter.py +++ b/diff_cover/violationsreporters/java_violations_reporter.py @@ -14,7 +14,6 @@ Violation, ) - # Report checkstyle violations. # http://checkstyle.sourceforge.net/apidocs/com/puppycrawl/tools/checkstyle/DefaultLogger.html # https://github.com/checkstyle/checkstyle/blob/master/src/main/java/com/puppycrawl/tools/checkstyle/AuditEventDefaultFormatter.java diff --git a/pyproject.toml b/pyproject.toml index b184edb6..4009953c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -183,9 +183,9 @@ ignore-names = ["i", "e", "ex", "setUp", "tearDown"] ignore-decorators = ["^_"] [tool.ruff.lint.isort] +case-sensitive = false +combine-as-imports = true known-first-party = ["diff_cover", "tests"] section-order = ["future", "standard-library", "third-party", "first-party", "local-folder"] -lines-between-types = 0 -lines-after-imports = 2 -combine-as-imports = true +lines-after-imports = -1 split-on-trailing-comma = false diff --git a/tests/helpers.py b/tests/helpers.py index a7e0259f..fd6ebd11 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -5,7 +5,6 @@ import os.path import random - HUNK_BUFFER = 2 MAX_LINE_LENGTH = 300 LINE_STRINGS = ["test", "+ has a plus sign", "- has a minus sign"] diff --git a/tests/test_config_parser.py b/tests/test_config_parser.py index f7ec8988..eb0e300f 100644 --- a/tests/test_config_parser.py +++ b/tests/test_config_parser.py @@ -3,7 +3,6 @@ from diff_cover import config_parser from diff_cover.config_parser import ParserError, TOMLParser, Tool, get_config - tools = pytest.mark.parametrize("tool", list(Tool)) diff --git a/tests/test_snippets.py b/tests/test_snippets.py index a21bb84c..4f285f2a 100644 --- a/tests/test_snippets.py +++ b/tests/test_snippets.py @@ -11,7 +11,6 @@ from diff_cover.snippets import Snippet from tests.helpers import fixture_path, load_fixture - SRC_TOKENS = [ (Token.Comment, "# Test source"), (Token.Text, "\n"), From ac6fdc9702c7a2c4bc6ee5d6ac9d72237a97c957 Mon Sep 17 00:00:00 2001 From: kingbuzzman Date: Sun, 22 Jun 2025 03:46:53 -0400 Subject: [PATCH 30/39] . --- diff_cover/config_parser.py | 6 ++++-- diff_cover/diff_cover_tool.py | 9 ++++++--- diff_cover/diff_reporter.py | 9 ++++----- pyproject.toml | 11 +++++------ tests/test_config_parser.py | 6 +++--- tests/test_diff_cover_main.py | 2 +- tests/test_diff_cover_tool.py | 2 +- tests/test_diff_reporter.py | 10 +++++----- tests/test_integration.py | 4 +++- verify.sh | 2 +- 10 files changed, 33 insertions(+), 28 deletions(-) diff --git a/diff_cover/config_parser.py b/diff_cover/config_parser.py index e9281eb4..f1b466ef 100644 --- a/diff_cover/config_parser.py +++ b/diff_cover/config_parser.py @@ -53,7 +53,8 @@ def parse(self): config = config.get("tool", {}).get(self._section, {}) if not config: - raise ParserError(f"No 'tool.{self._section}' configuration available") + message = f"No 'tool.{self._section}' configuration available" + raise ParserError(message) return config @@ -67,7 +68,8 @@ def _parse_config_file(file_name, tool): if config: return config - raise ParserError(f"No config parser could handle {file_name}") + message = f"No config parser could handle {file_name}" + raise ParserError(message) def get_config(parser, argv, defaults, tool): diff --git a/diff_cover/diff_cover_tool.py b/diff_cover/diff_cover_tool.py index d77e699b..16d2291d 100644 --- a/diff_cover/diff_cover_tool.py +++ b/diff_cover/diff_cover_tool.py @@ -295,7 +295,8 @@ def handle_old_format(description, argv): ) warnings.warn( "The --html-report option is deprecated. " - f"Use --format html:{known_args.html_report} instead." + f"Use --format html:{known_args.html_report} instead.", + stacklevel=1, ) format_["html"] = known_args.html_report if known_args.json_report: @@ -305,7 +306,8 @@ def handle_old_format(description, argv): ) warnings.warn( "The --json-report option is deprecated. " - f"Use --format json:{known_args.json_report} instead." + f"Use --format json:{known_args.json_report} instead.", + stacklevel=1, ) format_["json"] = known_args.json_report if known_args.markdown_report: @@ -315,7 +317,8 @@ def handle_old_format(description, argv): ) warnings.warn( "The --markdown-report option is deprecated. " - f"Use --format markdown:{known_args.markdown_report} instead." + f"Use --format markdown:{known_args.markdown_report} instead.", + stacklevel=1, ) format_["markdown"] = known_args.markdown_report if format_: diff --git a/diff_cover/diff_reporter.py b/diff_cover/diff_reporter.py index 4b50c8f7..59fcda62 100644 --- a/diff_cover/diff_reporter.py +++ b/diff_cover/diff_reporter.py @@ -271,10 +271,9 @@ def _validate_path_to_diff(self, src_path: str) -> bool: _, extension = os.path.splitext(src_path) extension = extension[1:].lower() - if self._supported_extensions and extension not in self._supported_extensions: - return False - - return True + return not ( + self._supported_extensions and extension not in self._supported_extensions + ) # Regular expressions used to parse the diff output SRC_FILE_RE = re.compile(r'^diff --git "?a/.*"? "?b/([^\n"]*)"?') @@ -328,7 +327,7 @@ def _parse_source_sections(self, diff_str): # If the line starts with "diff --git" # or "diff --cc" (in the case of a merge conflict) # then it is the start of a new source file - if line.startswith("diff --git") or line.startswith("diff --cc"): + if line.startswith(("diff --git", "diff --cc")): # Retrieve the name of the source file src_path = self._parse_source_line(line) diff --git a/pyproject.toml b/pyproject.toml index 4009953c..4b3a948d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -122,10 +122,13 @@ ignore = [ "ANN", # allow TODO comments (equivalent to "fixme") "FIX", + "TD", "D200", # Disables One-line docstring should fit on one line + "D203", "D205", # Disables 1 blank line required between summary line and description "D212", # Disables Multi-line docstring summary should start at the first line + "D213", "D400", # Disables First line should end with a period "D401", # Disables First line of docstring should be in imperative mood "D415", # Disables First line should end with a period, question mark, or exclamation point @@ -134,9 +137,6 @@ ignore = [ # allow disables (equivalent to "locally-disabled", "suppressed-message") "RUF100", - # covered by isort (equivalent to "ungrouped-imports") - "I", - # allow classes and functions w/o docstring (equivalent to "missing-docstring") "D1", @@ -155,9 +155,8 @@ ignore = [ # we are a command line tool (equivalent to "raise-missing-from") "B904", - # Resolve incompatible docstring rules - "D203", # Keep D211 (no-blank-line-before-class) - "D213", # Keep D212 (multi-line-summary-first-line) + "E501", # Line too long + "PTH", # Avoid formatter conflicts "COM812", diff --git a/tests/test_config_parser.py b/tests/test_config_parser.py index eb0e300f..3be4d758 100644 --- a/tests/test_config_parser.py +++ b/tests/test_config_parser.py @@ -20,7 +20,7 @@ def test_parse_but_no_tomli_installed(self, tool, mocker): parser.parse() @pytest.mark.parametrize( - "tool,content", + ("tool", "content"), [ (Tool.DIFF_COVER, ""), (Tool.DIFF_COVER, "[tool.diff_quality]"), @@ -37,7 +37,7 @@ def test_parse_but_no_data(self, tool, content, tmp_path): parser.parse() @pytest.mark.parametrize( - "tool,content,expected", + ("tool", "content", "expected"), [ (Tool.DIFF_COVER, "[tool.diff_cover]\nquiet=true", {"quiet": True}), (Tool.DIFF_QUALITY, "[tool.diff_quality]\nquiet=true", {"quiet": True}), @@ -60,7 +60,7 @@ def test_get_config_unrecognized_file(mocker, tool): @pytest.mark.parametrize( - "tool,cli_config,defaults,file_content,expected", + ("tool", "cli_config", "defaults", "file_content", "expected"), [ ( Tool.DIFF_COVER, diff --git a/tests/test_diff_cover_main.py b/tests/test_diff_cover_main.py index 262d8fc0..9c1fc097 100644 --- a/tests/test_diff_cover_main.py +++ b/tests/test_diff_cover_main.py @@ -25,8 +25,8 @@ def test_parse_range_notation(capsys): assert arg_dict["coverage_files"] == ["build/tests/coverage.xml"] assert arg_dict["diff_range_notation"] == ".." + argv = ["build/tests/coverage.xml", "--diff-range-notation=FOO"] with pytest.raises(SystemExit) as e: - argv = ["build/tests/coverage.xml", "--diff-range-notation=FOO"] parse_coverage_args(argv) assert e.value.code == 2 diff --git a/tests/test_diff_cover_tool.py b/tests/test_diff_cover_tool.py index c06e158e..f9a63326 100644 --- a/tests/test_diff_cover_tool.py +++ b/tests/test_diff_cover_tool.py @@ -91,7 +91,7 @@ def test_parse_with_multiple_old_reports(recwarn): ), ], ) -def test_parse_mixing_new_with_old_reports(recwarn, old_report, expected_error): +def test_parse_mixing_new_with_old_reports(old_report, expected_error): argv = [ "reports/coverage.xml", *old_report, diff --git a/tests/test_diff_reporter.py b/tests/test_diff_reporter.py index 08e5367f..2627afc6 100644 --- a/tests/test_diff_reporter.py +++ b/tests/test_diff_reporter.py @@ -75,7 +75,7 @@ def test_name_include_untracked(git_diff): @pytest.mark.parametrize( - "include,exclude,expected", + ("include", "exclude", "expected"), [ # no include/exclude --> use all paths ([], [], ["file3.py", "README.md", "subdir1/file1.py", "subdir2/file2.py"]), @@ -124,7 +124,7 @@ def test_git_path_selection(diff, git_diff, include, exclude, expected): {"subdir1/file1.py": line_numbers(3, 10) + line_numbers(34, 47)} ), git_diff_output({"subdir2/file2.py": line_numbers(3, 10), "file3.py": [0]}), - git_diff_output(dict(), deleted_files=["README.md"]), + git_diff_output({}, deleted_files=["README.md"]), ) # Get the source paths in the diff @@ -148,7 +148,7 @@ def test_git_source_paths(diff, git_diff): {"subdir/file1.py": line_numbers(3, 10) + line_numbers(34, 47)} ), git_diff_output({"subdir/file2.py": line_numbers(3, 10), "file3.py": [0]}), - git_diff_output(dict(), deleted_files=["README.md"]), + git_diff_output({}, deleted_files=["README.md"]), ) # Get the source paths in the diff @@ -225,7 +225,7 @@ def test_git_lines_changed(diff, git_diff): {"subdir/file1.py": line_numbers(3, 10) + line_numbers(34, 47)} ), git_diff_output({"subdir/file2.py": line_numbers(3, 10), "file3.py": [0]}), - git_diff_output(dict(), deleted_files=["README.md"]), + git_diff_output({}, deleted_files=["README.md"]), ) # Get the lines changed in the diff @@ -286,7 +286,7 @@ def test_git_deleted_lines(diff, git_diff): {"subdir/file1.py": line_numbers(3, 10) + line_numbers(34, 47)} ), git_diff_output({"subdir/file2.py": line_numbers(3, 10), "file3.py": [0]}), - git_diff_output(dict(), deleted_files=["README.md"]), + git_diff_output({}, deleted_files=["README.md"]), ) # Get the lines changed in the diff diff --git a/tests/test_integration.py b/tests/test_integration.py index e15888f2..63c61dbe 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -69,6 +69,8 @@ def set_stderr(self, value): def set_returncode(self, value): self.returncode = value + helper = Wrapper() + def patch_diff(command, **kwargs): if command[0:6] == [ "git", @@ -91,7 +93,6 @@ def patch_diff(command, **kwargs): return Popen(command, **kwargs) patch_popen.side_effect = patch_diff - helper = Wrapper() return helper @@ -633,6 +634,7 @@ class DoNothingDriver(QualityDriver): """Dummy class that implements necessary abstract functions.""" def parse_reports(self, reports): + del reports return defaultdict(list) def installed(self): diff --git a/verify.sh b/verify.sh index 249d7f62..d8419ab7 100755 --- a/verify.sh +++ b/verify.sh @@ -4,7 +4,7 @@ IFS=$'\n\t' COMPARE_BRANCH=${COMPARE_BRANCH:-origin/main} ruff format --check -ruff check --select I +ruff check --extend-select I python -m pytest -n auto --cov-context test --cov --cov-report=xml tests git fetch origin main:refs/remotes/origin/main diff-cover --version From a6f1f827b2d7a6535cd9d55f2e8c407484f4c1d2 Mon Sep 17 00:00:00 2001 From: kingbuzzman Date: Sun, 22 Jun 2025 04:53:09 -0400 Subject: [PATCH 31/39] Fixes tests --- diff_cover/diff_quality_tool.py | 3 ++- tests/fixtures/pylint_violations_report.txt | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/diff_cover/diff_quality_tool.py b/diff_cover/diff_quality_tool.py index 48c59173..8adc67e6 100644 --- a/diff_cover/diff_quality_tool.py +++ b/diff_cover/diff_quality_tool.py @@ -329,8 +329,9 @@ def main(argv=None, directory=None): try: for path in arg_dict["input_reports"]: if not pathlib.Path(path).exists(): - logger.exception("Could not load report '%s'", path) + logger.error("Could not load report '%s'", path) return 1 + input_reports.append(open(path, "rb")) if driver is not None: # If we've been given pre-generated reports, # try to open the files diff --git a/tests/fixtures/pylint_violations_report.txt b/tests/fixtures/pylint_violations_report.txt index c0a65a9a..e8c59933 100644 --- a/tests/fixtures/pylint_violations_report.txt +++ b/tests/fixtures/pylint_violations_report.txt @@ -4,9 +4,9 @@ Quality Report: pylint Diff: origin/main...HEAD, staged and unstaged changes ------------- violations_test_file.py (77.8%): -violations_test_file.py:1: C0114: (missing-module-docstring), : Missing module docstring -violations_test_file.py:1: C0116: (missing-function-docstring), func_1: Missing function or method docstring -violations_test_file.py:11: W0612: (unused-variable), func_2: Unused variable 'unused' +violations_test_file.py:1: C0111: Missing docstring +violations_test_file.py:1: C0111: func_1: Missing docstring +violations_test_file.py:2: C0322: func_1: Operator not preceded by a space ------------- Total: 9 lines Violations: 2 lines From 72616696407c74dabaadf465df241d86a4d518b3 Mon Sep 17 00:00:00 2001 From: kingbuzzman Date: Sun, 22 Jun 2025 05:01:59 -0400 Subject: [PATCH 32/39] . --- diff_cover/config_parser.py | 3 ++- diff_cover/report_generator.py | 5 +---- tests/test_diff_reporter.py | 4 ++-- tests/test_git_diff.py | 4 ++-- 4 files changed, 7 insertions(+), 9 deletions(-) diff --git a/diff_cover/config_parser.py b/diff_cover/config_parser.py index f1b466ef..5cd2c0cf 100644 --- a/diff_cover/config_parser.py +++ b/diff_cover/config_parser.py @@ -46,7 +46,8 @@ def parse(self): return None if not _HAS_TOML: - raise ParserError("No Toml lib installed") + msg = "No Toml lib installed" + raise ParserError(msg) with open(self._file_name, "rb") as file_handle: config = toml.load(file_handle) diff --git a/diff_cover/report_generator.py b/diff_cover/report_generator.py index d3d96b67..6352688f 100644 --- a/diff_cover/report_generator.py +++ b/diff_cover/report_generator.py @@ -337,10 +337,7 @@ def _context(self): """ # Include snippet style info if we're displaying # source code snippets - if self.include_snippets: - snippet_style = Snippet.style_defs() - else: - snippet_style = None + snippet_style = Snippet.style_defs() if self.include_snippets else None context = super().report_dict() context.update({"css_url": self.css_url, "snippet_style": snippet_style}) diff --git a/tests/test_diff_reporter.py b/tests/test_diff_reporter.py index 2627afc6..cac637df 100644 --- a/tests/test_diff_reporter.py +++ b/tests/test_diff_reporter.py @@ -239,7 +239,7 @@ def test_ignore_lines_outside_src(diff, git_diff): # Add some lines at the start of the diff, before any # source files are specified diff_output = git_diff_output({"subdir/file1.py": line_numbers(3, 10)}) - main_diff = "\n".join(["- deleted line", "+ added line", diff_output]) + main_diff = f"- deleted line\n+ added line\n{diff_output}" # Configure the git diff output _set_git_diff_output(diff, git_diff, main_diff, "", "") @@ -645,7 +645,7 @@ def open_side_effect(*args, **kwargs): @pytest.mark.parametrize( - "excluded, supported_extensions, path", + ("excluded", "supported_extensions", "path"), [ (["file.bin"], ["py"], "file.bin"), ([], ["py"], "file.bin"), diff --git a/tests/test_git_diff.py b/tests/test_git_diff.py index dec8e02e..811f85ad 100644 --- a/tests/test_git_diff.py +++ b/tests/test_git_diff.py @@ -128,7 +128,7 @@ def test_diff_staged(tool, subprocess, set_git_diff_output): ) -def test_diff_missing_branch_error(set_git_diff_output, tool, subprocess): +def test_diff_missing_branch_error(set_git_diff_output, tool): # Override the default compare branch set_git_diff_output("test output", "fatal error", 1) with pytest.raises(CommandError): @@ -184,7 +184,7 @@ def test_errors(set_git_diff_output, tool): @pytest.mark.parametrize( - "output,expected", + ("output", "expected"), [ ("", []), ("\n", []), From bb53e060084f796ee209849aa481f44341d7d3a3 Mon Sep 17 00:00:00 2001 From: kingbuzzman Date: Sun, 22 Jun 2025 05:16:12 -0400 Subject: [PATCH 33/39] Fixes bug modifying GitPathTool object --- diff_cover/diff_quality_tool.py | 7 +++---- pyproject.toml | 2 ++ tests/test_clover_violations_reporter.py | 7 ++++--- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/diff_cover/diff_quality_tool.py b/diff_cover/diff_quality_tool.py index 8adc67e6..c7771ae8 100644 --- a/diff_cover/diff_quality_tool.py +++ b/diff_cover/diff_quality_tool.py @@ -357,16 +357,15 @@ def main(argv=None, directory=None): ) if percent_passing >= fail_under: return 0 - - logger.error("Failure. Quality is below %i.", fail_under) - return 1 - except ImportError: logger.exception("Quality tool not installed: '%s'", tool) return 1 except OSError as exc: logger.exception("Failure: '%s'", str(exc)) return 1 + else: + logger.error("Failure. Quality is below %i.", fail_under) + return 1 # Close any reports we opened finally: for file_handle in input_reports: diff --git a/pyproject.toml b/pyproject.toml index 4b3a948d..3a32827c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -175,6 +175,8 @@ ignore-names = ["i", "e", "ex", "setUp", "tearDown"] "PLR2004", # Disables Magic value "PT011", # Disables Exception is too broad "RUF012", # Disables Mutable class attributes should be annotated with typing.ClassVar + "S314", # Disables xml security checks + "SLF001", # Disables complains about accessing private members/methods ] [tool.ruff.lint.pydocstyle] diff --git a/tests/test_clover_violations_reporter.py b/tests/test_clover_violations_reporter.py index 59fb549f..431f73d3 100644 --- a/tests/test_clover_violations_reporter.py +++ b/tests/test_clover_violations_reporter.py @@ -9,9 +9,10 @@ # https://github.com/Bachmann1234/diff_cover/issues/190 -def test_get_src_path_clover(datadir): - GitPathTool._cwd = "/" - GitPathTool._root = "/" +def test_get_src_path_clover(datadir, monkeypatch): + monkeypatch.setattr(GitPathTool, "_cwd", "/") + monkeypatch.setattr(GitPathTool, "_root", "/") + clover_report = ET.parse(str(datadir / "test.xml")) result = XmlCoverageReporter.get_src_path_line_nodes_clover( clover_report, "isLucky.js" From 217f3afa8710281f5ee23e6df6f4f527a6b0869d Mon Sep 17 00:00:00 2001 From: kingbuzzman Date: Sun, 22 Jun 2025 05:28:07 -0400 Subject: [PATCH 34/39] . --- diff_cover/snippets.py | 43 ++++++++++-------------- tests/test_clover_violations_reporter.py | 2 +- 2 files changed, 19 insertions(+), 26 deletions(-) diff --git a/diff_cover/snippets.py b/diff_cover/snippets.py index 613d7c08..c0e7f24e 100644 --- a/diff_cover/snippets.py +++ b/diff_cover/snippets.py @@ -117,32 +117,25 @@ def markdown(self): Return a Markdown representation of the snippet using Markdown fenced code blocks. See https://github.github.com/gfm/#fenced-code-blocks. """ - line_number_length = len(str(self._last_line)) - - text = "" - for i, line in enumerate(self.text().splitlines(), start=self._start_line): - if i > self._start_line: - text += "\n" - - notice = " " - if i in self._violation_lines: - notice = "!" - - format_string = "{} {:>" + str(line_number_length) + "} {}" - text += format_string.format(notice, i, line) - - header = "Lines %d-%d\n\n" % (self._start_line, self._last_line) - if self._lexer_name in self.LEXER_TO_MARKDOWN_CODE_HINT: - return header + ( - "```" - + self.LEXER_TO_MARKDOWN_CODE_HINT[self._lexer_name] - + "\n" - + text - + "\n```\n" - ) + line_no_width = len(str(self._last_line)) + + # Build each formatted line of the snippet, highlighting violations with '!'. + formatted_lines: list[str] = [] + for line_no, source_line in enumerate(self.text().splitlines(), start=self._start_line): + marker = "!" if line_no in self._violation_lines else " " + formatted_lines.append(f"{marker} {line_no:>{line_no_width}} {source_line}") + + body = "\n".join(formatted_lines) + + # Prefer a syntax-highlighted fenced block when we know the language. + code_hint = self.LEXER_TO_MARKDOWN_CODE_HINT.get(self._lexer_name) + + if code_hint: + header = f"Lines {self._start_line}-{self._last_line}\n\n" + return f"{header}```{code_hint}\n{body}\n```\n" - # unknown programming language, return a non-decorated fenced code block: - return "```\n" + text + "\n```\n" + # Fallback: plain fenced code block with no language hint. + return f"```\n{body}\n```\n" def terminal(self): """ diff --git a/tests/test_clover_violations_reporter.py b/tests/test_clover_violations_reporter.py index 431f73d3..19e8d8b4 100644 --- a/tests/test_clover_violations_reporter.py +++ b/tests/test_clover_violations_reporter.py @@ -13,7 +13,7 @@ def test_get_src_path_clover(datadir, monkeypatch): monkeypatch.setattr(GitPathTool, "_cwd", "/") monkeypatch.setattr(GitPathTool, "_root", "/") - clover_report = ET.parse(str(datadir / "test.xml")) + clover_report = ET.parse(datadir / "test.xml") result = XmlCoverageReporter.get_src_path_line_nodes_clover( clover_report, "isLucky.js" ) From 25af280bb1c132e3a2e3ed939a0a8b625bcacd8e Mon Sep 17 00:00:00 2001 From: kingbuzzman Date: Sun, 22 Jun 2025 05:30:00 -0400 Subject: [PATCH 35/39] . --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 3d64770f..3c802eb6 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,7 @@ lib lib64 __pycache__ .pytest_cache +.ruff_cache # Installer logs pip-log.txt @@ -34,6 +35,7 @@ pip-log.txt .tox /coverage.xml /report.html +htmlcov/ # Translations *.mo From 66f22b295a696a8e18fb41521ade3a284b9c0309 Mon Sep 17 00:00:00 2001 From: kingbuzzman Date: Sun, 22 Jun 2025 06:04:47 -0400 Subject: [PATCH 36/39] . --- diff_cover/diff_quality_tool.py | 4 ++-- diff_cover/snippets.py | 4 +++- pyproject.toml | 3 +++ tests/test_config_parser.py | 2 +- tests/test_integration.py | 11 ++++++----- 5 files changed, 15 insertions(+), 9 deletions(-) diff --git a/diff_cover/diff_quality_tool.py b/diff_cover/diff_quality_tool.py index c7771ae8..1826cd66 100644 --- a/diff_cover/diff_quality_tool.py +++ b/diff_cover/diff_quality_tool.py @@ -360,8 +360,8 @@ def main(argv=None, directory=None): except ImportError: logger.exception("Quality tool not installed: '%s'", tool) return 1 - except OSError as exc: - logger.exception("Failure: '%s'", str(exc)) + except OSError: + logger.exception("Failure") return 1 else: logger.error("Failure. Quality is below %i.", fail_under) diff --git a/diff_cover/snippets.py b/diff_cover/snippets.py index c0e7f24e..a6139413 100644 --- a/diff_cover/snippets.py +++ b/diff_cover/snippets.py @@ -121,7 +121,9 @@ def markdown(self): # Build each formatted line of the snippet, highlighting violations with '!'. formatted_lines: list[str] = [] - for line_no, source_line in enumerate(self.text().splitlines(), start=self._start_line): + for line_no, source_line in enumerate( + self.text().splitlines(), start=self._start_line + ): marker = "!" if line_no in self._violation_lines else " " formatted_lines.append(f"{marker} {line_no:>{line_no_width}} {source_line}") diff --git a/pyproject.toml b/pyproject.toml index 3a32827c..c189a47c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -160,6 +160,8 @@ ignore = [ # Avoid formatter conflicts "COM812", + + "PYI024", # Disables complains about unused type hints ] # The equivalent of pylint's good-names @@ -177,6 +179,7 @@ ignore-names = ["i", "e", "ex", "setUp", "tearDown"] "RUF012", # Disables Mutable class attributes should be annotated with typing.ClassVar "S314", # Disables xml security checks "SLF001", # Disables complains about accessing private members/methods + "RUF001", # Disables complains about greek letters ] [tool.ruff.lint.pydocstyle] diff --git a/tests/test_config_parser.py b/tests/test_config_parser.py index 3be4d758..2ebca56d 100644 --- a/tests/test_config_parser.py +++ b/tests/test_config_parser.py @@ -14,7 +14,7 @@ def test_parse_no_toml_file(self, tool): @tools def test_parse_but_no_tomli_installed(self, tool, mocker): - mocker.patch.object(config_parser, "_HAS_TOML", False) + mocker.patch.object(config_parser, "_HAS_TOML", new=False) parser = TOMLParser("myfile.toml", tool) with pytest.raises(ParserError): parser.parse() diff --git a/tests/test_integration.py b/tests/test_integration.py index 63c61dbe..37a65644 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -4,6 +4,7 @@ """High-level integration tests of diff-cover tool.""" import json +import logging import os import os.path import re @@ -599,7 +600,7 @@ def test_tool_not_recognized(self, runbin, patch_git_command, mocker): assert runbin(["--violations=garbage", "pylint_report.txt"]) == 1 logger.error.assert_called_with("Quality tool not recognized: '%s'", "garbage") - def test_tool_not_installed(self, mocker, runbin, patch_git_command): + def test_tool_not_installed(self, mocker, runbin, patch_git_command, caplog): # Pretend we support a tool named not_installed mocker.patch.dict( diff_quality_tool.QUALITY_DRIVERS, @@ -610,11 +611,11 @@ def test_tool_not_installed(self, mocker, runbin, patch_git_command): }, ) patch_git_command.set_stdout("git_diff_add.txt") - logger = mocker.patch("diff_cover.diff_quality_tool.logger") assert runbin(["--violations=not_installed"]) == 1 - logger.exception.assert_called_with( - "Failure: '%s'", "not_installed is not installed" - ) + assert caplog.record_tuples == [ + ("diff_cover.diff_quality_tool", logging.ERROR, "Failure") + ] + assert "not_installed is not installed" in caplog.text def test_do_nothing_reporter(self): # Pedantic, but really. This reporter From ed7c7d50d95d303a0bd49ce595ec11aebcd06d22 Mon Sep 17 00:00:00 2001 From: kingbuzzman Date: Sun, 22 Jun 2025 12:33:13 -0400 Subject: [PATCH 37/39] More fixes --- diff_cover/diff_quality_tool.py | 1 - diff_cover/git_diff.py | 2 +- diff_cover/snippets.py | 12 ++++++------ diff_cover/util.py | 2 +- pyproject.toml | 1 + tests/test_diff_reporter.py | 9 ++++----- 6 files changed, 13 insertions(+), 14 deletions(-) diff --git a/diff_cover/diff_quality_tool.py b/diff_cover/diff_quality_tool.py index 1826cd66..20c446cf 100644 --- a/diff_cover/diff_quality_tool.py +++ b/diff_cover/diff_quality_tool.py @@ -88,7 +88,6 @@ INCLUDE_HELP = "Files to include (glob pattern)" REPORT_ROOT_PATH_HELP = "The root path used to generate a report" - logger = logging.getLogger(__name__) diff --git a/diff_cover/git_diff.py b/diff_cover/git_diff.py index 745cceca..594adf3b 100644 --- a/diff_cover/git_diff.py +++ b/diff_cover/git_diff.py @@ -119,7 +119,7 @@ def untracked(self): class GitDiffFileTool(GitDiffTool): def __init__(self, diff_file_path): self.diff_file_path = diff_file_path - super().__init__("...", False) + super().__init__(range_notation="...", ignore_whitespace=False) def diff_committed(self, compare_branch="origin/main"): """ diff --git a/diff_cover/snippets.py b/diff_cover/snippets.py index a6139413..2c6cd0eb 100644 --- a/diff_cover/snippets.py +++ b/diff_cover/snippets.py @@ -36,11 +36,11 @@ class Snippet: # See https://github.com/github/linguist/blob/master/lib/linguist/languages.yml # for typical values of accepted programming language hints in Markdown code fenced blocks - LEXER_TO_MARKDOWN_CODE_HINT = { - "Python": "python", - "C++": "cpp", + LEXER_TO_MARKDOWN_CODE_HINT = ( + ("Python", "python"), + ("C++", "cpp"), # TODO: expand this list... - } + ) def __init__( self, @@ -130,7 +130,7 @@ def markdown(self): body = "\n".join(formatted_lines) # Prefer a syntax-highlighted fenced block when we know the language. - code_hint = self.LEXER_TO_MARKDOWN_CODE_HINT.get(self._lexer_name) + code_hint = dict(self.LEXER_TO_MARKDOWN_CODE_HINT).get(self._lexer_name) if code_hint: header = f"Lines {self._start_line}-{self._last_line}\n\n" @@ -214,7 +214,7 @@ def load_contents(cls, src_path): # We failed to decode the file. # if this is happening a lot I should just bite the bullet # and write a parameter to let people list their file encodings - print( + print( # noqa: T201 "Warning: I was not able to decode your src file. " "I can continue but code snippets in the final report may look wrong" ) diff --git a/diff_cover/util.py b/diff_cover/util.py index b608812a..84c22778 100644 --- a/diff_cover/util.py +++ b/diff_cover/util.py @@ -33,7 +33,7 @@ def open_file(path, mode, encoding="utf-8"): def to_unix_path(path): - """ + r""" Tries to ensure tha the path is a normalized unix path. This seems to be the solution cobertura used.... https://github.com/cobertura/cobertura/blob/642a46eb17e14f51272c6962e64e56e0960918af/cobertura/src/main/java/net/sourceforge/cobertura/instrument/ClassPattern.java#L84 diff --git a/pyproject.toml b/pyproject.toml index c189a47c..317fa129 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -174,6 +174,7 @@ ignore-names = ["i", "e", "ex", "setUp", "tearDown"] [tool.ruff.lint.per-file-ignores] "tests/*" = [ "S101", # Disables Assert statements + "S311", # Disables random checks "PLR2004", # Disables Magic value "PT011", # Disables Exception is too broad "RUF012", # Disables Mutable class attributes should be annotated with typing.ClassVar diff --git a/tests/test_diff_reporter.py b/tests/test_diff_reporter.py index cac637df..6515d2fe 100644 --- a/tests/test_diff_reporter.py +++ b/tests/test_diff_reporter.py @@ -6,7 +6,6 @@ import tempfile from pathlib import Path from textwrap import dedent -from unittest.mock import patch import pytest @@ -97,7 +96,7 @@ def test_name_include_untracked(git_diff): ), ], ) -def test_git_path_selection(diff, git_diff, include, exclude, expected): +def test_git_path_selection(diff, git_diff, include, exclude, expected, monkeypatch): old_cwd = os.getcwd() with tempfile.TemporaryDirectory() as tmp_dir: # change the working directory into the temp directory so that globs are working @@ -128,8 +127,8 @@ def test_git_path_selection(diff, git_diff, include, exclude, expected): ) # Get the source paths in the diff - with patch.object(os.path, "abspath", lambda path: f"{tmp_dir}/{path}"): - source_paths = diff.src_paths_changed() + monkeypatch.setattr(os.path, "abspath", lambda path: f"{tmp_dir}/{path}") + source_paths = diff.src_paths_changed() # Validate the source paths # They should be in alphabetical order @@ -630,7 +629,7 @@ def open_side_effect(*args, **kwargs): nonlocal raise_count raise_count += 1 - raise UnicodeDecodeError("utf-8", b"", 0, 1, "invalid start byte") + raise UnicodeDecodeError("utf-8", b"", 0, 1, "invalid start byte") # noqa: EM101 return base_open_mock(*args, **kwargs) mocker.patch("diff_cover.diff_reporter.open", open_side_effect) From f5cce2b087809c44a79d2a773c9128e1659daf51 Mon Sep 17 00:00:00 2001 From: kingbuzzman Date: Sun, 22 Jun 2025 12:44:34 -0400 Subject: [PATCH 38/39] Nicer --- diff_cover/violationsreporters/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/diff_cover/violationsreporters/base.py b/diff_cover/violationsreporters/base.py index 99dc34a4..c61cc5d3 100644 --- a/diff_cover/violationsreporters/base.py +++ b/diff_cover/violationsreporters/base.py @@ -131,7 +131,7 @@ def __init__(self, driver, reports=None, options=None): """ super().__init__(driver.name) - self.reports = self._load_reports(reports) if reports else None + self.reports = self._load_reports(reports or []) self.violations_dict = defaultdict(list) self.driver = driver self.options = options From eb257b8555c064dbfa94fc1d7522da6cc62b0ce7 Mon Sep 17 00:00:00 2001 From: kingbuzzman Date: Sun, 22 Jun 2025 12:58:11 -0400 Subject: [PATCH 39/39] . --- diff_cover/diff_quality_tool.py | 4 +-- diff_cover/diff_reporter.py | 35 ++++++++++++-------------- diff_cover/violationsreporters/base.py | 14 +---------- 3 files changed, 19 insertions(+), 34 deletions(-) diff --git a/diff_cover/diff_quality_tool.py b/diff_cover/diff_quality_tool.py index 20c446cf..cc6ee17f 100644 --- a/diff_cover/diff_quality_tool.py +++ b/diff_cover/diff_quality_tool.py @@ -80,8 +80,8 @@ "shellcheck": shellcheck_driver, } -VIOLATION_CMD_HELP = "Which code quality tool to use (%s)" % "/".join( - sorted(QUALITY_DRIVERS) +VIOLATION_CMD_HELP = "Which code quality tool to use ({})".format( + "/".join(sorted(QUALITY_DRIVERS)) ) INPUT_REPORTS_HELP = "Which violations reports to use" OPTIONS_HELP = "Options to be passed to the violations tool" diff --git a/diff_cover/diff_reporter.py b/diff_cover/diff_reporter.py index 59fcda62..def286d5 100644 --- a/diff_cover/diff_reporter.py +++ b/diff_cover/diff_reporter.py @@ -436,11 +436,11 @@ def _parse_source_line(self, line): # Parse for the source file path groups = regex.findall(line) - if len(groups) == 1: - return groups[0] + if len(groups) != 1: + msg = f"Could not parse source path in line '{line}'" + raise GitDiffError(msg) - msg = f"Could not parse source path in line '{line}'" - raise GitDiffError(msg) + return groups[0] def _parse_hunk_line(self, line): """ @@ -465,24 +465,21 @@ def _parse_hunk_line(self, line): # the line starts with '@@'. The second component should # be the hunk information, and any additional components # are excerpts from the code. - if len(components) >= 2: - hunk_info = components[1] - groups = self.HUNK_LINE_RE.findall(hunk_info) - - if len(groups) == 1: - try: - return int(groups[0]) - - except ValueError as e: - msg = f"Could not parse '{groups[0]}' as a line number" - raise GitDiffError(msg) from e + if len(components) <= 1: + msg = f"Could not parse hunk in line '{line}'" + raise GitDiffError(msg) - else: - msg = f"Could not find start of hunk in line '{line}'" - raise GitDiffError(msg) + hunk_info = components[1] + groups = self.HUNK_LINE_RE.findall(hunk_info) + if len(groups) == 1: + try: + return int(groups[0]) + except ValueError as e: + msg = f"Could not parse '{groups[0]}' as a line number" + raise GitDiffError(msg) from e else: - msg = f"Could not parse hunk in line '{line}'" + msg = f"Could not find start of hunk in line '{line}'" raise GitDiffError(msg) @staticmethod diff --git a/diff_cover/violationsreporters/base.py b/diff_cover/violationsreporters/base.py index c61cc5d3..cb464af4 100644 --- a/diff_cover/violationsreporters/base.py +++ b/diff_cover/violationsreporters/base.py @@ -131,24 +131,12 @@ def __init__(self, driver, reports=None, options=None): """ super().__init__(driver.name) - self.reports = self._load_reports(reports or []) + self.reports = [fh.read().decode("utf-8", "replace") for fh in reports or []] self.violations_dict = defaultdict(list) self.driver = driver self.options = options self.driver_tool_installed = None - def _load_reports(self, report_files): - """ - Args: - report_files: list[file] reports to read in - - """ - contents = [] - for file_handle in report_files: - # Convert to unicode, replacing unreadable chars - contents.append(file_handle.read().decode("utf-8", "replace")) - return contents - def violations(self, src_path): """ Return a list of Violations recorded in `src_path`.