diff options
Diffstat (limited to 'coverage/files.py')
-rw-r--r-- | coverage/files.py | 159 |
1 files changed, 107 insertions, 52 deletions
diff --git a/coverage/files.py b/coverage/files.py index f7fc9693..44997d12 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,100 +12,152 @@ 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 -class FileLocator(object): - """Understand how filenames work.""" +os = isolate_module(os) - 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 set_relative_directory(): + """Set the directory that `relative_filename` will be relative to.""" + global RELATIVE_DIR, CANONICAL_FILENAME_CACHE - def relative_filename(self, filename): - """Return the relative form of `filename`. + # The absolute path to our current directory. + RELATIVE_DIR = os.path.normcase(abs_file(os.curdir) + os.sep) - The filename will be relative to the current directory when the - `FileLocator` was constructed. + # Cache of results of calling the canonical_filename() method, to + # avoid duplicating work. + CANONICAL_FILENAME_CACHE = {} - """ - 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`. +def relative_directory(): + """Return the directory that `relative_filename` is relative to.""" + return RELATIVE_DIR - 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] +@contract(returns='unicode') +def relative_filename(filename): + """Return the relative form of `filename`. + + 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 unicode_filename(filename) + + +@contract(returns='unicode') +def canonical_filename(filename): + """Return a canonical file name for `filename`. + + An absolute path with no redundant components and normalized case. + + """ + if filename not in 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) + CANONICAL_FILENAME_CACHE[filename] = cf + 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 = {} + _ACTUAL_PATH_LIST_CACHE = {} + 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] + if env.PY2 and isinstance(path, unicode_class): + path = path.encode(sys.getfilesystemencoding()) + if path in _ACTUAL_PATH_CACHE: + return _ACTUAL_PATH_CACHE[path] head, tail = os.path.split(path) if not tail: - actpath = head + # This means head is the drive spec: normalize it. + actpath = head.upper() elif not head: actpath = tail else: head = actual_path(head) - if head in actual_path.list_cache: - files = actual_path.list_cache[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 + _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 + _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 +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)) 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) @@ -181,7 +236,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 @@ -204,7 +259,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 @@ -228,12 +283,9 @@ class PathAliases(object): 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): + def __init__(self): self.aliases = [] - self.locator = locator def add(self, pattern, result): """Add the `pattern`/`result` pair to the list of aliases. @@ -286,6 +338,10 @@ 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. + """ for regex, result, pattern_sep, result_sep in self.aliases: m = regex.match(path) @@ -293,8 +349,7 @@ class PathAliases(object): 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) + new = canonical_filename(new) return new return path |