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/.git-blame-ignore-revs b/.git-blame-ignore-revs index cd2af3d4..a1070e14 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 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 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/config_parser.py b/diff_cover/config_parser.py index e9281eb4..5cd2c0cf 100644 --- a/diff_cover/config_parser.py +++ b/diff_cover/config_parser.py @@ -46,14 +46,16 @@ 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) 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 +69,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 07573648..16d2291d 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 @@ -51,7 +51,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): @@ -228,7 +228,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") ] @@ -238,7 +238,8 @@ def generate_coverage_report( if not coverage_file.endswith(".xml") ] if xml_roots and lcov_roots: - raise ValueError("Mixing LCov and XML reports is not supported yet") + 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: @@ -294,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: @@ -304,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: @@ -314,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_: @@ -370,7 +374,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 4c38549c..cc6ee17f 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 @@ -79,16 +80,15 @@ "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" INCLUDE_HELP = "Files to include (glob pattern)" 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): @@ -283,7 +283,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( handle_old_format(diff_cover.QUALITY_DESCRIPTION, argv[1:]) @@ -321,64 +320,55 @@ 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() + 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.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 + 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 + except ImportError: + logger.exception("Quality tool not installed: '%s'", tool) + return 1 + except OSError: + logger.exception("Failure") + return 1 else: - LOGGER.error("Quality tool not recognized: '%s'", tool) + logger.error("Failure. Quality is below %i.", fail_under) return 1 + # Close any reports we opened + finally: + for file_handle in input_reports: + file_handle.close() if __name__ == "__main__": diff --git a/diff_cover/diff_reporter.py b/diff_cover/diff_reporter.py index 3ecf2cc9..def286d5 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 @@ -276,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"]*)"?') @@ -297,7 +291,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 +313,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 = {} @@ -335,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) @@ -376,7 +368,6 @@ def _parse_lines(self, diff_lines): Raises a `GitDiffError` if the diff lines are in an invalid format. """ - added_lines = [] deleted_lines = [] @@ -445,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): """ @@ -474,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 @@ -500,7 +488,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 cf285519..594adf3b 100644 --- a/diff_cover/git_diff.py +++ b/diff_cover/git_diff.py @@ -110,20 +110,16 @@ def untracked(self): return self._untracked_cache output = execute(["git", "ls-files", "--exclude-standard", "--others"])[0] - self._untracked_cache = [] - if output: - 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 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"): """ @@ -131,8 +127,9 @@ 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, "r") 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/report_generator.py b/diff_cover/report_generator.py index 132f9d93..6352688f 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,13 +335,9 @@ def _context(self): 'total_percent_covered': TOTAL_PERCENT_COVERED } """ - # 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/diff_cover/snippets.py b/diff_cover/snippets.py index 11524148..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, @@ -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 @@ -116,33 +117,27 @@ 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_no_width = len(str(self._last_line)) - line_number_length = 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}") - text = "" - for i, line in enumerate(self.text().splitlines(), start=self._start_line): - if i > self._start_line: - text += "\n" + body = "\n".join(formatted_lines) - notice = " " - if i in self._violation_lines: - notice = "!" + # Prefer a syntax-highlighted fenced block when we know the language. + code_hint = dict(self.LEXER_TO_MARKDOWN_CODE_HINT).get(self._lexer_name) - format_string = "{} {:>" + str(line_number_length) + "} {}" - text += format_string.format(notice, i, line) + if code_hint: + header = f"Lines {self._start_line}-{self._last_line}\n\n" + return f"{header}```{code_hint}\n{body}\n```\n" - 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" - ) - - # 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): """ @@ -188,7 +183,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) @@ -220,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" ) @@ -266,7 +260,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 +295,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/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/diff_cover/violationsreporters/base.py b/diff_cover/violationsreporters/base.py index 26245c09..cb464af4 100644 --- a/diff_cover/violationsreporters/base.py +++ b/diff_cover/violationsreporters/base.py @@ -60,7 +60,7 @@ def measured_lines(self, src_path): """ # An existing quality plugin "sqlfluff" depends on this # being not abstract and returning None - return None + raise NotImplementedError def name(self): """ @@ -86,6 +86,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 @@ -101,6 +102,7 @@ def parse_reports(self, reports): Return: A dict[Str:Violation] Violation is a simple named tuple Defined above + """ @abstractmethod @@ -115,7 +117,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): @@ -125,25 +128,15 @@ 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 + 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`. @@ -178,7 +171,7 @@ def measured_lines(self, src_path): """ Quality Reports Consider all lines measured """ - return None + del src_path def name(self): """ @@ -203,13 +196,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) @@ -223,6 +217,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 a2f748d2..26f2538f 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 @@ -58,10 +58,11 @@ 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: - 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"): @@ -96,10 +97,11 @@ 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: - 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") @@ -120,9 +122,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 @@ -141,10 +145,11 @@ 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: - 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"): @@ -160,8 +165,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 b050f27b..3daddd5e 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 @@ -318,27 +315,39 @@ def parse(lcov_file): dict ) # { source_file: { func_name: (line_no, hit_count) } } 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 - elif 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}" + source_file = None + skippable = { + "TN", + "FNF", + "FNH", + "FN", + "FNDA", + "LH", + "LF", + "BRF", + "BRH", + "BRDA", + "VER", + } + + 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(":") + + if directive in skippable: + continue + + if directive == "SF": + source_file = util.to_unix_path(GitPathTool.relative_path(content)) + 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) + line_no = int(line_no) + hits = int(hits) + lcov_report[source_file][line_no] = ( + lcov_report[source_file].get(line_no, 0) + hits ) if line_no not in lcov_report[source_file]: lcov_report[source_file][line_no] = 0 @@ -433,7 +442,6 @@ def parse(lcov_file): else: raise ValueError(f"Unknown syntax in lcov report: {line}") - lcov.close() return lcov_report def _cache_file(self, src_path): @@ -495,7 +503,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[ @@ -514,7 +521,6 @@ def violations(self, src_path): """ See base class comments. """ - self._cache_file(src_path) # Yield all lines not covered @@ -657,9 +663,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", @@ -726,6 +733,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: @@ -780,9 +788,10 @@ class CppcheckDriver(QualityDriver): def __init__(self): """ - args: + Args: expression: regex used to parse report See super for other args + """ super().__init__( "cppcheck", @@ -804,6 +813,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/pyproject.toml b/pyproject.toml index 32d7c945..317fa129 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -70,22 +70,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 @@ -109,45 +93,6 @@ exclude_also = [ 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 @@ -157,3 +102,95 @@ markers = [ [tool.doc8] max_line_length = 120 + +[tool.ruff] +line-length = 88 +target-version = "py39" +src = ["diff_cover", "tests"] +exclude = ["tests/fixtures/*"] + +[tool.ruff.format] +quote-style = "double" +indent-style = "space" +skip-magic-trailing-comma = false +line-ending = "auto" + +[tool.ruff.lint] +select = ["ALL"] +ignore = [ + # Disable all annotations + "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 + "D417", # Disables Missing argument descriptions in the docstring + + # allow disables (equivalent to "locally-disabled", "suppressed-message") + "RUF100", + + # 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", + + "E501", # Line too long + "PTH", + + # Avoid formatter conflicts + "COM812", + + "PYI024", # Disables complains about unused type hints +] + +# 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.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 + "S314", # Disables xml security checks + "SLF001", # Disables complains about accessing private members/methods + "RUF001", # Disables complains about greek letters +] + +[tool.ruff.lint.pydocstyle] +# Equivalent to pylint's no-docstring-rgx +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-after-imports = -1 +split-on-trailing-comma = false diff --git a/tests/helpers.py b/tests/helpers.py index 18932850..fd6ebd11 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -63,7 +63,6 @@ def git_diff_output(diff_dict, deleted_files=None): Returns a byte string. """ - output = [] # Entries for deleted files @@ -85,7 +84,6 @@ def _deleted_file_entries(deleted_files): Returns a list of lines in the diff output. """ - output = [] if deleted_files is not None: @@ -115,7 +113,6 @@ def _source_file_entry(src_file, modified_lines): Returns a list of lines in the diff output. """ - output = [] # Line for the file names @@ -182,7 +179,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_clover_violations_reporter.py b/tests/test_clover_violations_reporter.py index e79df37a..19e8d8b4 100644 --- a/tests/test_clover_violations_reporter.py +++ b/tests/test_clover_violations_reporter.py @@ -2,17 +2,18 @@ """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 # https://github.com/Bachmann1234/diff_cover/issues/190 -def test_get_src_path_clover(datadir): - GitPathTool._cwd = "/" - GitPathTool._root = "/" - clover_report = etree.parse(str(datadir / "test.xml")) +def test_get_src_path_clover(datadir, monkeypatch): + monkeypatch.setattr(GitPathTool, "_cwd", "/") + monkeypatch.setattr(GitPathTool, "_root", "/") + + clover_report = ET.parse(datadir / "test.xml") result = XmlCoverageReporter.get_src_path_line_nodes_clover( clover_report, "isLucky.js" ) diff --git a/tests/test_config_parser.py b/tests/test_config_parser.py index eb0e300f..2ebca56d 100644 --- a/tests/test_config_parser.py +++ b/tests/test_config_parser.py @@ -14,13 +14,13 @@ 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() @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..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 @@ -75,7 +74,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"]), @@ -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 @@ -124,12 +123,12 @@ 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 - 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 @@ -148,7 +147,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 +224,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 @@ -239,7 +238,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, "", "") @@ -286,7 +285,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 @@ -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) @@ -645,7 +644,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", []), diff --git a/tests/test_integration.py b/tests/test_integration.py index 39b84af5..459e0b86 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 @@ -69,6 +70,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 +94,6 @@ def patch_diff(command, **kwargs): return Popen(command, **kwargs) patch_popen.side_effect = patch_diff - helper = Wrapper() return helper @@ -145,7 +147,7 @@ class TestDiffCoverIntegration: @pytest.fixture def runbin(self, cwd): - del cwd # fixtures cannot use pytest.mark.usefixtures + del cwd return lambda x: diff_cover_tool.main(["diff-cover", *x]) def test_added_file_html(self, runbin, patch_git_command): @@ -186,7 +188,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") @@ -376,7 +378,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" @@ -468,7 +470,7 @@ class TestDiffQualityIntegration: @pytest.fixture def runbin(self, cwd): - del cwd # fixtures cannot use pytest.mark.usefixtures + del cwd return lambda x: diff_quality_tool.main(["diff-quality", *x]) def test_git_diff_error_diff_quality(self, runbin, patch_git_command): @@ -657,11 +659,11 @@ 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") - 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, @@ -672,11 +674,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.error.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 @@ -696,6 +698,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/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 a1d9996f..ca43f30d 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)) @@ -1847,9 +1847,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): """ diff --git a/verify.sh b/verify.sh index 99996d59..d8419ab7 100755 --- a/verify.sh +++ b/verify.sh @@ -1,15 +1,15 @@ #!/bin/bash set -euo pipefail IFS=$'\n\t' +COMPARE_BRANCH=${COMPARE_BRANCH:-origin/main} -black diff_cover tests --check -isort diff_cover tests --check +ruff format --check +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 diff-quality --version -diff-cover coverage.xml --include-untracked -diff-quality --violations flake8 --include-untracked -diff-quality --violations pylint --include-untracked +diff-cover coverage.xml --include-untracked --compare-branch=$COMPARE_BRANCH +diff-quality --violations ruff.check --include-untracked --compare-branch=$COMPARE_BRANCH doc8 README.rst --ignore D001