eric6/DebugClients/Python/coverage/inorout.py

changeset 7427
362cd1b6f81a
child 7702
f8b97639deb5
equal deleted inserted replaced
7426:dc171b1d8261 7427:362cd1b6f81a
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

eric ide

mercurial