eric7/DebugClients/Python/coverage/inorout.py

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

eric ide

mercurial