summaryrefslogtreecommitdiff
path: root/coverage/files.py
diff options
context:
space:
mode:
Diffstat (limited to 'coverage/files.py')
-rw-r--r--coverage/files.py159
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