Third Party packages eric7

Sun, 20 Mar 2022 17:49:44 +0100

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Sun, 20 Mar 2022 17:49:44 +0100
branch
eric7
changeset 8991
2fc945191992
parent 8990
ca8e477c590c
child 8992
350b128d2752

Third Party packages
- upgraded coverage to 6.3.2

docs/changelog file | annotate | diff | comparison | revisions
eric7/DebugClients/Python/coverage/cmdline.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/debug.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/doc/CONTRIBUTORS.txt file | annotate | diff | comparison | revisions
eric7/DebugClients/Python/coverage/doc/README.rst file | annotate | diff | comparison | revisions
eric7/DebugClients/Python/coverage/env.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/jsonreport.py file | annotate | diff | comparison | revisions
eric7/DebugClients/Python/coverage/lcovreport.py file | annotate | diff | comparison | revisions
eric7/DebugClients/Python/coverage/multiproc.py file | annotate | diff | comparison | revisions
eric7/DebugClients/Python/coverage/parser.py file | annotate | diff | comparison | revisions
eric7/DebugClients/Python/coverage/pytracer.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/version.py file | annotate | diff | comparison | revisions
--- a/docs/changelog	Sun Mar 20 17:26:35 2022 +0100
+++ b/docs/changelog	Sun Mar 20 17:49:44 2022 +0100
@@ -8,6 +8,7 @@
 - pip Interface
   -- added a vulnerability check for installed packages based on "Safety DB"
 - Third Party packages
+  -- upgraded coverage to 6.3.2
   -- upgraded mccabe to version 0.7.0
 
 Version 22.3:
--- a/eric7/DebugClients/Python/coverage/cmdline.py	Sun Mar 20 17:26:35 2022 +0100
+++ b/eric7/DebugClients/Python/coverage/cmdline.py	Sun Mar 20 17:49:44 2022 +0100
@@ -3,7 +3,6 @@
 
 """Command-line support for coverage.py."""
 
-
 import glob
 import optparse     # pylint: disable=deprecated-module
 import os
@@ -18,16 +17,22 @@
 from coverage import env
 from coverage.collector import CTracer
 from coverage.config import CoverageConfig
+from coverage.control import DEFAULT_DATAFILE
 from coverage.data import combinable_files, debug_data_file
-from coverage.debug import info_formatter, info_header, short_stack
+from coverage.debug import info_header, short_stack, write_formatted_info
 from coverage.exceptions import _BaseCoverageException, _ExceptionDuringRun, NoSource
 from coverage.execfile import PyRunner
 from coverage.results import Numbers, should_fail_under
 
+# When adding to this file, alphabetization is important.  Look for
+# "alphabetize" comments throughout.
 
 class Opts:
     """A namespace class for individual options we'll build parsers from."""
 
+    # Keep these entries alphabetized (roughly) by the option name as it
+    # appears on the command line.
+
     append = optparse.make_option(
         '-a', '--append', action='store_true',
         help="Append coverage data to .coverage, otherwise it starts clean each time.",
@@ -52,13 +57,33 @@
         help="The context label to record for this coverage run.",
     )
     contexts = optparse.make_option(
-        '', '--contexts', action='store',
-        metavar="REGEX1,REGEX2,...",
+        '', '--contexts', action='store', metavar="REGEX1,REGEX2,...",
         help=(
             "Only display data from lines covered in the given contexts. " +
             "Accepts Python regexes, which must be quoted."
         ),
     )
+    combine_datafile = optparse.make_option(
+        '', '--data-file', action='store', metavar="DATAFILE",
+        help=(
+            "Base name of the data files to operate on. " +
+            "Defaults to '.coverage'. [env: COVERAGE_FILE]"
+        ),
+    )
+    input_datafile = optparse.make_option(
+        '', '--data-file', action='store', metavar="INFILE",
+        help=(
+            "Read coverage data for report generation from this file. " +
+            "Defaults to '.coverage'. [env: COVERAGE_FILE]"
+        ),
+    )
+    output_datafile = optparse.make_option(
+        '', '--data-file', action='store', metavar="OUTFILE",
+        help=(
+            "Write the recorded coverage data to this file. " +
+            "Defaults to '.coverage'. [env: COVERAGE_FILE]"
+        ),
+    )
     debug = optparse.make_option(
         '', '--debug', action='store', metavar="OPTS",
         help="Debug options, separated by commas. [env: COVERAGE_DEBUG]",
@@ -80,8 +105,7 @@
         help="Ignore errors while reading source files.",
     )
     include = optparse.make_option(
-        '', '--include', action='store',
-        metavar="PAT1,PAT2,...",
+        '', '--include', action='store', metavar="PAT1,PAT2,...",
         help=(
             "Include only files whose paths match one of these patterns. " +
             "Accepts shell-style wildcards, which must be quoted."
@@ -106,23 +130,24 @@
         ),
     )
     omit = optparse.make_option(
-        '', '--omit', action='store',
-        metavar="PAT1,PAT2,...",
+        '', '--omit', action='store', metavar="PAT1,PAT2,...",
         help=(
             "Omit files whose paths match one of these patterns. " +
             "Accepts shell-style wildcards, which must be quoted."
         ),
     )
     output_xml = optparse.make_option(
-        '-o', '', action='store', dest="outfile",
-        metavar="OUTFILE",
+        '-o', '', action='store', dest="outfile", metavar="OUTFILE",
         help="Write the XML report to this file. Defaults to 'coverage.xml'",
     )
     output_json = optparse.make_option(
-        '-o', '', action='store', dest="outfile",
-        metavar="OUTFILE",
+        '-o', '', action='store', dest="outfile", metavar="OUTFILE",
         help="Write the JSON report to this file. Defaults to 'coverage.json'",
     )
+    output_lcov = optparse.make_option(
+        '-o', '', action='store', dest='outfile', metavar="OUTFILE",
+        help="Write the LCOV report to this file. Defaults to 'coverage.lcov'",
+    )
     json_pretty_print = optparse.make_option(
         '', '--pretty-print', action='store_true',
         help="Format the JSON for human readers.",
@@ -131,7 +156,7 @@
         '-p', '--parallel-mode', action='store_true',
         help=(
             "Append the machine name, process id and random number to the " +
-            ".coverage data file name to simplify collecting data from " +
+            "data file name to simplify collecting data from " +
             "many processes."
         ),
     )
