|
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) |
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: |