--- a/DebugClients/Python/coverage/files.py Sun Oct 04 13:35:09 2015 +0200 +++ b/DebugClients/Python/coverage/files.py Sun Oct 04 22:37:56 2015 +0200 @@ -1,115 +1,123 @@ +# 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.""" -from .backward import to_string -from .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) - if isinstance(self.relative_dir, str): - self.relative_dir = self.relative_dir.decode(sys.getfilesystemencoding()) - - # 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. - - """ - if isinstance(filename, str): - filename = filename.decode(sys.getfilesystemencoding()) - 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. +import fnmatch +import ntpath +import os +import os.path +import posixpath +import re +import sys - """ - 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. +from coverage import env +from coverage.backward import unicode_class +from coverage.misc import CoverageException, join_regex - 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 +RELATIVE_DIR = None +CANONICAL_FILENAME_CACHE = {} -if sys.platform == 'win32': +def set_relative_directory(): + """Set the directory that `relative_filename` will be relative to.""" + global RELATIVE_DIR, CANONICAL_FILENAME_CACHE + + # The absolute path to our current directory. + RELATIVE_DIR = os.path.normcase(abs_file(os.curdir) + os.sep) + + # Cache of results of calling the canonical_filename() method, to + # avoid duplicating work. + CANONICAL_FILENAME_CACHE = {} + +def relative_directory(): + """Return the directory that `relative_filename` is relative to.""" + return RELATIVE_DIR + +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 filename + +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.""" @@ -141,7 +149,7 @@ """ prepped = [] for p in patterns or []: - if p.startswith("*") or p.startswith("?"): + if p.startswith(("*", "?")): prepped.append(p) else: prepped.append(abs_file(p)) @@ -151,7 +159,7 @@ class TreeMatcher(object): """A matcher for files in a tree.""" def __init__(self, directories): - self.dirs = directories[:] + self.dirs = list(directories) def __repr__(self): return "<TreeMatcher %r>" % self.dirs @@ -160,10 +168,6 @@ """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: @@ -177,10 +181,49 @@ return False +class ModuleMatcher(object): + """A matcher for modules in a tree.""" + def __init__(self, module_names): + self.modules = list(module_names) + + def __repr__(self): + return "<ModuleMatcher %r>" % (self.modules) + + def info(self): + """A list of strings for displaying when dumping state.""" + return self.modules + + def match(self, module_name): + """Does `module_name` indicate a module in one of our packages?""" + if not module_name: + return False + + for m in self.modules: + if module_name.startswith(m): + if module_name == m: + return True + if module_name[len(m)] == '.': + # This is a module in the package + return True + + return False + + 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 + # of treating / and \ as equivalent. But on other platforms, we need to + # take care of that ourselves. + fnpats = (fnmatch.translate(p) for p in pats) + fnpats = (p.replace(r"\/", r"[\\/]") for p in fnpats) + if env.WINDOWS: + # Windows is also case-insensitive. BTW: the regex docs say that + # flags like (?i) have to be at the beginning, but fnmatch puts + # them at the end, and having two there seems to work fine. + fnpats = (p + "(?i)" for p in fnpats) + self.re = re.compile(join_regex(fnpats)) def __repr__(self): return "<FnmatchMatcher %r>" % self.pats @@ -190,11 +233,8 @@ 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 + """Does `fpath` match one of our file name patterns?""" + return self.re.match(fpath) is not None def sep(s): @@ -217,12 +257,9 @@ 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. @@ -249,11 +286,10 @@ pattern = abs_file(pattern) pattern += pattern_sep - # Make a regex from the pattern. fnmatch always adds a \Z or $ to + # 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(', '(') - 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"[\\/]") @@ -276,6 +312,10 @@ 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) @@ -283,8 +323,7 @@ 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 @@ -295,7 +334,7 @@ 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 + best, but sub-directories are checked for a __init__.py to be sure we only find the importable files. """ @@ -311,6 +350,3 @@ # characters that probably mean they are editor junk. if re.match(r"^[^.#~!$@%^&*()+=,]+\.pyw?$", filename): yield os.path.join(dirpath, filename) - -# -# eflag: FileType = Python2