@@ -172,8 +197,10 @@
     )
     sort = optparse.make_option(
         '--sort', action='store', metavar='COLUMN',
-        help="Sort the report by the named column: name, stmts, miss, branch, brpart, or cover. " +
+        help=(
+            "Sort the report by the named column: name, stmts, miss, branch, brpart, or cover. " +
              "Default is name."
+        ),
     )
     source = optparse.make_option(
         '', '--source', action='store', metavar="SRC1,SRC2,...",
@@ -209,12 +236,14 @@
             add_help_option=False, *args, **kwargs
             )
         self.set_defaults(
+            # Keep these arguments alphabetized by their names.
             action=None,
             append=None,
             branch=None,
             concurrency=None,
             context=None,
             contexts=None,
+            data_file=None,
             debug=None,
             directory=None,
             fail_under=None,
@@ -313,6 +342,11 @@
         # Include the sub-command for this parser as part of the command.
         return f"{program_name} {self.cmd}"
 
+# In lists of Opts, keep them alphabetized by the option names as they appear
+# on the command line, since these lists determine the order of the options in
+# the help output.
+#
+# In COMMANDS, keep the keys (command names) alphabetized.
 
 GLOBAL_ARGS = [
     Opts.debug,
@@ -320,11 +354,12 @@
     Opts.rcfile,
     ]
 
-CMDS = {
+COMMANDS = {
     'annotate': CmdOptionParser(
         "annotate",
         [
             Opts.directory,
+            Opts.input_datafile,
             Opts.ignore_errors,
             Opts.include,
             Opts.omit,
@@ -340,6 +375,7 @@
         "combine",
         [
             Opts.append,
+            Opts.combine_datafile,
             Opts.keep,
             Opts.quiet,
             ] + GLOBAL_ARGS,
@@ -364,12 +400,16 @@
                 "'data' to show a summary of the collected data; " +
                 "'sys' to show installation information; " +
                 "'config' to show the configuration; " +
-                "'premain' to show what is calling coverage."
+                "'premain' to show what is calling coverage; " +
+                "'pybehave' to show internal flags describing Python behavior."
         ),
     ),
 
     'erase': CmdOptionParser(
-        "erase", GLOBAL_ARGS,
+        "erase",
+        [
+            Opts.combine_datafile
+            ] + GLOBAL_ARGS,
         description="Erase previously collected coverage data.",
     ),
 
@@ -384,6 +424,7 @@
         [
             Opts.contexts,
             Opts.directory,
+            Opts.input_datafile,
             Opts.fail_under,
             Opts.ignore_errors,
             Opts.include,
@@ -408,6 +449,7 @@
         "json",
         [
             Opts.contexts,
+            Opts.input_datafile,
             Opts.fail_under,
             Opts.ignore_errors,
             Opts.include,
@@ -418,13 +460,29 @@
             Opts.show_contexts,
             ] + GLOBAL_ARGS,
         usage="[options] [modules]",
-        description="Generate a JSON report of coverage results."
+        description="Generate a JSON report of coverage results.",
+    ),
+
+    'lcov': CmdOptionParser(
+        "lcov",
+        [
+            Opts.input_datafile,
+            Opts.fail_under,
+            Opts.ignore_errors,
+            Opts.include,
+            Opts.output_lcov,
+            Opts.omit,
+            Opts.quiet,
+        ] + GLOBAL_ARGS,
+        usage="[options] [modules]",
+        description="Generate an LCOV report of coverage results.",
     ),
 
     'report': CmdOptionParser(
         "report",
         [
             Opts.contexts,
+            Opts.input_datafile,
             Opts.fail_under,
             Opts.ignore_errors,
             Opts.include,
@@ -437,7 +495,7 @@
             Opts.skip_empty,
             ] + GLOBAL_ARGS,
         usage="[options] [modules]",
-        description="Report coverage statistics on modules."
+        description="Report coverage statistics on modules.",
     ),
 
     'run': CmdOptionParser(
@@ -447,6 +505,7 @@
             Opts.branch,
             Opts.concurrency,
             Opts.context,
+            Opts.output_datafile,
             Opts.include,
             Opts.module,
             Opts.omit,
@@ -456,12 +515,13 @@
             Opts.timid,
             ] + GLOBAL_ARGS,
         usage="[options] <pyfile> [program options]",
-        description="Run a Python program, measuring code execution."
+        description="Run a Python program, measuring code execution.",
     ),
 
     'xml': CmdOptionParser(
         "xml",
         [
+            Opts.input_datafile,
             Opts.fail_under,
             Opts.ignore_errors,
             Opts.include,
@@ -471,7 +531,7 @@
             Opts.skip_empty,
             ] + GLOBAL_ARGS,
         usage="[options] [modules]",
-        description="Generate an XML report of coverage results."
+        description="Generate an XML report of coverage results.",
     ),
 }
 
@@ -546,7 +606,7 @@
         if self.global_option:
             parser = GlobalOptionParser()
         else:
-            parser = CMDS.get(argv[0])
+            parser = COMMANDS.get(argv[0])
             if not parser:
                 show_help(f"Unknown command: {argv[0]!r}")
                 return ERR
@@ -574,6 +634,7 @@
 
         # Do something.
         self.coverage = Coverage(
+            data_file=options.data_file or DEFAULT_DATAFILE,
             data_suffix=options.parallel_mode,
             cover_pylib=options.pylib,
             timid=options.timid,
@@ -625,10 +686,10 @@
         total = None
         if options.action == "report":
             total = self.coverage.report(
+                precision=options.precision,
                 show_missing=options.show_missing,
                 skip_covered=options.skip_covered,
                 skip_empty=options.skip_empty,
-                precision=options.precision,
                 sort=options.sort,
                 **report_args
                 )
@@ -637,27 +698,31 @@
         elif options.action == "html":
             total = self.coverage.html_report(
                 directory=options.directory,
-                title=options.title,
+                precision=options.precision,
                 skip_covered=options.skip_covered,
                 skip_empty=options.skip_empty,
                 show_contexts=options.show_contexts,
-                precision=options.precision,
+                title=options.title,
                 **report_args
                 )
         elif options.action == "xml":
-            outfile = options.outfile
             total = self.coverage.xml_report(
-                outfile=outfile, skip_empty=options.skip_empty,
+                outfile=options.outfile,
+                skip_empty=options.skip_empty,
                 **report_args
                 )
         elif options.action == "json":
-            outfile = options.outfile
             total = self.coverage.json_report(
-                outfile=outfile,
+                outfile=options.outfile,
                 pretty_print=options.pretty_print,
                 show_contexts=options.show_contexts,
                 **report_args
-            )
+                )
+        elif options.action == "lcov":
+            total = self.coverage.lcov_report(
+                outfile=options.outfile,
+                **report_args
+                )
         else:
             # There are no other possible actions.
             raise AssertionError
@@ -667,6 +732,8 @@
             # value, so we can get fail_under from the config file.
             if options.fail_under is not None:
                 self.coverage.set_option("report:fail_under", options.fail_under)
+            if options.precision is not None:
+                self.coverage.set_option("report:precision", options.precision)
 
             fail_under = self.coverage.get_option("report:fail_under")
             precision = self.coverage.get_option("report:precision")
@@ -698,7 +765,7 @@
         if options.action == "help":
             if args:
                 for a in args:
-                    parser = CMDS.get(a)
+                    parser = COMMANDS.get(a)
                     if parser:
                         show_help(parser=parser)
                     else:
@@ -777,32 +844,28 @@
         """Implementation of 'coverage debug'."""
 
         if not args:
-            show_help("What information would you like: config, data, sys, premain?")
+            show_help("What information would you like: config, data, sys, premain, pybehave?")
             return ERR
         if args[1:]:
             show_help("Only one topic at a time, please")
             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':
+        if args[0] == "sys":
+            write_formatted_info(print, "sys", self.coverage.sys_info())
+        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] == "config":
+            write_formatted_info(print, "config", self.coverage.config.debug_info())
         elif args[0] == "premain":
             print(info_header("premain"))
             print(short_stack())
+        elif args[0] == "pybehave":
+            write_formatted_info(print, "pybehave", env.debug_info())
         else:
             show_help(f"Don't know what you mean by {args[0]!r}")
             return ERR
@@ -852,6 +915,7 @@
             help        Get help on using coverage.py.
             html        Create an HTML report.
             json        Create a JSON report of coverage results.
+            lcov        Create an LCOV report of coverage results.
             report      Report coverage stats on modules.
             run         Run a Python program and measure code execution.
             xml         Create an XML report of coverage results.
--- a/eric7/DebugClients/Python/coverage/config.py	Sun Mar 20 17:26:35 2022 +0100
+++ b/eric7/DebugClients/Python/coverage/config.py	Sun Mar 20 17:49:44 2022 +0100
@@ -11,7 +11,7 @@
 import re
 
 from coverage.exceptions import ConfigError
-from coverage.misc import contract, isolate_module, substitute_variables
+from coverage.misc import contract, isolate_module, human_sorted_items, substitute_variables
 
 from coverage.tomlconfig import TomlConfigParser, TomlDecodeError
 
@@ -227,6 +227,9 @@
         self.json_pretty_print = False
         self.json_show_contexts = False
 
