Upgraded coverage to version 6.2.0. eric7

Sat, 22 Jan 2022 14:44:56 +0100

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Sat, 22 Jan 2022 14:44:56 +0100
branch
eric7
changeset 8929
fcca2fa618bf
parent 8928
d856023fbeb0
child 8930
5e3bd6f49a84

Upgraded coverage to version 6.2.0.

docs/changelog file | annotate | diff | comparison | revisions
eric7/DebugClients/Python/coverage/cmdline.py file | annotate | diff | comparison | revisions
eric7/DebugClients/Python/coverage/collector.py file | annotate | diff | comparison | revisions
eric7/DebugClients/Python/coverage/config.py file | annotate | diff | comparison | revisions
eric7/DebugClients/Python/coverage/control.py file | annotate | diff | comparison | revisions
eric7/DebugClients/Python/coverage/data.py file | annotate | diff | comparison | revisions
eric7/DebugClients/Python/coverage/disposition.py file | annotate | diff | comparison | revisions
eric7/DebugClients/Python/coverage/doc/CHANGES.rst file | annotate | diff | comparison | revisions
eric7/DebugClients/Python/coverage/env.py file | annotate | diff | comparison | revisions
eric7/DebugClients/Python/coverage/exceptions.py file | annotate | diff | comparison | revisions
eric7/DebugClients/Python/coverage/execfile.py file | annotate | diff | comparison | revisions
eric7/DebugClients/Python/coverage/files.py file | annotate | diff | comparison | revisions
eric7/DebugClients/Python/coverage/html.py file | annotate | diff | comparison | revisions
eric7/DebugClients/Python/coverage/inorout.py file | annotate | diff | comparison | revisions
eric7/DebugClients/Python/coverage/misc.py file | annotate | diff | comparison | revisions
eric7/DebugClients/Python/coverage/parser.py file | annotate | diff | comparison | revisions
eric7/DebugClients/Python/coverage/plugin.py file | annotate | diff | comparison | revisions
eric7/DebugClients/Python/coverage/plugin_support.py file | annotate | diff | comparison | revisions
eric7/DebugClients/Python/coverage/report.py file | annotate | diff | comparison | revisions
eric7/DebugClients/Python/coverage/results.py file | annotate | diff | comparison | revisions
eric7/DebugClients/Python/coverage/sqldata.py file | annotate | diff | comparison | revisions
eric7/DebugClients/Python/coverage/summary.py file | annotate | diff | comparison | revisions
eric7/DebugClients/Python/coverage/tomlconfig.py file | annotate | diff | comparison | revisions
eric7/DebugClients/Python/coverage/version.py file | annotate | diff | comparison | revisions
eric7/DebugClients/Python/coverage/xmlreport.py file | annotate | diff | comparison | revisions
--- a/docs/changelog	Sun Jan 16 20:28:42 2022 +0100
+++ b/docs/changelog	Sat Jan 22 14:44:56 2022 +0100
@@ -19,6 +19,8 @@
 - Styles and Themes
   -- added a dark blueish style (QSS and Highlighters) and an associated theme
      (dark_blue.ethj and dark_blue_with_stylesheet.ethj)
+- Third Party packages
+  -- upgraded coverage to 6.2.0
 
 Version 22.1.1:
 - bug fix
--- a/eric7/DebugClients/Python/coverage/cmdline.py	Sun Jan 16 20:28:42 2022 +0100
+++ b/eric7/DebugClients/Python/coverage/cmdline.py	Sat Jan 22 14:44:56 2022 +0100
@@ -17,11 +17,11 @@
 from coverage import Coverage
 from coverage import env
 from coverage.collector import CTracer
-from coverage.data import line_counts
+from coverage.config import CoverageConfig
+from coverage.data import combinable_files, debug_data_file
 from coverage.debug import info_formatter, info_header, short_stack
-from coverage.exceptions import BaseCoverageException, ExceptionDuringRun, NoSource
+from coverage.exceptions import _BaseCoverageException, _ExceptionDuringRun, NoSource
 from coverage.execfile import PyRunner
-from coverage.misc import human_sorted
 from coverage.results import Numbers, should_fail_under
 
 
@@ -40,16 +40,12 @@
         '', '--branch', action='store_true',
         help="Measure branch coverage in addition to statement coverage.",
     )
