DebugClients/Python/coverage/files.py

changeset 4491
0d8612e24fef
parent 4490
3f58261e7bb1
child 5051
3586ebd9fac8
equal deleted inserted replaced
4487:4ba7a8ab24f2 4491:0d8612e24fef
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 if isinstance(self.relative_dir, str): 17
15 self.relative_dir = self.relative_dir.decode(sys.getfilesystemencoding()) 18
16 19 RELATIVE_DIR = None
17 # Cache of results of calling the canonical_filename() method, to 20 CANONICAL_FILENAME_CACHE = {}
18 # avoid duplicating work. 21
19 self.canonical_filename_cache = {} 22
20 23 def set_relative_directory():
21 def relative_filename(self, filename): 24 """Set the directory that `relative_filename` will be relative to."""
22 """Return the relative form of `filename`. 25 global RELATIVE_DIR, CANONICAL_FILENAME_CACHE
23 26
24 The filename will be relative to the current directory when the 27 # The absolute path to our current directory.
25 `FileLocator` was constructed. 28 RELATIVE_DIR = os.path.normcase(abs_file(os.curdir) + os.sep)
26 29
27 """ 30 # Cache of results of calling the canonical_filename() method, to
28 if isinstance(filename, str): 31 # avoid duplicating work.
29 filename = filename.decode(sys.getfilesystemencoding()) 32 CANONICAL_FILENAME_CACHE = {}
30 fnorm = os.path.normcase(filename) 33
31 if fnorm.startswith(self.relative_dir): 34 def relative_directory():
32 filename = filename[len(self.relative_dir):] 35 """Return the directory that `relative_filename` is relative to."""
33 return filename 36 return RELATIVE_DIR
34 37
35 def canonical_filename(self, filename): 38 def relative_filename(filename):
36 """Return a canonical filename for `filename`. 39 """Return the relative form of `filename`.
37 40
38 An absolute path with no redundant components and normalized case. 41 The file name will be relative to the current directory when the
39 42 `set_relative_directory` was called.
40 """ 43
41 if filename not in self.canonical_filename_cache: 44 """
42 if not os.path.isabs(filename): 45 fnorm = os.path.normcase(filename)
43 for path in [os.curdir] + sys.path: 46 if fnorm.startswith(RELATIVE_DIR):
44 if path is None: 47 filename = filename[len(RELATIVE_DIR):]
45 continue 48 return filename
46 f = os.path.join(path, filename) 49
47 if os.path.exists(f): 50 def canonical_filename(filename):
48 filename = f 51 """Return a canonical file name for `filename`.
49 break 52
50 cf = abs_file(filename) 53 An absolute path with no redundant components and normalized case.
51 self.canonical_filename_cache[filename] = cf 54
52 return self.canonical_filename_cache[filename] 55 """
53 56 if filename not in CANONICAL_FILENAME_CACHE:
54 def get_zip_data(self, filename): 57 if not os.path.isabs(filename):
55 """Get data from `filename` if it is a zip file path. 58 for path in [os.curdir] + sys.path:
56 59 if path is None:
57 Returns the string data read from the zip file, or None if no zip file
58 could be found or `filename` isn't in it. The data returned will be
59 an empty string if the file is empty.
60
61 """
62 import zipimport
63 markers = ['.zip'+os.sep, '.egg'+os.sep]
64 for marker in markers:
65 if marker in filename:
66 parts = filename.split(marker)
67 try:
68 zi = zipimport.zipimporter(parts[0]+marker[:-1])
69 except zipimport.ZipImportError:
70 continue 60 continue
71 try: 61 f = path + os.sep + filename
72 data = zi.get_data(parts[1]) 62 if os.path.exists(f):
73 except IOError: 63 filename = f
74 continue 64 break
75 return to_string(data) 65 cf = abs_file(filename)
76 return None 66 CANONICAL_FILENAME_CACHE[filename] = cf
77 67 return CANONICAL_FILENAME_CACHE[filename]
78 68
79 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 = {}
80 88
81 def actual_path(path): 89 def actual_path(path):
82 """Get the actual path of `path`, including the correct case.""" 90 """Get the actual path of `path`, including the correct case."""
83 if path in actual_path.cache: 91 if env.PY2 and isinstance(path, unicode_class):
84 return actual_path.cache[path] 92 path = path.encode(sys.getfilesystemencoding())
93 if path in _ACTUAL_PATH_CACHE:
94 return _ACTUAL_PATH_CACHE[path]
85 95
86 head, tail = os.path.split(path) 96 head, tail = os.path.split(path)
87 if not tail: 97 if not tail:
88 actpath = head 98 # This means head is the drive spec: normalize it.
99 actpath = head.upper()
89 elif not head: 100 elif not head:
90 actpath = tail 101 actpath = tail
91 else: 102 else:
92 head = actual_path(head) 103 head = actual_path(head)
93 if head in actual_path.list_cache: 104 if head in _ACTUAL_PATH_LIST_CACHE:
94 files = actual_path.list_cache[head] 105 files = _ACTUAL_PATH_LIST_CACHE[head]
95 else: 106 else:
96 try: 107 try:
97 files = os.listdir(head) 108 files = os.listdir(head)
98 except OSError: 109 except OSError:
99 files = [] 110 files = []
100 actual_path.list_cache[head] = files 111 _ACTUAL_PATH_LIST_CACHE[head] = files
101 normtail = os.path.normcase(tail) 112 normtail = os.path.normcase(tail)
102 for f in files: 113 for f in files:
103 if os.path.normcase(f) == normtail: 114 if os.path.normcase(f) == normtail:
104 tail = f 115 tail = f
105 break 116 break
106 actpath = os.path.join(head, tail) 117 actpath = head.strip(os.sep) + os.sep + tail
107 actual_path.cache[path] = actpath 118 _ACTUAL_PATH_CACHE[path] = actpath
108 return actpath 119 return actpath
109
110 actual_path.cache = {}
111 actual_path.list_cache = {}
112 120
113 else: 121 else:
114 def actual_path(filename): 122 def actual_path(filename):
115 """The actual path for non-Windows platforms.""" 123 """The actual path for non-Windows platforms."""
116 return filename 124 return filename
139 If `patterns` is None, an empty list is returned. 147 If `patterns` is None, an empty list is returned.
140 148
141 """ 149 """
142 prepped = [] 150 prepped = []
143 for p in patterns or []: 151 for p in patterns or []:
144 if p.startswith("*") or p.startswith("?"): 152 if p.startswith(("*", "?")):
145 prepped.append(p) 153 prepped.append(p)
146 else: 154 else:
147 prepped.append(abs_file(p)) 155 prepped.append(abs_file(p))
148 return prepped 156 return prepped
149 157
150 158
151 class TreeMatcher(object): 159 class TreeMatcher(object):
152 """A matcher for files in a tree.""" 160 """A matcher for files in a tree."""
153 def __init__(self, directories): 161 def __init__(self, directories):
154 self.dirs = directories[:] 162 self.dirs = list(directories)
155 163
156 def __repr__(self): 164 def __repr__(self):
157 return "<TreeMatcher %r>" % self.dirs 165 return "<TreeMatcher %r>" % self.dirs
158 166
159 def info(self): 167 def info(self):
160 """A list of strings for displaying when dumping state.""" 168 """A list of strings for displaying when dumping state."""
161 return self.dirs 169 return self.dirs
162
163 def add(self, directory):
164 """Add another directory to the list we match for."""
165 self.dirs.append(directory)
166 170
167 def match(self, fpath): 171 def match(self, fpath):
168 """Does `fpath` indicate a file in one of our trees?""" 172 """Does `fpath` indicate a file in one of our trees?"""
169 for d in self.dirs: 173 for d in self.dirs:
170 if fpath.startswith(d): 174 if fpath.startswith(d):
175 # This is a file in the directory 179 # This is a file in the directory
176 return True 180 return True
177 return False 181 return False
178 182
179 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
180 class FnmatchMatcher(object): 212 class FnmatchMatcher(object):
181 """A matcher for files by filename pattern.""" 213 """A matcher for files by file name pattern."""
182 def __init__(self, pats): 214 def __init__(self, pats):
183 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))
184 227
185 def __repr__(self): 228 def __repr__(self):
186 return "<FnmatchMatcher %r>" % self.pats 229 return "<FnmatchMatcher %r>" % self.pats
187 230
188 def info(self): 231 def info(self):
189 """A list of strings for displaying when dumping state.""" 232 """A list of strings for displaying when dumping state."""
190 return self.pats 233 return self.pats
191 234
192 def match(self, fpath): 235 def match(self, fpath):
193 """Does `fpath` match one of our filename patterns?""" 236 """Does `fpath` match one of our file name patterns?"""
194 for pat in self.pats: 237 return self.re.match(fpath) is not None
195 if fnmatch.fnmatch(fpath, pat):
196 return True
197 return False
198 238
199 239
200 def sep(s): 240 def sep(s):
201 """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."""
202 sep_match = re.search(r"[\\/]", s) 242 sep_match = re.search(r"[\\/]", s)
215 serialized checkouts on continuous integration machines. 255 serialized checkouts on continuous integration machines.
216 256
217 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
218 map a path through those aliases to produce a unified path. 258 map a path through those aliases to produce a unified path.
219 259
220 `locator` is a FileLocator that is used to canonicalize the results. 260 """
221 261 def __init__(self):
222 """
223 def __init__(self, locator=None):
224 self.aliases = [] 262 self.aliases = []
225 self.locator = locator
226 263
227 def add(self, pattern, result): 264 def add(self, pattern, result):
228 """Add the `pattern`/`result` pair to the list of aliases. 265 """Add the `pattern`/`result` pair to the list of aliases.
229 266
230 `pattern` is an `fnmatch`-style pattern. `result` is a simple 267 `pattern` is an `fnmatch`-style pattern. `result` is a simple
247 # unless it already is, or is meant to match any prefix. 284 # unless it already is, or is meant to match any prefix.
248 if not pattern.startswith('*') and not isabs_anywhere(pattern): 285 if not pattern.startswith('*') and not isabs_anywhere(pattern):
249 pattern = abs_file(pattern) 286 pattern = abs_file(pattern)
250 pattern += pattern_sep 287 pattern += pattern_sep
251 288
252 # 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
253 # match the whole string, which we don't want. 290 # match the whole string, which we don't want.
254 regex_pat = fnmatch.translate(pattern).replace(r'\Z(', '(') 291 regex_pat = fnmatch.translate(pattern).replace(r'\Z(', '(')
255 if regex_pat.endswith("$"): 292
256 regex_pat = regex_pat[:-1]
257 # 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
258 # either separator. 294 # either separator.
259 regex_pat = regex_pat.replace(r"\/", r"[\\/]") 295 regex_pat = regex_pat.replace(r"\/", r"[\\/]")
260 # We want case-insensitive matching, so add that flag. 296 # We want case-insensitive matching, so add that flag.
261 regex = re.compile(r"(?i)" + regex_pat) 297 regex = re.compile(r"(?i)" + regex_pat)
273 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
274 returned unchanged. 310 returned unchanged.
275 311
276 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
277 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.
278 318
279 """ 319 """
280 for regex, result, pattern_sep, result_sep in self.aliases: 320 for regex, result, pattern_sep, result_sep in self.aliases:
281 m = regex.match(path) 321 m = regex.match(path)
282 if m: 322 if m:
283 new = path.replace(m.group(0), result) 323 new = path.replace(m.group(0), result)
284 if pattern_sep != result_sep: 324 if pattern_sep != result_sep:
285 new = new.replace(pattern_sep, result_sep) 325 new = new.replace(pattern_sep, result_sep)
286 if self.locator: 326 new = canonical_filename(new)
287 new = self.locator.canonical_filename(new)
288 return new 327 return new
289 return path 328 return path
290 329
291 330
292 def find_python_files(dirname): 331 def find_python_files(dirname):
293 """Yield all of the importable Python files in `dirname`, recursively. 332 """Yield all of the importable Python files in `dirname`, recursively.
294 333
295 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,
296 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
297 assumption is that `dirname` was specified directly, so the user knows 336 assumption is that `dirname` was specified directly, so the user knows
298 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
299 find the importable files. 338 find the importable files.
300 339
301 """ 340 """
302 for i, (dirpath, dirnames, filenames) in enumerate(os.walk(dirname)): 341 for i, (dirpath, dirnames, filenames) in enumerate(os.walk(dirname)):
303 if i > 0 and '__init__.py' not in filenames: 342 if i > 0 and '__init__.py' not in filenames:
308 for filename in filenames: 347 for filename in filenames:
309 # We're only interested in files that look like reasonable Python 348 # We're only interested in files that look like reasonable Python
310 # files: Must end with .py or .pyw, and must not have certain funny 349 # files: Must end with .py or .pyw, and must not have certain funny
311 # characters that probably mean they are editor junk. 350 # characters that probably mean they are editor junk.
312 if re.match(r"^[^.#~!$@%^&*()+=,]+\.pyw?$", filename): 351 if re.match(r"^[^.#~!$@%^&*()+=,]+\.pyw?$", filename):
313 yield os.path.join(dirpath, filename) 352 yield dirpath + os.sep + filename
314 353
315 # 354 #
316 # eflag: FileType = Python2 355 # eflag: FileType = Python2

eric ide

mercurial