+        # Defaults for [lcov]
+        self.lcov_output = "coverage.lcov"
+
         # Defaults for [paths]
         self.paths = collections.OrderedDict()
 
@@ -397,6 +400,9 @@
         ('json_output', 'json:output'),
         ('json_pretty_print', 'json:pretty_print', 'boolean'),
         ('json_show_contexts', 'json:show_contexts', 'boolean'),
+
+        # [lcov]
+        ('lcov_output', 'lcov:output'),
     ]
 
     def _set_attr_from_config_option(self, cp, attr, where, type_=''):
@@ -489,6 +495,12 @@
             for k, v in self.paths.items()
         )
 
+    def debug_info(self):
+        """Make a list of (name, value) pairs for writing debug info."""
+        return human_sorted_items(
+            (k, v) for k, v in self.__dict__.items() if not k.startswith("_")
+            )
+
 
 def config_files_to_try(config_file):
     """What config files should we try to read?
--- a/eric7/DebugClients/Python/coverage/control.py	Sun Mar 20 17:26:35 2022 +0100
+++ b/eric7/DebugClients/Python/coverage/control.py	Sun Mar 20 17:49:44 2022 +0100
@@ -9,7 +9,9 @@
 import os
 import os.path
 import platform
+import signal
 import sys
+import threading
 import time
 import warnings
 
@@ -26,7 +28,8 @@
 from coverage.html import HtmlReporter
 from coverage.inorout import InOrOut
 from coverage.jsonreport import JsonReporter
-from coverage.misc import bool_or_none, join_regex, human_sorted, human_sorted_items
+from coverage.lcovreport import LcovReporter
+from coverage.misc import bool_or_none, join_regex, human_sorted
 from coverage.misc import DefaultValue, ensure_dir_for_file, isolate_module
 from coverage.plugin import FileReporter
 from coverage.plugin_support import Plugins
@@ -60,7 +63,8 @@
         cov.config = original_config
 
 
-_DEFAULT_DATAFILE = DefaultValue("MISSING")
+DEFAULT_DATAFILE = DefaultValue("MISSING")
+_DEFAULT_DATAFILE = DEFAULT_DATAFILE  # Just in case, for backwards compatibility
 
 class Coverage:
     """Programmatic access to coverage.py.
@@ -101,7 +105,7 @@
             return None
 
     def __init__(
-        self, data_file=_DEFAULT_DATAFILE, data_suffix=None, cover_pylib=None,
+        self, data_file=DEFAULT_DATAFILE, data_suffix=None, cover_pylib=None,
         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,
@@ -198,7 +202,7 @@
         # data_file=None means no disk file at all. data_file missing means
         # use the value from the config file.
         self._no_disk = data_file is None
-        if data_file is _DEFAULT_DATAFILE:
+        if data_file is DEFAULT_DATAFILE:
             data_file = None
 
         self.config = None
@@ -227,6 +231,7 @@
         self._exclude_re = None
         self._debug = None
         self._file_mapper = None
+        self._old_sigterm = None
 
         # State machine variables:
         # Have we initialized everything?
@@ -310,22 +315,25 @@
         """Write out debug info at startup if needed."""
         wrote_any = False
         with self._debug.without_callers():
-            if self._debug.should('config'):
-                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)
+            if self._debug.should("config"):
+                config_info = self.config.debug_info()
+                write_formatted_info(self._debug.write, "config", config_info)
                 wrote_any = True
 
-            if self._debug.should('sys'):
-                write_formatted_info(self._debug, "sys", self.sys_info())
+            if self._debug.should("sys"):
+                write_formatted_info(self._debug.write, "sys", self.sys_info())
                 for plugin in self._plugins:
                     header = "sys: " + plugin._coverage_plugin_name
                     info = plugin.sys_info()
-                    write_formatted_info(self._debug, header, info)
+                    write_formatted_info(self._debug.write, header, info)
+                wrote_any = True
+
+            if self._debug.should("pybehave"):
+                write_formatted_info(self._debug.write, "pybehave", env.debug_info())
                 wrote_any = True
 
         if wrote_any:
-            write_formatted_info(self._debug, "end", ())
+            write_formatted_info(self._debug.write, "end", ())
 
     def _should_trace(self, filename, frame):
         """Decide whether to trace execution in `filename`.
@@ -454,6 +462,8 @@
                 raise ConfigError(                      # pragma: only jython
                     "multiprocessing is not supported on this Python"
                 )
+            if self.config.config_file is None:
+                raise ConfigError("multiprocessing requires a configuration file")
             patch_multiprocessing(rcfile=self.config.config_file)
 
         dycon = self.config.dynamic_context
@@ -524,7 +534,14 @@
         # It's useful to write debug info after initing for start.
         self._should_write_debug = True
 
+        # Register our clean-up handlers.
         atexit.register(self._atexit)
+        is_main = (threading.current_thread() == threading.main_thread())
+        if is_main and not env.WINDOWS:
+            # The Python docs seem to imply that SIGTERM works uniformly even
+            # on Windows, but that's not my experience, and this agrees:
+            # https://stackoverflow.com/questions/35772001/x/35792192#35792192
+            self._old_sigterm = signal.signal(signal.SIGTERM, self._on_sigterm)
 
     def _init_data(self, suffix):
         """Create a data file if we don't have one yet."""
@@ -582,15 +599,23 @@
             self._collector.stop()
         self._started = False
 
-    def _atexit(self):
+    def _atexit(self, event="atexit"):
         """Clean up on process shutdown."""
         if self._debug.should("process"):
-            self._debug.write(f"atexit: pid: {os.getpid()}, instance: {self!r}")
+            self._debug.write(f"{event}: pid: {os.getpid()}, instance: {self!r}")
         if self._started:
             self.stop()
         if self._auto_save:
             self.save()
 
