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