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

branch
eric7
changeset 9209
b99e7fd55fd3
parent 9099
0e511e0e94a3
child 9374
ed79209469ad
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 """Core control stuff for coverage.py."""
5
6 import atexit
7 import collections
8 import contextlib
9 import os
10 import os.path
11 import platform
12 import signal
13 import sys
14 import threading
15 import time
16 import warnings
17
18 from coverage import env
19 from coverage.annotate import AnnotateReporter
20 from coverage.collector import Collector, CTracer
21 from coverage.config import read_coverage_config
22 from coverage.context import should_start_context_test_function, combine_context_switchers
23 from coverage.data import CoverageData, combine_parallel_data
24 from coverage.debug import DebugControl, short_stack, write_formatted_info
25 from coverage.disposition import disposition_debug_msg
26 from coverage.exceptions import ConfigError, CoverageException, CoverageWarning, PluginError
27 from coverage.files import PathAliases, abs_file, relative_filename, set_relative_directory
28 from coverage.html import HtmlReporter
29 from coverage.inorout import InOrOut
30 from coverage.jsonreport import JsonReporter
31 from coverage.lcovreport import LcovReporter
32 from coverage.misc import bool_or_none, join_regex, human_sorted
33 from coverage.misc import DefaultValue, ensure_dir_for_file, isolate_module
34 from coverage.plugin import FileReporter
35 from coverage.plugin_support import Plugins
36 from coverage.python import PythonFileReporter
37 from coverage.report import render_report
38 from coverage.results import Analysis
39 from coverage.summary import SummaryReporter
40 from coverage.xmlreport import XmlReporter
41
42 try:
43 from coverage.multiproc import patch_multiprocessing
44 except ImportError: # pragma: only jython
45 # Jython has no multiprocessing module.
46 patch_multiprocessing = None
47
48 os = isolate_module(os)
49
50 @contextlib.contextmanager
51 def override_config(cov, **kwargs):
52 """Temporarily tweak the configuration of `cov`.
53
54 The arguments are applied to `cov.config` with the `from_args` method.
55 At the end of the with-statement, the old configuration is restored.
56 """
57 original_config = cov.config
58 cov.config = cov.config.copy()
59 try:
60 cov.config.from_args(**kwargs)
61 yield
62 finally:
63 cov.config = original_config
64
65
66 DEFAULT_DATAFILE = DefaultValue("MISSING")
67 _DEFAULT_DATAFILE = DEFAULT_DATAFILE # Just in case, for backwards compatibility
68
69 class Coverage:
70 """Programmatic access to coverage.py.
71
72 To use::
73
74 from coverage import Coverage
75
76 cov = Coverage()
77 cov.start()
78 #.. call your code ..
79 cov.stop()
80 cov.html_report(directory='covhtml')
81
82 Note: in keeping with Python custom, names starting with underscore are
83 not part of the public API. They might stop working at any point. Please
84 limit yourself to documented methods to avoid problems.
85
86 Methods can raise any of the exceptions described in :ref:`api_exceptions`.
87
88 """
89
90 # The stack of started Coverage instances.
91 _instances = []
92
93 @classmethod
94 def current(cls):
95 """Get the latest started `Coverage` instance, if any.
96
97 Returns: a `Coverage` instance, or None.
98
99 .. versionadded:: 5.0
100
101 """
102 if cls._instances:
103 return cls._instances[-1]
104 else:
105 return None
106
107 def __init__(
108 self, data_file=DEFAULT_DATAFILE, data_suffix=None, cover_pylib=None,
109 auto_data=False, timid=None, branch=None, config_file=True,
110 source=None, source_pkgs=None, omit=None, include=None, debug=None,
111 concurrency=None, check_preimported=False, context=None,
112 messages=False,
113 ): # pylint: disable=too-many-arguments
114 """
115 Many of these arguments duplicate and override values that can be
116 provided in a configuration file. Parameters that are missing here
117 will use values from the config file.
118
119 `data_file` is the base name of the data file to use. The config value
120 defaults to ".coverage". None can be provided to prevent writing a data
121 file. `data_suffix` is appended (with a dot) to `data_file` to create
122 the final file name. If `data_suffix` is simply True, then a suffix is
123 created with the machine and process identity included.
124
125 `cover_pylib` is a boolean determining whether Python code installed
126 with the Python interpreter is measured. This includes the Python
127 standard library and any packages installed with the interpreter.
128
129 If `auto_data` is true, then any existing data file will be read when
130 coverage measurement starts, and data will be saved automatically when
131 measurement stops.
132
133 If `timid` is true, then a slower and simpler trace function will be
134 used. This is important for some environments where manipulation of
135 tracing functions breaks the faster trace function.
136
137 If `branch` is true, then branch coverage will be measured in addition
138 to the usual statement coverage.
139
140 `config_file` determines what configuration file to read:
141
142 * If it is ".coveragerc", it is interpreted as if it were True,
143 for backward compatibility.
144
145 * If it is a string, it is the name of the file to read. If the
146 file can't be read, it is an error.
147
148 * If it is True, then a few standard files names are tried
149 (".coveragerc", "setup.cfg", "tox.ini"). It is not an error for
150 these files to not be found.
151
152 * If it is False, then no configuration file is read.
153
154 `source` is a list of file paths or package names. Only code located
155 in the trees indicated by the file paths or package names will be
156 measured.
157
158 `source_pkgs` is a list of package names. It works the same as
159 `source`, but can be used to name packages where the name can also be
160 interpreted as a file path.
161
162 `include` and `omit` are lists of file name patterns. Files that match
163 `include` will be measured, files that match `omit` will not. Each
164 will also accept a single string argument.
165
166 `debug` is a list of strings indicating what debugging information is
167 desired.
168
169 `concurrency` is a string indicating the concurrency library being used
170 in the measured code. Without this, coverage.py will get incorrect
171 results if these libraries are in use. Valid strings are "greenlet",
172 "eventlet", "gevent", "multiprocessing", or "thread" (the default).
173 This can also be a list of these strings.
174
175 If `check_preimported` is true, then when coverage is started, the
176 already-imported files will be checked to see if they should be
177 measured by coverage. Importing measured files before coverage is
178 started can mean that code is missed.
179
180 `context` is a string to use as the :ref:`static context
181 <static_contexts>` label for collected data.
182
183 If `messages` is true, some messages will be printed to stdout
184 indicating what is happening.
185
186 .. versionadded:: 4.0
187 The `concurrency` parameter.
188
189 .. versionadded:: 4.2
190 The `concurrency` parameter can now be a list of strings.
191
192 .. versionadded:: 5.0
193 The `check_preimported` and `context` parameters.
194
195 .. versionadded:: 5.3
196 The `source_pkgs` parameter.
197
198 .. versionadded:: 6.0
199 The `messages` parameter.
200
201 """
202 # data_file=None means no disk file at all. data_file missing means
203 # use the value from the config file.
204 self._no_disk = data_file is None
205 if data_file is DEFAULT_DATAFILE:
206 data_file = None
207
208 self.config = None
209
210 # This is injectable by tests.
211 self._debug_file = None
212
213 self._auto_load = self._auto_save = auto_data
214 self._data_suffix_specified = data_suffix
215
216 # Is it ok for no data to be collected?
217 self._warn_no_data = True
218 self._warn_unimported_source = True
219 self._warn_preimported_source = check_preimported
220 self._no_warn_slugs = None
221 self._messages = messages
222
223 # A record of all the warnings that have been issued.
224 self._warnings = []
225
226 # Other instance attributes, set later.
227 self._data = self._collector = None
228 self._plugins = None
229 self._inorout = None
230 self._data_suffix = self._run_suffix = None
231 self._exclude_re = None
232 self._debug = None
233 self._file_mapper = None
234 self._old_sigterm = None
235
236 # State machine variables:
237 # Have we initialized everything?
238 self._inited = False
239 self._inited_for_start = False
240 # Have we started collecting and not stopped it?
241 self._started = False
242 # Should we write the debug output?
243 self._should_write_debug = True
244
245 # Build our configuration from a number of sources.
246 self.config = read_coverage_config(
247 config_file=config_file, warn=self._warn,
248 data_file=data_file, cover_pylib=cover_pylib, timid=timid,
249 branch=branch, parallel=bool_or_none(data_suffix),
250 source=source, source_pkgs=source_pkgs, run_omit=omit, run_include=include, debug=debug,
251 report_omit=omit, report_include=include,
252 concurrency=concurrency, context=context,
253 )
254
255 # If we have sub-process measurement happening automatically, then we
256 # want any explicit creation of a Coverage object to mean, this process
257 # is already coverage-aware, so don't auto-measure it. By now, the
258 # auto-creation of a Coverage object has already happened. But we can
259 # find it and tell it not to save its data.
260 if not env.METACOV:
261 _prevent_sub_process_measurement()
262
263 def _init(self):
264 """Set all the initial state.
265
266 This is called by the public methods to initialize state. This lets us
267 construct a :class:`Coverage` object, then tweak its state before this
268 function is called.
269
270 """
271 if self._inited:
272 return
273
274 self._inited = True
275
276 # Create and configure the debugging controller. COVERAGE_DEBUG_FILE
277 # is an environment variable, the name of a file to append debug logs
278 # to.
279 self._debug = DebugControl(self.config.debug, self._debug_file)
280
281 if "multiprocessing" in (self.config.concurrency or ()):
282 # Multi-processing uses parallel for the subprocesses, so also use
283 # it for the main process.
284 self.config.parallel = True
285
286 # _exclude_re is a dict that maps exclusion list names to compiled regexes.
287 self._exclude_re = {}
288
289 set_relative_directory()
290 self._file_mapper = relative_filename if self.config.relative_files else abs_file
291
292 # Load plugins
293 self._plugins = Plugins.load_plugins(self.config.plugins, self.config, self._debug)
294
295 # Run configuring plugins.
296 for plugin in self._plugins.configurers:
297 # We need an object with set_option and get_option. Either self or
298 # self.config will do. Choosing randomly stops people from doing
299 # other things with those objects, against the public API. Yes,
300 # this is a bit childish. :)
301 plugin.configure([self, self.config][int(time.time()) % 2])
302
303 def _post_init(self):
304 """Stuff to do after everything is initialized."""
305 if self._should_write_debug:
306 self._should_write_debug = False
307 self._write_startup_debug()
308
309 # '[run] _crash' will raise an exception if the value is close by in
310 # the call stack, for testing error handling.
311 if self.config._crash and self.config._crash in short_stack(limit=4):
312 raise Exception(f"Crashing because called by {self.config._crash}")
313
314 def _write_startup_debug(self):
315 """Write out debug info at startup if needed."""
316 wrote_any = False
317 with self._debug.without_callers():
318 if self._debug.should("config"):
319 config_info = self.config.debug_info()
320 write_formatted_info(self._debug.write, "config", config_info)
321 wrote_any = True
322
323 if self._debug.should("sys"):
324 write_formatted_info(self._debug.write, "sys", self.sys_info())
325 for plugin in self._plugins:
326 header = "sys: " + plugin._coverage_plugin_name
327 info = plugin.sys_info()
328 write_formatted_info(self._debug.write, header, info)
329 wrote_any = True
330
331 if self._debug.should("pybehave"):
332 write_formatted_info(self._debug.write, "pybehave", env.debug_info())
333 wrote_any = True
334
335 if wrote_any:
336 write_formatted_info(self._debug.write, "end", ())
337
338 def _should_trace(self, filename, frame):
339 """Decide whether to trace execution in `filename`.
340
341 Calls `_should_trace_internal`, and returns the FileDisposition.
342
343 """
344 disp = self._inorout.should_trace(filename, frame)
345 if self._debug.should('trace'):
346 self._debug.write(disposition_debug_msg(disp))
347 return disp
348
349 def _check_include_omit_etc(self, filename, frame):
350 """Check a file name against the include/omit/etc, rules, verbosely.
351
352 Returns a boolean: True if the file should be traced, False if not.
353
354 """
355 reason = self._inorout.check_include_omit_etc(filename, frame)
356 if self._debug.should('trace'):
357 if not reason:
358 msg = f"Including {filename!r}"
359 else:
360 msg = f"Not including {filename!r}: {reason}"
361 self._debug.write(msg)
362
363 return not reason
364
365 def _warn(self, msg, slug=None, once=False):
366 """Use `msg` as a warning.
367
368 For warning suppression, use `slug` as the shorthand.
369
370 If `once` is true, only show this warning once (determined by the
371 slug.)
372
373 """
374 if self._no_warn_slugs is None:
375 if self.config is not None:
376 self._no_warn_slugs = list(self.config.disable_warnings)
377
378 if self._no_warn_slugs is not None:
379 if slug in self._no_warn_slugs:
380 # Don't issue the warning
381 return
382
383 self._warnings.append(msg)
384 if slug:
385 msg = f"{msg} ({slug})"
386 if self._debug is not None and self._debug.should('pid'):
387 msg = f"[{os.getpid()}] {msg}"
388 warnings.warn(msg, category=CoverageWarning, stacklevel=2)
389
390 if once:
391 self._no_warn_slugs.append(slug)
392
393 def _message(self, msg):
394 """Write a message to the user, if configured to do so."""
395 if self._messages:
396 print(msg)
397
398 def get_option(self, option_name):
399 """Get an option from the configuration.
400
401 `option_name` is a colon-separated string indicating the section and
402 option name. For example, the ``branch`` option in the ``[run]``
403 section of the config file would be indicated with `"run:branch"`.
404
405 Returns the value of the option. The type depends on the option
406 selected.
407
408 As a special case, an `option_name` of ``"paths"`` will return an
409 OrderedDict with the entire ``[paths]`` section value.
410
411 .. versionadded:: 4.0
412
413 """
414 return self.config.get_option(option_name)
415
416 def set_option(self, option_name, value):
417 """Set an option in the configuration.
418
419 `option_name` is a colon-separated string indicating the section and
420 option name. For example, the ``branch`` option in the ``[run]``
421 section of the config file would be indicated with ``"run:branch"``.
422
423 `value` is the new value for the option. This should be an
424 appropriate Python value. For example, use True for booleans, not the
425 string ``"True"``.
426
427 As an example, calling::
428
429 cov.set_option("run:branch", True)
430
431 has the same effect as this configuration file::
432
433 [run]
434 branch = True
435
436 As a special case, an `option_name` of ``"paths"`` will replace the
437 entire ``[paths]`` section. The value should be an OrderedDict.
438
439 .. versionadded:: 4.0
440
441 """
442 self.config.set_option(option_name, value)
443
444 def load(self):
445 """Load previously-collected coverage data from the data file."""
446 self._init()
447 if self._collector:
448 self._collector.reset()
449 should_skip = self.config.parallel and not os.path.exists(self.config.data_file)
450 if not should_skip:
451 self._init_data(suffix=None)
452 self._post_init()
453 if not should_skip:
454 self._data.read()
455
456 def _init_for_start(self):
457 """Initialization for start()"""
458 # Construct the collector.
459 concurrency = self.config.concurrency or []
460 if "multiprocessing" in concurrency:
461 if not patch_multiprocessing:
462 raise ConfigError( # pragma: only jython
463 "multiprocessing is not supported on this Python"
464 )
465 if self.config.config_file is None:
466 raise ConfigError("multiprocessing requires a configuration file")
467 patch_multiprocessing(rcfile=self.config.config_file)
468
469 dycon = self.config.dynamic_context
470 if not dycon or dycon == "none":
471 context_switchers = []
472 elif dycon == "test_function":
473 context_switchers = [should_start_context_test_function]
474 else:
475 raise ConfigError(f"Don't understand dynamic_context setting: {dycon!r}")
476
477 context_switchers.extend(
478 plugin.dynamic_context for plugin in self._plugins.context_switchers
479 )
480
481 should_start_context = combine_context_switchers(context_switchers)
482
483 self._collector = Collector(
484 should_trace=self._should_trace,
485 check_include=self._check_include_omit_etc,
486 should_start_context=should_start_context,
487 file_mapper=self._file_mapper,
488 timid=self.config.timid,
489 branch=self.config.branch,
490 warn=self._warn,
491 concurrency=concurrency,
492 )
493
494 suffix = self._data_suffix_specified
495 if suffix:
496 if not isinstance(suffix, str):
497 # if data_suffix=True, use .machinename.pid.random
498 suffix = True
499 elif self.config.parallel:
500 if suffix is None:
501 suffix = True
502 elif not isinstance(suffix, str):
503 suffix = bool(suffix)
504 else:
505 suffix = None
506
507 self._init_data(suffix)
508
509 self._collector.use_data(self._data, self.config.context)
510
511 # Early warning if we aren't going to be able to support plugins.
512 if self._plugins.file_tracers and not self._collector.supports_plugins:
513 self._warn(
514 "Plugin file tracers ({}) aren't supported with {}".format(
515 ", ".join(
516 plugin._coverage_plugin_name
517 for plugin in self._plugins.file_tracers
518 ),
519 self._collector.tracer_name(),
520 )
521 )
522 for plugin in self._plugins.file_tracers:
523 plugin._coverage_enabled = False
524
525 # Create the file classifying substructure.
526 self._inorout = InOrOut(
527 warn=self._warn,
528 debug=(self._debug if self._debug.should('trace') else None),
529 )
530 self._inorout.configure(self.config)
531 self._inorout.plugins = self._plugins
532 self._inorout.disp_class = self._collector.file_disposition_class
533
534 # It's useful to write debug info after initing for start.
535 self._should_write_debug = True
536
537 # Register our clean-up handlers.
538 atexit.register(self._atexit)
539 if self.config.sigterm:
540 is_main = (threading.current_thread() == threading.main_thread())
541 if is_main and not env.WINDOWS:
542 # The Python docs seem to imply that SIGTERM works uniformly even
543 # on Windows, but that's not my experience, and this agrees:
544 # https://stackoverflow.com/questions/35772001/x/35792192#35792192
545 self._old_sigterm = signal.signal(signal.SIGTERM, self._on_sigterm)
546
547 def _init_data(self, suffix):
548 """Create a data file if we don't have one yet."""
549 if self._data is None:
550 # Create the data file. We do this at construction time so that the
551 # data file will be written into the directory where the process
552 # started rather than wherever the process eventually chdir'd to.
553 ensure_dir_for_file(self.config.data_file)
554 self._data = CoverageData(
555 basename=self.config.data_file,
556 suffix=suffix,
557 warn=self._warn,
558 debug=self._debug,
559 no_disk=self._no_disk,
560 )
561
562 def start(self):
563 """Start measuring code coverage.
564
565 Coverage measurement only occurs in functions called after
566 :meth:`start` is invoked. Statements in the same scope as
567 :meth:`start` won't be measured.
568
569 Once you invoke :meth:`start`, you must also call :meth:`stop`
570 eventually, or your process might not shut down cleanly.
571
572 """
573 self._init()
574 if not self._inited_for_start:
575 self._inited_for_start = True
576 self._init_for_start()
577 self._post_init()
578
579 # Issue warnings for possible problems.
580 self._inorout.warn_conflicting_settings()
581
582 # See if we think some code that would eventually be measured has
583 # already been imported.
584 if self._warn_preimported_source:
585 self._inorout.warn_already_imported_files()
586
587 if self._auto_load:
588 self.load()
589
590 self._collector.start()
591 self._started = True
592 self._instances.append(self)
593
594 def stop(self):
595 """Stop measuring code coverage."""
596 if self._instances:
597 if self._instances[-1] is self:
598 self._instances.pop()
599 if self._started:
600 self._collector.stop()
601 self._started = False
602
603 def _atexit(self, event="atexit"):
604 """Clean up on process shutdown."""
605 if self._debug.should("process"):
606 self._debug.write(f"{event}: pid: {os.getpid()}, instance: {self!r}")
607 if self._started:
608 self.stop()
609 if self._auto_save:
610 self.save()
611
612 def _on_sigterm(self, signum_unused, frame_unused):
613 """A handler for signal.SIGTERM."""
614 self._atexit("sigterm")
615 # Statements after here won't be seen by metacov because we just wrote
616 # the data, and are about to kill the process.
617 signal.signal(signal.SIGTERM, self._old_sigterm) # pragma: not covered
618 os.kill(os.getpid(), signal.SIGTERM) # pragma: not covered
619
620 def erase(self):
621 """Erase previously collected coverage data.
622
623 This removes the in-memory data collected in this session as well as
624 discarding the data file.
625
626 """
627 self._init()
628 self._post_init()
629 if self._collector:
630 self._collector.reset()
631 self._init_data(suffix=None)
632 self._data.erase(parallel=self.config.parallel)
633 self._data = None
634 self._inited_for_start = False
635
636 def switch_context(self, new_context):
637 """Switch to a new dynamic context.
638
639 `new_context` is a string to use as the :ref:`dynamic context
640 <dynamic_contexts>` label for collected data. If a :ref:`static
641 context <static_contexts>` is in use, the static and dynamic context
642 labels will be joined together with a pipe character.
643
644 Coverage collection must be started already.
645
646 .. versionadded:: 5.0
647
648 """
649 if not self._started: # pragma: part started
650 raise CoverageException("Cannot switch context, coverage is not started")
651
652 if self._collector.should_start_context:
653 self._warn("Conflicting dynamic contexts", slug="dynamic-conflict", once=True)
654
655 self._collector.switch_context(new_context)
656
657 def clear_exclude(self, which='exclude'):
658 """Clear the exclude list."""
659 self._init()
660 setattr(self.config, which + "_list", [])
661 self._exclude_regex_stale()
662
663 def exclude(self, regex, which='exclude'):
664 """Exclude source lines from execution consideration.
665
666 A number of lists of regular expressions are maintained. Each list
667 selects lines that are treated differently during reporting.
668
669 `which` determines which list is modified. The "exclude" list selects
670 lines that are not considered executable at all. The "partial" list
671 indicates lines with branches that are not taken.
672
673 `regex` is a regular expression. The regex is added to the specified
674 list. If any of the regexes in the list is found in a line, the line
675 is marked for special treatment during reporting.
676
677 """
678 self._init()
679 excl_list = getattr(self.config, which + "_list")
680 excl_list.append(regex)
681 self._exclude_regex_stale()
682
683 def _exclude_regex_stale(self):
684 """Drop all the compiled exclusion regexes, a list was modified."""
685 self._exclude_re.clear()
686
687 def _exclude_regex(self, which):
688 """Return a compiled regex for the given exclusion list."""
689 if which not in self._exclude_re:
690 excl_list = getattr(self.config, which + "_list")
691 self._exclude_re[which] = join_regex(excl_list)
692 return self._exclude_re[which]
693
694 def get_exclude_list(self, which='exclude'):
695 """Return a list of excluded regex patterns.
696
697 `which` indicates which list is desired. See :meth:`exclude` for the
698 lists that are available, and their meaning.
699
700 """
701 self._init()
702 return getattr(self.config, which + "_list")
703
704 def save(self):
705 """Save the collected coverage data to the data file."""
706 data = self.get_data()
707 data.write()
708
709 def combine(self, data_paths=None, strict=False, keep=False):
710 """Combine together a number of similarly-named coverage data files.
711
712 All coverage data files whose name starts with `data_file` (from the
713 coverage() constructor) will be read, and combined together into the
714 current measurements.
715
716 `data_paths` is a list of files or directories from which data should
717 be combined. If no list is passed, then the data files from the
718 directory indicated by the current data file (probably the current
719 directory) will be combined.
720
721 If `strict` is true, then it is an error to attempt to combine when
722 there are no data files to combine.
723
724 If `keep` is true, then original input data files won't be deleted.
725
726 .. versionadded:: 4.0
727 The `data_paths` parameter.
728
729 .. versionadded:: 4.3
730 The `strict` parameter.
731
732 .. versionadded: 5.5
733 The `keep` parameter.
734 """
735 self._init()
736 self._init_data(suffix=None)
737 self._post_init()
738 self.get_data()
739
740 aliases = None
741 if self.config.paths:
742 aliases = PathAliases(relative=self.config.relative_files)
743 for paths in self.config.paths.values():
744 result = paths[0]
745 for pattern in paths[1:]:
746 aliases.add(pattern, result)
747
748 combine_parallel_data(
749 self._data,
750 aliases=aliases,
751 data_paths=data_paths,
752 strict=strict,
753 keep=keep,
754 message=self._message,
755 )
756
757 def get_data(self):
758 """Get the collected data.
759
760 Also warn about various problems collecting data.
761
762 Returns a :class:`coverage.CoverageData`, the collected coverage data.
763
764 .. versionadded:: 4.0
765
766 """
767 self._init()
768 self._init_data(suffix=None)
769 self._post_init()
770
771 for plugin in self._plugins:
772 if not plugin._coverage_enabled:
773 self._collector.plugin_was_disabled(plugin)
774
775 if self._collector and self._collector.flush_data():
776 self._post_save_work()
777
778 return self._data
779
780 def _post_save_work(self):
781 """After saving data, look for warnings, post-work, etc.
782
783 Warn about things that should have happened but didn't.
784 Look for unexecuted files.
785
786 """
787 # If there are still entries in the source_pkgs_unmatched list,
788 # then we never encountered those packages.
789 if self._warn_unimported_source:
790 self._inorout.warn_unimported_source()
791
792 # Find out if we got any data.
793 if not self._data and self._warn_no_data:
794 self._warn("No data was collected.", slug="no-data-collected")
795
796 # Touch all the files that could have executed, so that we can
797 # mark completely unexecuted files as 0% covered.
798 if self._data is not None:
799 file_paths = collections.defaultdict(list)
800 for file_path, plugin_name in self._inorout.find_possibly_unexecuted_files():
801 file_path = self._file_mapper(file_path)
802 file_paths[plugin_name].append(file_path)
803 for plugin_name, paths in file_paths.items():
804 self._data.touch_files(paths, plugin_name)
805
806 if self.config.note:
807 self._warn("The '[run] note' setting is no longer supported.")
808
809 # Backward compatibility with version 1.
810 def analysis(self, morf):
811 """Like `analysis2` but doesn't return excluded line numbers."""
812 f, s, _, m, mf = self.analysis2(morf)
813 return f, s, m, mf
814
815 def analysis2(self, morf):
816 """Analyze a module.
817
818 `morf` is a module or a file name. It will be analyzed to determine
819 its coverage statistics. The return value is a 5-tuple:
820
821 * The file name for the module.
822 * A list of line numbers of executable statements.
823 * A list of line numbers of excluded statements.
824 * A list of line numbers of statements not run (missing from
825 execution).
826 * A readable formatted string of the missing line numbers.
827
828 The analysis uses the source file itself and the current measured
829 coverage data.
830
831 """
832 analysis = self._analyze(morf)
833 return (
834 analysis.filename,
835 sorted(analysis.statements),
836 sorted(analysis.excluded),
837 sorted(analysis.missing),
838 analysis.missing_formatted(),
839 )
840
841 def _analyze(self, it):
842 """Analyze a single morf or code unit.
843
844 Returns an `Analysis` object.
845
846 """
847 # All reporting comes through here, so do reporting initialization.
848 self._init()
849 self._post_init()
850
851 data = self.get_data()
852 if not isinstance(it, FileReporter):
853 it = self._get_file_reporter(it)
854
855 return Analysis(data, self.config.precision, it, self._file_mapper)
856
857 def _get_file_reporter(self, morf):
858 """Get a FileReporter for a module or file name."""
859 plugin = None
860 file_reporter = "python"
861
862 if isinstance(morf, str):
863 mapped_morf = self._file_mapper(morf)
864 plugin_name = self._data.file_tracer(mapped_morf)
865 if plugin_name:
866 plugin = self._plugins.get(plugin_name)
867
868 if plugin:
869 file_reporter = plugin.file_reporter(mapped_morf)
870 if file_reporter is None:
871 raise PluginError(
872 "Plugin {!r} did not provide a file reporter for {!r}.".format(
873 plugin._coverage_plugin_name, morf
874 )
875 )
876
877 if file_reporter == "python":
878 file_reporter = PythonFileReporter(morf, self)
879
880 return file_reporter
881
882 def _get_file_reporters(self, morfs=None):
883 """Get a list of FileReporters for a list of modules or file names.
884
885 For each module or file name in `morfs`, find a FileReporter. Return
886 the list of FileReporters.
887
888 If `morfs` is a single module or file name, this returns a list of one
889 FileReporter. If `morfs` is empty or None, then the list of all files
890 measured is used to find the FileReporters.
891
892 """
893 if not morfs:
894 morfs = self._data.measured_files()
895
896 # Be sure we have a collection.
897 if not isinstance(morfs, (list, tuple, set)):
898 morfs = [morfs]
899
900 file_reporters = [self._get_file_reporter(morf) for morf in morfs]
901 return file_reporters
902
903 def report(
904 self, morfs=None, show_missing=None, ignore_errors=None,
905 file=None, omit=None, include=None, skip_covered=None,
906 contexts=None, skip_empty=None, precision=None, sort=None
907 ):
908 """Write a textual summary report to `file`.
909
910 Each module in `morfs` is listed, with counts of statements, executed
911 statements, missing statements, and a list of lines missed.
912
913 If `show_missing` is true, then details of which lines or branches are
914 missing will be included in the report. If `ignore_errors` is true,
915 then a failure while reporting a single file will not stop the entire
916 report.
917
918 `file` is a file-like object, suitable for writing.
919
920 `include` is a list of file name patterns. Files that match will be
921 included in the report. Files matching `omit` will not be included in
922 the report.
923
924 If `skip_covered` is true, don't report on files with 100% coverage.
925
926 If `skip_empty` is true, don't report on empty files (those that have
927 no statements).
928
929 `contexts` is a list of regular expressions. Only data from
930 :ref:`dynamic contexts <dynamic_contexts>` that match one of those
931 expressions (using :func:`re.search <python:re.search>`) will be
932 included in the report.
933
934 `precision` is the number of digits to display after the decimal
935 point for percentages.
936
937 All of the arguments default to the settings read from the
938 :ref:`configuration file <config>`.
939
940 Returns a float, the total percentage covered.
941
942 .. versionadded:: 4.0
943 The `skip_covered` parameter.
944
945 .. versionadded:: 5.0
946 The `contexts` and `skip_empty` parameters.
947
948 .. versionadded:: 5.2
949 The `precision` parameter.
950
951 """
952 with override_config(
953 self,
954 ignore_errors=ignore_errors, report_omit=omit, report_include=include,
955 show_missing=show_missing, skip_covered=skip_covered,
956 report_contexts=contexts, skip_empty=skip_empty, precision=precision,
957 sort=sort
958 ):
959 reporter = SummaryReporter(self)
960 return reporter.report(morfs, outfile=file)
961
962 def annotate(
963 self, morfs=None, directory=None, ignore_errors=None,
964 omit=None, include=None, contexts=None,
965 ):
966 """Annotate a list of modules.
967
968 .. note::
969
970 This method has been obsoleted by more modern reporting tools,
971 including the :meth:`html_report` method. It will be removed in a
972 future version.
973
974 Each module in `morfs` is annotated. The source is written to a new
975 file, named with a ",cover" suffix, with each line prefixed with a
976 marker to indicate the coverage of the line. Covered lines have ">",
977 excluded lines have "-", and missing lines have "!".
978
979 See :meth:`report` for other arguments.
980
981 """
982 print("The annotate command will be removed in a future version.")
983 print("Get in touch if you still use it: ned@nedbatchelder.com")
984
985 with override_config(self,
986 ignore_errors=ignore_errors, report_omit=omit,
987 report_include=include, report_contexts=contexts,
988 ):
989 reporter = AnnotateReporter(self)
990 reporter.report(morfs, directory=directory)
991
992 def html_report(
993 self, morfs=None, directory=None, ignore_errors=None,
994 omit=None, include=None, extra_css=None, title=None,
995 skip_covered=None, show_contexts=None, contexts=None,
996 skip_empty=None, precision=None,
997 ):
998 """Generate an HTML report.
999
1000 The HTML is written to `directory`. The file "index.html" is the
1001 overview starting point, with links to more detailed pages for
1002 individual modules.
1003
1004 `extra_css` is a path to a file of other CSS to apply on the page.
1005 It will be copied into the HTML directory.
1006
1007 `title` is a text string (not HTML) to use as the title of the HTML
1008 report.
1009
1010 See :meth:`report` for other arguments.
1011
1012 Returns a float, the total percentage covered.
1013
1014 .. note::
1015
1016 The HTML report files are generated incrementally based on the
1017 source files and coverage results. If you modify the report files,
1018 the changes will not be considered. You should be careful about
1019 changing the files in the report folder.
1020
1021 """
1022 with override_config(self,
1023 ignore_errors=ignore_errors, report_omit=omit, report_include=include,
1024 html_dir=directory, extra_css=extra_css, html_title=title,
1025 html_skip_covered=skip_covered, show_contexts=show_contexts, report_contexts=contexts,
1026 html_skip_empty=skip_empty, precision=precision,
1027 ):
1028 reporter = HtmlReporter(self)
1029 ret = reporter.report(morfs)
1030 return ret
1031
1032 def xml_report(
1033 self, morfs=None, outfile=None, ignore_errors=None,
1034 omit=None, include=None, contexts=None, skip_empty=None,
1035 ):
1036 """Generate an XML report of coverage results.
1037
1038 The report is compatible with Cobertura reports.
1039
1040 Each module in `morfs` is included in the report. `outfile` is the
1041 path to write the file to, "-" will write to stdout.
1042
1043 See :meth:`report` for other arguments.
1044
1045 Returns a float, the total percentage covered.
1046
1047 """
1048 with override_config(self,
1049 ignore_errors=ignore_errors, report_omit=omit, report_include=include,
1050 xml_output=outfile, report_contexts=contexts, skip_empty=skip_empty,
1051 ):
1052 return render_report(self.config.xml_output, XmlReporter(self), morfs, self._message)
1053
1054 def json_report(
1055 self, morfs=None, outfile=None, ignore_errors=None,
1056 omit=None, include=None, contexts=None, pretty_print=None,
1057 show_contexts=None
1058 ):
1059 """Generate a JSON report of coverage results.
1060
1061 Each module in `morfs` is included in the report. `outfile` is the
1062 path to write the file to, "-" will write to stdout.
1063
1064 See :meth:`report` for other arguments.
1065
1066 Returns a float, the total percentage covered.
1067
1068 .. versionadded:: 5.0
1069
1070 """
1071 with override_config(self,
1072 ignore_errors=ignore_errors, report_omit=omit, report_include=include,
1073 json_output=outfile, report_contexts=contexts, json_pretty_print=pretty_print,
1074 json_show_contexts=show_contexts
1075 ):
1076 return render_report(self.config.json_output, JsonReporter(self), morfs, self._message)
1077
1078 def lcov_report(
1079 self, morfs=None, outfile=None, ignore_errors=None,
1080 omit=None, include=None, contexts=None,
1081 ):
1082 """Generate an LCOV report of coverage results.
1083
1084 Each module in 'morfs' is included in the report. 'outfile' is the
1085 path to write the file to, "-" will write to stdout.
1086
1087 See :meth 'report' for other arguments.
1088
1089 .. versionadded:: 6.3
1090 """
1091 with override_config(self,
1092 ignore_errors=ignore_errors, report_omit=omit, report_include=include,
1093 lcov_output=outfile, report_contexts=contexts,
1094 ):
1095 return render_report(self.config.lcov_output, LcovReporter(self), morfs, self._message)
1096
1097 def sys_info(self):
1098 """Return a list of (key, value) pairs showing internal information."""
1099
1100 import coverage as covmod
1101
1102 self._init()
1103 self._post_init()
1104
1105 def plugin_info(plugins):
1106 """Make an entry for the sys_info from a list of plug-ins."""
1107 entries = []
1108 for plugin in plugins:
1109 entry = plugin._coverage_plugin_name
1110 if not plugin._coverage_enabled:
1111 entry += " (disabled)"
1112 entries.append(entry)
1113 return entries
1114
1115 info = [
1116 ('coverage_version', covmod.__version__),
1117 ('coverage_module', covmod.__file__),
1118 ('tracer', self._collector.tracer_name() if self._collector else "-none-"),
1119 ('CTracer', 'available' if CTracer else "unavailable"),
1120 ('plugins.file_tracers', plugin_info(self._plugins.file_tracers)),
1121 ('plugins.configurers', plugin_info(self._plugins.configurers)),
1122 ('plugins.context_switchers', plugin_info(self._plugins.context_switchers)),
1123 ('configs_attempted', self.config.attempted_config_files),
1124 ('configs_read', self.config.config_files_read),
1125 ('config_file', self.config.config_file),
1126 ('config_contents',
1127 repr(self.config._config_contents)
1128 if self.config._config_contents
1129 else '-none-'
1130 ),
1131 ('data_file', self._data.data_filename() if self._data is not None else "-none-"),
1132 ('python', sys.version.replace('\n', '')),
1133 ('platform', platform.platform()),
1134 ('implementation', platform.python_implementation()),
1135 ('executable', sys.executable),
1136 ('def_encoding', sys.getdefaultencoding()),
1137 ('fs_encoding', sys.getfilesystemencoding()),
1138 ('pid', os.getpid()),
1139 ('cwd', os.getcwd()),
1140 ('path', sys.path),
1141 ('environment', human_sorted(
1142 f"{k} = {v}"
1143 for k, v in os.environ.items()
1144 if (
1145 any(slug in k for slug in ("COV", "PY")) or
1146 (k in ("HOME", "TEMP", "TMP"))
1147 )
1148 )),
1149 ('command_line', " ".join(getattr(sys, 'argv', ['-none-']))),
1150 ]
1151
1152 if self._inorout:
1153 info.extend(self._inorout.sys_info())
1154
1155 info.extend(CoverageData.sys_info())
1156
1157 return info
1158
1159
1160 # Mega debugging...
1161 # $set_env.py: COVERAGE_DEBUG_CALLS - Lots and lots of output about calls to Coverage.
1162 if int(os.environ.get("COVERAGE_DEBUG_CALLS", 0)): # pragma: debugging
1163 from coverage.debug import decorate_methods, show_calls
1164
1165 Coverage = decorate_methods(show_calls(show_args=True), butnot=['get_data'])(Coverage)
1166
1167
1168 def process_startup():
1169 """Call this at Python start-up to perhaps measure coverage.
1170
1171 If the environment variable COVERAGE_PROCESS_START is defined, coverage
1172 measurement is started. The value of the variable is the config file
1173 to use.
1174
1175 There are two ways to configure your Python installation to invoke this
1176 function when Python starts:
1177
1178 #. Create or append to sitecustomize.py to add these lines::
1179
1180 import coverage
1181 coverage.process_startup()
1182
1183 #. Create a .pth file in your Python installation containing::
1184
1185 import coverage; coverage.process_startup()
1186
1187 Returns the :class:`Coverage` instance that was started, or None if it was
1188 not started by this call.
1189
1190 """
1191 cps = os.environ.get("COVERAGE_PROCESS_START")
1192 if not cps:
1193 # No request for coverage, nothing to do.
1194 return None
1195
1196 # This function can be called more than once in a process. This happens
1197 # because some virtualenv configurations make the same directory visible
1198 # twice in sys.path. This means that the .pth file will be found twice,
1199 # and executed twice, executing this function twice. We set a global
1200 # flag (an attribute on this function) to indicate that coverage.py has
1201 # already been started, so we can avoid doing it twice.
1202 #
1203 # https://github.com/nedbat/coveragepy/issues/340 has more details.
1204
1205 if hasattr(process_startup, "coverage"):
1206 # We've annotated this function before, so we must have already
1207 # started coverage.py in this process. Nothing to do.
1208 return None
1209
1210 cov = Coverage(config_file=cps)
1211 process_startup.coverage = cov
1212 cov._warn_no_data = False
1213 cov._warn_unimported_source = False
1214 cov._warn_preimported_source = False
1215 cov._auto_save = True
1216 cov.start()
1217
1218 return cov
1219
1220
1221 def _prevent_sub_process_measurement():
1222 """Stop any subprocess auto-measurement from writing data."""
1223 auto_created_coverage = getattr(process_startup, "coverage", None)
1224 if auto_created_coverage is not None:
1225 auto_created_coverage._auto_save = False

eric ide

mercurial