+    def _on_sigterm(self, signum_unused, frame_unused):
+        """A handler for signal.SIGTERM."""
+        self._atexit("sigterm")
+        # Statements after here won't be seen by metacov because we just wrote
+        # the data, and are about to kill the process.
+        signal.signal(signal.SIGTERM, self._old_sigterm)    # pragma: not covered
+        os.kill(os.getpid(), signal.SIGTERM)                # pragma: not covered
+
     def erase(self):
         """Erase previously collected coverage data.
 
@@ -1049,6 +1074,25 @@
         ):
             return render_report(self.config.json_output, JsonReporter(self), morfs, self._message)
 
+    def lcov_report(
+        self, morfs=None, outfile=None, ignore_errors=None,
+        omit=None, include=None, contexts=None,
+    ):
+        """Generate an LCOV report of coverage results.
+
+        Each module in 'morfs' is included in the report. 'outfile' is the
+        path to write the file to, "-" will write to stdout.
+
+        See :meth 'report' for other arguments.
+
+        .. versionadded:: 6.3
+        """
+        with override_config(self,
+            ignore_errors=ignore_errors, report_omit=omit, report_include=include,
+            lcov_output=outfile,  report_contexts=contexts,
+        ):
+            return render_report(self.config.lcov_output, LcovReporter(self), morfs, self._message)
+
     def sys_info(self):
         """Return a list of (key, value) pairs showing internal information."""
 
--- a/eric7/DebugClients/Python/coverage/debug.py	Sun Mar 20 17:26:35 2022 +0100
+++ b/eric7/DebugClients/Python/coverage/debug.py	Sun Mar 20 17:49:44 2022 +0100
@@ -118,7 +118,10 @@
     for label, data in info:
         if data == []:
             data = "-none-"
-        if isinstance(data, (list, set, tuple)):
+        if isinstance(data, tuple) and len(repr(tuple(data))) < 30:
+            # Convert to tuple to scrub namedtuples.
+            yield "%*s: %r" % (label_len, label, tuple(data))
+        elif isinstance(data, (list, set, tuple)):
             prefix = "%*s:" % (label_len, label)
             for e in data:
                 yield "%*s %s" % (label_len+1, prefix, e)
@@ -127,11 +130,18 @@
             yield "%*s: %s" % (label_len, label, data)
 
 
-def write_formatted_info(writer, header, info):
-    """Write a sequence of (label,data) pairs nicely."""
-    writer.write(info_header(header))
+def write_formatted_info(write, header, info):
+    """Write a sequence of (label,data) pairs nicely.
+
+    `write` is a function write(str) that accepts each line of output.
+    `header` is a string to start the section.  `info` is a sequence of
+    (label, data) pairs, where label is a str, and data can be a single
+    value, or a list/set/tuple.
+
+    """
+    write(info_header(header))
     for line in info_formatter(info):
-        writer.write(" %s" % line)
+        write(f" {line}")
 
 
 def short_stack(limit=None, skip=0):
--- a/eric7/DebugClients/Python/coverage/disposition.py	Sun Mar 20 17:26:35 2022 +0100
+++ b/eric7/DebugClients/Python/coverage/disposition.py	Sun Mar 20 17:49:44 2022 +0100
@@ -6,7 +6,9 @@
 
 class FileDisposition:
     """A simple value type for recording what to do with a file."""
-    pass
+
+    def __repr__(self):
+        return f"<FileDisposition {self.canonical_filename!r}: trace={self.trace}>"
 
 
 # FileDisposition "methods": FileDisposition is a pure value object, so it can
--- a/eric7/DebugClients/Python/coverage/doc/CHANGES.rst	Sun Mar 20 17:26:35 2022 +0100
+++ b/eric7/DebugClients/Python/coverage/doc/CHANGES.rst	Sun Mar 20 17:49:44 2022 +0100
@@ -9,8 +9,6 @@
 different from a strict chronological order when there are two branches in
 development at the same time, such as 4.5.x and 5.0.
 
-This list is detailed and covers changes in each pre-release version.
-
     .. When updating the "Unreleased" header to a specific version, use this
     .. format.  Don't forget the jump target:
     ..
@@ -19,6 +17,96 @@
     ..  Version 9.8.1 — 2027-07-27
     ..  --------------------------
 
+.. _changes_632:
+
+Version 6.3.2 — 2022-02-20
+--------------------------
+
+- Fix: adapt to pypy3.9's decorator tracing behavior.  It now traces function
+  decorators like CPython 3.8: both the @-line and the def-line are traced.
+  Fixes `issue 1326`_.
+
+- Debug: added ``pybehave`` to the list of :ref:`cmd_debug` and
+  :ref:`cmd_run_debug` options.
+
+- Fix: show an intelligible error message if ``--concurrency=multiprocessing``
+  is used without a configuration file.  Closes `issue 1320`_.
+
+.. _issue 1320: https://github.com/nedbat/coveragepy/issues/1320
+.. _issue 1326: https://github.com/nedbat/coveragepy/issues/1326
+
+
+.. _changes_631:
+
+Version 6.3.1 — 2022-02-01
+--------------------------
+
+- Fix: deadlocks could occur when terminating processes.  Some of these
+  deadlocks (described in `issue 1310`_) are now fixed.
+
+- Fix: a signal handler was being set from multiple threads, causing an error:
+  "ValueError: signal only works in main thread".  This is now fixed, closing
+  `issue 1312`_.
+
+- Fix: ``--precision`` on the command-line was being ignored while considering
+  ``--fail-under``.  This is now fixed, thanks to
+  `Marcelo Trylesinski <pull 1317_>`_.
+
+- Fix: releases no longer provide 3.11.0-alpha wheels. Coverage.py uses CPython
+  internal fields which are moving during the alpha phase. Fixes `issue 1316`_.
+
+.. _issue 1310: https://github.com/nedbat/coveragepy/issues/1310
+.. _issue 1312: https://github.com/nedbat/coveragepy/issues/1312
+.. _issue 1316: https://github.com/nedbat/coveragepy/issues/1316
+.. _pull 1317: https://github.com/nedbat/coveragepy/pull/1317
+
+
+.. _changes_63:
+
+Version 6.3 — 2022-01-25
+------------------------
+
+- Feature: Added the ``lcov`` command to generate reports in LCOV format.
+  Thanks, `Bradley Burns <pull 1289_>`_. Closes issues `587 <issue 587_>`_
+  and `626 <issue 626_>`_.
+
+- Feature: the coverage data file can now be specified on the command line with
+  the ``--data-file`` option in any command that reads or writes data.  This is
+  in addition to the existing ``COVERAGE_FILE`` environment variable.  Closes
+  `issue 624`_. Thanks, `Nikita Bloshchanevich <pull 1304_>`_.
+
+- Feature: coverage measurement data will now be written when a SIGTERM signal
+  is received by the process.  This includes
+  :meth:`Process.terminate <python:multiprocessing.Process.terminate>`,
+  and other ways to terminate a process.  Currently this is only on Linux and
+  Mac; Windows is not supported.  Fixes `issue 1307`_.
+
+- Dropped support for Python 3.6, which reached end-of-life on 2021-12-23.
+
+- Updated Python 3.11 support to 3.11.0a4, fixing `issue 1294`_.
+
+- Fix: the coverage data file is now created in a more robust way, to avoid
+  problems when multiple processes are trying to write data at once. Fixes
+  issues `1303 <issue 1303_>`_ and `883 <issue 883_>`_.
+
+- Fix: a .gitignore file will only be written into the HTML report output
+  directory if the directory is empty.  This should prevent certain unfortunate
+  accidents of writing the file where it is not wanted.
+
+- Releases now have MacOS arm64 wheels for Apple Silicon, fixing `issue 1288`_.
+
+.. _issue 587: https://github.com/nedbat/coveragepy/issues/587
+.. _issue 624: https://github.com/nedbat/coveragepy/issues/624
+.. _issue 626: https://github.com/nedbat/coveragepy/issues/626
+.. _issue 883: https://github.com/nedbat/coveragepy/issues/883
+.. _issue 1288: https://github.com/nedbat/coveragepy/issues/1288
+.. _issue 1294: https://github.com/nedbat/coveragepy/issues/1294
+.. _issue 1303: https://github.com/nedbat/coveragepy/issues/1303
+.. _issue 1307: https://github.com/nedbat/coveragepy/issues/1307
+.. _pull 1289: https://github.com/nedbat/coveragepy/pull/1289
+.. _pull 1304: https://github.com/nedbat/coveragepy/pull/1304
+
+
 .. _changes_62:
 
 Version 6.2 — 2021-11-26
@@ -48,16 +136,16 @@
   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
+- 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
+- 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 1082: https://github.com/nedbat/coveragepy/issues/1082
 .. _issue 1203: https://github.com/nedbat/coveragepy/issues/1203
 
 
--- a/eric7/DebugClients/Python/coverage/doc/CONTRIBUTORS.txt	Sun Mar 20 17:26:35 2022 +0100
+++ b/eric7/DebugClients/Python/coverage/doc/CONTRIBUTORS.txt	Sun Mar 20 17:49:44 2022 +0100
@@ -24,6 +24,7 @@
 Ben Finney
 Bernát Gábor
 Bill Hart
+Bradley Burns
 Brandon Rhodes
 Brett Cannon
 Bruno P. Kinoshita
@@ -95,6 +96,7 @@
 Lex Berezhny
 Loïc Dachary
 Marc Abramowitz
+Marcelo Trylesinski
 Marcus Cobden
 Marius Gedminas
 Mark van der Wal
@@ -108,6 +110,7 @@
 Mike Fiedler
 Naveen Yadav
 Nathan Land
+Nikita Bloshchanevich
 Nils Kattenbeck
 Noel O'Boyle
 Olivier Grisel
--- a/eric7/DebugClients/Python/coverage/doc/README.rst	Sun Mar 20 17:26:35 2022 +0100
+++ b/eric7/DebugClients/Python/coverage/doc/README.rst	Sun Mar 20 17:49:44 2022 +0100
@@ -8,7 +8,7 @@
 Code coverage testing for Python.
 
 |  |license| |versions| |status|
-|  |test-status| |quality-status| |docs| |codecov|
+|  |test-status| |quality-status| |docs| |metacov|
 |  |kit| |downloads| |format| |repos|
 |  |stars| |forks| |contributors|
 |  |tidelift| |twitter-coveragepy| |twitter-nedbat|
@@ -19,8 +19,10 @@
 
 Coverage.py runs on these versions of Python:
 
-* CPython 3.6 through 3.11.
-* PyPy3 7.3.7.
+.. PYVERSIONS
+
+* CPython 3.7 through 3.11.0a4.
+* PyPy3 7.3.8.
 
 Documentation is on `Read the Docs`_.  Code repository and issue tracker are on
 `GitHub`_.
@@ -29,8 +31,9 @@
 .. _GitHub: https://github.com/nedbat/coveragepy
 
 
-**New in 6.x:** dropped support for Python 2.7 and 3.5; added support for 3.10
-match/case statements.
+**New in 6.x:** dropped support for Python 2.7, 3.5, and 3.6;
+write data on SIGTERM;
+added support for 3.10 match/case statements.
 
 
 For Enterprise
@@ -121,9 +124,9 @@
 .. |license| image:: https://img.shields.io/pypi/l/coverage.svg
     :target: https://pypi.org/project/coverage/
     :alt: License
-.. |codecov| image:: https://codecov.io/github/nedbat/coveragepy/coverage.svg?branch=master&precision=2
-    :target: https://codecov.io/github/nedbat/coveragepy?branch=master
-    :alt: Coverage!
+.. |metacov| image:: https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/nedbat/8c6980f77988a327348f9b02bbaf67f5/raw/metacov.json
+    :target: https://nedbat.github.io/coverage-reports/latest.html
+    :alt: Coverage reports
 .. |repos| image:: https://repology.org/badge/tiny-repos/python:coverage.svg
     :target: https://repology.org/project/python:coverage/versions
     :alt: Packaging status
--- a/eric7/DebugClients/Python/coverage/env.py	Sun Mar 20 17:26:35 2022 +0100
+++ b/eric7/DebugClients/Python/coverage/env.py	Sun Mar 20 17:49:44 2022 +0100
@@ -39,36 +39,26 @@
     else:
         optimize_if_debug = not pep626
 
-    # Is "if not __debug__" optimized away?
-    optimize_if_not_debug = (not PYPY) and (PYVERSION >= (3, 7, 0, 'alpha', 4))
+    # Is "if not __debug__" optimized away? The exact details have changed
+    # across versions.
     if pep626:
-        optimize_if_not_debug = False
-    if PYPY:
-        optimize_if_not_debug = True
-
-    # Is "if not __debug__" optimized away even better?
-    optimize_if_not_debug2 = (not PYPY) and (PYVERSION >= (3, 8, 0, 'beta', 1))
-    if pep626:
-        optimize_if_not_debug2 = False
-
-    # Yet another way to optimize "if not __debug__"?
-    optimize_if_not_debug3 = (PYPY and PYVERSION >= (3, 8))
+        optimize_if_not_debug = 1
+    elif PYPY:
+        if PYVERSION >= (3, 9):
+            optimize_if_not_debug = 2
+        elif PYVERSION[:2] == (3, 8):
+            optimize_if_not_debug = 3
+        else:
+            optimize_if_not_debug = 1
+    else:
+        if PYVERSION >= (3, 8, 0, 'beta', 1):
+            optimize_if_not_debug = 2
+        else:
+            optimize_if_not_debug = 1
 
     # Can co_lnotab have negative deltas?
     negative_lnotab = not (PYPY and PYPYVERSION < (7, 2))
 
-    # Do .pyc files conform to PEP 552? Hash-based pyc's.
-    hashed_pyc_pep552 = (PYVERSION >= (3, 7, 0, 'alpha', 4))
-
-    # Python 3.7.0b3 changed the behavior of the sys.path[0] entry for -m. It
-    # used to be an empty string (meaning the current directory). It changed
-    # to be the actual path to the current directory, so that os.chdir wouldn't
-    # affect the outcome.
-    actual_syspath0_dash_m = (
-        (CPYTHON and (PYVERSION >= (3, 7, 0, 'beta', 3))) or
-        (PYPY and (PYPYVERSION >= (7, 3, 4)))
-        )
-
     # 3.7 changed how functions with only docstrings are numbered.
     docstring_only_function = (not PYPY) and ((3, 7, 0, 'beta', 5) <= PYVERSION <= (3, 10))
 
@@ -81,13 +71,21 @@
     # When a function is decorated, does the trace function get called for the
     # @-line and also the def-line (new behavior in 3.8)? Or just the @-line
     # (old behavior)?
-    trace_decorated_def = (CPYTHON and PYVERSION >= (3, 8))
+    trace_decorated_def = (CPYTHON and PYVERSION >= (3, 8)) or (PYPY and PYVERSION >= (3, 9))
+
+    # Functions are no longer claimed to start at their earliest decorator even though
+    # the decorators are traced?
+    def_ast_no_decorator = (PYPY and PYVERSION >= (3, 9))
+
+    # CPython 3.11 now jumps to the decorator line again while executing
+    # the decorator.
+    trace_decorator_line_again = (CPYTHON and PYVERSION > (3, 11, 0, 'alpha', 3, 0))
 
     # Are while-true loops optimized into absolute jumps with no loop setup?
     nix_while_true = (PYVERSION >= (3, 8))
 
-    # Python 3.9a1 made sys.argv[0] and other reported files absolute paths.
-    report_absolute_files = (PYVERSION >= (3, 9))
+    # CPython 3.9a1 made sys.argv[0] and other reported files absolute paths.
+    report_absolute_files = (CPYTHON and PYVERSION >= (3, 9))
 
     # Lines after break/continue/return/raise are no longer compiled into the
     # bytecode.  They used to be marked as missing, now they aren't executable.
@@ -129,4 +127,22 @@
 # Environment COVERAGE_NO_CONTRACTS=1 can turn off contracts while debugging
 # tests to remove noise from stack traces.
 # $set_env.py: COVERAGE_NO_CONTRACTS - Disable PyContracts to simplify stack traces.
-USE_CONTRACTS = TESTING and not bool(int(os.environ.get("COVERAGE_NO_CONTRACTS", 0)))
+USE_CONTRACTS = (
+    TESTING
+    and not bool(int(os.environ.get("COVERAGE_NO_CONTRACTS", 0)))
+    and (PYVERSION < (3, 11))
+)
+
+def debug_info():
+    """Return a list of (name, value) pairs for printing debug information."""
+    info = [
+        (name, value) for name, value in globals().items()
+        if not name.startswith("_") and
+            name not in {"PYBEHAVIOR", "debug_info"} and
+            not isinstance(value, type(os))
+    ]
+    info += [
+        (name, value) for name, value in PYBEHAVIOR.__dict__.items()
+        if not name.startswith("_")
+    ]
+    return sorted(info)
--- a/eric7/DebugClients/Python/coverage/execfile.py	Sun Mar 20 17:26:35 2022 +0100
+++ b/eric7/DebugClients/Python/coverage/execfile.py	Sun Mar 20 17:49:44 2022 +0100
@@ -80,10 +80,7 @@
         This needs to happen before any importing, and without importing anything.
         """
         if self.as_module:
