--- a/eric7/DebugClients/Python/coverage/control.py Fri Nov 19 19:28:47 2021 +0100 +++ b/eric7/DebugClients/Python/coverage/control.py Sat Nov 20 16:47:38 2021 +0100 @@ -11,27 +11,28 @@ import platform import sys import time +import warnings from coverage import env from coverage.annotate import AnnotateReporter -from coverage.backward import string_class, iitems from coverage.collector import Collector, CTracer from coverage.config import read_coverage_config from coverage.context import should_start_context_test_function, combine_context_switchers from coverage.data import CoverageData, combine_parallel_data from coverage.debug import DebugControl, short_stack, write_formatted_info from coverage.disposition import disposition_debug_msg +from coverage.exceptions import CoverageException, CoverageWarning from coverage.files import PathAliases, abs_file, relative_filename, set_relative_directory from coverage.html import HtmlReporter from coverage.inorout import InOrOut from coverage.jsonreport import JsonReporter -from coverage.misc import CoverageException, bool_or_none, join_regex +from coverage.misc import bool_or_none, join_regex, human_sorted, human_sorted_items from coverage.misc import DefaultValue, ensure_dir_for_file, isolate_module from coverage.plugin import FileReporter from coverage.plugin_support import Plugins from coverage.python import PythonFileReporter from coverage.report import render_report -from coverage.results import Analysis, Numbers +from coverage.results import Analysis from coverage.summary import SummaryReporter from coverage.xmlreport import XmlReporter @@ -61,7 +62,7 @@ _DEFAULT_DATAFILE = DefaultValue("MISSING") -class Coverage(object): +class Coverage: """Programmatic access to coverage.py. To use:: @@ -102,6 +103,7 @@ auto_data=False, timid=None, branch=None, config_file=True, source=None, source_pkgs=None, omit=None, include=None, debug=None, concurrency=None, check_preimported=False, context=None, + messages=False, ): # pylint: disable=too-many-arguments """ Many of these arguments duplicate and override values that can be @@ -172,6 +174,9 @@ `context` is a string to use as the :ref:`static context <static_contexts>` label for collected data. + If `messages` is true, some messages will be printed to stdout + indicating what is happening. + .. versionadded:: 4.0 The `concurrency` parameter. @@ -184,6 +189,9 @@ .. versionadded:: 5.3 The `source_pkgs` parameter. + .. versionadded:: 6.0 + The `messages` parameter. + """ # data_file=None means no disk file at all. data_file missing means # use the value from the config file. @@ -191,15 +199,7 @@ if data_file is _DEFAULT_DATAFILE: data_file = None - # Build our configuration from a number of sources. - self.config = read_coverage_config( - config_file=config_file, - data_file=data_file, cover_pylib=cover_pylib, timid=timid, - branch=branch, parallel=bool_or_none(data_suffix), - source=source, source_pkgs=source_pkgs, run_omit=omit, run_include=include, debug=debug, - report_omit=omit, report_include=include, - concurrency=concurrency, context=context, - ) + self.config = None # This is injectable by tests. self._debug_file = None @@ -212,6 +212,7 @@ self._warn_unimported_source = True self._warn_preimported_source = check_preimported self._no_warn_slugs = None + self._messages = messages # A record of all the warnings that have been issued. self._warnings = [] @@ -234,6 +235,16 @@ # Should we write the debug output? self._should_write_debug = True + # Build our configuration from a number of sources. + self.config = read_coverage_config( + config_file=config_file, warn=self._warn, + data_file=data_file, cover_pylib=cover_pylib, timid=timid, + branch=branch, parallel=bool_or_none(data_suffix), + source=source, source_pkgs=source_pkgs, run_omit=omit, run_include=include, debug=debug, + report_omit=omit, report_include=include, + concurrency=concurrency, context=context, + ) + # If we have sub-process measurement happening automatically, then we # want any explicit creation of a Coverage object to mean, this process # is already coverage-aware, so don't auto-measure it. By now, the @@ -291,14 +302,14 @@ # '[run] _crash' will raise an exception if the value is close by in # the call stack, for testing error handling. if self.config._crash and self.config._crash in short_stack(limit=4): - raise Exception("Crashing because called by {}".format(self.config._crash)) + raise Exception(f"Crashing because called by {self.config._crash}") def _write_startup_debug(self): """Write out debug info at startup if needed.""" wrote_any = False with self._debug.without_callers(): if self._debug.should('config'): - config_info = sorted(self.config.__dict__.items()) + config_info = human_sorted_items(self.config.__dict__.items()) config_info = [(k, v) for k, v in config_info if not k.startswith('_')] write_formatted_info(self._debug, "config", config_info) wrote_any = True @@ -334,9 +345,9 @@ reason = self._inorout.check_include_omit_etc(filename, frame) if self._debug.should('trace'): if not reason: - msg = "Including %r" % (filename,) + msg = f"Including {filename!r}" else: - msg = "Not including %r: %s" % (filename, reason) + msg = f"Not including {filename!r}: {reason}" self._debug.write(msg) return not reason @@ -351,22 +362,29 @@ """ if self._no_warn_slugs is None: - self._no_warn_slugs = list(self.config.disable_warnings) + if self.config is not None: + self._no_warn_slugs = list(self.config.disable_warnings) - if slug in self._no_warn_slugs: - # Don't issue the warning - return + if self._no_warn_slugs is not None: + if slug in self._no_warn_slugs: + # Don't issue the warning + return self._warnings.append(msg) if slug: - msg = "%s (%s)" % (msg, slug) - if self._debug.should('pid'): - msg = "[%d] %s" % (os.getpid(), msg) - sys.stderr.write("Coverage.py warning: %s\n" % msg) + msg = f"{msg} ({slug})" + if self._debug is not None and self._debug.should('pid'): + msg = f"[{os.getpid()}] {msg}" + warnings.warn(msg, category=CoverageWarning, stacklevel=2) if once: self._no_warn_slugs.append(slug) + def _message(self, msg): + """Write a message to the user, if configured to do so.""" + if self._messages: + print(msg) + def get_option(self, option_name): """Get an option from the configuration. @@ -442,9 +460,7 @@ elif dycon == "test_function": context_switchers = [should_start_context_test_function] else: - raise CoverageException( - "Don't understand dynamic_context setting: {!r}".format(dycon) - ) + raise CoverageException(f"Don't understand dynamic_context setting: {dycon!r}") context_switchers.extend( plugin.dynamic_context for plugin in self._plugins.context_switchers @@ -465,7 +481,7 @@ suffix = self._data_suffix_specified if suffix or self.config.parallel: - if not isinstance(suffix, string_class): + if not isinstance(suffix, str): # if data_suffix=True, use .machinename.pid.random suffix = True else: @@ -478,7 +494,7 @@ # Early warning if we aren't going to be able to support plugins. if self._plugins.file_tracers and not self._collector.supports_plugins: self._warn( - "Plugin file tracers (%s) aren't supported with %s" % ( + "Plugin file tracers ({}) aren't supported with {}".format( ", ".join( plugin._coverage_plugin_name for plugin in self._plugins.file_tracers @@ -562,7 +578,7 @@ def _atexit(self): """Clean up on process shutdown.""" if self._debug.should("process"): - self._debug.write("atexit: pid: {}, instance: {!r}".format(os.getpid(), self)) + self._debug.write(f"atexit: pid: {os.getpid()}, instance: {self!r}") if self._started: self.stop() if self._auto_save: @@ -598,9 +614,7 @@ """ if not self._started: # pragma: part started - raise CoverageException( - "Cannot switch context, coverage is not started" - ) + raise CoverageException("Cannot switch context, coverage is not started") if self._collector.should_start_context: self._warn("Conflicting dynamic contexts", slug="dynamic-conflict", once=True) @@ -692,7 +706,7 @@ aliases = None if self.config.paths: - aliases = PathAliases() + aliases = PathAliases(relative=self.config.relative_files) for paths in self.config.paths.values(): result = paths[0] for pattern in paths[1:]: @@ -704,6 +718,7 @@ data_paths=data_paths, strict=strict, keep=keep, + message=self._message, ) def get_data(self): @@ -798,21 +813,20 @@ """ # All reporting comes through here, so do reporting initialization. self._init() - Numbers.set_precision(self.config.precision) self._post_init() data = self.get_data() if not isinstance(it, FileReporter): it = self._get_file_reporter(it) - return Analysis(data, it, self._file_mapper) + return Analysis(data, self.config.precision, it, self._file_mapper) def _get_file_reporter(self, morf): """Get a FileReporter for a module or file name.""" plugin = None file_reporter = "python" - if isinstance(morf, string_class): + if isinstance(morf, str): mapped_morf = self._file_mapper(morf) plugin_name = self._data.file_tracer(mapped_morf) if plugin_name: @@ -822,7 +836,7 @@ file_reporter = plugin.file_reporter(mapped_morf) if file_reporter is None: raise CoverageException( - "Plugin %r did not provide a file reporter for %r." % ( + "Plugin {!r} did not provide a file reporter for {!r}.".format( plugin._coverage_plugin_name, morf ) ) @@ -918,6 +932,11 @@ ): """Annotate a list of modules. + .. note:: + This method has been obsoleted by more modern reporting tools, + including the :meth:`html_report` method. It will be removed in a + future version. + Each module in `morfs` is annotated. The source is written to a new file, named with a ",cover" suffix, with each line prefixed with a marker to indicate the coverage of the line. Covered lines have ">", @@ -926,6 +945,9 @@ See :meth:`report` for other arguments. """ + print("The annotate command will be removed in a future version.") + print("Get in touch if you still use it: ned@nedbatchelder.com") + with override_config(self, ignore_errors=ignore_errors, report_omit=omit, report_include=include, report_contexts=contexts, @@ -969,7 +991,8 @@ html_skip_empty=skip_empty, precision=precision, ): reporter = HtmlReporter(self) - return reporter.report(morfs) + ret = reporter.report(morfs) + return ret def xml_report( self, morfs=None, outfile=None, ignore_errors=None, @@ -991,7 +1014,7 @@ ignore_errors=ignore_errors, report_omit=omit, report_include=include, xml_output=outfile, report_contexts=contexts, skip_empty=skip_empty, ): - return render_report(self.config.xml_output, XmlReporter(self), morfs) + return render_report(self.config.xml_output, XmlReporter(self), morfs, self._message) def json_report( self, morfs=None, outfile=None, ignore_errors=None, @@ -1015,7 +1038,7 @@ json_output=outfile, report_contexts=contexts, json_pretty_print=pretty_print, json_show_contexts=show_contexts ): - return render_report(self.config.json_output, JsonReporter(self), morfs) + return render_report(self.config.json_output, JsonReporter(self), morfs, self._message) def sys_info(self): """Return a list of (key, value) pairs showing internal information.""" @@ -1036,8 +1059,8 @@ return entries info = [ - ('version', covmod.__version__), - ('coverage', covmod.__file__), + ('coverage_version', covmod.__version__), + ('coverage_module', covmod.__file__), ('tracer', self._collector.tracer_name() if self._collector else "-none-"), ('CTracer', 'available' if CTracer else "unavailable"), ('plugins.file_tracers', plugin_info(self._plugins.file_tracers)), @@ -1061,10 +1084,13 @@ ('pid', os.getpid()), ('cwd', os.getcwd()), ('path', sys.path), - ('environment', sorted( - ("%s = %s" % (k, v)) - for k, v in iitems(os.environ) - if any(slug in k for slug in ("COV", "PY")) + ('environment', human_sorted( + f"{k} = {v}" + for k, v in os.environ.items() + if ( + any(slug in k for slug in ("COV", "PY")) or + (k in ("HOME", "TEMP", "TMP")) + ) )), ('command_line', " ".join(getattr(sys, 'argv', ['-none-']))), ]