DebugClients/Python3/coverage/files.py

changeset 4489
d0d6e4ad31bd
parent 3495
fac17a82b431
child 5051
3586ebd9fac8
equal deleted inserted replaced
4481:456c58fc64b0 4489:d0d6e4ad31bd
1 # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
2 # For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt
3
1 """File wrangling.""" 4 """File wrangling."""
2 5
3 from .backward import to_string 6 import fnmatch
4 from .misc import CoverageException 7 import ntpath
5 import fnmatch, os, os.path, re, sys 8 import os
6 import ntpath, posixpath 9 import os.path
7 10 import posixpath
8 class FileLocator(object): 11 import re
9 """Understand how filenames work.""" 12 import sys
10 13
11 def __init__(self): 14 from coverage import env
12 # The absolute path to our current directory. 15 from coverage.backward import unicode_class
13 self.relative_dir = os.path.normcase(abs_file(os.curdir) + os.sep) 16 from coverage.misc import CoverageException, join_regex
14 17
15 # Cache of results of calling the canonical_filename() method, to 18
16 # avoid duplicating work. 19 RELATIVE_DIR = None
17 self.canonical_filename_cache = {} 20 CANONICAL_FILENAME_CACHE = {}
18 21
19 def relative_filename(self, filename): 22
20 """Return the relative form of `filename`. 23 def set_relative_directory():
21 24 """Set the directory that `relative_filename` will be relative to."""
22 The filename will be relative to the current directory when the 25 global RELATIVE_DIR, CANONICAL_FILENAME_CACHE
23 `FileLocator` was constructed. 26
24 27 # The absolute path to our current directory.
25 """ 28 RELATIVE_DIR = os.path.normcase(abs_file(os.curdir) + os.sep)
26 fnorm = os.path.normcase(filename) 29
27 if fnorm.startswith(self.relative_dir): 30 # Cache of results of calling the canonical_filename() method, to
28 filename = filename[len(self.relative_dir):] 31 # avoid duplicating work.
29 return filename 32 CANONICAL_FILENAME_CACHE = {}
30 33
31 def canonical_filename(self, filename): 34 def relative_directory():
32 """Return a canonical filename for `filename`. 35 """Return the directory that `relative_filename` is relative to."""
33 36 return RELATIVE_DIR
34 An absolute path with no redundant components and normalized case. 37
35 38 def relative_filename(filename):
36 """ 39 """Return the relative form of `filename`.
37 if filename not in self.canonical_filename_cache: 40
38 if not os.path.isabs(filename): 41 The file name will be relative to the current directory when the
39 for path in [os.curdir] + sys.path: 42 `set_relative_directory` was called.
40 if path is None: 43
41 continue 44 """
42 f = os.path.join(path, filename) 45 fnorm = os.path.normcase(filename)
43 if os.path.exists(f): 46 if fnorm.startswith(RELATIVE_DIR):
44 filename = f 47 filename = filename[len(RELATIVE_DIR):]
45 break 48 return filename
46 cf = abs_file(filename) 49
47 self.canonical_filename_cache[filename] = cf 50 def canonical_filename(filename):
48 return self.canonical_filename_cache[filename] 51 """Return a canonical file name for `filename`.
49 52
50 def get_zip_data(self, filename): 53 An absolute path with no redundant components and normalized case.
51 """Get data from `filename` if it is a zip file path. 54
52 55 """
53 Returns the string data read from the zip file, or None if no zip file 56 if filename not in CANONICAL_FILENAME_CACHE:
54 could be found or `filename` isn't in it. The data returned will be 57 if not os.path.isabs(filename):
55 an empty string if the file is empty. 58 for path in [os.curdir] + sys.path:
56 59 if path is None:
57 """
58 import zipimport
59 markers = ['.zip'+os.sep, '.egg'+os.sep]
60 for marker in markers:
61 if marker in filename:
62 parts = filename.split(marker)
63 try:
64 zi = zipimport.zipimporter(parts[0]+marker[:-1])
65 except zipimport.ZipImportError:
66 continue 60 continue
67 try: 61 f = os.path.join(path, filename)
68 data = zi.get_data(parts[1]) 62 if os.path.exists(f):
69 except IOError: 63 filename = f
70 continue 64 break
71 return to_string(data) 65 cf = abs_file(filename)
72 return None 66 CANONICAL_FILENAME_CACHE[filename] = cf
73 67 return CANONICAL_FILENAME_CACHE[filename]
74 68
75 if sys.platform == 'win32': 69
70 def flat_rootname(filename):
71 """A base for a flat file name to correspond to this file.
72
73 Useful for writing files about the code where you want all the files in
74 the same directory, but need to differentiate same-named files from
75 different directories.
76
77 For example, the file a/b/c.py will return 'a_b_c_py'
78
79 """
80 name = ntpath.splitdrive(filename)[1]
81 return re.sub(r"[\\/.:]", "_", name)
82
83
84 if env.WINDOWS:
85
86 _ACTUAL_PATH_CACHE = {}
87 _ACTUAL_PATH_LIST_CACHE = {}
76 88
77 def actual_path(path): 89 def actual_path(path):
78 """Get the actual path of `path`, including the correct case.""" 90 """Get the actual path of `path`, including the correct case."""
79 if path in actual_path.cache: 91 if env.PY2 and isinstance(path, unicode_class):
80 return actual_path.cache[path] 92 path = path.encode(sys.getfilesystemencoding())
93 if path in _ACTUAL_PATH_CACHE:
94 return _ACTUAL_PATH_CACHE[path]
81 95
82 head, tail = os.path.split(path) 96 head, tail = os.path.split(path)
83 if not tail: 97 if not tail:
84 actpath = head 98 # This means head is the drive spec: normalize it.
99 actpath = head.upper()
85 elif not head: 100 elif not head:
86 actpath = tail 101 actpath = tail
87 else: 102 else:
88 head = actual_path(head) 103 head = actual_path(head)
89 if head in actual_path.list_cache: 104 if head in _ACTUAL_PATH_LIST_CACHE:
90 files = actual_path.list_cache[head] 105 files = _ACTUAL_PATH_LIST_CACHE[head]
91 else: 106 else:
92 try: 107 try:
93 files = os.listdir(head) 108 files = os.listdir(head)
94 except OSError: 109 except OSError:
95 files = [] 110 files = []
96 actual_path.list_cache[head] = files 111 _ACTUAL_PATH_LIST_CACHE[head] = files
97 normtail = os.path.normcase(tail) 112 normtail = os.path.normcase(tail)
98 for f in files: 113 for f in files:
99 if os.path.normcase(f) == normtail: 114 if os.path.normcase(f) == normtail:
100 tail = f 115 tail = f
101 break 116 break
102 actpath = os.path.join(head, tail) 117 actpath = os.path.join(head, tail)
103 actual_path.cache[path] = actpath 118 _ACTUAL_PATH_CACHE[path] = actpath
104 return actpath 119 return actpath
105
106 actual_path.cache = {}
107 actual_path.list_cache = {}
108 120
109 else: 121 else:
110 def actual_path(filename): 122 def actual_path(filename):
111 """The actual path for non-Windows platforms.""" 123 """The actual path for non-Windows platforms."""
112 return filename 124 return filename
135 If `patterns` is None, an empty list is returned. 147 If `patterns` is None, an empty list is returned.
136 148
137 """ 149 """
138 prepped = [] 150 prepped = []
139 for p in patterns or []: 151 for p in patterns or []:
140 if p.startswith("*") or p.startswith("?"): 152 if p.startswith(("*", "?")):
141 prepped.append(p) 153 prepped.append(p)
142 else: 154 else:
143 prepped.append(abs_file(p)) 155 prepped.append(abs_file(p))
144 return prepped 156 return prepped
145 157
146 158
147 class TreeMatcher(object): 159 class TreeMatcher(object):
148 """A matcher for files in a tree.""" 160 """A matcher for files in a tree."""
149 def __init__(self, directories): 161 def __init__(self, directories):
150 self.dirs = directories[:] 162 self.dirs = list(directories)
151 163
152 def __repr__(self): 164 def __repr__(self):
153 return "<TreeMatcher %r>" % self.dirs 165 return "<TreeMatcher %r>" % self.dirs
154 166
155 def info(self): 167 def info(self):
156 """A list of strings for displaying when dumping state.""" 168 """A list of strings for displaying when dumping state."""
157 return self.dirs 169 return self.dirs
158
159 def add(self, directory):
160 """Add another directory to the list we match for."""
161 self.dirs.append(directory)
162 170
163 def match(self, fpath): 171 def match(self, fpath):
164 """Does `fpath` indicate a file in one of our trees?""" 172 """Does `fpath` indicate a file in one of our trees?"""
165 for d in self.dirs: 173 for d in self.dirs:
166 if fpath.startswith(d): 174 if fpath.startswith(d):
171 # This is a file in the directory 179 # This is a file in the directory
172 return True 180 return True
173 return False 181 return False
174 182
175 183
184 class ModuleMatcher(object):
185 """A matcher for modules in a tree."""
186 def __init__(self, module_names):
187 self.modules = list(module_names)
188
189 def __repr__(self):
190 return "<ModuleMatcher %r>" % (self.modules)
191
192 def info(self):
193 """A list of strings for displaying when dumping state."""
194 return self.modules
195
196 def match(self, module_name):
197 """Does `module_name` indicate a module in one of our packages?"""
198 if not module_name:
199 return False
200
201 for m in self.modules:
202 if module_name.startswith(m):
203 if module_name == m:
204 return True
205 if module_name[len(m)] == '.':
206 # This is a module in the package
207 return True
208
209 return False
210
211
176 class FnmatchMatcher(object): 212 class FnmatchMatcher(object):
177 """A matcher for files by filename pattern.""" 213 """A matcher for files by file name pattern."""
178 def __init__(self, pats): 214 def __init__(self, pats):
179 self.pats = pats[:] 215 self.pats = pats[:]
216 # fnmatch is platform-specific. On Windows, it does the Windows thing
217 # of treating / and \ as equivalent. But on other platforms, we need to
218 # take care of that ourselves.
219 fnpats = (fnmatch.translate(p) for p in pats)
220 fnpats = (p.replace(r"\/", r"[\\/]") for p in fnpats)
221 if env.WINDOWS:
222 # Windows is also case-insensitive. BTW: the regex docs say that
223 # flags like (?i) have to be at the beginning, but fnmatch puts
224 # them at the end, and having two there seems to work fine.
225 fnpats = (p + "(?i)" for p in fnpats)
226 self.re = re.compile(join_regex(fnpats))
180 227
181 def __repr__(self): 228 def __repr__(self):
182 return "<FnmatchMatcher %r>" % self.pats 229 return "<FnmatchMatcher %r>" % self.pats
183 230
184 def info(self): 231 def info(self):
185 """A list of strings for displaying when dumping state.""" 232 """A list of strings for displaying when dumping state."""
186 return self.pats 233 return self.pats
187 234
188 def match(self, fpath): 235 def match(self, fpath):
189 """Does `fpath` match one of our filename patterns?""" 236 """Does `fpath` match one of our file name patterns?"""
190 for pat in self.pats: 237 return self.re.match(fpath) is not None
191 if fnmatch.fnmatch(fpath, pat):
192 return True
193 return False
194 238
195 239
196 def sep(s): 240 def sep(s):
197 """Find the path separator used in this string, or os.sep if none.""" 241 """Find the path separator used in this string, or os.sep if none."""
198 sep_match = re.search(r"[\\/]", s) 242 sep_match = re.search(r"[\\/]", s)
211 serialized checkouts on continuous integration machines. 255 serialized checkouts on continuous integration machines.
212 256
213 A `PathAliases` object tracks a list of pattern/result pairs, and can 257 A `PathAliases` object tracks a list of pattern/result pairs, and can
214 map a path through those aliases to produce a unified path. 258 map a path through those aliases to produce a unified path.
215 259
216 `locator` is a FileLocator that is used to canonicalize the results. 260 """
217 261 def __init__(self):
218 """
219 def __init__(self, locator=None):
220 self.aliases = [] 262 self.aliases = []
221 self.locator = locator
222 263
223 def add(self, pattern, result): 264 def add(self, pattern, result):
224 """Add the `pattern`/`result` pair to the list of aliases. 265 """Add the `pattern`/`result` pair to the list of aliases.
225 266
226 `pattern` is an `fnmatch`-style pattern. `result` is a simple 267 `pattern` is an `fnmatch`-style pattern. `result` is a simple
243 # unless it already is, or is meant to match any prefix. 284 # unless it already is, or is meant to match any prefix.
244 if not pattern.startswith('*') and not isabs_anywhere(pattern): 285 if not pattern.startswith('*') and not isabs_anywhere(pattern):
245 pattern = abs_file(pattern) 286 pattern = abs_file(pattern)
246 pattern += pattern_sep 287 pattern += pattern_sep
247 288
248 # Make a regex from the pattern. fnmatch always adds a \Z or $ to 289 # Make a regex from the pattern. fnmatch always adds a \Z to
249 # match the whole string, which we don't want. 290 # match the whole string, which we don't want.
250 regex_pat = fnmatch.translate(pattern).replace(r'\Z(', '(') 291 regex_pat = fnmatch.translate(pattern).replace(r'\Z(', '(')
251 if regex_pat.endswith("$"): 292
252 regex_pat = regex_pat[:-1]
253 # We want */a/b.py to match on Windows too, so change slash to match 293 # We want */a/b.py to match on Windows too, so change slash to match
254 # either separator. 294 # either separator.
255 regex_pat = regex_pat.replace(r"\/", r"[\\/]") 295 regex_pat = regex_pat.replace(r"\/", r"[\\/]")
256 # We want case-insensitive matching, so add that flag. 296 # We want case-insensitive matching, so add that flag.
257 regex = re.compile(r"(?i)" + regex_pat) 297 regex = re.compile(r"(?i)" + regex_pat)
269 Only one pattern is ever used. If no patterns match, `path` is 309 Only one pattern is ever used. If no patterns match, `path` is
270 returned unchanged. 310 returned unchanged.
271 311
272 The separator style in the result is made to match that of the result 312 The separator style in the result is made to match that of the result
273 in the alias. 313 in the alias.
314
315 Returns the mapped path. If a mapping has happened, this is a
316 canonical path. If no mapping has happened, it is the original value
317 of `path` unchanged.
274 318
275 """ 319 """
276 for regex, result, pattern_sep, result_sep in self.aliases: 320 for regex, result, pattern_sep, result_sep in self.aliases:
277 m = regex.match(path) 321 m = regex.match(path)
278 if m: 322 if m:
279 new = path.replace(m.group(0), result) 323 new = path.replace(m.group(0), result)
280 if pattern_sep != result_sep: 324 if pattern_sep != result_sep:
281 new = new.replace(pattern_sep, result_sep) 325 new = new.replace(pattern_sep, result_sep)
282 if self.locator: 326 new = canonical_filename(new)
283 new = self.locator.canonical_filename(new)
284 return new 327 return new
285 return path 328 return path
286 329
287 330
288 def find_python_files(dirname): 331 def find_python_files(dirname):
289 """Yield all of the importable Python files in `dirname`, recursively. 332 """Yield all of the importable Python files in `dirname`, recursively.
290 333
291 To be importable, the files have to be in a directory with a __init__.py, 334 To be importable, the files have to be in a directory with a __init__.py,
292 except for `dirname` itself, which isn't required to have one. The 335 except for `dirname` itself, which isn't required to have one. The
293 assumption is that `dirname` was specified directly, so the user knows 336 assumption is that `dirname` was specified directly, so the user knows
294 best, but subdirectories are checked for a __init__.py to be sure we only 337 best, but sub-directories are checked for a __init__.py to be sure we only
295 find the importable files. 338 find the importable files.
296 339
297 """ 340 """
298 for i, (dirpath, dirnames, filenames) in enumerate(os.walk(dirname)): 341 for i, (dirpath, dirnames, filenames) in enumerate(os.walk(dirname)):
299 if i > 0 and '__init__.py' not in filenames: 342 if i > 0 and '__init__.py' not in filenames:

eric ide

mercurial