-            if env.PYBEHAVIOR.actual_syspath0_dash_m:
-                path0 = os.getcwd()
-            else:
-                path0 = ""
+            path0 = os.getcwd()
         elif os.path.isdir(self.arg0):
             # Running a directory means running the __main__.py file in that
             # directory.
@@ -295,18 +292,14 @@
         if 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:
-            flags = struct.unpack('<L', fpyc.read(4))[0]
-            hash_based = flags & 0x01
-            if hash_based:
-                fpyc.read(8)    # Skip the hash.
-                date_based = False
-        if date_based:
+        flags = struct.unpack('<L', fpyc.read(4))[0]
+        hash_based = flags & 0x01
+        if hash_based:
+            fpyc.read(8)    # Skip the hash.
+        else:
             # Skip the junk in the header that we don't need.
-            fpyc.read(4)            # Skip the moddate.
-            # 3.3 added another long to the header (size), skip it.
-            fpyc.read(4)
+            fpyc.read(4)    # Skip the moddate.
+            fpyc.read(4)    # Skip the size.
 
         # The rest of the file is the code object we want.
         code = marshal.load(fpyc)
--- a/eric7/DebugClients/Python/coverage/files.py	Sun Mar 20 17:26:35 2022 +0100
+++ b/eric7/DebugClients/Python/coverage/files.py	Sun Mar 20 17:49:44 2022 +0100
@@ -145,13 +145,7 @@
 @contract(returns='unicode')
 def abs_file(path):
     """Return the absolute normalized form of `path`."""
