src/eric7/DebugClients/Python/coverage/inorout.py

branch
eric7
changeset 9209
b99e7fd55fd3
parent 9099
0e511e0e94a3
equal deleted inserted replaced
9208:3fc8dfeb6ebe 9209:b99e7fd55fd3
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 import importlib.util
7 import inspect
8 import itertools
9 import os
10 import platform
11 import re
12 import sys
13 import sysconfig
14 import traceback
15
16 from coverage import env
17 from coverage.disposition import FileDisposition, disposition_init
18 from coverage.exceptions import CoverageException, PluginError
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 sys_modules_saved
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 def file_and_path_for_module(modulename):
112 """Find the file and search path for `modulename`.
113
114 Returns:
115 filename: The filename of the module, or None.
116 path: A list (possibly empty) of directories to find submodules in.
117
118 """
119 filename = None
120 path = []
121 try:
122 spec = importlib.util.find_spec(modulename)
123 except Exception:
124 pass
125 else:
126 if spec is not None:
127 filename = spec.origin
128 path = list(spec.submodule_search_locations or ())
129 return filename, path
130
131
132 def add_stdlib_paths(paths):
133 """Add paths where the stdlib can be found to the set `paths`."""
134 # Look at where some standard modules are located. That's the
135 # indication for "installed with the interpreter". In some
136 # environments (virtualenv, for example), these modules may be
137 # spread across a few locations. Look at all the candidate modules
138 # we've imported, and take all the different ones.
139 modules_we_happen_to_have = [
140 inspect, itertools, os, platform, re, sysconfig, traceback,
141 _pypy_irc_topic, _structseq,
142 ]
143 for m in modules_we_happen_to_have:
144 if m is not None and hasattr(m, "__file__"):
145 paths.add(canonical_path(m, directory=True))
146
147 if _structseq and not hasattr(_structseq, '__file__'):
148 # PyPy 2.4 has no __file__ in the builtin modules, but the code
149 # objects still have the file names. So dig into one to find
150 # the path to exclude. The "filename" might be synthetic,
151 # don't be fooled by those.
152 structseq_file = _structseq.structseq_new.__code__.co_filename
153 if not structseq_file.startswith("<"):
154 paths.add(canonical_path(structseq_file))
155
156
157 def add_third_party_paths(paths):
158 """Add locations for third-party packages to the set `paths`."""
159 # Get the paths that sysconfig knows about.
160 scheme_names = set(sysconfig.get_scheme_names())
161
162 for scheme in scheme_names:
163 # https://foss.heptapod.net/pypy/pypy/-/issues/3433
164 better_scheme = "pypy_posix" if scheme == "pypy" else scheme
165 if os.name in better_scheme.split("_"):
166 config_paths = sysconfig.get_paths(scheme)
167 for path_name in ["platlib", "purelib", "scripts"]:
168 paths.add(config_paths[path_name])
169
170
171 def add_coverage_paths(paths):
172 """Add paths where coverage.py code can be found to the set `paths`."""
173 cover_path = canonical_path(__file__, directory=True)
174 paths.add(cover_path)
175 if env.TESTING:
176 # Don't include our own test code.
177 paths.add(os.path.join(cover_path, "tests"))
178
179 # When testing, we use PyContracts, which should be considered
180 # part of coverage.py, and it uses six. Exclude those directories
181 # just as we exclude ourselves.
182 if env.USE_CONTRACTS:
183 import contracts
184 import six
185 for mod in [contracts, six]:
186 paths.add(canonical_path(mod))
187
188
189 class InOrOut:
190 """Machinery for determining what files to measure."""
191
192 def __init__(self, warn, debug):
193 self.warn = warn
194 self.debug = debug
195
196 # The matchers for should_trace.
197 self.source_match = None
198 self.source_pkgs_match = None
199 self.pylib_paths = self.cover_paths = self.third_paths = None
200 self.pylib_match = self.cover_match = self.third_match = None
201 self.include_match = self.omit_match = None
202 self.plugins = []
203 self.disp_class = FileDisposition
204
205 # The source argument can be directories or package names.
206 self.source = []
207 self.source_pkgs = []
208 self.source_pkgs_unmatched = []
209 self.omit = self.include = None
210
211 # Is the source inside a third-party area?
212 self.source_in_third = False
213
214 def configure(self, config):
215 """Apply the configuration to get ready for decision-time."""
216 self.source_pkgs.extend(config.source_pkgs)
217 for src in config.source or []:
218 if os.path.isdir(src):
219 self.source.append(canonical_filename(src))
220 else:
221 self.source_pkgs.append(src)
222 self.source_pkgs_unmatched = self.source_pkgs[:]
223
224 self.omit = prep_patterns(config.run_omit)
225 self.include = prep_patterns(config.run_include)
226
227 # The directories for files considered "installed with the interpreter".
228 self.pylib_paths = set()
229 if not config.cover_pylib:
230 add_stdlib_paths(self.pylib_paths)
231
232 # To avoid tracing the coverage.py code itself, we skip anything
233 # located where we are.
234 self.cover_paths = set()
235 add_coverage_paths(self.cover_paths)
236
237 # Find where third-party packages are installed.
238 self.third_paths = set()
239 add_third_party_paths(self.third_paths)
240
241 def debug(msg):
242 if self.debug:
243 self.debug.write(msg)
244
245 # Generally useful information
246 debug("sys.path:" + "".join(f"\n {p}" for p in sys.path))
247
248 # Create the matchers we need for should_trace
249 if self.source or self.source_pkgs:
250 against = []
251 if self.source:
252 self.source_match = TreeMatcher(self.source, "source")
253 against.append(f"trees {self.source_match!r}")
254 if self.source_pkgs:
255 self.source_pkgs_match = ModuleMatcher(self.source_pkgs, "source_pkgs")
256 against.append(f"modules {self.source_pkgs_match!r}")
257 debug("Source matching against " + " and ".join(against))
258 else:
259 if self.pylib_paths:
260 self.pylib_match = TreeMatcher(self.pylib_paths, "pylib")
261 debug(f"Python stdlib matching: {self.pylib_match!r}")
262 if self.include:
263 self.include_match = FnmatchMatcher(self.include, "include")
264 debug(f"Include matching: {self.include_match!r}")
265 if self.omit:
266 self.omit_match = FnmatchMatcher(self.omit, "omit")
267 debug(f"Omit matching: {self.omit_match!r}")
268
269 self.cover_match = TreeMatcher(self.cover_paths, "coverage")
270 debug(f"Coverage code matching: {self.cover_match!r}")
271
272 self.third_match = TreeMatcher(self.third_paths, "third")
273 debug(f"Third-party lib matching: {self.third_match!r}")
274
275 # Check if the source we want to measure has been installed as a
276 # third-party package.
277 with sys_modules_saved():
278 for pkg in self.source_pkgs:
279 try:
280 modfile, path = file_and_path_for_module(pkg)
281 debug(f"Imported source package {pkg!r} as {modfile!r}")
282 except CoverageException as exc:
283 debug(f"Couldn't import source package {pkg!r}: {exc}")
284 continue
285 if modfile:
286 if self.third_match.match(modfile):
287 debug(
288 f"Source is in third-party because of source_pkg {pkg!r} at {modfile!r}"
289 )
290 self.source_in_third = True
291 else:
292 for pathdir in path:
293 if self.third_match.match(pathdir):
294 debug(
295 f"Source is in third-party because of {pkg!r} path directory " +
296 f"at {pathdir!r}"
297 )
298 self.source_in_third = True
299
300 for src in self.source:
301 if self.third_match.match(src):
302 debug(f"Source is in third-party because of source directory {src!r}")
303 self.source_in_third = True
304
305 def should_trace(self, filename, frame=None):
306 """Decide whether to trace execution in `filename`, with a reason.
307
308 This function is called from the trace function. As each new file name
309 is encountered, this function determines whether it is traced or not.
310
311 Returns a FileDisposition object.
312
313 """
314 original_filename = filename
315 disp = disposition_init(self.disp_class, filename)
316
317 def nope(disp, reason):
318 """Simple helper to make it easy to return NO."""
319 disp.trace = False
320 disp.reason = reason
321 return disp
322
323 if original_filename.startswith('<'):
324 return nope(disp, "not a real original file name")
325
326 if frame is not None:
327 # Compiled Python files have two file names: frame.f_code.co_filename is
328 # the file name at the time the .pyc was compiled. The second name is
329 # __file__, which is where the .pyc was actually loaded from. Since
330 # .pyc files can be moved after compilation (for example, by being
331 # installed), we look for __file__ in the frame and prefer it to the
332 # co_filename value.
333 dunder_file = frame.f_globals and frame.f_globals.get('__file__')
334 if dunder_file:
335 filename = source_for_file(dunder_file)
336 if original_filename and not original_filename.startswith('<'):
337 orig = os.path.basename(original_filename)
338 if orig != os.path.basename(filename):
339 # Files shouldn't be renamed when moved. This happens when
340 # exec'ing code. If it seems like something is wrong with
341 # the frame's file name, then just use the original.
342 filename = original_filename
343
344 if not filename:
345 # Empty string is pretty useless.
346 return nope(disp, "empty string isn't a file name")
347
348 if filename.startswith('memory:'):
349 return nope(disp, "memory isn't traceable")
350
351 if filename.startswith('<'):
352 # Lots of non-file execution is represented with artificial
353 # file names like "<string>", "<doctest readme.txt[0]>", or
354 # "<exec_function>". Don't ever trace these executions, since we
355 # can't do anything with the data later anyway.
356 return nope(disp, "not a real file name")
357
358 # Jython reports the .class file to the tracer, use the source file.
359 if filename.endswith("$py.class"):
360 filename = filename[:-9] + ".py"
361
362 canonical = canonical_filename(filename)
363 disp.canonical_filename = canonical
364
365 # Try the plugins, see if they have an opinion about the file.
366 plugin = None
367 for plugin in self.plugins.file_tracers:
368 if not plugin._coverage_enabled:
369 continue
370
371 try:
372 file_tracer = plugin.file_tracer(canonical)
373 if file_tracer is not None:
374 file_tracer._coverage_plugin = plugin
375 disp.trace = True
376 disp.file_tracer = file_tracer
377 if file_tracer.has_dynamic_source_filename():
378 disp.has_dynamic_filename = True
379 else:
380 disp.source_filename = canonical_filename(
381 file_tracer.source_filename()
382 )
383 break
384 except Exception:
385 plugin_name = plugin._coverage_plugin_name
386 tb = traceback.format_exc()
387 self.warn(f"Disabling plug-in {plugin_name!r} due to an exception:\n{tb}")
388 plugin._coverage_enabled = False
389 continue
390 else:
391 # No plugin wanted it: it's Python.
392 disp.trace = True
393 disp.source_filename = canonical
394
395 if not disp.has_dynamic_filename:
396 if not disp.source_filename:
397 raise PluginError(
398 f"Plugin {plugin!r} didn't set source_filename for '{disp.original_filename}'"
399 )
400 reason = self.check_include_omit_etc(disp.source_filename, frame)
401 if reason:
402 nope(disp, reason)
403
404 return disp
405
406 def check_include_omit_etc(self, filename, frame):
407 """Check a file name against the include, omit, etc, rules.
408
409 Returns a string or None. String means, don't trace, and is the reason
410 why. None means no reason found to not trace.
411
412 """
413 modulename = name_for_module(filename, frame)
414
415 # If the user specified source or include, then that's authoritative
416 # about the outer bound of what to measure and we don't have to apply
417 # any canned exclusions. If they didn't, then we have to exclude the
418 # stdlib and coverage.py directories.
419 if self.source_match or self.source_pkgs_match:
420 extra = ""
421 ok = False
422 if self.source_pkgs_match:
423 if self.source_pkgs_match.match(modulename):
424 ok = True
425 if modulename in self.source_pkgs_unmatched:
426 self.source_pkgs_unmatched.remove(modulename)
427 else:
428 extra = f"module {modulename!r} "
429 if not ok and self.source_match:
430 if self.source_match.match(filename):
431 ok = True
432 if not ok:
433 return extra + "falls outside the --source spec"
434 if not self.source_in_third:
435 if self.third_match.match(filename):
436 return "inside --source, but is third-party"
437 elif self.include_match:
438 if not self.include_match.match(filename):
439 return "falls outside the --include trees"
440 else:
441 # We exclude the coverage.py code itself, since a little of it
442 # will be measured otherwise.
443 if self.cover_match.match(filename):
444 return "is part of coverage.py"
445
446 # If we aren't supposed to trace installed code, then check if this
447 # is near the Python standard library and skip it if so.
448 if self.pylib_match and self.pylib_match.match(filename):
449 return "is in the stdlib"
450
451 # Exclude anything in the third-party installation areas.
452 if self.third_match.match(filename):
453 return "is a third-party module"
454
455 # Check the file against the omit pattern.
456 if self.omit_match and self.omit_match.match(filename):
457 return "is inside an --omit pattern"
458
459 # No point tracing a file we can't later write to SQLite.
460 try:
461 filename.encode("utf-8")
462 except UnicodeEncodeError:
463 return "non-encodable filename"
464
465 # No reason found to skip this file.
466 return None
467
468 def warn_conflicting_settings(self):
469 """Warn if there are settings that conflict."""
470 if self.include:
471 if self.source or self.source_pkgs:
472 self.warn("--include is ignored because --source is set", slug="include-ignored")
473
474 def warn_already_imported_files(self):
475 """Warn if files have already been imported that we will be measuring."""
476 if self.include or self.source or self.source_pkgs:
477 warned = set()
478 for mod in list(sys.modules.values()):
479 filename = getattr(mod, "__file__", None)
480 if filename is None:
481 continue
482 if filename in warned:
483 continue
484
485 if len(getattr(mod, "__path__", ())) > 1:
486 # A namespace package, which confuses this code, so ignore it.
487 continue
488
489 disp = self.should_trace(filename)
490 if disp.has_dynamic_filename:
491 # A plugin with dynamic filenames: the Python file
492 # shouldn't cause a warning, since it won't be the subject
493 # of tracing anyway.
494 continue
495 if disp.trace:
496 msg = f"Already imported a file that will be measured: {filename}"
497 self.warn(msg, slug="already-imported")
498 warned.add(filename)
499 elif self.debug and self.debug.should('trace'):
500 self.debug.write(
501 "Didn't trace already imported file {!r}: {}".format(
502 disp.original_filename, disp.reason
503 )
504 )
505
506 def warn_unimported_source(self):
507 """Warn about source packages that were of interest, but never traced."""
508 for pkg in self.source_pkgs_unmatched:
509 self._warn_about_unmeasured_code(pkg)
510
511 def _warn_about_unmeasured_code(self, pkg):
512 """Warn about a package or module that we never traced.
513
514 `pkg` is a string, the name of the package or module.
515
516 """
517 mod = sys.modules.get(pkg)
518 if mod is None:
519 self.warn(f"Module {pkg} was never imported.", slug="module-not-imported")
520 return
521
522 if module_is_namespace(mod):
523 # A namespace package. It's OK for this not to have been traced,
524 # since there is no code directly in it.
525 return
526
527 if not module_has_file(mod):
528 self.warn(f"Module {pkg} has no Python source.", slug="module-not-python")
529 return
530
531 # The module was in sys.modules, and seems like a module with code, but
532 # we never measured it. I guess that means it was imported before
533 # coverage even started.
534 msg = f"Module {pkg} was previously imported, but not measured"
535 self.warn(msg, slug="module-not-measured")
536
537 def find_possibly_unexecuted_files(self):
538 """Find files in the areas of interest that might be untraced.
539
540 Yields pairs: file path, and responsible plug-in name.
541 """
542 for pkg in self.source_pkgs:
543 if (not pkg in sys.modules or
544 not module_has_file(sys.modules[pkg])):
545 continue
546 pkg_file = source_for_file(sys.modules[pkg].__file__)
547 yield from self._find_executable_files(canonical_path(pkg_file))
548
549 for src in self.source:
550 yield from self._find_executable_files(src)
551
552 def _find_plugin_files(self, src_dir):
553 """Get executable files from the plugins."""
554 for plugin in self.plugins.file_tracers:
555 for x_file in plugin.find_executable_files(src_dir):
556 yield x_file, plugin._coverage_plugin_name
557
558 def _find_executable_files(self, src_dir):
559 """Find executable files in `src_dir`.
560
561 Search for files in `src_dir` that can be executed because they
562 are probably importable. Don't include ones that have been omitted
563 by the configuration.
564
565 Yield the file path, and the plugin name that handles the file.
566
567 """
568 py_files = ((py_file, None) for py_file in find_python_files(src_dir))
569 plugin_files = self._find_plugin_files(src_dir)
570
571 for file_path, plugin_name in itertools.chain(py_files, plugin_files):
572 file_path = canonical_filename(file_path)
573 if self.omit_match and self.omit_match.match(file_path):
574 # Turns out this file was omitted, so don't pull it back
575 # in as unexecuted.
576 continue
577 yield file_path, plugin_name
578
579 def sys_info(self):
580 """Our information for Coverage.sys_info.
581
582 Returns a list of (key, value) pairs.
583 """
584 info = [
585 ("coverage_paths", self.cover_paths),
586 ("stdlib_paths", self.pylib_paths),
587 ("third_party_paths", self.third_paths),
588 ]
589
590 matcher_names = [
591 'source_match', 'source_pkgs_match',
592 'include_match', 'omit_match',
593 'cover_match', 'pylib_match', 'third_match',
594 ]
595
596 for matcher_name in matcher_names:
597 matcher = getattr(self, matcher_name)
598 if matcher:
599 matcher_info = matcher.info()
600 else:
601 matcher_info = '-none-'
602 info.append((matcher_name, matcher_info))
603
604 return info

eric ide

mercurial