From 17eff8074ac0698669edadbc4aae1477c8bff07a Mon Sep 17 00:00:00 2001 From: kingbuzzman Date: Sun, 22 Jun 2025 16:54:56 -0400 Subject: [PATCH 1/5] Adds ruff format --- diff_cover/diff_quality_tool.py | 2 ++ diff_cover/report_generator.py | 11 +++++-- diff_cover/violationsreporters/base.py | 22 +++++++++----- .../violations_reporter.py | 30 +++++++++++++++++-- 4 files changed, 51 insertions(+), 14 deletions(-) diff --git a/diff_cover/diff_quality_tool.py b/diff_cover/diff_quality_tool.py index 4c38549c..33842342 100644 --- a/diff_cover/diff_quality_tool.py +++ b/diff_cover/diff_quality_tool.py @@ -58,6 +58,7 @@ pycodestyle_driver, pydocstyle_driver, pyflakes_driver, + ruff_format_driver, ruff_check_driver, shellcheck_driver, ) @@ -68,6 +69,7 @@ "pyflakes": pyflakes_driver, "pylint": PylintDriver(), "ruff.check": ruff_check_driver, + "ruff.format": ruff_format_driver, "flake8": flake8_driver, "jshint": jshint_driver, "eslint": EslintDriver(), diff --git a/diff_cover/report_generator.py b/diff_cover/report_generator.py index 132f9d93..5f9832c5 100644 --- a/diff_cover/report_generator.py +++ b/diff_cover/report_generator.py @@ -11,6 +11,7 @@ from jinja2 import Environment, PackageLoader, select_autoescape from diff_cover.snippets import Snippet +from diff_cover.violationsreporters.base import Violation class DiffViolations: @@ -19,9 +20,10 @@ class DiffViolations: """ def __init__(self, violations, measured_lines, diff_lines): - self.lines = {violation.line for violation in violations}.intersection( - diff_lines - ) + _lines = {violation.line for violation in violations} + self.lines = _lines.intersection(diff_lines) + if Violation.ALL_LINES in _lines: + self.lines.add(Violation.ALL_LINES) self.violations = { violation for violation in violations if violation.line in self.lines @@ -99,6 +101,9 @@ def percent_covered(self, src_path): if diff_violations is None: return None + if Violation.ALL_LINES in diff_violations.lines: + return 0 + # Protect against a divide by zero num_measured = len(diff_violations.measured_lines) if num_measured > 0: diff --git a/diff_cover/violationsreporters/base.py b/diff_cover/violationsreporters/base.py index 26245c09..c2f580ec 100644 --- a/diff_cover/violationsreporters/base.py +++ b/diff_cover/violationsreporters/base.py @@ -7,7 +7,9 @@ from diff_cover.command_runner import execute, run_command_for_code -Violation = namedtuple("Violation", "line, message") + +class Violation(namedtuple("Violation", "line, message")): + ALL_LINES = -1 class QualityReporterError(Exception): @@ -227,18 +229,22 @@ def parse_reports(self, reports): violations_dict = defaultdict(list) for report in reports: if self.expression.flags & re.MULTILINE: - matches = (match for match in re.finditer(self.expression, report)) + matches = re.finditer(self.expression, report) else: matches = (self.expression.match(line) for line in report.split("\n")) for match in matches: - if match is not None: - src, line_number, message = match.groups() - # Transform src to a relative path, if it isn't already - src = os.path.relpath(src) - violation = Violation(int(line_number), message) - violations_dict[src].append(violation) + if match is None: + continue + src, violation = self._get_violation(match) + violations_dict[src].append(violation) return violations_dict + def _get_violation(self, match): + src, line_number, message = match.groups() + # Transform src to a relative path, if it isn't already + src = os.path.relpath(src) + return src, Violation(int(line_number), message) + def installed(self): """ Method checks if the provided tool is installed. diff --git a/diff_cover/violationsreporters/violations_reporter.py b/diff_cover/violationsreporters/violations_reporter.py index 57561cdb..42ea2ddc 100644 --- a/diff_cover/violationsreporters/violations_reporter.py +++ b/diff_cover/violationsreporters/violations_reporter.py @@ -235,9 +235,10 @@ 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: @@ -490,6 +491,29 @@ def measured_lines(self, src_path): exit_codes=[0, 1], ) + +class RuffFormatDriver(RegexBasedDriver): + def _get_violation(self, match): + src = match.groups()[0] + # Transform src to a relative path, if it isn't already + src = os.path.relpath(src) + return src, Violation(Violation.ALL_LINES, "Needs reformat") + + +ruff_format_driver = RuffFormatDriver( + name="ruff.format", + supported_extensions=["py"], + command=["ruff", "format", "--check"], + # Match lines of the form: + # Would reformat: path/to/file.py + # Would reformat: path/to/file2.py + expression=r"^Would reformat: ([^:]+)$", + 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 c3b2086bff775685b86b2f53601a204df36624f3 Mon Sep 17 00:00:00 2001 From: kingbuzzman Date: Sun, 22 Jun 2025 16:56:53 -0400 Subject: [PATCH 2/5] . --- diff_cover/diff_quality_tool.py | 2 +- diff_cover/violationsreporters/violations_reporter.py | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/diff_cover/diff_quality_tool.py b/diff_cover/diff_quality_tool.py index 33842342..a8a39a3a 100644 --- a/diff_cover/diff_quality_tool.py +++ b/diff_cover/diff_quality_tool.py @@ -58,8 +58,8 @@ pycodestyle_driver, pydocstyle_driver, pyflakes_driver, - ruff_format_driver, ruff_check_driver, + ruff_format_driver, shellcheck_driver, ) diff --git a/diff_cover/violationsreporters/violations_reporter.py b/diff_cover/violationsreporters/violations_reporter.py index 42ea2ddc..74d4df53 100644 --- a/diff_cover/violationsreporters/violations_reporter.py +++ b/diff_cover/violationsreporters/violations_reporter.py @@ -235,10 +235,12 @@ 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: From d391b034378458c11cf287e7bd393571b55d5ebd Mon Sep 17 00:00:00 2001 From: kingbuzzman Date: Mon, 23 Jun 2025 04:55:57 -0400 Subject: [PATCH 3/5] . --- diff_cover/util.py | 24 ++++++++++++++++++++++++ diff_cover/violationsreporters/base.py | 6 +++++- tests/test_util.py | 17 +++++++++++++++++ 3 files changed, 46 insertions(+), 1 deletion(-) diff --git a/diff_cover/util.py b/diff_cover/util.py index b608812a..c7713038 100644 --- a/diff_cover/util.py +++ b/diff_cover/util.py @@ -84,3 +84,27 @@ def to_unescaped_filename(filename: str) -> str: i += 1 return "".join(result) + + +def merge_ranges(nums): + if not nums: + return [] + nums = sorted(nums) + ranges = [] + start = prev = nums[0] + + for n in nums[1:]: + if n == prev + 1: + prev = n + continue + if start == prev: + ranges.append(f"{start}") + else: + ranges.append(f"{start}-{prev}") + start = prev = n + # Add the last range + if start == prev: + ranges.append(f"{start}") + else: + ranges.append(f"{start}-{prev}") + return ranges diff --git a/diff_cover/violationsreporters/base.py b/diff_cover/violationsreporters/base.py index c2f580ec..e6f0f0f4 100644 --- a/diff_cover/violationsreporters/base.py +++ b/diff_cover/violationsreporters/base.py @@ -8,9 +8,13 @@ from diff_cover.command_runner import execute, run_command_for_code -class Violation(namedtuple("Violation", "line, message")): +class Violation: ALL_LINES = -1 + def __init__(self, line, message): + self.line = line + self.message = message + class QualityReporterError(Exception): """ diff --git a/tests/test_util.py b/tests/test_util.py index b5185e24..3b5ba944 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -74,3 +74,20 @@ def test_open_file_encoding_binary(tmp_path): with util.open_file(tmp_path / "some_file.txt", "br", encoding="utf-16") as f: assert not hasattr(f, "encoding") assert f.read() == b"cafe naive resume" + + +@pytest.mark.parametrize( + ( + "nums", + "expected", + ), + ( + ([], []), + ([1, 2, 3], ["1-3"]), + ([1, 2, 3, 5, 6, 7], ["1-3", "5-7"]), + ([1, 3, 6, 8], ["1", "3", "6", "8"]), + (range(1, 101), ["1-100"]), + ), +) +def test_merge_ranges(nums, expected): + assert util.merge_ranges(nums) == expected From 659ae2bfd37149e5203a598ae0f03c5eebf8a2c1 Mon Sep 17 00:00:00 2001 From: kingbuzzman Date: Mon, 23 Jun 2025 05:04:57 -0400 Subject: [PATCH 4/5] . --- diff_cover/util.py | 25 +++++++++++++++---------- tests/test_util.py | 1 + 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/diff_cover/util.py b/diff_cover/util.py index c7713038..7787a899 100644 --- a/diff_cover/util.py +++ b/diff_cover/util.py @@ -87,24 +87,29 @@ def to_unescaped_filename(filename: str) -> str: def merge_ranges(nums): + """ + Merge a list of numbers into a list of ranges. + Given a list of numbers, merge consecutive numbers + into ranges of strings e.g. [1, 2, 3] -> ["1-3"] + """ if not nums: return [] - nums = sorted(nums) + nums = sorted(set(nums)) ranges = [] start = prev = nums[0] + def add_range(start_, end_): + """Helper function to add a range to the list.""" + if start_ == end_: + ranges.append(str(start_)) + else: + ranges.append(f"{start_}-{end_}") + for n in nums[1:]: if n == prev + 1: prev = n continue - if start == prev: - ranges.append(f"{start}") - else: - ranges.append(f"{start}-{prev}") + add_range(start, prev) start = prev = n - # Add the last range - if start == prev: - ranges.append(f"{start}") - else: - ranges.append(f"{start}-{prev}") + add_range(start, prev) return ranges diff --git a/tests/test_util.py b/tests/test_util.py index 3b5ba944..01c401e6 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -84,6 +84,7 @@ def test_open_file_encoding_binary(tmp_path): ( ([], []), ([1, 2, 3], ["1-3"]), + ([1, 2, 1, 2, 2, 3, 3], ["1-3"]), ([1, 2, 3, 5, 6, 7], ["1-3", "5-7"]), ([1, 3, 6, 8], ["1", "3", "6", "8"]), (range(1, 101), ["1-100"]), From d694872f208fc65c8ee6e037718b46686b45cf9f Mon Sep 17 00:00:00 2001 From: kingbuzzman Date: Sun, 29 Jun 2025 06:42:23 -0400 Subject: [PATCH 5/5] . --- diff_cover/util.py | 2 +- diff_cover/violationsreporters/base.py | 32 ++++++++++++++++++++++---- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/diff_cover/util.py b/diff_cover/util.py index 7787a899..a4dd8b4c 100644 --- a/diff_cover/util.py +++ b/diff_cover/util.py @@ -89,7 +89,7 @@ def to_unescaped_filename(filename: str) -> str: def merge_ranges(nums): """ Merge a list of numbers into a list of ranges. - Given a list of numbers, merge consecutive numbers + Given a list of numbers, merge consecutive numbers into ranges of strings e.g. [1, 2, 3] -> ["1-3"] """ if not nums: diff --git a/diff_cover/violationsreporters/base.py b/diff_cover/violationsreporters/base.py index e6f0f0f4..d8d4196b 100644 --- a/diff_cover/violationsreporters/base.py +++ b/diff_cover/violationsreporters/base.py @@ -4,16 +4,40 @@ import sys from abc import ABC, abstractmethod from collections import defaultdict, namedtuple +from functools import lru_cache from diff_cover.command_runner import execute, run_command_for_code -class Violation: +class Violation(namedtuple("_", "line, message")): ALL_LINES = -1 - def __init__(self, line, message): - self.line = line - self.message = message + +class SourceFile: + __slots__ = ("path", "violations") + + def __init__(self, path): + self.path = path + self.violations = [] + + def add_violation(self, violation): + self.violations.add(violation) + + @property + @lru_cache(maxsize=1) + def content(self): + with open(self.path, "r", encoding="utf-8") as f: + return f.read() + + @property + @lru_cache(maxsize=1) + def size(self): + return len(self.content) + + @property + @lru_cache(maxsize=1) + def lines(self): + return set(self.content.splitlines()) class QualityReporterError(Exception):