-    try:
-        path = os.path.realpath(path)
-    except UnicodeError:
-        pass
-    path = os.path.abspath(path)
-    path = actual_path(path)
-    return path
+    return actual_path(os.path.abspath(os.path.realpath(path)))
 
 
 def python_reported_file(filename):
--- a/eric7/DebugClients/Python/coverage/html.py	Sun Mar 20 17:26:35 2022 +0100
+++ b/eric7/DebugClients/Python/coverage/html.py	Sun Mar 20 17:49:44 2022 +0100
@@ -164,6 +164,7 @@
         self.incr = IncrementalChecker(self.directory)
         self.datagen = HtmlDataGeneration(self.coverage)
         self.totals = Numbers(precision=self.config.precision)
+        self.directory_was_empty = False
 
         self.template_globals = {
             # Functions available in the templates.
@@ -224,11 +225,11 @@
         for static in self.STATIC_FILES:
             shutil.copyfile(data_filename(static), os.path.join(self.directory, static))
 
+        # Only write the .gitignore file if the directory was originally empty.
         # .gitignore can't be copied from the source tree because it would
         # prevent the static files from being checked in.
-        gitigore_path = os.path.join(self.directory, ".gitignore")
-        if not os.path.exists(gitigore_path):
-            with open(gitigore_path, "w") as fgi:
+        if self.directory_was_empty:
+            with open(os.path.join(self.directory, ".gitignore"), "w") as fgi:
                 fgi.write("# Created by coverage.py\n*\n")
 
         # The user may have extra CSS they want copied.
@@ -240,6 +241,8 @@
         rootname = flat_rootname(fr.relative_filename())
         html_filename = rootname + ".html"
         ensure_dir(self.directory)
+        if not os.listdir(self.directory):
+            self.directory_was_empty = True
         html_path = os.path.join(self.directory, html_filename)
 
         # Get the numbers for this file.
--- a/eric7/DebugClients/Python/coverage/inorout.py	Sun Mar 20 17:26:35 2022 +0100
+++ b/eric7/DebugClients/Python/coverage/inorout.py	Sun Mar 20 17:49:44 2022 +0100
@@ -124,8 +124,7 @@
         pass
     else:
         if spec is not None:
-            if spec.origin != "namespace":
-                filename = spec.origin
+            filename = spec.origin
             path = list(spec.submodule_search_locations or ())
     return filename, path
 
--- a/eric7/DebugClients/Python/coverage/jsonreport.py	Sun Mar 20 17:26:35 2022 +0100
+++ b/eric7/DebugClients/Python/coverage/jsonreport.py	Sun Mar 20 17:49:44 2022 +0100
@@ -2,6 +2,7 @@
 # For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt
 
 """Json reporting for coverage.py"""
+
 import datetime
 import json
 import sys
@@ -27,7 +28,7 @@
 
         `morfs` is a list of modules or file names.
 
-        `outfile` is a file object to write the json to
+        `outfile` is a file object to write the json to.
 
         """
         outfile = outfile or sys.stdout
@@ -75,7 +76,7 @@
         return self.total.n_statements and self.total.pc_covered
 
     def report_one_file(self, coverage_data, analysis):
-        """Extract the relevant report data for a single file"""
+        """Extract the relevant report data for a single file."""
         nums = analysis.numbers
         self.total += nums
         summary = {
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/eric7/DebugClients/Python/coverage/lcovreport.py	Sun Mar 20 17:49:44 2022 +0100
@@ -0,0 +1,106 @@
+# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
+# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt
+
+"""LCOV reporting for coverage.py."""
+
+import sys
+import base64
+from hashlib import md5
+
+from coverage.report import get_analysis_to_report
+
+
+class LcovReporter:
+    """A reporter for writing LCOV coverage reports."""
+
+    report_type = "LCOV report"
+
+    def __init__(self, coverage):
+        self.coverage = coverage
+        self.config = self.coverage.config
+
+    def report(self, morfs, outfile=None):
+        """Renders the full lcov report.
+
+        'morfs' is a list of modules or filenames
+
+        outfile is the file object to write the file into.
+        """
+
+        self.coverage.get_data()
+        outfile = outfile or sys.stdout
+
+        for fr, analysis in get_analysis_to_report(self.coverage, morfs):
+            self.get_lcov(fr, analysis, outfile)
+
+    def get_lcov(self, fr, analysis, outfile=None):
+        """Produces the lcov data for a single file.
+
+        This currently supports both line and branch coverage,
+        however function coverage is not supported.
+        """
+        outfile.write("TN:\n")
+        outfile.write(f"SF:{fr.relative_filename()}\n")
+        source_lines = fr.source().splitlines()
+
+        for covered in sorted(analysis.executed):
+            # Note: Coverage.py currently only supports checking *if* a line
+            # has been executed, not how many times, so we set this to 1 for
+            # nice output even if it's technically incorrect.
+
+            # The lines below calculate a 64-bit encoded md5 hash of the line
+            # corresponding to the DA lines in the lcov file, for either case
+            # of the line being covered or missed in coverage.py. The final two
+            # characters of the encoding ("==") are removed from the hash to
+            # allow genhtml to run on the resulting lcov file.
+            if source_lines:
+                line = source_lines[covered-1].encode("utf-8")
+            else:
+                line = b""
+            hashed = base64.b64encode(md5(line).digest()).decode().rstrip("=")
+            outfile.write(f"DA:{covered},1,{hashed}\n")
+
+        for missed in sorted(analysis.missing):
+            assert source_lines
+            line = source_lines[missed-1].encode("utf-8")
+            hashed = base64.b64encode(md5(line).digest()).decode().rstrip("=")
+            outfile.write(f"DA:{missed},0,{hashed}\n")
+
+        outfile.write(f"LF:{len(analysis.statements)}\n")
+        outfile.write(f"LH:{len(analysis.executed)}\n")
+
+        # More information dense branch coverage data.
+        missing_arcs = analysis.missing_branch_arcs()
+        executed_arcs = analysis.executed_branch_arcs()
+        for block_number, block_line_number in enumerate(
+            sorted(analysis.branch_stats().keys())
+        ):
+            for branch_number, line_number in enumerate(
+                sorted(missing_arcs[block_line_number])
+            ):
+                # The exit branches have a negative line number,
+                # this will not produce valid lcov. Setting
+                # the line number of the exit branch to 0 will allow
+                # for valid lcov, while preserving the data.
+                line_number = max(line_number, 0)
+                outfile.write(f"BRDA:{line_number},{block_number},{branch_number},-\n")
+
+            # The start value below allows for the block number to be
+            # preserved between these two for loops (stopping the loop from
+            # resetting the value of the block number to 0).
+            for branch_number, line_number in enumerate(
+                sorted(executed_arcs[block_line_number]),
+                start=len(missing_arcs[block_line_number]),
+            ):
+                line_number = max(line_number, 0)
+                outfile.write(f"BRDA:{line_number},{block_number},{branch_number},1\n")
+
+        # Summary of the branch coverage.
+        if analysis.has_arcs():
+            branch_stats = analysis.branch_stats()
+            brf = sum(t for t, k in branch_stats.values())
+            brh = brf - sum(t - k for t, k in branch_stats.values())
+            outfile.write(f"BRF:{brf}\n")
+            outfile.write(f"BRH:{brh}\n")
+
+        outfile.write("end_of_record\n")
--- a/eric7/DebugClients/Python/coverage/multiproc.py	Sun Mar 20 17:26:35 2022 +0100
+++ b/eric7/DebugClients/Python/coverage/multiproc.py	Sun Mar 20 17:49:44 2022 +0100
@@ -27,7 +27,7 @@
         """Wrapper around _bootstrap to start coverage."""
         try:
             from coverage import Coverage       # avoid circular import
-            cov = Coverage(data_suffix=True)
+            cov = Coverage(data_suffix=True, auto_data=True)
             cov._warn_preimported_source = False
             cov.start()
             debug = cov._debug
--- a/eric7/DebugClients/Python/coverage/parser.py	Sun Mar 20 17:26:35 2022 +0100
+++ b/eric7/DebugClients/Python/coverage/parser.py	Sun Mar 20 17:49:44 2022 +0100
@@ -644,7 +644,7 @@
         self.missing_arc_fragments = collections.defaultdict(list)
         self.block_stack = []
 
-        # $set_env.py: COVERAGE_TRACK_ARCS - Trace every arc added while parsing code.
+        # $set_env.py: COVERAGE_TRACK_ARCS - Trace possible arcs added while parsing code.
         self.debug = bool(int(os.environ.get("COVERAGE_TRACK_ARCS", 0)))
 
     def analyze(self):
@@ -664,8 +664,8 @@
     def add_arc(self, start, end, smsg=None, emsg=None):
         """Add an arc, including message fragments to use if it is missing."""
         if self.debug:                      # pragma: debugging
-            print(f"\nAdding arc: ({start}, {end}): {smsg!r}, {emsg!r}")
-            print(short_stack(limit=6))
+            print(f"\nAdding possible arc: ({start}, {end}): {smsg!r}, {emsg!r}")
+            print(short_stack(limit=10))
         self.arcs.add((start, end))
 
         if smsg is not None or emsg is not None:
@@ -692,7 +692,7 @@
     def _line_decorated(self, node):
         """Compute first line number for things that can be decorated (classes and functions)."""
         lineno = node.lineno
-        if env.PYBEHAVIOR.trace_decorated_def:
+        if env.PYBEHAVIOR.trace_decorated_def or env.PYBEHAVIOR.def_ast_no_decorator:
             if node.decorator_list:
                 lineno = node.decorator_list[0].lineno
         return lineno
@@ -944,10 +944,11 @@
     def _handle_decorated(self, node):
         """Add arcs for things that can be decorated (classes and functions)."""
         main_line = last = node.lineno
-        if node.decorator_list:
-            if env.PYBEHAVIOR.trace_decorated_def:
+        decs = node.decorator_list
+        if decs:
+            if env.PYBEHAVIOR.trace_decorated_def or env.PYBEHAVIOR.def_ast_no_decorator:
                 last = None
-            for dec_node in node.decorator_list:
+            for dec_node in decs:
                 dec_start = self.line_for_node(dec_node)
                 if last is not None and dec_start != last:
                     self.add_arc(last, dec_start)
@@ -955,6 +956,11 @@
             if env.PYBEHAVIOR.trace_decorated_def:
                 self.add_arc(last, main_line)
                 last = main_line
+            if env.PYBEHAVIOR.trace_decorator_line_again:
+                for top, bot in zip(decs, decs[1:]):
+                    self.add_arc(self.line_for_node(bot), self.line_for_node(top))
+                self.add_arc(self.line_for_node(decs[0]), main_line)
+                self.add_arc(main_line, self.line_for_node(decs[-1]))
             # The definition line may have been missed, but we should have it
             # in `self.statements`.  For some constructs, `line_for_node` is
             # not what we'd think of as the first line in the statement, so map
--- a/eric7/DebugClients/Python/coverage/pytracer.py	Sun Mar 20 17:26:35 2022 +0100
+++ b/eric7/DebugClients/Python/coverage/pytracer.py	Sun Mar 20 17:49:44 2022 +0100
@@ -10,14 +10,18 @@
 from coverage import env
 
 # We need the YIELD_VALUE opcode below, in a comparison-friendly form.
-YIELD_VALUE = dis.opmap['YIELD_VALUE']
+RESUME = dis.opmap.get('RESUME')
+RETURN_VALUE = dis.opmap['RETURN_VALUE']
+if RESUME is None:
+    YIELD_VALUE = dis.opmap['YIELD_VALUE']
+    YIELD_FROM = dis.opmap['YIELD_FROM']
+    YIELD_FROM_OFFSET = 0 if env.PYPY else 2
 
 # When running meta-coverage, this file can try to trace itself, which confuses
 # everything.  Don't trace ourselves.
 
 THIS_FILE = __file__.rstrip("co")
 
-
 class PyTracer:
     """Python implementation of the raw data tracer."""
 
@@ -64,7 +68,7 @@
         atexit.register(setattr, self, 'in_atexit', True)
 
     def __repr__(self):
-        return "<PyTracer at {}: {} lines in {} files>".format(
+        return "<PyTracer at 0x{:x}: {} lines in {} files>".format(
             id(self),
             sum(len(v) for v in self.data.values()),
             len(self.data),
@@ -78,13 +82,13 @@
                 id(self),
                 len(self.data_stack),
             ))
-            if 0:
+            if 0:   # if you want thread ids..
                 f.write(".{:x}.{:x}".format(
                     self.thread.ident,
                     self.threading.current_thread().ident,
                 ))
             f.write(" {}".format(" ".join(map(str, args))))
-            if 0:
+            if 0:   # if you want callers..
                 f.write(" | ")
                 stack = " / ".join(
                     (fname or "???").rpartition("/")[-1]
@@ -132,8 +136,7 @@
             else:
                 self.started_context = False
 
-            # Entering a new frame.  Decide if we should trace
-            # in this file.
+            # Entering a new frame.  Decide if we should trace in this file.
             self._activity = True
             self.data_stack.append(
                 (
@@ -160,7 +163,14 @@
             # function calls and re-entering generators.  The f_lasti field is
             # -1 for calls, and a real offset for generators.  Use <0 as the
             # line number for calls, and the real line number for generators.
-            if getattr(frame, 'f_lasti', -1) < 0:
+            if RESUME is not None:
+                # The current opcode is guaranteed to be RESUME. The argument
+                # determines what kind of resume it is.
+                oparg = frame.f_code.co_code[frame.f_lasti + 1]
+                real_call = (oparg == 0)
+            else:
+                real_call = (getattr(frame, 'f_lasti', -1) < 0)
+            if real_call:
                 self.last_line = -frame.f_code.co_firstlineno
             else:
                 self.last_line = frame.f_lineno
@@ -178,9 +188,27 @@
             if self.trace_arcs and self.cur_file_data:
                 # Record an arc leaving the function, but beware that a
                 # "return" event might just mean yielding from a generator.
-                # Jython seems to have an empty co_code, so just assume return.
                 code = frame.f_code.co_code
-                if (not code) or code[frame.f_lasti] != YIELD_VALUE:
+                lasti = frame.f_lasti
+                if RESUME is not None:
+                    if len(code) == lasti + 2:
+                        # A return from the end of a code object is a real return.
+                        real_return = True
+                    else:
+                        # it's a real return.
+                        real_return = (code[lasti + 2] != RESUME)
+                else:
+                    if code[lasti] == RETURN_VALUE:
+                        real_return = True
+                    elif code[lasti] == YIELD_VALUE:
+                        real_return = False
+                    elif len(code) <= lasti + YIELD_FROM_OFFSET:
+                        real_return = True
+                    elif code[lasti + YIELD_FROM_OFFSET] == YIELD_FROM:
+                        real_return = False
+                    else:
+                        real_return = True
+                if real_return:
                     first = frame.f_code.co_firstlineno
                     self.cur_file_data.add((self.last_line, -first))
             # Leaving this function, pop the filename stack.
@@ -238,8 +266,10 @@
             # has changed to None.
             dont_warn = (env.PYPY and env.PYPYVERSION >= (5, 4) and self.in_atexit and tf is None)
             if (not dont_warn) and tf != self._trace:   # pylint: disable=comparison-with-callable
-                msg = f"Trace function changed, measurement is likely wrong: {tf!r}"
-                self.warn(msg, slug="trace-changed")
+                self.warn(
+                    f"Trace function changed, data is likely wrong: {tf!r} != {self._trace!r}",
+                    slug="trace-changed",
+                )
 
     def activity(self):
         """Has there been any activity?"""
--- a/eric7/DebugClients/Python/coverage/results.py	Sun Mar 20 17:26:35 2022 +0100
+++ b/eric7/DebugClients/Python/coverage/results.py	Sun Mar 20 17:49:44 2022 +0100
@@ -136,6 +136,21 @@
                 mba[l1].append(l2)
         return mba
 
+    @contract(returns='dict(int: list(int))')
+    def executed_branch_arcs(self):
+        """Return arcs that were executed from branch lines.
+
+        Returns {l1:[l2a,l2b,...], ...}
+
+        """
+        executed = self.arcs_executed()
+        branch_lines = set(self._branch_lines())
+        eba = collections.defaultdict(list)
+        for l1, l2 in executed:
+            if l1 in branch_lines:
+                eba[l1].append(l2)
+        return eba
+
     @contract(returns='dict(int: tuple(int, int))')
     def branch_stats(self):
         """Get stats about branches.
--- a/eric7/DebugClients/Python/coverage/sqldata.py	Sun Mar 20 17:26:35 2022 +0100
+++ b/eric7/DebugClients/Python/coverage/sqldata.py	Sun Mar 20 17:49:44 2022 +0100
@@ -215,7 +215,7 @@
         self._dbs = {}
         self._pid = os.getpid()
         # Synchronize the operations used during collection.
-        self._lock = threading.Lock()
+        self._lock = threading.RLock()
 
         # Are we in sync with the data file?
         self._have_used = False
@@ -231,7 +231,11 @@
         """A decorator for methods that should hold self._lock."""
         @functools.wraps(method)
         def _wrapped(self, *args, **kwargs):
+            if self._debug.should("lock"):
+                self._debug.write(f"Locking {self._lock!r} for {method.__name__}")
             with self._lock:
+                if self._debug.should("lock"):
+                    self._debug.write(f"Locked  {self._lock!r} for {method.__name__}")
                 # pylint: disable=not-callable
                 return method(self, *args, **kwargs)
         return _wrapped
@@ -256,26 +260,6 @@
         self._have_used = False
         self._current_context_id = None
 
-    def _create_db(self):
-        """Create a db file that doesn't exist yet.
-
-        Initializes the schema and certain metadata.
-        """
-        if self._debug.should("dataio"):
-            self._debug.write(f"Creating data file {self._filename!r}")
-        self._dbs[threading.get_ident()] = db = SqliteDb(self._filename, self._debug)
-        with db:
-            db.executescript(SCHEMA)
-            db.execute("insert into coverage_schema (version) values (?)", (SCHEMA_VERSION,))
-            db.executemany(
-                "insert into meta (key, value) values (?, ?)",
-                [
-                    ("sys_argv", str(getattr(sys, "argv", None))),
-                    ("version", __version__),
-                    ("when", datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")),
-                ]
-            )
-
     def _open_db(self):
         """Open an existing db file, and read its metadata."""
         if self._debug.should("dataio"):
@@ -289,11 +273,14 @@
             try:
                 schema_version, = db.execute_one("select version from coverage_schema")
             except Exception as exc:
-                raise DataError(
-                    "Data file {!r} doesn't seem to be a coverage data file: {}".format(
-                        self._filename, exc
-                    )
-                ) from exc
+                if "no such table: coverage_schema" in str(exc):
+                    self._init_db(db)
+                else:
+                    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 DataError(
@@ -309,13 +296,25 @@
             for path, file_id in db.execute("select path, id from file"):
                 self._file_map[path] = file_id
 
+    def _init_db(self, db):
+        """Write the initial contents of the database."""
+        if self._debug.should("dataio"):
+            self._debug.write(f"Initing data file {self._filename!r}")
+        db.executescript(SCHEMA)
+        db.execute("insert into coverage_schema (version) values (?)", (SCHEMA_VERSION,))
+        db.executemany(
+            "insert or ignore into meta (key, value) values (?, ?)",
+            [
+                ("sys_argv", str(getattr(sys, "argv", None))),
+                ("version", __version__),
+                ("when", datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")),
+            ]
+        )
+
     def _connect(self):
         """Get the SqliteDb object to use."""
         if threading.get_ident() not in self._dbs:
-            if os.path.exists(self._filename):
-                self._open_db()
-            else:
-                self._create_db()
+            self._open_db()
         return self._dbs[threading.get_ident()]
 
     def __bool__(self):
@@ -349,7 +348,8 @@
         if self._debug.should("dataio"):
             self._debug.write(f"Dumping data from data file {self._filename!r}")
         with self._connect() as con:
-            return b"z" + zlib.compress(con.dump().encode("utf-8"))
+            script = con.dump()
+            return b"z" + zlib.compress(script.encode("utf-8"))
 
     @contract(data="bytes")
     def loads(self, data):
@@ -501,6 +501,9 @@
             self._set_context_id()
             for filename, arcs in arc_data.items():
                 file_id = self._file_id(filename, add=True)
+                from coverage import env
+                if env.PYVERSION == (3, 11, 0, "alpha", 4, 0):
+                    arcs = [(a, b) for a, b in arcs if a is not None and b is not None]
                 data = [(file_id, self._current_context_id, fromno, tono) for fromno, tono in arcs]
                 con.executemany(
                     "insert or ignore into arc " +
@@ -513,15 +516,19 @@
         assert lines or arcs
         assert not (lines and arcs)
         if lines and self._has_arcs:
+            if self._debug.should("dataop"):
+                self._debug.write("Error: 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:
+            if self._debug.should("dataop"):
+                self._debug.write("Error: 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
             with self._connect() as con:
                 con.execute(
-                    "insert into meta (key, value) values (?, ?)",
+                    "insert or ignore into meta (key, value) values (?, ?)",
                     ("has_arcs", str(int(arcs)))
                 )
 
--- a/eric7/DebugClients/Python/coverage/version.py	Sun Mar 20 17:26:35 2022 +0100
+++ b/eric7/DebugClients/Python/coverage/version.py	Sun Mar 20 17:49:44 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, 2, 0, "final", 0)
+version_info = (6, 3, 2, "final", 0)
 
 
 def _make_version(major, minor, micro, releaselevel, serial):

eric ide

mercurial