-    CONCURRENCY_CHOICES = [
-        "thread", "gevent", "greenlet", "eventlet", "multiprocessing",
-    ]
     concurrency = optparse.make_option(
-        '', '--concurrency', action='store', metavar="LIB",
-        choices=CONCURRENCY_CHOICES,
+        '', '--concurrency', action='store', metavar="LIBS",
         help=(
             "Properly measure code using a concurrency library. " +
-            "Valid values are: {}."
-        ).format(", ".join(CONCURRENCY_CHOICES)),
+            "Valid values are: {}, or a comma-list of them."
+        ).format(", ".join(sorted(CoverageConfig.CONCURRENCY_CHOICES))),
     )
     context = optparse.make_option(
         '', '--context', action='store', metavar="LABEL",
@@ -571,6 +567,11 @@
         debug = unshell_list(options.debug)
         contexts = unshell_list(options.contexts)
 
+        if options.concurrency is not None:
+            concurrency = options.concurrency.split(",")
+        else:
+            concurrency = None
+
         # Do something.
         self.coverage = Coverage(
             data_suffix=options.parallel_mode,
@@ -582,7 +583,7 @@
             omit=omit,
             include=include,
             debug=debug,
-            concurrency=options.concurrency,
+            concurrency=concurrency,
             check_preimported=True,
             context=options.context,
             messages=not options.quiet,
@@ -601,8 +602,8 @@
         elif options.action == "combine":
             if options.append:
                 self.coverage.load()
-            data_dirs = args or None
-            self.coverage.combine(data_dirs, strict=True, keep=bool(options.keep))
+            data_paths = args or None
+            self.coverage.combine(data_paths, strict=True, keep=bool(options.keep))
             self.coverage.save()
             return OK
 
@@ -778,42 +779,33 @@
         if not args:
             show_help("What information would you like: config, data, sys, premain?")
             return ERR
+        if args[1:]:
+            show_help("Only one topic at a time, please")
+            return ERR
 
-        for info in args:
-            if info == 'sys':
-                sys_info = self.coverage.sys_info()
-                print(info_header("sys"))
-                for line in info_formatter(sys_info):
-                    print(f" {line}")
-            elif info == 'data':
-                self.coverage.load()
-                data = self.coverage.get_data()
-                print(info_header("data"))
-                print(f"path: {data.data_filename()}")
-                if data:
-                    print(f"has_arcs: {data.has_arcs()!r}")
-                    summary = line_counts(data, fullpath=True)
-                    filenames = human_sorted(summary.keys())
-                    print(f"\n{len(filenames)} files:")
-                    for f in filenames:
-                        line = f"{f}: {summary[f]} lines"
-                        plugin = data.file_tracer(f)
-                        if plugin:
-                            line += f" [{plugin}]"
-                        print(line)
-                else:
-                    print("No data collected")
-            elif info == 'config':
-                print(info_header("config"))
-                config_info = self.coverage.config.__dict__.items()
-                for line in info_formatter(config_info):
-                    print(f" {line}")
-            elif info == "premain":
-                print(info_header("premain"))
-                print(short_stack())
-            else:
-                show_help(f"Don't know what you mean by {info!r}")
-                return ERR
+        if args[0] == 'sys':
+            sys_info = self.coverage.sys_info()
+            print(info_header("sys"))
+            for line in info_formatter(sys_info):
+                print(f" {line}")
+        elif args[0] == 'data':
+            print(info_header("data"))
+            data_file = self.coverage.config.data_file
+            debug_data_file(data_file)
+            for filename in combinable_files(data_file):
+                print("-----")
+                debug_data_file(filename)
+        elif args[0] == 'config':
+            print(info_header("config"))
+            config_info = sorted(self.coverage.config.__dict__.items())
+            for line in info_formatter(config_info):
+                print(f" {line}")
+        elif args[0] == "premain":
+            print(info_header("premain"))
+            print(short_stack())
+        else:
+            show_help(f"Don't know what you mean by {args[0]!r}")
+            return ERR
 
         return OK
 
@@ -887,13 +879,13 @@
         argv = sys.argv[1:]
     try:
         status = CoverageScript().command_line(argv)
-    except ExceptionDuringRun as err:
+    except _ExceptionDuringRun as err:
         # An exception was caught while running the product code.  The
-        # sys.exc_info() return tuple is packed into an ExceptionDuringRun
+        # sys.exc_info() return tuple is packed into an _ExceptionDuringRun
         # exception.
         traceback.print_exception(*err.args)    # pylint: disable=no-value-for-parameter
         status = ERR
-    except BaseCoverageException as err:
+    except _BaseCoverageException as err:
         # A controlled error inside coverage.py: print the message to the user.
         msg = err.args[0]
         print(msg)
--- a/eric7/DebugClients/Python/coverage/collector.py	Sun Jan 16 20:28:42 2022 +0100
+++ b/eric7/DebugClients/Python/coverage/collector.py	Sat Jan 22 14:44:56 2022 +0100
@@ -7,9 +7,10 @@
 import sys
 
 from coverage import env
+from coverage.config import CoverageConfig
 from coverage.debug import short_stack
 from coverage.disposition import FileDisposition
-from coverage.exceptions import CoverageException
+from coverage.exceptions import ConfigError
 from coverage.misc import human_sorted, isolate_module
 from coverage.pytracer import PyTracer
 
@@ -55,7 +56,7 @@
     _collectors = []
 
     # The concurrency settings we support here.
-    SUPPORTED_CONCURRENCIES = {"greenlet", "eventlet", "gevent", "thread"}
+    LIGHT_THREADS = {"greenlet", "eventlet", "gevent"}
 
     def __init__(
         self, should_trace, check_include, should_start_context, file_mapper,
@@ -93,19 +94,21 @@
 
         `concurrency` is a list of strings indicating the concurrency libraries
         in use.  Valid values are "greenlet", "eventlet", "gevent", or "thread"
-        (the default).  Of these four values, only one can be supplied.  Other
-        values are ignored.
+        (the default).  "thread" can be combined with one of the other three.
+        Other values are ignored.
 
         """
         self.should_trace = should_trace
         self.check_include = check_include
         self.should_start_context = should_start_context
         self.file_mapper = file_mapper
+        self.branch = branch
         self.warn = warn
-        self.branch = branch
+        self.concurrency = concurrency
+        assert isinstance(self.concurrency, list), f"Expected a list: {self.concurrency!r}"
+
         self.threading = None
         self.covdata = None
-
         self.static_context = None
 
         self.origin = short_stack()
@@ -113,39 +116,6 @@
         self.concur_id_func = None
         self.mapped_file_cache = {}
 
-        # We can handle a few concurrency options here, but only one at a time.
-        these_concurrencies = self.SUPPORTED_CONCURRENCIES.intersection(concurrency)
-        if len(these_concurrencies) > 1:
-            raise CoverageException(f"Conflicting concurrency settings: {concurrency}")
-        self.concurrency = these_concurrencies.pop() if these_concurrencies else ''
-
-        try:
-            if self.concurrency == "greenlet":
-                import greenlet
-                self.concur_id_func = greenlet.getcurrent
-            elif self.concurrency == "eventlet":
-                import eventlet.greenthread     # pylint: disable=import-error,useless-suppression
-                self.concur_id_func = eventlet.greenthread.getcurrent
-            elif self.concurrency == "gevent":
-                import gevent                   # pylint: disable=import-error,useless-suppression
-                self.concur_id_func = gevent.getcurrent
-            elif self.concurrency == "thread" or not self.concurrency:
-                # It's important to import threading only if we need it.  If
-                # it's imported early, and the program being measured uses
-                # gevent, then gevent's monkey-patching won't work properly.
-                import threading
-                self.threading = threading
-            else:
-                raise CoverageException(f"Don't understand concurrency={concurrency}")
-        except ImportError as ex:
-            raise CoverageException(
-                "Couldn't trace with concurrency={}, the module isn't installed.".format(
-                    self.concurrency,
-                )
-            ) from ex
-
-        self.reset()
-
         if timid:
             # Being timid: use the simple Python trace function.
             self._trace_class = PyTracer
@@ -163,6 +133,54 @@
             self.supports_plugins = False
             self.packed_arcs = False
 
+        # We can handle a few concurrency options here, but only one at a time.
+        concurrencies = set(self.concurrency)
+        unknown = concurrencies - CoverageConfig.CONCURRENCY_CHOICES
+        if unknown:
+            show = ", ".join(sorted(unknown))
+            raise ConfigError(f"Unknown concurrency choices: {show}")
+        light_threads = concurrencies & self.LIGHT_THREADS
+        if len(light_threads) > 1:
+            show = ", ".join(sorted(light_threads))
+            raise ConfigError(f"Conflicting concurrency settings: {show}")
+        do_threading = False
+
+        try:
+            if "greenlet" in concurrencies:
+                tried = "greenlet"
+                import greenlet
+                self.concur_id_func = greenlet.getcurrent
+            elif "eventlet" in concurrencies:
+                tried = "eventlet"
+                import eventlet.greenthread     # pylint: disable=import-error,useless-suppression
+                self.concur_id_func = eventlet.greenthread.getcurrent
+            elif "gevent" in concurrencies:
+                tried = "gevent"
+                import gevent                   # pylint: disable=import-error,useless-suppression
+                self.concur_id_func = gevent.getcurrent
+
+            if "thread" in concurrencies:
+                do_threading = True
+        except ImportError as ex:
+            msg = f"Couldn't trace with concurrency={tried}, the module isn't installed."
+            raise ConfigError(msg) from ex
+
+        if self.concur_id_func and not hasattr(self._trace_class, "concur_id_func"):
+            raise ConfigError(
+                "Can't support concurrency={} with {}, only threads are supported.".format(
+                    tried, self.tracer_name(),
+                )
+            )
+
+        if do_threading or not concurrencies:
+            # It's important to import threading only if we need it.  If
+            # it's imported early, and the program being measured uses
+            # gevent, then gevent's monkey-patching won't work properly.
+            import threading
+            self.threading = threading
+
+        self.reset()
+
     def __repr__(self):
         return f"<Collector at 0x{id(self):x}: {self.tracer_name()}>"
 
@@ -244,13 +262,6 @@
 
         if hasattr(tracer, 'concur_id_func'):
             tracer.concur_id_func = self.concur_id_func
-        elif self.concur_id_func:
-            raise CoverageException(
-                "Can't support concurrency={} with {}, only threads are supported".format(
-                    self.concurrency, self.tracer_name(),
-                )
-            )
-
         if hasattr(tracer, 'file_tracers'):
             tracer.file_tracers = self.file_tracers
         if hasattr(tracer, 'threading'):
--- a/eric7/DebugClients/Python/coverage/config.py	Sun Jan 16 20:28:42 2022 +0100
+++ b/eric7/DebugClients/Python/coverage/config.py	Sat Jan 22 14:44:56 2022 +0100
@@ -10,7 +10,7 @@
 import os.path
 import re
 
-from coverage.exceptions import CoverageException
+from coverage.exceptions import ConfigError
 from coverage.misc import contract, isolate_module, substitute_variables
 
 from coverage.tomlconfig import TomlConfigParser, TomlDecodeError
@@ -59,7 +59,7 @@
             real_section = section_prefix + section
             if configparser.RawConfigParser.has_section(self, real_section):
                 return configparser.RawConfigParser.options(self, real_section)
-        raise configparser.NoSectionError(section)
+        raise ConfigError(f"No section: {section!r}")
 
     def get_section(self, section):
         """Get the contents of a section, as a dictionary."""
@@ -83,7 +83,7 @@
             if configparser.RawConfigParser.has_option(self, real_section, option):
                 break
         else:
-            raise configparser.NoOptionError(option, section)
+            raise ConfigError(f"No option {option!r} in section: {section!r}")
 
         v = configparser.RawConfigParser.get(self, real_section, option, *args, **kwargs)
         v = substitute_variables(v, os.environ)
@@ -123,7 +123,7 @@
             try:
                 re.compile(value)
             except re.error as e:
-                raise CoverageException(
+                raise ConfigError(
                     f"Invalid [{section}].{option} value {value!r}: {e}"
                 ) from e
             if value:
@@ -233,11 +233,11 @@
         # Options for plugins
         self.plugin_options = {}
 
-    MUST_BE_LIST = [
+    MUST_BE_LIST = {
         "debug", "concurrency", "plugins",
         "report_omit", "report_include",
         "run_omit", "run_include",
-    ]
+    }
 
     def from_args(self, **kwargs):
         """Read config values from `kwargs`."""
@@ -272,7 +272,7 @@
         try:
             files_read = cp.read(filename)
         except (configparser.Error, TomlDecodeError) as err:
-            raise CoverageException(f"Couldn't read config file {filename}: {err}") from err
+            raise ConfigError(f"Couldn't read config file {filename}: {err}") from err
         if not files_read:
             return False
 
@@ -285,7 +285,7 @@
                 if was_set:
                     any_set = True
         except ValueError as err:
-            raise CoverageException(f"Couldn't read config file {filename}: {err}") from err
+            raise ConfigError(f"Couldn't read config file {filename}: {err}") from err
 
         # Check that there are no unrecognized options.
         all_options = collections.defaultdict(set)
@@ -334,6 +334,8 @@
         """Return a copy of the configuration."""
         return copy.deepcopy(self)
 
+    CONCURRENCY_CHOICES = {"thread", "gevent", "greenlet", "eventlet", "multiprocessing"}
+
     CONFIG_FILE_OPTIONS = [
         # These are *args for _set_attr_from_config_option:
         #   (attr, where, type_="")
@@ -443,7 +445,7 @@
             return
 
         # If we get here, we didn't find the option.
-        raise CoverageException(f"No such option: {option_name!r}")
+        raise ConfigError(f"No such option: {option_name!r}")
 
     def get_option(self, option_name):
         """Get an option from the configuration.
@@ -471,7 +473,7 @@
             return self.plugin_options.get(plugin_name, {}).get(key)
 
         # If we get here, we didn't find the option.
-        raise CoverageException(f"No such option: {option_name!r}")
+        raise ConfigError(f"No such option: {option_name!r}")
 
     def post_process_file(self, path):
         """Make final adjustments to a file path to make it usable."""
@@ -546,7 +548,7 @@
             if config_read:
                 break
             if specified_file:
-                raise CoverageException(f"Couldn't read {fname!r} as a config file")
+                raise ConfigError(f"Couldn't read {fname!r} as a config file")
 
     # $set_env.py: COVERAGE_DEBUG - Options for --debug.
     # 3) from environment variables:
--- a/eric7/DebugClients/Python/coverage/control.py	Sun Jan 16 20:28:42 2022 +0100
+++ b/eric7/DebugClients/Python/coverage/control.py	Sat Jan 22 14:44:56 2022 +0100
@@ -21,7 +21,7 @@
 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.exceptions import ConfigError, CoverageException, CoverageWarning, PluginError
 from coverage.files import PathAliases, abs_file, relative_filename, set_relative_directory
 from coverage.html import HtmlReporter
 from coverage.inorout import InOrOut
@@ -79,6 +79,8 @@
     not part of the public API. They might stop working at any point.  Please
     limit yourself to documented methods to avoid problems.
 
+    Methods can raise any of the exceptions described in :ref:`api_exceptions`.
+
     """
 
     # The stack of started Coverage instances.
@@ -446,10 +448,10 @@
     def _init_for_start(self):
         """Initialization for start()"""
         # Construct the collector.
-        concurrency = self.config.concurrency or ()
+        concurrency = self.config.concurrency or []
         if "multiprocessing" in concurrency:
             if not patch_multiprocessing:
-                raise CoverageException(                    # pragma: only jython
+                raise ConfigError(                      # pragma: only jython
                     "multiprocessing is not supported on this Python"
                 )
             patch_multiprocessing(rcfile=self.config.config_file)
@@ -460,7 +462,7 @@
         elif dycon == "test_function":
             context_switchers = [should_start_context_test_function]
         else:
-            raise CoverageException(f"Don't understand dynamic_context setting: {dycon!r}")
+            raise ConfigError(f"Don't understand dynamic_context setting: {dycon!r}")
 
         context_switchers.extend(
             plugin.dynamic_context for plugin in self._plugins.context_switchers
@@ -480,10 +482,15 @@
             )
 
         suffix = self._data_suffix_specified
-        if suffix or self.config.parallel:
+        if suffix:
             if not isinstance(suffix, str):
                 # if data_suffix=True, use .machinename.pid.random
                 suffix = True
+        elif self.config.parallel:
+            if suffix is None:
+                suffix = True
+            elif not isinstance(suffix, str):
+                suffix = bool(suffix)
         else:
             suffix = None
 
@@ -835,7 +842,7 @@
                 if plugin:
                     file_reporter = plugin.file_reporter(mapped_morf)
                     if file_reporter is None:
-                        raise CoverageException(
+                        raise PluginError(
                             "Plugin {!r} did not provide a file reporter for {!r}.".format(
                                 plugin._coverage_plugin_name, morf
                             )
@@ -933,9 +940,10 @@
         """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.
+
+            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
@@ -978,6 +986,7 @@
         Returns a float, the total percentage covered.
 
         .. note::
+
             The HTML report files are generated incrementally based on the
             source files and coverage results. If you modify the report files,
             the changes will not be considered.  You should be careful about
--- a/eric7/DebugClients/Python/coverage/data.py	Sun Jan 16 20:28:42 2022 +0100
+++ b/eric7/DebugClients/Python/coverage/data.py	Sat Jan 22 14:44:56 2022 +0100
@@ -13,8 +13,8 @@
 import glob
 import os.path
 
-from coverage.exceptions import CoverageException
-from coverage.misc import file_be_gone
+from coverage.exceptions import CoverageException, NoDataError
+from coverage.misc import file_be_gone, human_sorted, plural
 from coverage.sqldata import CoverageData
 
 
@@ -53,11 +53,36 @@
     hasher.update(data.file_tracer(filename))
 
 
+def combinable_files(data_file, data_paths=None):
+    """Make a list of data files to be combined.
+
+    `data_file` is a path to a data file.  `data_paths` is a list of files or
+    directories of files.
+
+    Returns a list of absolute file paths.
+    """
+    data_dir, local = os.path.split(os.path.abspath(data_file))
+
+    data_paths = data_paths or [data_dir]
+    files_to_combine = []
+    for p in data_paths:
+        if os.path.isfile(p):
+            files_to_combine.append(os.path.abspath(p))
+        elif os.path.isdir(p):
+            pattern = os.path.join(os.path.abspath(p), f"{local}.*")
+            files_to_combine.extend(glob.glob(pattern))
+        else:
+            raise NoDataError(f"Couldn't combine from non-existent path '{p}'")
+    return files_to_combine
+
+
 def combine_parallel_data(
     data, aliases=None, data_paths=None, strict=False, keep=False, message=None,
 ):
     """Combine a number of data files together.
 
+    `data` is a CoverageData.
+
     Treat `data.filename` as a file prefix, and combine the data from all
     of the data files starting with that prefix plus a dot.
 
@@ -79,24 +104,10 @@
     raised.
 
     """
-    # Because of the os.path.abspath in the constructor, data_dir will
-    # never be an empty string.
-    data_dir, local = os.path.split(data.base_filename())
-    localdot = local + '.*'
-
-    data_paths = data_paths or [data_dir]
-    files_to_combine = []
-    for p in data_paths:
-        if os.path.isfile(p):
-            files_to_combine.append(os.path.abspath(p))
-        elif os.path.isdir(p):
-            pattern = os.path.join(os.path.abspath(p), localdot)
-            files_to_combine.extend(glob.glob(pattern))
-        else:
-            raise CoverageException(f"Couldn't combine from non-existent path '{p}'")
+    files_to_combine = combinable_files(data.base_filename(), data_paths)
 
     if strict and not files_to_combine:
-        raise CoverageException("No data to combine")
+        raise NoDataError("No data to combine")
 
     files_combined = 0
     for f in files_to_combine:
@@ -127,4 +138,26 @@
                 file_be_gone(f)
 
     if strict and not files_combined:
-        raise CoverageException("No usable data files")
+        raise NoDataError("No usable data files")
+
+
+def debug_data_file(filename):
+    """Implementation of 'coverage debug data'."""
+    data = CoverageData(filename)
+    filename = data.data_filename()
+    print(f"path: {filename}")
+    if not os.path.exists(filename):
+        print("No data collected: file doesn't exist")
+        return
+    data.read()
+    print(f"has_arcs: {data.has_arcs()!r}")
+    summary = line_counts(data, fullpath=True)
+    filenames = human_sorted(summary.keys())
+    nfiles = len(filenames)
+    print(f"{nfiles} file{plural(nfiles)}:")
+    for f in filenames:
+        line = f"{f}: {summary[f]} line{plural(summary[f])}"
+        plugin = data.file_tracer(f)
+        if plugin:
+            line += f" [{plugin}]"
+        print(line)
--- a/eric7/DebugClients/Python/coverage/disposition.py	Sun Jan 16 20:28:42 2022 +0100
+++ b/eric7/DebugClients/Python/coverage/disposition.py	Sat Jan 22 14:44:56 2022 +0100
@@ -33,7 +33,7 @@
         if disp.original_filename != disp.source_filename:
             msg += f" as {disp.source_filename!r}"
         if disp.file_tracer:
-            msg += ": will be traced by %r" % disp.file_tracer
+            msg += f": will be traced by {disp.file_tracer!r}"
     else:
         msg = f"Not tracing {disp.original_filename!r}: {disp.reason}"
     return msg
--- a/eric7/DebugClients/Python/coverage/doc/CHANGES.rst	Sun Jan 16 20:28:42 2022 +0100
+++ b/eric7/DebugClients/Python/coverage/doc/CHANGES.rst	Sat Jan 22 14:44:56 2022 +0100
@@ -19,6 +19,48 @@
     ..  Version 9.8.1 — 2027-07-27
     ..  --------------------------
 
+.. _changes_62:
+
+Version 6.2 — 2021-11-26
+------------------------
+
+- Feature: Now the ``--concurrency`` setting can now have a list of values, so
+  that threads and another lightweight threading package can be measured
+  together, such as ``--concurrency=gevent,thread``.  Closes `issue 1012`_ and
+  `issue 1082`_.
+
+- Fix: A module specified as the ``source`` setting is imported during startup,
+  before the user program imports it.  This could cause problems if the rest of
+  the program isn't ready yet.  For example, `issue 1203`_ describes a Django
+  setting that is accessed before settings have been configured.  Now the early
+  import is wrapped in a try/except so errors then don't stop execution.
+
+- Fix: A colon in a decorator expression would cause an exclusion to end too
+  early, preventing the exclusion of the decorated function. This is now fixed.
+
+- Fix: The HTML report now will not overwrite a .gitignore file that already
+  exists in the HTML output directory (follow-on for `issue 1244`_).
+
+- API: The exceptions raised by Coverage.py have been specialized, to provide
+  finer-grained catching of exceptions by third-party code.
+
+- API: Using ``suffix=False`` when constructing a Coverage object with
+  multiprocessing wouldn't suppress the data file suffix (`issue 989`_).  This
+  is now fixed.
+
+- Debug: The `coverage debug data` command will now sniff out combinable data
+  files, and report on all of them.
+
+- Debug: The `coverage debug` command used to accept a number of topics at a
+  time, and show all of them, though this was never documented.  This no longer
+  works, to allow for command-line options in the future.
+
+.. _issue 989: https://github.com/nedbat/coveragepy/issues/989
+.. _issue 1012: https://github.com/nedbat/coveragepy/issues/1012
+.. _issue 1082: https://github.com/nedbat/coveragepy/issues/1802
+.. _issue 1203: https://github.com/nedbat/coveragepy/issues/1203
+
+
 .. _changes_612:
 
 Version 6.1.2 — 2021-11-10
--- a/eric7/DebugClients/Python/coverage/env.py	Sun Jan 16 20:28:42 2022 +0100
+++ b/eric7/DebugClients/Python/coverage/env.py	Sat Jan 22 14:44:56 2022 +0100
@@ -10,6 +10,7 @@
 # Operating systems.
 WINDOWS = sys.platform == "win32"
 LINUX = sys.platform.startswith("linux")
+OSX = sys.platform == "darwin"
 
 # Python implementations.
 CPYTHON = (platform.python_implementation() == "CPython")
--- a/eric7/DebugClients/Python/coverage/exceptions.py	Sun Jan 16 20:28:42 2022 +0100
+++ b/eric7/DebugClients/Python/coverage/exceptions.py	Sat Jan 22 14:44:56 2022 +0100
@@ -4,13 +4,27 @@
 """Exceptions coverage.py can raise."""
 
 
-class BaseCoverageException(Exception):
-    """The base of all Coverage exceptions."""
+class _BaseCoverageException(Exception):
+    """The base-base of all Coverage exceptions."""
+    pass
+
+
+class CoverageException(_BaseCoverageException):
+    """The base class of all exceptions raised by Coverage.py."""
     pass
 
 
-class CoverageException(BaseCoverageException):
-    """An exception raised by a coverage.py function."""
+class ConfigError(_BaseCoverageException):
+    """A problem with a config file, or a value in one."""
+    pass
+
+
+class DataError(CoverageException):
+    """An error in using a data file."""
+    pass
+
+class NoDataError(CoverageException):
+    """We didn't have data to work with."""
     pass
 
 
@@ -29,7 +43,12 @@
     pass
 
 
-class ExceptionDuringRun(CoverageException):
+class PluginError(CoverageException):
+    """A plugin misbehaved."""
+    pass
+
+
+class _ExceptionDuringRun(CoverageException):
     """An exception happened while running customer code.
 
     Construct it with three arguments, the values from `sys.exc_info`.
@@ -38,7 +57,7 @@
     pass
 
 
-class StopEverything(BaseCoverageException):
+class _StopEverything(_BaseCoverageException):
     """An exception that means everything should stop.
 
     The CoverageTest class converts these to SkipTest, so that when running
--- a/eric7/DebugClients/Python/coverage/execfile.py	Sun Jan 16 20:28:42 2022 +0100
+++ b/eric7/DebugClients/Python/coverage/execfile.py	Sat Jan 22 14:44:56 2022 +0100
@@ -13,7 +13,7 @@
 import types
 
 from coverage import env
-from coverage.exceptions import CoverageException, ExceptionDuringRun, NoCode, NoSource
+from coverage.exceptions import CoverageException, _ExceptionDuringRun, NoCode, NoSource
 from coverage.files import canonical_filename, python_reported_file
 from coverage.misc import isolate_module
 from coverage.phystokens import compile_unicode
@@ -144,7 +144,7 @@
                     self.arg0 = try_filename
                     break
             else:
-                raise NoSource("Can't find '__main__' module in '%s'" % self.arg0)
+                raise NoSource(f"Can't find '__main__' module in '{self.arg0}'")
 
             # Make a spec. I don't know if this is the right way to do it.
             try_filename = python_reported_file(try_filename)
@@ -233,7 +233,7 @@
                 err2.__traceback__ = err2.__traceback__.tb_next
                 sys.__excepthook__(typ2, err2, tb2.tb_next)
                 sys.stderr.write("\nOriginal exception was:\n")
-                raise ExceptionDuringRun(typ, err, tb.tb_next) from exc
+                raise _ExceptionDuringRun(typ, err, tb.tb_next) from exc
             else:
                 sys.exit(1)
         finally:
@@ -293,7 +293,7 @@
         # match or we won't run the file.
         magic = fpyc.read(4)
         if magic != PYC_MAGIC_NUMBER:
-            raise NoCode(f"Bad magic number in .pyc file: {magic} != {PYC_MAGIC_NUMBER}")
+            raise NoCode(f"Bad magic number in .pyc file: {magic!r} != {PYC_MAGIC_NUMBER!r}")
 
         date_based = True
         if env.PYBEHAVIOR.hashed_pyc_pep552:
--- a/eric7/DebugClients/Python/coverage/files.py	Sun Jan 16 20:28:42 2022 +0100
+++ b/eric7/DebugClients/Python/coverage/files.py	Sat Jan 22 14:44:56 2022 +0100
@@ -13,7 +13,7 @@
 import sys
 
 from coverage import env
-from coverage.exceptions import CoverageException
+from coverage.exceptions import ConfigError
 from coverage.misc import contract, human_sorted, isolate_module, join_regex
 
 
@@ -137,9 +137,9 @@
         return actpath
 
 else:
-    def actual_path(filename):
+    def actual_path(path):
         """The actual path for non-Windows platforms."""
-        return filename
+        return path
 
 
 @contract(returns='unicode')
@@ -356,7 +356,7 @@
 
         # The pattern can't end with a wildcard component.
         if pattern.endswith("*"):
-            raise CoverageException("Pattern must not end with wildcards.")
+            raise ConfigError("Pattern must not end with wildcards.")
 
         # The pattern is meant to match a filepath.  Let's make it absolute
         # unless it already is, or is meant to match any prefix.
--- a/eric7/DebugClients/Python/coverage/html.py	Sun Jan 16 20:28:42 2022 +0100
+++ b/eric7/DebugClients/Python/coverage/html.py	Sat Jan 22 14:44:56 2022 +0100
@@ -12,10 +12,10 @@
 
 import coverage
 from coverage.data import add_data_to_hash
-from coverage.exceptions import CoverageException
+from coverage.exceptions import NoDataError
 from coverage.files import flat_rootname
 from coverage.misc import ensure_dir, file_be_gone, Hasher, isolate_module, format_local_datetime
-from coverage.misc import human_sorted
+from coverage.misc import human_sorted, plural
 from coverage.report import get_analysis_to_report
 from coverage.results import Numbers
 from coverage.templite import Templite
@@ -208,7 +208,7 @@
             self.html_file(fr, analysis)
 
         if not self.all_files_nums:
-            raise CoverageException("No data to report.")
+            raise NoDataError("No data to report.")
 
         self.totals = sum(self.all_files_nums)
 
@@ -226,8 +226,10 @@
 
         # .gitignore can't be copied from the source tree because it would
         # prevent the static files from being checked in.
-        with open(os.path.join(self.directory, ".gitignore"), "w") as fgi:
-            fgi.write("# Created by coverage.py\n*\n")
+        gitigore_path = os.path.join(self.directory, ".gitignore")
+        if not os.path.exists(gitigore_path):
+            with open(gitigore_path, "w") as fgi:
+                fgi.write("# Created by coverage.py\n*\n")
 
         # The user may have extra CSS they want copied.
         if self.extra_css:
@@ -329,17 +331,11 @@
 
         skipped_covered_msg = skipped_empty_msg = ""
         if self.skipped_covered_count:
-            msg = "{} {} skipped due to complete coverage."
-            skipped_covered_msg = msg.format(
-                self.skipped_covered_count,
-                "file" if self.skipped_covered_count == 1 else "files",
-            )
+            n = self.skipped_covered_count
+            skipped_covered_msg = f"{n} file{plural(n)} skipped due to complete coverage."
         if self.skipped_empty_count:
-            msg = "{} empty {} skipped."
-            skipped_empty_msg = msg.format(
-                self.skipped_empty_count,
-                "file" if self.skipped_empty_count == 1 else "files",
-            )
+            n = self.skipped_empty_count
+            skipped_empty_msg = f"{n} empty file{plural(n)} skipped."
 
         html = index_tmpl.render({
             'files': self.file_summaries,
--- a/eric7/DebugClients/Python/coverage/inorout.py	Sun Jan 16 20:28:42 2022 +0100
+++ b/eric7/DebugClients/Python/coverage/inorout.py	Sat Jan 22 14:44:56 2022 +0100
@@ -15,7 +15,7 @@
 
 from coverage import env
 from coverage.disposition import FileDisposition, disposition_init
-from coverage.exceptions import CoverageException
+from coverage.exceptions import CoverageException, PluginError
 from coverage.files import TreeMatcher, FnmatchMatcher, ModuleMatcher
 from coverage.files import prep_patterns, find_python_files, canonical_filename
 from coverage.misc import sys_modules_saved
@@ -120,7 +120,7 @@
     path = []
     try:
         spec = importlib.util.find_spec(modulename)
-    except ImportError:
+    except Exception:
         pass
     else:
         if spec is not None:
@@ -243,6 +243,9 @@
             if self.debug:
                 self.debug.write(msg)
 
+        # Generally useful information
+        debug("sys.path:" + "".join(f"\n    {p}" for p in sys.path))
+
         # Create the matchers we need for should_trace
         if self.source or self.source_pkgs:
             against = []
@@ -392,7 +395,7 @@
 
         if not disp.has_dynamic_filename:
             if not disp.source_filename:
-                raise CoverageException(
+                raise PluginError(
                     f"Plugin {plugin!r} didn't set source_filename for '{disp.original_filename}'"
                 )
             reason = self.check_include_omit_etc(disp.source_filename, frame)
--- a/eric7/DebugClients/Python/coverage/misc.py	Sun Jan 16 20:28:42 2022 +0100
+++ b/eric7/DebugClients/Python/coverage/misc.py	Sat Jan 22 14:44:56 2022 +0100
@@ -12,9 +12,7 @@
 import locale
 import os
 import os.path
-import random
 import re
-import socket
 import sys
 import types
 
@@ -184,7 +182,7 @@
 
 def join_regex(regexes):
     """Combine a list of regexes into one that matches any of them."""
-    return "|".join("(?:%s)" % r for r in regexes)
+    return "|".join(f"(?:{r})" for r in regexes)
 
 
 def file_be_gone(path):
@@ -222,26 +220,6 @@
     return encoding
 
 
-def filename_suffix(suffix):
-    """Compute a filename suffix for a data file.
-
-    If `suffix` is a string or None, simply return it. If `suffix` is True,
-    then build a suffix incorporating the hostname, process id, and a random
-    number.
-
-    Returns a string or None.
-
-    """
-    if suffix is True:
-        # If data_suffix was a simple true value, then make a suffix with
-        # plenty of distinguishing information.  We do this here in
-        # `save()` at the last minute so that the pid will be correct even
-        # if the process forks.
-        dice = random.Random(os.urandom(8)).randint(0, 999999)
-        suffix = "%s.%s.%06d" % (socket.gethostname(), os.getpid(), dice)
-    return suffix
-
-
 class Hasher:
     """Hashes Python data for fingerprinting."""
     def __init__(self):
@@ -415,3 +393,14 @@
     Returns the sorted list of items.
     """
     return sorted(items, key=lambda pair: (human_key(pair[0]), pair[1]), reverse=reverse)
+
+
+def plural(n, thing="", things=""):
+    """Pluralize a word.
+
+    If n is 1, return thing.  Otherwise return things, or thing+s.
+    """
+    if n == 1:
+        return thing
+    else:
+        return things or (thing + "s")
--- a/eric7/DebugClients/Python/coverage/parser.py	Sun Jan 16 20:28:42 2022 +0100
+++ b/eric7/DebugClients/Python/coverage/parser.py	Sat Jan 22 14:44:56 2022 +0100
@@ -13,7 +13,7 @@
 from coverage import env
 from coverage.bytecode import code_objects
 from coverage.debug import short_stack
-from coverage.exceptions import NoSource, NotPython, StopEverything
+from coverage.exceptions import NoSource, NotPython, _StopEverything
 from coverage.misc import contract, join_regex, new_contract, nice_pair, one_of
 from coverage.phystokens import compile_unicode, generate_tokens, neuter_encoding_declaration
 
@@ -67,9 +67,6 @@
         # The raw line numbers of excluded lines of code, as marked by pragmas.
         self.raw_excluded = set()
 
-        # The line numbers of class and function definitions.
-        self.raw_classdefs = set()
-
         # The line numbers of docstring lines.
         self.raw_docstrings = set()
 
@@ -120,6 +117,7 @@
         first_line = None
         empty = True
         first_on_line = True
+        nesting = 0
 
         tokgen = generate_tokens(self.text)
         for toktype, ttext, (slineno, _), (elineno, _), ltext in tokgen:
@@ -132,14 +130,8 @@
                 indent += 1
             elif toktype == token.DEDENT:
                 indent -= 1
-            elif toktype == token.NAME:
-                if ttext == 'class':
-                    # Class definitions look like branches in the bytecode, so
-                    # we need to exclude them.  The simplest way is to note the
-                    # lines with the 'class' keyword.
-                    self.raw_classdefs.add(slineno)
             elif toktype == token.OP:
-                if ttext == ':':
+                if ttext == ':' and nesting == 0:
                     should_exclude = (elineno in self.raw_excluded) or excluding_decorators
                     if not excluding and should_exclude:
                         # Start excluding a suite.  We trigger off of the colon
@@ -155,6 +147,10 @@
                         excluding_decorators = True
                     if excluding_decorators:
                         self.raw_excluded.add(elineno)
+                elif ttext in "([{":
+                    nesting += 1
+                elif ttext in ")]}":
+                    nesting -= 1
             elif toktype == token.STRING and prev_toktype == token.INDENT:
                 # Strings that are first on an indented line are docstrings.
                 # (a trick from trace.py in the stdlib.) This works for
@@ -296,12 +292,6 @@
                 continue
             exit_counts[l1] += 1
 
-        # Class definitions have one extra exit, so remove one for each:
-        for l in self.raw_classdefs:
-            # Ensure key is there: class definitions can include excluded lines.
-            if l in exit_counts:
-                exit_counts[l] -= 1
-
         return exit_counts
 
     def missing_arc_description(self, start, end, executed_arcs=None):
@@ -366,7 +356,7 @@
         # attributes on code objects that we need to do the analysis.
         for attr in ['co_lnotab', 'co_firstlineno']:
             if not hasattr(self.code, attr):
-                raise StopEverything(                   # pragma: only jython
+                raise _StopEverything(                  # pragma: only jython
                     "This implementation of Python doesn't support code analysis.\n" +
                     "Run coverage.py under another Python for this command."
                 )
--- a/eric7/DebugClients/Python/coverage/plugin.py	Sun Jan 16 20:28:42 2022 +0100
+++ b/eric7/DebugClients/Python/coverage/plugin.py	Sat Jan 22 14:44:56 2022 +0100
@@ -112,6 +112,8 @@
 
 """
 
+import functools
+
 from coverage import files
 from coverage.misc import contract, _needs_to_implement
 
@@ -315,6 +317,7 @@
         return lineno, lineno
 
 
+@functools.total_ordering
 class FileReporter:
     """Support needed for files during the analysis and reporting phases.
 
@@ -509,25 +512,10 @@
         for line in self.source().splitlines():
             yield [('txt', line)]
 
-    # Annoying comparison operators. Py3k wants __lt__ etc, and Py2k needs all
-    # of them defined.
-
     def __eq__(self, other):
         return isinstance(other, FileReporter) and self.filename == other.filename
 
-    def __ne__(self, other):
-        return not (self == other)
-
     def __lt__(self, other):
-        return self.filename < other.filename
-
-    def __le__(self, other):
-        return self.filename <= other.filename
-
-    def __gt__(self, other):
-        return self.filename > other.filename
-
-    def __ge__(self, other):
-        return self.filename >= other.filename
+        return isinstance(other, FileReporter) and self.filename < other.filename
 
     __hash__ = None     # This object doesn't need to be hashed.
--- a/eric7/DebugClients/Python/coverage/plugin_support.py	Sun Jan 16 20:28:42 2022 +0100
+++ b/eric7/DebugClients/Python/coverage/plugin_support.py	Sat Jan 22 14:44:56 2022 +0100
@@ -7,7 +7,7 @@
 import os.path
 import sys
 
-from coverage.exceptions import CoverageException
+from coverage.exceptions import PluginError
 from coverage.misc import isolate_module
 from coverage.plugin import CoveragePlugin, FileTracer, FileReporter
 
@@ -44,7 +44,7 @@
 
             coverage_init = getattr(mod, "coverage_init", None)
             if not coverage_init:
-                raise CoverageException(
+                raise PluginError(
                     f"Plugin module {module!r} didn't define a coverage_init function"
                 )
 
@@ -110,11 +110,9 @@
         if specialized is not None:
             specialized.append(plugin)
 
-    def __nonzero__(self):
+    def __bool__(self):
         return bool(self.order)
 
-    __bool__ = __nonzero__
-
     def __iter__(self):
         return iter(self.order)
 
--- a/eric7/DebugClients/Python/coverage/report.py	Sun Jan 16 20:28:42 2022 +0100
+++ b/eric7/DebugClients/Python/coverage/report.py	Sat Jan 22 14:44:56 2022 +0100
@@ -5,7 +5,7 @@
 
 import sys
 
-from coverage.exceptions import CoverageException, NotPython
+from coverage.exceptions import CoverageException, NoDataError, NotPython
 from coverage.files import prep_patterns, FnmatchMatcher
 from coverage.misc import ensure_dir_for_file, file_be_gone
 
@@ -65,7 +65,7 @@
         file_reporters = [fr for fr in file_reporters if not matcher.match(fr.filename)]
 
     if not file_reporters:
-        raise CoverageException("No data to report.")
+        raise NoDataError("No data to report.")
 
     for fr in sorted(file_reporters):
         try:
--- a/eric7/DebugClients/Python/coverage/results.py	Sun Jan 16 20:28:42 2022 +0100
+++ b/eric7/DebugClients/Python/coverage/results.py	Sat Jan 22 14:44:56 2022 +0100
@@ -6,7 +6,7 @@
 import collections
 
 from coverage.debug import SimpleReprMixin
-from coverage.exceptions import CoverageException
+from coverage.exceptions import ConfigError
 from coverage.misc import contract, nice_pair
 
 
@@ -317,7 +317,7 @@
             for ex in sorted(exits):
                 if line not in lines and ex not in lines:
                     dest = (ex if ex > 0 else "exit")
-                    line_items.append((line, "%d->%s" % (line, dest)))
+                    line_items.append((line, f"{line}->{dest}"))
 
     ret = ', '.join(t[-1] for t in sorted(line_items))
     return ret
@@ -337,7 +337,7 @@
     # We can never achieve higher than 100% coverage, or less than zero.
     if not (0 <= fail_under <= 100.0):
         msg = f"fail_under={fail_under} is invalid. Must be between 0 and 100."
-        raise CoverageException(msg)
+        raise ConfigError(msg)
 
     # Special case for fail_under=100, it must really be 100.
     if fail_under == 100.0 and total != 100.0:
--- a/eric7/DebugClients/Python/coverage/sqldata.py	Sun Jan 16 20:28:42 2022 +0100
+++ b/eric7/DebugClients/Python/coverage/sqldata.py	Sat Jan 22 14:44:56 2022 +0100
@@ -12,16 +12,18 @@
 import glob
 import itertools
 import os
+import random
 import re
+import socket
 import sqlite3
 import sys
 import threading
 import zlib
 
 from coverage.debug import NoDebugging, SimpleReprMixin, clipped_repr
-from coverage.exceptions import CoverageException
+from coverage.exceptions import CoverageException, DataError
 from coverage.files import PathAliases
-from coverage.misc import contract, file_be_gone, filename_suffix, isolate_module
+from coverage.misc import contract, file_be_gone, isolate_module
 from coverage.numbits import numbits_to_nums, numbits_union, nums_to_numbits
 from coverage.version import __version__
 
@@ -191,7 +193,7 @@
 
         Arguments:
             basename (str): the base name of the data file, defaulting to
-                ".coverage".
+                ".coverage". This can be a path to a file in another directory.
             suffix (str or bool): has the same meaning as the `data_suffix`
                 argument to :class:`coverage.Coverage`.
             no_disk (bool): if True, keep all data in memory, and don't
@@ -287,14 +289,14 @@
             try:
                 schema_version, = db.execute_one("select version from coverage_schema")
             except Exception as exc:
-                raise CoverageException(
+                raise DataError(
                     "Data file {!r} doesn't seem to be a coverage data file: {}".format(
                         self._filename, exc
                     )
                 ) from exc
             else:
                 if schema_version != SCHEMA_VERSION:
-                    raise CoverageException(
+                    raise DataError(
                         "Couldn't use data file {!r}: wrong schema: {} instead of {}".format(
                             self._filename, schema_version, SCHEMA_VERSION
                         )
@@ -316,7 +318,7 @@
                 self._create_db()
         return self._dbs[threading.get_ident()]
 
-    def __nonzero__(self):
+    def __bool__(self):
         if (threading.get_ident() not in self._dbs and not os.path.exists(self._filename)):
             return False
         try:
@@ -326,8 +328,6 @@
         except CoverageException:
             return False
 
-    __bool__ = __nonzero__
-
     @contract(returns="bytes")
     def dumps(self):
         """Serialize the current data to a byte string.
@@ -370,9 +370,9 @@
         if self._debug.should("dataio"):
             self._debug.write(f"Loading data into data file {self._filename!r}")
         if data[:1] != b"z":
-            raise CoverageException(
+            raise DataError(
                 f"Unrecognized serialization: {data[:40]!r} (head of {len(data)} bytes)"
-                )
+            )
         script = zlib.decompress(data[1:]).decode("utf-8")
         self._dbs[threading.get_ident()] = db = SqliteDb(self._filename, self._debug)
         with db:
@@ -513,9 +513,9 @@
         assert lines or arcs
         assert not (lines and arcs)
         if lines and self._has_arcs:
-            raise CoverageException("Can't add line measurements to existing branch data")
+            raise DataError("Can't add line measurements to existing branch data")
         if arcs and self._has_lines:
-            raise CoverageException("Can't add branch measurements to existing line data")
+            raise DataError("Can't add branch measurements to existing line data")
         if not self._has_arcs and not self._has_lines:
             self._has_lines = lines
             self._has_arcs = arcs
@@ -541,14 +541,14 @@
             for filename, plugin_name in file_tracers.items():
                 file_id = self._file_id(filename)
                 if file_id is None:
-                    raise CoverageException(
+                    raise DataError(
                         f"Can't add file tracer data for unmeasured file '{filename}'"
                     )
 
                 existing_plugin = self.file_tracer(filename)
                 if existing_plugin:
                     if existing_plugin != plugin_name:
-                        raise CoverageException(
+                        raise DataError(
                             "Conflicting file tracer name for '{}': {!r} vs {!r}".format(
                                 filename, existing_plugin, plugin_name,
                             )
@@ -578,7 +578,7 @@
         self._start_using()
         with self._connect(): # Use this to get one transaction.
             if not self._has_arcs and not self._has_lines:
-                raise CoverageException("Can't touch files in an empty CoverageData")
+                raise DataError("Can't touch files in an empty CoverageData")
 
             for filename in filenames:
                 self._file_id(filename, add=True)
@@ -597,9 +597,9 @@
                 getattr(other_data, "_filename", "???"),
             ))
         if self._has_lines and other_data._has_arcs:
-            raise CoverageException("Can't combine arc data with line data")
+            raise DataError("Can't combine arc data with line data")
         if self._has_arcs and other_data._has_lines:
-            raise CoverageException("Can't combine line data with arc data")
+            raise DataError("Can't combine line data with arc data")
 
         aliases = aliases or PathAliases()
 
@@ -692,7 +692,7 @@
                 other_tracer = tracers.get(path, "")
                 # If there is no tracer, there is always the None tracer.
                 if this_tracer is not None and this_tracer != other_tracer:
-                    raise CoverageException(
+                    raise DataError(
                         "Conflicting file tracer name for '{}': {!r} vs {!r}".format(
                             path, this_tracer, other_tracer
                         )
@@ -1004,6 +1004,26 @@
         ]
 
 
+def filename_suffix(suffix):
+    """Compute a filename suffix for a data file.
+
+    If `suffix` is a string or None, simply return it. If `suffix` is True,
+    then build a suffix incorporating the hostname, process id, and a random
+    number.
+
+    Returns a string or None.
+
+    """
+    if suffix is True:
+        # If data_suffix was a simple true value, then make a suffix with
+        # plenty of distinguishing information.  We do this here in
+        # `save()` at the last minute so that the pid will be correct even
+        # if the process forks.
+        dice = random.Random(os.urandom(8)).randint(0, 999999)
+        suffix = "%s.%s.%06d" % (socket.gethostname(), os.getpid(), dice)
+    return suffix
+
+
 class SqliteDb(SimpleReprMixin):
     """A simple abstraction over a SQLite database.
 
@@ -1035,7 +1055,7 @@
         try:
             self.con = sqlite3.connect(self.filename, check_same_thread=False)
         except sqlite3.Error as exc:
-            raise CoverageException(f"Couldn't use data file {self.filename!r}: {exc}") from exc
+            raise DataError(f"Couldn't use data file {self.filename!r}: {exc}") from exc
 
         self.con.create_function("REGEXP", 2, _regexp)
 
@@ -1068,7 +1088,7 @@
             except Exception as exc:
                 if self.debug:
                     self.debug.write(f"EXCEPTION from __exit__: {exc}")
-                raise CoverageException(f"Couldn't end data file {self.filename!r}: {exc}") from exc
+                raise DataError(f"Couldn't end data file {self.filename!r}: {exc}") from exc
 
     def execute(self, sql, parameters=()):
         """Same as :meth:`python:sqlite3.Connection.execute`."""
@@ -1099,7 +1119,7 @@
                 pass
             if self.debug:
                 self.debug.write(f"EXCEPTION from execute: {msg}")
-            raise CoverageException(f"Couldn't use data file {self.filename!r}: {msg}") from exc
+            raise DataError(f"Couldn't use data file {self.filename!r}: {msg}") from exc
 
     def execute_one(self, sql, parameters=()):
         """Execute a statement and return the one row that results.
--- a/eric7/DebugClients/Python/coverage/summary.py	Sun Jan 16 20:28:42 2022 +0100
+++ b/eric7/DebugClients/Python/coverage/summary.py	Sat Jan 22 14:44:56 2022 +0100
@@ -5,7 +5,7 @@
 
 import sys
 
-from coverage.exceptions import CoverageException
+from coverage.exceptions import ConfigError, NoDataError
 from coverage.misc import human_sorted_items
 from coverage.report import get_analysis_to_report
 from coverage.results import Numbers
@@ -103,7 +103,7 @@
         else:
             position = column_order.get(sort_option)
             if position is None:
-                raise CoverageException(f"Invalid sorting option: {self.config.sort!r}")
+                raise ConfigError(f"Invalid sorting option: {self.config.sort!r}")
             lines.sort(key=lambda l: (l[1][position], l[0]), reverse=reverse)
 
         for line in lines:
@@ -122,7 +122,7 @@
 
         # Write other final lines.
         if not self.total.n_files and not self.skipped_count:
-            raise CoverageException("No data to report.")
+            raise NoDataError("No data to report.")
 
         if self.config.skip_covered and self.skipped_count:
             self.writeout(
--- a/eric7/DebugClients/Python/coverage/tomlconfig.py	Sun Jan 16 20:28:42 2022 +0100
+++ b/eric7/DebugClients/Python/coverage/tomlconfig.py	Sat Jan 22 14:44:56 2022 +0100
@@ -7,7 +7,7 @@
 import os
 import re
 
-from coverage.exceptions import CoverageException
+from coverage.exceptions import ConfigError
 from coverage.misc import import_third_party, substitute_variables
 
 # TOML support is an install-time extra option. (Import typing is here because
@@ -57,7 +57,7 @@
             if self.our_file or has_toml:
                 # Looks like they meant to read TOML, but we can't read it.
                 msg = "Can't read {!r} without TOML support. Install with [toml] extra"
-                raise CoverageException(msg.format(filename))
+                raise ConfigError(msg.format(filename))
             return []
 
     def _get_section(self, section):
@@ -148,7 +148,7 @@
             try:
                 re.compile(value)
             except re.error as e:
-                raise CoverageException(f"Invalid [{name}].{option} value {value!r}: {e}") from e
+                raise ConfigError(f"Invalid [{name}].{option} value {value!r}: {e}") from e
         return values
 
     def getint(self, section, option):
--- a/eric7/DebugClients/Python/coverage/version.py	Sun Jan 16 20:28:42 2022 +0100
+++ b/eric7/DebugClients/Python/coverage/version.py	Sat Jan 22 14:44:56 2022 +0100
@@ -5,7 +5,7 @@
 # This file is exec'ed in setup.py, don't import anything!
 
 # Same semantics as sys.version_info.
-version_info = (6, 1, 2, "final", 0)
+version_info = (6, 2, 0, "final", 0)
 
 
 def _make_version(major, minor, micro, releaselevel, serial):
@@ -16,7 +16,7 @@
         version += ".%d" % (micro,)
     if releaselevel != 'final':
         short = {'alpha': 'a', 'beta': 'b', 'candidate': 'rc'}[releaselevel]
-        version += "%s%d" % (short, serial)
+        version += f"{short}{serial}"
     return version
 
 
--- a/eric7/DebugClients/Python/coverage/xmlreport.py	Sun Jan 16 20:28:42 2022 +0100
+++ b/eric7/DebugClients/Python/coverage/xmlreport.py	Sat Jan 22 14:44:56 2022 +0100
@@ -67,9 +67,9 @@
         xcoverage.setAttribute("version", __version__)
         xcoverage.setAttribute("timestamp", str(int(time.time()*1000)))
         xcoverage.appendChild(self.xml_out.createComment(
-            " Generated by coverage.py: %s " % __url__
+            f" Generated by coverage.py: {__url__} "
             ))
-        xcoverage.appendChild(self.xml_out.createComment(" Based on %s " % DTD_URL))
+        xcoverage.appendChild(self.xml_out.createComment(f" Based on {DTD_URL} "))
 
         # Call xml_file for each file in the data.
         for fr, analysis in get_analysis_to_report(self.coverage, morfs):

eric ide

mercurial