diff options
Diffstat (limited to 'coverage/files.py')
-rw-r--r-- | coverage/files.py | 90 |
1 files changed, 73 insertions, 17 deletions
diff --git a/coverage/files.py b/coverage/files.py index 154954d6..af2fe52f 100644 --- a/coverage/files.py +++ b/coverage/files.py @@ -1,3 +1,6 @@ +# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 +# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt + """File wrangling.""" import fnmatch @@ -9,11 +12,11 @@ import re import sys from coverage import env -from coverage.misc import CoverageException, join_regex +from coverage.backward import unicode_class +from coverage.misc import contract, CoverageException, join_regex, isolate_module -RELATIVE_DIR = None -CANONICAL_FILENAME_CACHE = {} +os = isolate_module(os) def set_relative_directory(): @@ -27,24 +30,29 @@ def set_relative_directory(): # avoid duplicating work. CANONICAL_FILENAME_CACHE = {} + def relative_directory(): """Return the directory that `relative_filename` is relative to.""" return RELATIVE_DIR + +@contract(returns='unicode') def relative_filename(filename): """Return the relative form of `filename`. - The filename will be relative to the current directory when the + The file name will be relative to the current directory when the `set_relative_directory` was called. """ fnorm = os.path.normcase(filename) if fnorm.startswith(RELATIVE_DIR): filename = filename[len(RELATIVE_DIR):] - return filename + return unicode_filename(filename) + +@contract(returns='unicode') def canonical_filename(filename): - """Return a canonical filename for `filename`. + """Return a canonical file name for `filename`. An absolute path with no redundant components and normalized case. @@ -55,7 +63,11 @@ def canonical_filename(filename): if path is None: continue f = os.path.join(path, filename) - if os.path.exists(f): + try: + exists = os.path.exists(f) + except UnicodeError: + exists = False + if exists: filename = f break cf = abs_file(filename) @@ -63,6 +75,20 @@ def canonical_filename(filename): return CANONICAL_FILENAME_CACHE[filename] +def flat_rootname(filename): + """A base for a flat file name to correspond to this file. + + Useful for writing files about the code where you want all the files in + the same directory, but need to differentiate same-named files from + different directories. + + For example, the file a/b/c.py will return 'a_b_c_py' + + """ + name = ntpath.splitdrive(filename)[1] + return re.sub(r"[\\/.:]", "_", name) + + if env.WINDOWS: _ACTUAL_PATH_CACHE = {} @@ -70,6 +96,8 @@ if env.WINDOWS: def actual_path(path): """Get the actual path of `path`, including the correct case.""" + if env.PY2 and isinstance(path, unicode_class): + path = path.encode(sys.getfilesystemencoding()) if path in _ACTUAL_PATH_CACHE: return _ACTUAL_PATH_CACHE[path] @@ -81,7 +109,7 @@ if env.WINDOWS: actpath = tail else: head = actual_path(head) - if head in _ACTUAL_PATH_LIST_CACHE: + if head in _ACTUAL_PATH_LIST_CACHE: files = _ACTUAL_PATH_LIST_CACHE[head] else: try: @@ -104,14 +132,40 @@ else: return filename +if env.PY2: + @contract(returns='unicode') + def unicode_filename(filename): + """Return a Unicode version of `filename`.""" + if isinstance(filename, str): + encoding = sys.getfilesystemencoding() or sys.getdefaultencoding() + filename = filename.decode(encoding, "replace") + return filename +else: + @contract(filename='unicode', returns='unicode') + def unicode_filename(filename): + """Return a Unicode version of `filename`.""" + return filename + + +@contract(returns='unicode') 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)) + try: + path = os.path.realpath(path) + except UnicodeError: + pass + path = os.path.abspath(path) path = actual_path(path) + path = unicode_filename(path) return path +RELATIVE_DIR = None +CANONICAL_FILENAME_CACHE = None +set_relative_directory() + + def isabs_anywhere(filename): """Is `filename` an absolute path on any OS?""" return ntpath.isabs(filename) or posixpath.isabs(filename) @@ -190,7 +244,7 @@ class ModuleMatcher(object): class FnmatchMatcher(object): - """A matcher for files by filename pattern.""" + """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 @@ -213,7 +267,7 @@ class FnmatchMatcher(object): return self.pats def match(self, fpath): - """Does `fpath` match one of our filename patterns?""" + """Does `fpath` match one of our file name patterns?""" return self.re.match(fpath) is not None @@ -267,8 +321,11 @@ class PathAliases(object): pattern += pattern_sep # Make a regex from the pattern. fnmatch always adds a \Z to - # match the whole string, which we don't want. - regex_pat = fnmatch.translate(pattern).replace(r'\Z(', '(') + # 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. @@ -292,10 +349,9 @@ class PathAliases(object): The separator style in the result is made to match that of the result in the alias. - Returns: - The mapped path. If a mapping has happened, this is a - canonical path. If no mapping has happened, it is the - original value of `path` unchanged. + Returns the mapped path. If a mapping has happened, this is a + canonical path. If no mapping has happened, it is the original value + of `path` unchanged. """ for regex, result, pattern_sep, result_sep in self.aliases: |