From b66a1cd80f819ceb4da40a807ca22c1efbaa576b Mon Sep 17 00:00:00 2001 From: Adam Groszer Date: Mon, 13 Nov 2017 14:32:35 +0100 Subject: [PATCH] added diff-cover level exclude files (some func taken from flake8) --- diff_cover/diff_reporter.py | 52 ++++++++++++++++++++++++-- diff_cover/tests/test_args.py | 34 +++++++++++++++++ diff_cover/tests/test_diff_reporter.py | 36 +++++++++++++++++- diff_cover/tool.py | 45 ++++++++++++++++++---- 4 files changed, 156 insertions(+), 11 deletions(-) diff --git a/diff_cover/diff_reporter.py b/diff_cover/diff_reporter.py index 6471b769..c44367a5 100644 --- a/diff_cover/diff_reporter.py +++ b/diff_cover/diff_reporter.py @@ -4,6 +4,7 @@ from __future__ import unicode_literals from abc import ABCMeta, abstractmethod from diff_cover.git_diff import GitDiffError +import fnmatch import os import re @@ -14,13 +15,15 @@ class BaseDiffReporter(object): """ __metaclass__ = ABCMeta + _exclude = None - def __init__(self, name): + def __init__(self, name, exclude=None): """ Provide a `name` for the diff report, which will be included in the diff coverage report. """ self._name = name + self._exclude = exclude @abstractmethod def src_paths_changed(self): @@ -49,13 +52,54 @@ def name(self): """ return self._name + def _fnmatch(self, filename, patterns, default=True): + """Wrap :func:`fnmatch.fnmatch` to add some functionality. + + :param str filename: + Name of the file we're trying to match. + :param list patterns: + Patterns we're using to try to match the filename. + :param bool default: + The default value if patterns is empty + :returns: + True if a pattern matches the filename, False if it doesn't. + ``default`` if patterns is empty. + """ + if not patterns: + return default + return any(fnmatch.fnmatch(filename, pattern) for pattern in patterns) + + def _is_path_excluded(self, path): + """ + Check if a path is excluded. + + :param str path: + Path to check against the exclude patterns. + :returns: + True if there are exclude patterns and the path matches, + otherwise False. + """ + exclude = self._exclude + if not exclude: + return False + basename = os.path.basename(path) + if self._fnmatch(basename, exclude): + return True + + absolute_path = os.path.abspath(path) + match = self._fnmatch(absolute_path, exclude) + return match + class GitDiffReporter(BaseDiffReporter): """ Query information from a Git diff between branches. """ - def __init__(self, compare_branch='origin/master', git_diff=None, ignore_staged=None, ignore_unstaged=None, supported_extensions=None): + def __init__(self, compare_branch='origin/master', git_diff=None, + ignore_staged=None, ignore_unstaged=None, + supported_extensions=None, + exclude=None): """ Configure the reporter to use `git_diff` as the wrapper for the `git diff` tool. (Should have same interface @@ -76,7 +120,7 @@ def __init__(self, compare_branch='origin/master', git_diff=None, ignore_staged= # Apply and + changes to the last option name += " and " + options[-1] + " changes" - super(GitDiffReporter, self).__init__(name) + super(GitDiffReporter, self).__init__(name, exclude) self._compare_branch = compare_branch self._git_diff_tool = git_diff @@ -154,6 +198,8 @@ def _git_diff(self): diff_dict = self._parse_diff_str(diff_str) for src_path in diff_dict.keys(): + if self._is_path_excluded(src_path): + continue # If no _supported_extensions provided, or extension present: process root, extension = os.path.splitext(src_path) extension = extension[1:].lower() diff --git a/diff_cover/tests/test_args.py b/diff_cover/tests/test_args.py index c088487e..acf9525b 100644 --- a/diff_cover/tests/test_args.py +++ b/diff_cover/tests/test_args.py @@ -72,6 +72,23 @@ def test_parse_invalid_arg(self): with nostderr(): parse_coverage_args(argv) + def test_parse_with_exclude(self): + argv = ['reports/coverage.xml'] + arg_dict = parse_coverage_args(argv) + self.assertEqual(arg_dict.get('exclude'), None) + + argv = ['reports/coverage.xml', '--exclude', 'noneed/*.py'] + + arg_dict = parse_coverage_args(argv) + self.assertEqual(arg_dict.get('exclude'), ['noneed/*.py']) + + argv = ['reports/coverage.xml', '--exclude', 'noneed/*.py', + 'other/**/*.py'] + + arg_dict = parse_coverage_args(argv) + self.assertEqual(arg_dict.get('exclude'), + ['noneed/*.py', 'other/**/*.py']) + class ParseQualityArgsTest(unittest.TestCase): @@ -138,6 +155,23 @@ def test_parse_invalid_arg(self): print("args = {0}".format(argv)) parse_quality_args(argv) + def test_parse_with_exclude(self): + argv = ['--violations', 'pep8'] + arg_dict = parse_quality_args(argv) + self.assertEqual(arg_dict.get('exclude'), None) + + argv = ['--violations', 'pep8', '--exclude', 'noneed/*.py'] + + arg_dict = parse_quality_args(argv) + self.assertEqual(arg_dict.get('exclude'), ['noneed/*.py']) + + argv = ['--violations', 'pep8', '--exclude', 'noneed/*.py', + 'other/**/*.py'] + + arg_dict = parse_quality_args(argv) + self.assertEqual(arg_dict.get('exclude'), + ['noneed/*.py', 'other/**/*.py']) + class MainTest(unittest.TestCase): """Tests for the main() function in tool.py""" diff --git a/diff_cover/tests/test_diff_reporter.py b/diff_cover/tests/test_diff_reporter.py index b6d9d7b4..d158de8a 100644 --- a/diff_cover/tests/test_diff_reporter.py +++ b/diff_cover/tests/test_diff_reporter.py @@ -52,6 +52,26 @@ def test_name_ignore_staged_and_unstaged(self): 'origin/master...HEAD' ) + def test_git_exclude(self): + self.diff = GitDiffReporter(git_diff=self._git_diff, exclude=['file1.py']) + + # Configure the git diff output + self._set_git_diff_output( + 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']) + ) + + # Get the source paths in the diff + source_paths = self.diff.src_paths_changed() + + # Validate the source paths + # They should be in alphabetical order + self.assertEqual(len(source_paths), 3) + self.assertEqual('file3.py', source_paths[0]) + self.assertEqual('README.md', source_paths[1]) + self.assertEqual('subdir2/file2.py', source_paths[2]) + def test_git_source_paths(self): # Configure the git diff output @@ -446,7 +466,6 @@ def test_ignore_unstaged_inclusion(self): self.assertEqual(2, len(self.diff._get_included_diff_results())) self.assertEqual(['', ''], self.diff._get_included_diff_results()) - def test_ignore_staged_and_unstaged_inclusion(self): self.diff = GitDiffReporter(git_diff=self._git_diff, ignore_staged=True, ignore_unstaged=True) @@ -457,6 +476,21 @@ def test_ignore_staged_and_unstaged_inclusion(self): self.assertEqual(1, len(self.diff._get_included_diff_results())) self.assertEqual([''], self.diff._get_included_diff_results()) + def test_fnmatch(self): + """Verify that our fnmatch wrapper works as expected.""" + self.assertEqual(self.diff._fnmatch('foo.py', []), True) + self.assertEqual(self.diff._fnmatch('foo.py', ['*.pyc']), False) + self.assertEqual(self.diff._fnmatch('foo.pyc', ['*.pyc']), True) + self.assertEqual( + self.diff._fnmatch('foo.pyc', ['*.swp', '*.pyc', '*.py']), True) + + def test_fnmatch_returns_the_default_with_empty_default(self): + """The default parameter should be returned when no patterns are given. + """ + sentinel = object() + self.assertTrue( + self.diff._fnmatch('file.py', [], default=sentinel) is sentinel) + def _set_git_diff_output(self, committed_diff, staged_diff, unstaged_diff): """ diff --git a/diff_cover/tool.py b/diff_cover/tool.py index caf44618..cea52fd0 100644 --- a/diff_cover/tool.py +++ b/diff_cover/tool.py @@ -46,6 +46,7 @@ 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" +EXCLUDE_HELP = "Exclude files, more patterns supported" LOGGER = logging.getLogger(__name__) @@ -105,8 +106,8 @@ def parse_coverage_args(argv): type=float, default='0', help=FAIL_UNDER_HELP - ) - + ) + parser.add_argument( '--ignore-staged', action='store_true', @@ -121,6 +122,14 @@ def parse_coverage_args(argv): help=IGNORE_UNSTAGED_HELP ) + parser.add_argument( + '--exclude', + metavar='EXCLUDE', + type=str, + nargs='+', + help=EXCLUDE_HELP + ) + return vars(parser.parse_args(argv)) @@ -196,7 +205,7 @@ def parse_quality_args(argv): default='0', help=FAIL_UNDER_HELP ) - + parser.add_argument( '--ignore-staged', action='store_true', @@ -211,14 +220,27 @@ def parse_quality_args(argv): help=IGNORE_UNSTAGED_HELP ) + parser.add_argument( + '--exclude', + metavar='EXCLUDE', + type=str, + nargs='+', + help=EXCLUDE_HELP + ) + return vars(parser.parse_args(argv)) -def generate_coverage_report(coverage_xml, compare_branch, html_report=None, css_file=None, ignore_staged=False, ignore_unstaged=False): +def generate_coverage_report(coverage_xml, compare_branch, + html_report=None, css_file=None, + ignore_staged=False, ignore_unstaged=False, + exclude=None): """ Generate the diff coverage report, using kwargs from `parse_args()`. """ - diff = GitDiffReporter(compare_branch, git_diff=GitDiffTool(), ignore_staged=ignore_staged, ignore_unstaged=ignore_unstaged) + diff = GitDiffReporter( + compare_branch, git_diff=GitDiffTool(), ignore_staged=ignore_staged, + ignore_unstaged=ignore_unstaged, exclude=exclude) xml_roots = [cElementTree.parse(xml_root) for xml_root in coverage_xml] coverage = XmlCoverageReporter(xml_roots) @@ -243,11 +265,18 @@ def generate_coverage_report(coverage_xml, compare_branch, html_report=None, css return reporter.total_percent_covered() -def generate_quality_report(tool, compare_branch, html_report=None, css_file=None, ignore_staged=False, ignore_unstaged=False): +def generate_quality_report(tool, compare_branch, + html_report=None, css_file=None, + ignore_staged=False, ignore_unstaged=False, + exclude=None): """ Generate the quality report, using kwargs from `parse_args()`. """ - diff = GitDiffReporter(compare_branch, git_diff=GitDiffTool(), ignore_staged=ignore_staged, ignore_unstaged=ignore_unstaged, supported_extensions=tool.driver.supported_extensions) + diff = GitDiffReporter( + compare_branch, git_diff=GitDiffTool(), + ignore_staged=ignore_staged, ignore_unstaged=ignore_unstaged, + supported_extensions=tool.driver.supported_extensions, + exclude=exclude) if html_report is not None: css_url = css_file @@ -302,6 +331,7 @@ def main(argv=None, directory=None): css_file=arg_dict['external_css_file'], ignore_staged=arg_dict['ignore_staged'], ignore_unstaged=arg_dict['ignore_unstaged'], + exclude=arg_dict['exclude'], ) if percent_covered >= fail_under: @@ -343,6 +373,7 @@ def main(argv=None, directory=None): css_file=arg_dict['external_css_file'], ignore_staged=arg_dict['ignore_staged'], ignore_unstaged=arg_dict['ignore_unstaged'], + exclude=arg_dict['exclude'], ) if percent_passing >= fail_under: return 0