| Index: third_party/coverage/files.py
|
| diff --git a/third_party/coverage/files.py b/third_party/coverage/files.py
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..464535a81653ca833ba33b462467941e373c0927
|
| --- /dev/null
|
| +++ b/third_party/coverage/files.py
|
| @@ -0,0 +1,309 @@
|
| +"""File wrangling."""
|
| +
|
| +from coverage.backward import to_string
|
| +from coverage.misc import CoverageException
|
| +import fnmatch, os, os.path, re, sys
|
| +import ntpath, posixpath
|
| +
|
| +class FileLocator(object):
|
| + """Understand how filenames work."""
|
| +
|
| + def __init__(self):
|
| + # The absolute path to our current directory.
|
| + self.relative_dir = os.path.normcase(abs_file(os.curdir) + os.sep)
|
| +
|
| + # Cache of results of calling the canonical_filename() method, to
|
| + # avoid duplicating work.
|
| + self.canonical_filename_cache = {}
|
| +
|
| + def relative_filename(self, filename):
|
| + """Return the relative form of `filename`.
|
| +
|
| + The filename will be relative to the current directory when the
|
| + `FileLocator` was constructed.
|
| +
|
| + """
|
| + fnorm = os.path.normcase(filename)
|
| + if fnorm.startswith(self.relative_dir):
|
| + filename = filename[len(self.relative_dir):]
|
| + return filename
|
| +
|
| + def canonical_filename(self, filename):
|
| + """Return a canonical filename for `filename`.
|
| +
|
| + An absolute path with no redundant components and normalized case.
|
| +
|
| + """
|
| + if filename not in self.canonical_filename_cache:
|
| + if not os.path.isabs(filename):
|
| + for path in [os.curdir] + sys.path:
|
| + if path is None:
|
| + continue
|
| + f = os.path.join(path, filename)
|
| + if os.path.exists(f):
|
| + filename = f
|
| + break
|
| + cf = abs_file(filename)
|
| + self.canonical_filename_cache[filename] = cf
|
| + return self.canonical_filename_cache[filename]
|
| +
|
| + def get_zip_data(self, filename):
|
| + """Get data from `filename` if it is a zip file path.
|
| +
|
| + Returns the string data read from the zip file, or None if no zip file
|
| + could be found or `filename` isn't in it. The data returned will be
|
| + an empty string if the file is empty.
|
| +
|
| + """
|
| + import zipimport
|
| + markers = ['.zip'+os.sep, '.egg'+os.sep]
|
| + for marker in markers:
|
| + if marker in filename:
|
| + parts = filename.split(marker)
|
| + try:
|
| + zi = zipimport.zipimporter(parts[0]+marker[:-1])
|
| + except zipimport.ZipImportError:
|
| + continue
|
| + try:
|
| + data = zi.get_data(parts[1])
|
| + except IOError:
|
| + continue
|
| + return to_string(data)
|
| + return None
|
| +
|
| +
|
| +if sys.platform == 'win32':
|
| +
|
| + def actual_path(path):
|
| + """Get the actual path of `path`, including the correct case."""
|
| + if path in actual_path.cache:
|
| + return actual_path.cache[path]
|
| +
|
| + head, tail = os.path.split(path)
|
| + if not tail:
|
| + actpath = head
|
| + elif not head:
|
| + actpath = tail
|
| + else:
|
| + head = actual_path(head)
|
| + if head in actual_path.list_cache:
|
| + files = actual_path.list_cache[head]
|
| + else:
|
| + try:
|
| + files = os.listdir(head)
|
| + except OSError:
|
| + files = []
|
| + actual_path.list_cache[head] = files
|
| + normtail = os.path.normcase(tail)
|
| + for f in files:
|
| + if os.path.normcase(f) == normtail:
|
| + tail = f
|
| + break
|
| + actpath = os.path.join(head, tail)
|
| + actual_path.cache[path] = actpath
|
| + return actpath
|
| +
|
| + actual_path.cache = {}
|
| + actual_path.list_cache = {}
|
| +
|
| +else:
|
| + def actual_path(filename):
|
| + """The actual path for non-Windows platforms."""
|
| + return filename
|
| +
|
| +
|
| +def abs_file(filename):
|
| + """Return the absolute normalized form of `filename`."""
|
| + path = os.path.expandvars(os.path.expanduser(filename))
|
| + path = os.path.abspath(os.path.realpath(path))
|
| + path = actual_path(path)
|
| + return path
|
| +
|
| +
|
| +def isabs_anywhere(filename):
|
| + """Is `filename` an absolute path on any OS?"""
|
| + return ntpath.isabs(filename) or posixpath.isabs(filename)
|
| +
|
| +
|
| +def prep_patterns(patterns):
|
| + """Prepare the file patterns for use in a `FnmatchMatcher`.
|
| +
|
| + If a pattern starts with a wildcard, it is used as a pattern
|
| + as-is. If it does not start with a wildcard, then it is made
|
| + absolute with the current directory.
|
| +
|
| + If `patterns` is None, an empty list is returned.
|
| +
|
| + """
|
| + prepped = []
|
| + for p in patterns or []:
|
| + if p.startswith("*") or p.startswith("?"):
|
| + prepped.append(p)
|
| + else:
|
| + prepped.append(abs_file(p))
|
| + return prepped
|
| +
|
| +
|
| +class TreeMatcher(object):
|
| + """A matcher for files in a tree."""
|
| + def __init__(self, directories):
|
| + self.dirs = directories[:]
|
| +
|
| + def __repr__(self):
|
| + return "<TreeMatcher %r>" % self.dirs
|
| +
|
| + def info(self):
|
| + """A list of strings for displaying when dumping state."""
|
| + return self.dirs
|
| +
|
| + def add(self, directory):
|
| + """Add another directory to the list we match for."""
|
| + self.dirs.append(directory)
|
| +
|
| + def match(self, fpath):
|
| + """Does `fpath` indicate a file in one of our trees?"""
|
| + for d in self.dirs:
|
| + if fpath.startswith(d):
|
| + if fpath == d:
|
| + # This is the same file!
|
| + return True
|
| + if fpath[len(d)] == os.sep:
|
| + # This is a file in the directory
|
| + return True
|
| + return False
|
| +
|
| +
|
| +class FnmatchMatcher(object):
|
| + """A matcher for files by filename pattern."""
|
| + def __init__(self, pats):
|
| + self.pats = pats[:]
|
| +
|
| + def __repr__(self):
|
| + return "<FnmatchMatcher %r>" % self.pats
|
| +
|
| + def info(self):
|
| + """A list of strings for displaying when dumping state."""
|
| + return self.pats
|
| +
|
| + def match(self, fpath):
|
| + """Does `fpath` match one of our filename patterns?"""
|
| + for pat in self.pats:
|
| + if fnmatch.fnmatch(fpath, pat):
|
| + return True
|
| + return False
|
| +
|
| +
|
| +def sep(s):
|
| + """Find the path separator used in this string, or os.sep if none."""
|
| + sep_match = re.search(r"[\\/]", s)
|
| + if sep_match:
|
| + the_sep = sep_match.group(0)
|
| + else:
|
| + the_sep = os.sep
|
| + return the_sep
|
| +
|
| +
|
| +class PathAliases(object):
|
| + """A collection of aliases for paths.
|
| +
|
| + When combining data files from remote machines, often the paths to source
|
| + code are different, for example, due to OS differences, or because of
|
| + serialized checkouts on continuous integration machines.
|
| +
|
| + A `PathAliases` object tracks a list of pattern/result pairs, and can
|
| + map a path through those aliases to produce a unified path.
|
| +
|
| + `locator` is a FileLocator that is used to canonicalize the results.
|
| +
|
| + """
|
| + def __init__(self, locator=None):
|
| + self.aliases = []
|
| + self.locator = locator
|
| +
|
| + def add(self, pattern, result):
|
| + """Add the `pattern`/`result` pair to the list of aliases.
|
| +
|
| + `pattern` is an `fnmatch`-style pattern. `result` is a simple
|
| + string. When mapping paths, if a path starts with a match against
|
| + `pattern`, then that match is replaced with `result`. This models
|
| + isomorphic source trees being rooted at different places on two
|
| + different machines.
|
| +
|
| + `pattern` can't end with a wildcard component, since that would
|
| + match an entire tree, and not just its root.
|
| +
|
| + """
|
| + # The pattern can't end with a wildcard component.
|
| + pattern = pattern.rstrip(r"\/")
|
| + if pattern.endswith("*"):
|
| + raise CoverageException("Pattern must not end with wildcards.")
|
| + pattern_sep = sep(pattern)
|
| +
|
| + # The pattern is meant to match a filepath. Let's make it absolute
|
| + # unless it already is, or is meant to match any prefix.
|
| + if not pattern.startswith('*') and not isabs_anywhere(pattern):
|
| + pattern = abs_file(pattern)
|
| + pattern += pattern_sep
|
| +
|
| + # Make a regex from the pattern. fnmatch always adds a \Z or $ to
|
| + # match the whole string, which we don't want.
|
| + regex_pat = fnmatch.translate(pattern).replace(r'\Z(', '(')
|
| + if regex_pat.endswith("$"):
|
| + regex_pat = regex_pat[:-1]
|
| + # 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)
|
| +
|
| + # Normalize the result: it must end with a path separator.
|
| + result_sep = sep(result)
|
| + result = result.rstrip(r"\/") + result_sep
|
| + self.aliases.append((regex, result, pattern_sep, result_sep))
|
| +
|
| + def map(self, path):
|
| + """Map `path` through the aliases.
|
| +
|
| + `path` is checked against all of the patterns. The first pattern to
|
| + match is used to replace the root of the path with the result root.
|
| + Only one pattern is ever used. If no patterns match, `path` is
|
| + returned unchanged.
|
| +
|
| + The separator style in the result is made to match that of the result
|
| + in the alias.
|
| +
|
| + """
|
| + for regex, result, pattern_sep, result_sep in self.aliases:
|
| + m = regex.match(path)
|
| + if m:
|
| + new = path.replace(m.group(0), result)
|
| + if pattern_sep != result_sep:
|
| + new = new.replace(pattern_sep, result_sep)
|
| + if self.locator:
|
| + new = self.locator.canonical_filename(new)
|
| + return new
|
| + return path
|
| +
|
| +
|
| +def find_python_files(dirname):
|
| + """Yield all of the importable Python files in `dirname`, recursively.
|
| +
|
| + To be importable, the files have to be in a directory with a __init__.py,
|
| + except for `dirname` itself, which isn't required to have one. The
|
| + assumption is that `dirname` was specified directly, so the user knows
|
| + best, but subdirectories are checked for a __init__.py to be sure we only
|
| + find the importable files.
|
| +
|
| + """
|
| + for i, (dirpath, dirnames, filenames) in enumerate(os.walk(dirname)):
|
| + if i > 0 and '__init__.py' not in filenames:
|
| + # If a directory doesn't have __init__.py, then it isn't
|
| + # importable and neither are its files
|
| + del dirnames[:]
|
| + continue
|
| + for filename in filenames:
|
| + # We're only interested in files that look like reasonable Python
|
| + # files: Must end with .py or .pyw, and must not have certain funny
|
| + # characters that probably mean they are editor junk.
|
| + if re.match(r"^[^.#~!$@%^&*()+=,]+\.pyw?$", filename):
|
| + yield os.path.join(dirpath, filename)
|
|
|