OLD | NEW |
1 """File wrangling.""" | 1 """File wrangling.""" |
2 | 2 |
3 from coverage.backward import to_string | 3 from coverage.backward import to_string |
4 from coverage.misc import CoverageException | 4 from coverage.misc import CoverageException |
5 import fnmatch, os, os.path, re, sys | 5 import fnmatch, os, os.path, re, sys |
| 6 import ntpath, posixpath |
6 | 7 |
7 class FileLocator(object): | 8 class FileLocator(object): |
8 """Understand how filenames work.""" | 9 """Understand how filenames work.""" |
9 | 10 |
10 def __init__(self): | 11 def __init__(self): |
11 # The absolute path to our current directory. | 12 # The absolute path to our current directory. |
12 self.relative_dir = os.path.normcase(abs_file(os.curdir) + os.sep) | 13 self.relative_dir = os.path.normcase(abs_file(os.curdir) + os.sep) |
13 | 14 |
14 # Cache of results of calling the canonical_filename() method, to | 15 # Cache of results of calling the canonical_filename() method, to |
15 # avoid duplicating work. | 16 # avoid duplicating work. |
(...skipping 87 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
103 return actpath | 104 return actpath |
104 | 105 |
105 actual_path.cache = {} | 106 actual_path.cache = {} |
106 actual_path.list_cache = {} | 107 actual_path.list_cache = {} |
107 | 108 |
108 else: | 109 else: |
109 def actual_path(filename): | 110 def actual_path(filename): |
110 """The actual path for non-Windows platforms.""" | 111 """The actual path for non-Windows platforms.""" |
111 return filename | 112 return filename |
112 | 113 |
| 114 |
113 def abs_file(filename): | 115 def abs_file(filename): |
114 """Return the absolute normalized form of `filename`.""" | 116 """Return the absolute normalized form of `filename`.""" |
115 path = os.path.abspath(os.path.realpath(filename)) | 117 path = os.path.expandvars(os.path.expanduser(filename)) |
| 118 path = os.path.abspath(os.path.realpath(path)) |
116 path = actual_path(path) | 119 path = actual_path(path) |
117 return path | 120 return path |
118 | 121 |
119 | 122 |
| 123 def isabs_anywhere(filename): |
| 124 """Is `filename` an absolute path on any OS?""" |
| 125 return ntpath.isabs(filename) or posixpath.isabs(filename) |
| 126 |
| 127 |
120 def prep_patterns(patterns): | 128 def prep_patterns(patterns): |
121 """Prepare the file patterns for use in a `FnmatchMatcher`. | 129 """Prepare the file patterns for use in a `FnmatchMatcher`. |
122 | 130 |
123 If a pattern starts with a wildcard, it is used as a pattern | 131 If a pattern starts with a wildcard, it is used as a pattern |
124 as-is. If it does not start with a wildcard, then it is made | 132 as-is. If it does not start with a wildcard, then it is made |
125 absolute with the current directory. | 133 absolute with the current directory. |
126 | 134 |
127 If `patterns` is None, an empty list is returned. | 135 If `patterns` is None, an empty list is returned. |
128 | 136 |
129 """ | 137 """ |
130 patterns = patterns or [] | |
131 prepped = [] | 138 prepped = [] |
132 for p in patterns or []: | 139 for p in patterns or []: |
133 if p.startswith("*") or p.startswith("?"): | 140 if p.startswith("*") or p.startswith("?"): |
134 prepped.append(p) | 141 prepped.append(p) |
135 else: | 142 else: |
136 prepped.append(abs_file(p)) | 143 prepped.append(abs_file(p)) |
137 return prepped | 144 return prepped |
138 | 145 |
139 | 146 |
140 class TreeMatcher(object): | 147 class TreeMatcher(object): |
141 """A matcher for files in a tree.""" | 148 """A matcher for files in a tree.""" |
142 def __init__(self, directories): | 149 def __init__(self, directories): |
143 self.dirs = directories[:] | 150 self.dirs = directories[:] |
144 | 151 |
145 def __repr__(self): | 152 def __repr__(self): |
146 return "<TreeMatcher %r>" % self.dirs | 153 return "<TreeMatcher %r>" % self.dirs |
147 | 154 |
| 155 def info(self): |
| 156 """A list of strings for displaying when dumping state.""" |
| 157 return self.dirs |
| 158 |
148 def add(self, directory): | 159 def add(self, directory): |
149 """Add another directory to the list we match for.""" | 160 """Add another directory to the list we match for.""" |
150 self.dirs.append(directory) | 161 self.dirs.append(directory) |
151 | 162 |
152 def match(self, fpath): | 163 def match(self, fpath): |
153 """Does `fpath` indicate a file in one of our trees?""" | 164 """Does `fpath` indicate a file in one of our trees?""" |
154 for d in self.dirs: | 165 for d in self.dirs: |
155 if fpath.startswith(d): | 166 if fpath.startswith(d): |
156 if fpath == d: | 167 if fpath == d: |
157 # This is the same file! | 168 # This is the same file! |
158 return True | 169 return True |
159 if fpath[len(d)] == os.sep: | 170 if fpath[len(d)] == os.sep: |
160 # This is a file in the directory | 171 # This is a file in the directory |
161 return True | 172 return True |
162 return False | 173 return False |
163 | 174 |
164 | 175 |
165 class FnmatchMatcher(object): | 176 class FnmatchMatcher(object): |
166 """A matcher for files by filename pattern.""" | 177 """A matcher for files by filename pattern.""" |
167 def __init__(self, pats): | 178 def __init__(self, pats): |
168 self.pats = pats[:] | 179 self.pats = pats[:] |
169 | 180 |
170 def __repr__(self): | 181 def __repr__(self): |
171 return "<FnmatchMatcher %r>" % self.pats | 182 return "<FnmatchMatcher %r>" % self.pats |
172 | 183 |
| 184 def info(self): |
| 185 """A list of strings for displaying when dumping state.""" |
| 186 return self.pats |
| 187 |
173 def match(self, fpath): | 188 def match(self, fpath): |
174 """Does `fpath` match one of our filename patterns?""" | 189 """Does `fpath` match one of our filename patterns?""" |
175 for pat in self.pats: | 190 for pat in self.pats: |
176 if fnmatch.fnmatch(fpath, pat): | 191 if fnmatch.fnmatch(fpath, pat): |
177 return True | 192 return True |
178 return False | 193 return False |
179 | 194 |
180 | 195 |
181 def sep(s): | 196 def sep(s): |
182 """Find the path separator used in this string, or os.sep if none.""" | 197 """Find the path separator used in this string, or os.sep if none.""" |
(...skipping 33 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
216 | 231 |
217 `pattern` can't end with a wildcard component, since that would | 232 `pattern` can't end with a wildcard component, since that would |
218 match an entire tree, and not just its root. | 233 match an entire tree, and not just its root. |
219 | 234 |
220 """ | 235 """ |
221 # The pattern can't end with a wildcard component. | 236 # The pattern can't end with a wildcard component. |
222 pattern = pattern.rstrip(r"\/") | 237 pattern = pattern.rstrip(r"\/") |
223 if pattern.endswith("*"): | 238 if pattern.endswith("*"): |
224 raise CoverageException("Pattern must not end with wildcards.") | 239 raise CoverageException("Pattern must not end with wildcards.") |
225 pattern_sep = sep(pattern) | 240 pattern_sep = sep(pattern) |
| 241 |
| 242 # The pattern is meant to match a filepath. Let's make it absolute |
| 243 # unless it already is, or is meant to match any prefix. |
| 244 if not pattern.startswith('*') and not isabs_anywhere(pattern): |
| 245 pattern = abs_file(pattern) |
226 pattern += pattern_sep | 246 pattern += pattern_sep |
227 | 247 |
228 # Make a regex from the pattern. fnmatch always adds a \Z or $ to | 248 # Make a regex from the pattern. fnmatch always adds a \Z or $ to |
229 # match the whole string, which we don't want. | 249 # match the whole string, which we don't want. |
230 regex_pat = fnmatch.translate(pattern).replace(r'\Z(', '(') | 250 regex_pat = fnmatch.translate(pattern).replace(r'\Z(', '(') |
231 if regex_pat.endswith("$"): | 251 if regex_pat.endswith("$"): |
232 regex_pat = regex_pat[:-1] | 252 regex_pat = regex_pat[:-1] |
233 # We want */a/b.py to match on Windows to, so change slash to match | 253 # We want */a/b.py to match on Windows too, so change slash to match |
234 # either separator. | 254 # either separator. |
235 regex_pat = regex_pat.replace(r"\/", r"[\\/]") | 255 regex_pat = regex_pat.replace(r"\/", r"[\\/]") |
236 # We want case-insensitive matching, so add that flag. | 256 # We want case-insensitive matching, so add that flag. |
237 regex = re.compile(r"(?i)" + regex_pat) | 257 regex = re.compile(r"(?i)" + regex_pat) |
238 | 258 |
239 # Normalize the result: it must end with a path separator. | 259 # Normalize the result: it must end with a path separator. |
240 result_sep = sep(result) | 260 result_sep = sep(result) |
241 result = result.rstrip(r"\/") + result_sep | 261 result = result.rstrip(r"\/") + result_sep |
242 self.aliases.append((regex, result, pattern_sep, result_sep)) | 262 self.aliases.append((regex, result, pattern_sep, result_sep)) |
243 | 263 |
(...skipping 32 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
276 | 296 |
277 """ | 297 """ |
278 for i, (dirpath, dirnames, filenames) in enumerate(os.walk(dirname)): | 298 for i, (dirpath, dirnames, filenames) in enumerate(os.walk(dirname)): |
279 if i > 0 and '__init__.py' not in filenames: | 299 if i > 0 and '__init__.py' not in filenames: |
280 # If a directory doesn't have __init__.py, then it isn't | 300 # If a directory doesn't have __init__.py, then it isn't |
281 # importable and neither are its files | 301 # importable and neither are its files |
282 del dirnames[:] | 302 del dirnames[:] |
283 continue | 303 continue |
284 for filename in filenames: | 304 for filename in filenames: |
285 # We're only interested in files that look like reasonable Python | 305 # We're only interested in files that look like reasonable Python |
286 # files: Must end with .py, and must not have certain funny | 306 # files: Must end with .py or .pyw, and must not have certain funny |
287 # characters that probably mean they are editor junk. | 307 # characters that probably mean they are editor junk. |
288 if re.match(r"^[^.#~!$@%^&*()+=,]+\.py$", filename): | 308 if re.match(r"^[^.#~!$@%^&*()+=,]+\.pyw?$", filename): |
289 yield os.path.join(dirpath, filename) | 309 yield os.path.join(dirpath, filename) |
OLD | NEW |