|
1 # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 |
|
2 # For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt |
|
3 |
|
4 """Determining whether files are being measured/reported or not.""" |
|
5 |
|
6 # For finding the stdlib |
|
7 import atexit |
|
8 import inspect |
|
9 import itertools |
|
10 import os |
|
11 import platform |
|
12 import re |
|
13 import sys |
|
14 import traceback |
|
15 |
|
16 from coverage import env |
|
17 from coverage.backward import code_object |
|
18 from coverage.disposition import FileDisposition, disposition_init |
|
19 from coverage.files import TreeMatcher, FnmatchMatcher, ModuleMatcher |
|
20 from coverage.files import prep_patterns, find_python_files, canonical_filename |
|
21 from coverage.misc import CoverageException |
|
22 from coverage.python import source_for_file, source_for_morf |
|
23 |
|
24 |
|
25 # Pypy has some unusual stuff in the "stdlib". Consider those locations |
|
26 # when deciding where the stdlib is. These modules are not used for anything, |
|
27 # they are modules importable from the pypy lib directories, so that we can |
|
28 # find those directories. |
|
29 _structseq = _pypy_irc_topic = None |
|
30 if env.PYPY: |
|
31 try: |
|
32 import _structseq |
|
33 except ImportError: |
|
34 pass |
|
35 |
|
36 try: |
|
37 import _pypy_irc_topic |
|
38 except ImportError: |
|
39 pass |
|
40 |
|
41 |
|
42 def canonical_path(morf, directory=False): |
|
43 """Return the canonical path of the module or file `morf`. |
|
44 |
|
45 If the module is a package, then return its directory. If it is a |
|
46 module, then return its file, unless `directory` is True, in which |
|
47 case return its enclosing directory. |
|
48 |
|
49 """ |
|
50 morf_path = canonical_filename(source_for_morf(morf)) |
|
51 if morf_path.endswith("__init__.py") or directory: |
|
52 morf_path = os.path.split(morf_path)[0] |
|
53 return morf_path |
|
54 |
|
55 |
|
56 def name_for_module(filename, frame): |
|
57 """Get the name of the module for a filename and frame. |
|
58 |
|
59 For configurability's sake, we allow __main__ modules to be matched by |
|
60 their importable name. |
|
61 |
|
62 If loaded via runpy (aka -m), we can usually recover the "original" |
|
63 full dotted module name, otherwise, we resort to interpreting the |
|
64 file name to get the module's name. In the case that the module name |
|
65 can't be determined, None is returned. |
|
66 |
|
67 """ |
|
68 module_globals = frame.f_globals if frame is not None else {} |
|
69 if module_globals is None: # pragma: only ironpython |
|
70 # IronPython doesn't provide globals: https://github.com/IronLanguages/main/issues/1296 |
|
71 module_globals = {} |
|
72 |
|
73 dunder_name = module_globals.get('__name__', None) |
|
74 |
|
75 if isinstance(dunder_name, str) and dunder_name != '__main__': |
|
76 # This is the usual case: an imported module. |
|
77 return dunder_name |
|
78 |
|
79 loader = module_globals.get('__loader__', None) |
|
80 for attrname in ('fullname', 'name'): # attribute renamed in py3.2 |
|
81 if hasattr(loader, attrname): |
|
82 fullname = getattr(loader, attrname) |
|
83 else: |
|
84 continue |
|
85 |
|
86 if isinstance(fullname, str) and fullname != '__main__': |
|
87 # Module loaded via: runpy -m |
|
88 return fullname |
|
89 |
|
90 # Script as first argument to Python command line. |
|
91 inspectedname = inspect.getmodulename(filename) |
|
92 if inspectedname is not None: |
|
93 return inspectedname |
|
94 else: |
|
95 return dunder_name |
|
96 |
|
97 |
|
98 def module_is_namespace(mod): |
|
99 """Is the module object `mod` a PEP420 namespace module?""" |
|
100 return hasattr(mod, '__path__') and getattr(mod, '__file__', None) is None |
|
101 |
|
102 |
|
103 def module_has_file(mod): |
|
104 """Does the module object `mod` have an existing __file__ ?""" |
|
105 mod__file__ = getattr(mod, '__file__', None) |
|
106 if mod__file__ is None: |
|
107 return False |
|
108 return os.path.exists(mod__file__) |
|
109 |
|
110 |
|
111 class InOrOut(object): |
|
112 """Machinery for determining what files to measure.""" |
|
113 |
|
114 def __init__(self, warn): |
|
115 self.warn = warn |
|
116 |
|
117 # The matchers for should_trace. |
|
118 self.source_match = None |
|
119 self.source_pkgs_match = None |
|
120 self.pylib_paths = self.cover_paths = None |
|
121 self.pylib_match = self.cover_match = None |
|
122 self.include_match = self.omit_match = None |
|
123 self.plugins = [] |
|
124 self.disp_class = FileDisposition |
|
125 |
|
126 # The source argument can be directories or package names. |
|
127 self.source = [] |
|
128 self.source_pkgs = [] |
|
129 self.source_pkgs_unmatched = [] |
|
130 self.omit = self.include = None |
|
131 |
|
132 def configure(self, config): |
|
133 """Apply the configuration to get ready for decision-time.""" |
|
134 for src in config.source or []: |
|
135 if os.path.isdir(src): |
|
136 self.source.append(canonical_filename(src)) |
|
137 else: |
|
138 self.source_pkgs.append(src) |
|
139 self.source_pkgs_unmatched = self.source_pkgs[:] |
|
140 |
|
141 self.omit = prep_patterns(config.run_omit) |
|
142 self.include = prep_patterns(config.run_include) |
|
143 |
|
144 # The directories for files considered "installed with the interpreter". |
|
145 self.pylib_paths = set() |
|
146 if not config.cover_pylib: |
|
147 # Look at where some standard modules are located. That's the |
|
148 # indication for "installed with the interpreter". In some |
|
149 # environments (virtualenv, for example), these modules may be |
|
150 # spread across a few locations. Look at all the candidate modules |
|
151 # we've imported, and take all the different ones. |
|
152 for m in (atexit, inspect, os, platform, _pypy_irc_topic, re, _structseq, traceback): |
|
153 if m is not None and hasattr(m, "__file__"): |
|
154 self.pylib_paths.add(canonical_path(m, directory=True)) |
|
155 |
|
156 if _structseq and not hasattr(_structseq, '__file__'): |
|
157 # PyPy 2.4 has no __file__ in the builtin modules, but the code |
|
158 # objects still have the file names. So dig into one to find |
|
159 # the path to exclude. The "filename" might be synthetic, |
|
160 # don't be fooled by those. |
|
161 structseq_file = code_object(_structseq.structseq_new).co_filename |
|
162 if not structseq_file.startswith("<"): |
|
163 self.pylib_paths.add(canonical_path(structseq_file)) |
|
164 |
|
165 # To avoid tracing the coverage.py code itself, we skip anything |
|
166 # located where we are. |
|
167 self.cover_paths = [canonical_path(__file__, directory=True)] |
|
168 if env.TESTING: |
|
169 # Don't include our own test code. |
|
170 self.cover_paths.append(os.path.join(self.cover_paths[0], "tests")) |
|
171 |
|
172 # When testing, we use PyContracts, which should be considered |
|
173 # part of coverage.py, and it uses six. Exclude those directories |
|
174 # just as we exclude ourselves. |
|
175 import contracts |
|
176 import six |
|
177 for mod in [contracts, six]: |
|
178 self.cover_paths.append(canonical_path(mod)) |
|
179 |
|
180 # Create the matchers we need for should_trace |
|
181 if self.source or self.source_pkgs: |
|
182 self.source_match = TreeMatcher(self.source) |
|
183 self.source_pkgs_match = ModuleMatcher(self.source_pkgs) |
|
184 else: |
|
185 if self.cover_paths: |
|
186 self.cover_match = TreeMatcher(self.cover_paths) |
|
187 if self.pylib_paths: |
|
188 self.pylib_match = TreeMatcher(self.pylib_paths) |
|
189 if self.include: |
|
190 self.include_match = FnmatchMatcher(self.include) |
|
191 if self.omit: |
|
192 self.omit_match = FnmatchMatcher(self.omit) |
|
193 |
|
194 def should_trace(self, filename, frame=None): |
|
195 """Decide whether to trace execution in `filename`, with a reason. |
|
196 |
|
197 This function is called from the trace function. As each new file name |
|
198 is encountered, this function determines whether it is traced or not. |
|
199 |
|
200 Returns a FileDisposition object. |
|
201 |
|
202 """ |
|
203 original_filename = filename |
|
204 disp = disposition_init(self.disp_class, filename) |
|
205 |
|
206 def nope(disp, reason): |
|
207 """Simple helper to make it easy to return NO.""" |
|
208 disp.trace = False |
|
209 disp.reason = reason |
|
210 return disp |
|
211 |
|
212 if frame is not None: |
|
213 # Compiled Python files have two file names: frame.f_code.co_filename is |
|
214 # the file name at the time the .pyc was compiled. The second name is |
|
215 # __file__, which is where the .pyc was actually loaded from. Since |
|
216 # .pyc files can be moved after compilation (for example, by being |
|
217 # installed), we look for __file__ in the frame and prefer it to the |
|
218 # co_filename value. |
|
219 dunder_file = frame.f_globals and frame.f_globals.get('__file__') |
|
220 if dunder_file: |
|
221 filename = source_for_file(dunder_file) |
|
222 if original_filename and not original_filename.startswith('<'): |
|
223 orig = os.path.basename(original_filename) |
|
224 if orig != os.path.basename(filename): |
|
225 # Files shouldn't be renamed when moved. This happens when |
|
226 # exec'ing code. If it seems like something is wrong with |
|
227 # the frame's file name, then just use the original. |
|
228 filename = original_filename |
|
229 |
|
230 if not filename: |
|
231 # Empty string is pretty useless. |
|
232 return nope(disp, "empty string isn't a file name") |
|
233 |
|
234 if filename.startswith('memory:'): |
|
235 return nope(disp, "memory isn't traceable") |
|
236 |
|
237 if filename.startswith('<'): |
|
238 # Lots of non-file execution is represented with artificial |
|
239 # file names like "<string>", "<doctest readme.txt[0]>", or |
|
240 # "<exec_function>". Don't ever trace these executions, since we |
|
241 # can't do anything with the data later anyway. |
|
242 return nope(disp, "not a real file name") |
|
243 |
|
244 # pyexpat does a dumb thing, calling the trace function explicitly from |
|
245 # C code with a C file name. |
|
246 if re.search(r"[/\\]Modules[/\\]pyexpat.c", filename): |
|
247 return nope(disp, "pyexpat lies about itself") |
|
248 |
|
249 # Jython reports the .class file to the tracer, use the source file. |
|
250 if filename.endswith("$py.class"): |
|
251 filename = filename[:-9] + ".py" |
|
252 |
|
253 canonical = canonical_filename(filename) |
|
254 disp.canonical_filename = canonical |
|
255 |
|
256 # Try the plugins, see if they have an opinion about the file. |
|
257 plugin = None |
|
258 for plugin in self.plugins.file_tracers: |
|
259 if not plugin._coverage_enabled: |
|
260 continue |
|
261 |
|
262 try: |
|
263 file_tracer = plugin.file_tracer(canonical) |
|
264 if file_tracer is not None: |
|
265 file_tracer._coverage_plugin = plugin |
|
266 disp.trace = True |
|
267 disp.file_tracer = file_tracer |
|
268 if file_tracer.has_dynamic_source_filename(): |
|
269 disp.has_dynamic_filename = True |
|
270 else: |
|
271 disp.source_filename = canonical_filename( |
|
272 file_tracer.source_filename() |
|
273 ) |
|
274 break |
|
275 except Exception: |
|
276 self.warn( |
|
277 "Disabling plug-in %r due to an exception:" % (plugin._coverage_plugin_name) |
|
278 ) |
|
279 traceback.print_exc() |
|
280 plugin._coverage_enabled = False |
|
281 continue |
|
282 else: |
|
283 # No plugin wanted it: it's Python. |
|
284 disp.trace = True |
|
285 disp.source_filename = canonical |
|
286 |
|
287 if not disp.has_dynamic_filename: |
|
288 if not disp.source_filename: |
|
289 raise CoverageException( |
|
290 "Plugin %r didn't set source_filename for %r" % |
|
291 (plugin, disp.original_filename) |
|
292 ) |
|
293 reason = self.check_include_omit_etc(disp.source_filename, frame) |
|
294 if reason: |
|
295 nope(disp, reason) |
|
296 |
|
297 return disp |
|
298 |
|
299 def check_include_omit_etc(self, filename, frame): |
|
300 """Check a file name against the include, omit, etc, rules. |
|
301 |
|
302 Returns a string or None. String means, don't trace, and is the reason |
|
303 why. None means no reason found to not trace. |
|
304 |
|
305 """ |
|
306 modulename = name_for_module(filename, frame) |
|
307 |
|
308 # If the user specified source or include, then that's authoritative |
|
309 # about the outer bound of what to measure and we don't have to apply |
|
310 # any canned exclusions. If they didn't, then we have to exclude the |
|
311 # stdlib and coverage.py directories. |
|
312 if self.source_match: |
|
313 if self.source_pkgs_match.match(modulename): |
|
314 if modulename in self.source_pkgs_unmatched: |
|
315 self.source_pkgs_unmatched.remove(modulename) |
|
316 elif not self.source_match.match(filename): |
|
317 return "falls outside the --source trees" |
|
318 elif self.include_match: |
|
319 if not self.include_match.match(filename): |
|
320 return "falls outside the --include trees" |
|
321 else: |
|
322 # If we aren't supposed to trace installed code, then check if this |
|
323 # is near the Python standard library and skip it if so. |
|
324 if self.pylib_match and self.pylib_match.match(filename): |
|
325 return "is in the stdlib" |
|
326 |
|
327 # We exclude the coverage.py code itself, since a little of it |
|
328 # will be measured otherwise. |
|
329 if self.cover_match and self.cover_match.match(filename): |
|
330 return "is part of coverage.py" |
|
331 |
|
332 # Check the file against the omit pattern. |
|
333 if self.omit_match and self.omit_match.match(filename): |
|
334 return "is inside an --omit pattern" |
|
335 |
|
336 # No point tracing a file we can't later write to SQLite. |
|
337 try: |
|
338 filename.encode("utf8") |
|
339 except UnicodeEncodeError: |
|
340 return "non-encodable filename" |
|
341 |
|
342 # No reason found to skip this file. |
|
343 return None |
|
344 |
|
345 def warn_conflicting_settings(self): |
|
346 """Warn if there are settings that conflict.""" |
|
347 if self.include: |
|
348 if self.source or self.source_pkgs: |
|
349 self.warn("--include is ignored because --source is set", slug="include-ignored") |
|
350 |
|
351 def warn_already_imported_files(self): |
|
352 """Warn if files have already been imported that we will be measuring.""" |
|
353 if self.include or self.source or self.source_pkgs: |
|
354 warned = set() |
|
355 for mod in list(sys.modules.values()): |
|
356 filename = getattr(mod, "__file__", None) |
|
357 if filename is None: |
|
358 continue |
|
359 if filename in warned: |
|
360 continue |
|
361 |
|
362 disp = self.should_trace(filename) |
|
363 if disp.trace: |
|
364 msg = "Already imported a file that will be measured: {}".format(filename) |
|
365 self.warn(msg, slug="already-imported") |
|
366 warned.add(filename) |
|
367 |
|
368 def warn_unimported_source(self): |
|
369 """Warn about source packages that were of interest, but never traced.""" |
|
370 for pkg in self.source_pkgs_unmatched: |
|
371 self._warn_about_unmeasured_code(pkg) |
|
372 |
|
373 def _warn_about_unmeasured_code(self, pkg): |
|
374 """Warn about a package or module that we never traced. |
|
375 |
|
376 `pkg` is a string, the name of the package or module. |
|
377 |
|
378 """ |
|
379 mod = sys.modules.get(pkg) |
|
380 if mod is None: |
|
381 self.warn("Module %s was never imported." % pkg, slug="module-not-imported") |
|
382 return |
|
383 |
|
384 if module_is_namespace(mod): |
|
385 # A namespace package. It's OK for this not to have been traced, |
|
386 # since there is no code directly in it. |
|
387 return |
|
388 |
|
389 if not module_has_file(mod): |
|
390 self.warn("Module %s has no Python source." % pkg, slug="module-not-python") |
|
391 return |
|
392 |
|
393 # The module was in sys.modules, and seems like a module with code, but |
|
394 # we never measured it. I guess that means it was imported before |
|
395 # coverage even started. |
|
396 self.warn( |
|
397 "Module %s was previously imported, but not measured" % pkg, |
|
398 slug="module-not-measured", |
|
399 ) |
|
400 |
|
401 def find_possibly_unexecuted_files(self): |
|
402 """Find files in the areas of interest that might be untraced. |
|
403 |
|
404 Yields pairs: file path, and responsible plug-in name. |
|
405 """ |
|
406 for pkg in self.source_pkgs: |
|
407 if (not pkg in sys.modules or |
|
408 not module_has_file(sys.modules[pkg])): |
|
409 continue |
|
410 pkg_file = source_for_file(sys.modules[pkg].__file__) |
|
411 for ret in self._find_executable_files(canonical_path(pkg_file)): |
|
412 yield ret |
|
413 |
|
414 for src in self.source: |
|
415 for ret in self._find_executable_files(src): |
|
416 yield ret |
|
417 |
|
418 def _find_plugin_files(self, src_dir): |
|
419 """Get executable files from the plugins.""" |
|
420 for plugin in self.plugins.file_tracers: |
|
421 for x_file in plugin.find_executable_files(src_dir): |
|
422 yield x_file, plugin._coverage_plugin_name |
|
423 |
|
424 def _find_executable_files(self, src_dir): |
|
425 """Find executable files in `src_dir`. |
|
426 |
|
427 Search for files in `src_dir` that can be executed because they |
|
428 are probably importable. Don't include ones that have been omitted |
|
429 by the configuration. |
|
430 |
|
431 Yield the file path, and the plugin name that handles the file. |
|
432 |
|
433 """ |
|
434 py_files = ((py_file, None) for py_file in find_python_files(src_dir)) |
|
435 plugin_files = self._find_plugin_files(src_dir) |
|
436 |
|
437 for file_path, plugin_name in itertools.chain(py_files, plugin_files): |
|
438 file_path = canonical_filename(file_path) |
|
439 if self.omit_match and self.omit_match.match(file_path): |
|
440 # Turns out this file was omitted, so don't pull it back |
|
441 # in as unexecuted. |
|
442 continue |
|
443 yield file_path, plugin_name |
|
444 |
|
445 def sys_info(self): |
|
446 """Our information for Coverage.sys_info. |
|
447 |
|
448 Returns a list of (key, value) pairs. |
|
449 """ |
|
450 info = [ |
|
451 ('cover_paths', self.cover_paths), |
|
452 ('pylib_paths', self.pylib_paths), |
|
453 ] |
|
454 |
|
455 matcher_names = [ |
|
456 'source_match', 'source_pkgs_match', |
|
457 'include_match', 'omit_match', |
|
458 'cover_match', 'pylib_match', |
|
459 ] |
|
460 |
|
461 for matcher_name in matcher_names: |
|
462 matcher = getattr(self, matcher_name) |
|
463 if matcher: |
|
464 matcher_info = matcher.info() |
|
465 else: |
|
466 matcher_info = '-none-' |
|
467 info.append((matcher_name, matcher_info)) |
|
468 |
|
469 return info |