diff options
| author | Ned Batchelder <ned@nedbatchelder.com> | 2018-09-02 09:04:49 -0400 | 
|---|---|---|
| committer | Ned Batchelder <ned@nedbatchelder.com> | 2018-09-02 10:03:43 -0400 | 
| commit | 56b9c7e4db40df6515d4ca5d913cb4678da2b753 (patch) | |
| tree | e5236fc3347124fbfb6c8bbde10a2c8ed87e5347 | |
| parent | 8bb8175d9e14f1a47180ccd356060d5068bc769b (diff) | |
| download | python-coveragepy-git-56b9c7e4db40df6515d4ca5d913cb4678da2b753.tar.gz | |
Move fiddly fnmatch logic into its own testable function
| -rw-r--r-- | coverage/files.py | 62 | ||||
| -rw-r--r-- | tests/test_files.py | 51 | 
2 files changed, 87 insertions, 26 deletions
diff --git a/coverage/files.py b/coverage/files.py index 70fde9db..5beb518d 100644 --- a/coverage/files.py +++ b/coverage/files.py @@ -260,19 +260,8 @@ class ModuleMatcher(object):  class FnmatchMatcher(object):      """A matcher for files by file name pattern."""      def __init__(self, pats): -        self.pats = pats[:] -        # fnmatch is platform-specific. On Windows, it does the Windows thing -        # of treating / and \ as equivalent. But on other platforms, we need to -        # take care of that ourselves. -        fnpats = (fnmatch.translate(p) for p in pats) -        # Python3.7 fnmatch translates "/" as "/", before that, it translates as "\/", -        # so we have to deal with maybe a backslash. -        fnpats = (re.sub(r"\\?/", r"[\\\\/]", p) for p in fnpats) -        flags = 0 -        if env.WINDOWS: -            # Windows is also case-insensitive, so make the regex case-insensitive. -            flags |= re.IGNORECASE -        self.re = re.compile(join_regex(fnpats), flags=flags) +        self.pats = list(pats) +        self.re = fnmatches_to_regex(self.pats, case_insensitive=env.WINDOWS)      def __repr__(self):          return "<FnmatchMatcher %r>" % self.pats @@ -296,6 +285,39 @@ def sep(s):      return the_sep +def fnmatches_to_regex(patterns, case_insensitive=False, partial=False): +    """Convert fnmatch patterns to a compiled regex that matches any of them. + +    Slashes are always converted to match either slash or backslash, for +    Windows support, even when running elsewhere. + +    If `partial` is true, then the pattern will match if the target string +    starts with the pattern. Otherwise, it must match the entire string. + +    Returns: a compiled regex object.  Use the .match method to compare target +    strings. + +    """ +    regexes = (fnmatch.translate(pattern) for pattern in patterns) +    # Python3.7 fnmatch translates "/" as "/". Before that, it translates as "\/", +    # so we have to deal with maybe a backslash. +    regexes = (re.sub(r"\\?/", r"[\\\\/]", regex) for regex in regexes) + +    if partial: +        # fnmatch always adds a \Z to match the whole string, which we don't +        # want, so we remove the \Z.  While removing it, we only replace \Z if +        # followed by paren (introducing flags), or at end, to keep from +        # destroying a literal \Z in the pattern. +        regexes = (re.sub(r'\\Z(\(\?|$)', r'\1', regex) for regex in regexes) + +    flags = 0 +    if case_insensitive: +        flags |= re.IGNORECASE +    compiled = re.compile(join_regex(regexes), flags=flags) + +    return compiled + +  class PathAliases(object):      """A collection of aliases for paths. @@ -343,18 +365,8 @@ class PathAliases(object):          if not pattern.endswith(pattern_sep):              pattern += pattern_sep -        # Make a regex from the pattern.  fnmatch always adds a \Z to -        # match the whole string, which we don't want, so we remove the \Z. -        # While removing it, we only replace \Z if followed by paren, or at -        # end, to keep from destroying a literal \Z in the pattern. -        regex_pat = fnmatch.translate(pattern) -        regex_pat = re.sub(r'\\Z(\(|$)', r'\1', regex_pat) - -        # We want */a/b.py to match on Windows too, so change slash to match -        # either separator. -        regex_pat = regex_pat.replace(r"\/", r"[\\/]") -        # We want case-insensitive matching, so add that flag. -        regex = re.compile(r"(?i)" + regex_pat) +        # Make a regex from the pattern. +        regex = fnmatches_to_regex([pattern], case_insensitive=True, partial=True)          # Normalize the result: it must end with a path separator.          result_sep = sep(result) diff --git a/tests/test_files.py b/tests/test_files.py index 2e705a1b..b4490ea6 100644 --- a/tests/test_files.py +++ b/tests/test_files.py @@ -12,7 +12,7 @@ import pytest  from coverage import files  from coverage.files import (      TreeMatcher, FnmatchMatcher, ModuleMatcher, PathAliases, -    find_python_files, abs_file, actual_path, flat_rootname, +    find_python_files, abs_file, actual_path, flat_rootname, fnmatches_to_regex,  )  from coverage.misc import CoverageException  from coverage import env @@ -77,6 +77,55 @@ def test_flat_rootname(original, flat):      assert flat_rootname(original) == flat +@pytest.mark.parametrize( +        "patterns, case_insensitive, partial," +            "matches," +            "nomatches", +[ +    ( +        ["abc", "xyz"], False, False, +            ["abc", "xyz"], +            ["ABC", "xYz", "abcx", "xabc", "axyz", "xyza"], +    ), +    ( +        ["abc", "xyz"], True, False, +            ["abc", "xyz", "Abc", "XYZ", "AbC"], +            ["abcx", "xabc", "axyz", "xyza"], +    ), +    ( +        ["abc/hi.py"], True, False, +            ["abc/hi.py", "ABC/hi.py", r"ABC\hi.py"], +            ["abc_hi.py", "abc/hi.pyc"], +    ), +    ( +        [r"abc\hi.py"], True, False, +            [r"abc\hi.py", r"ABC\hi.py"], +            ["abc/hi.py", "ABC/hi.py", "abc_hi.py", "abc/hi.pyc"], +    ), +    ( +        ["abc/*/hi.py"], True, False, +            ["abc/foo/hi.py", "ABC/foo/bar/hi.py", r"ABC\foo/bar/hi.py"], +            ["abc/hi.py", "abc/hi.pyc"], +    ), +    ( +        ["abc/[a-f]*/hi.py"], True, False, +            ["abc/foo/hi.py", "ABC/foo/bar/hi.py", r"ABC\foo/bar/hi.py"], +            ["abc/zoo/hi.py", "abc/hi.py", "abc/hi.pyc"], +    ), +    ( +        ["abc/"], True, True, +            ["abc/foo/hi.py", "ABC/foo/bar/hi.py", r"ABC\foo/bar/hi.py"], +            ["abcd/foo.py", "xabc/hi.py"], +    ), +]) +def test_fnmatches_to_regex(patterns, case_insensitive, partial, matches, nomatches): +    regex = fnmatches_to_regex(patterns, case_insensitive=case_insensitive, partial=partial) +    for s in matches: +        assert regex.match(s) +    for s in nomatches: +        assert not regex.match(s) + +  class MatcherTest(CoverageTest):      """Tests of file matchers."""  | 
