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