Upgraded coverage to 6.1.2. eric7

Sat, 20 Nov 2021 16:47:38 +0100

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Sat, 20 Nov 2021 16:47:38 +0100
branch
eric7
changeset 8775
0802ae193343
parent 8774
d728227e8ebb
child 8776
07b8c13b0607

Upgraded coverage to 6.1.2.

eric7/DebugClients/Python/coverage/__init__.py file | annotate | diff | comparison | revisions
eric7/DebugClients/Python/coverage/annotate.py file | annotate | diff | comparison | revisions
eric7/DebugClients/Python/coverage/backward.py 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/context.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/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/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/htmlfiles/coverage_html.js file | annotate | diff | comparison | revisions
eric7/DebugClients/Python/coverage/htmlfiles/favicon_32.png file | annotate | diff | comparison | revisions
eric7/DebugClients/Python/coverage/htmlfiles/index.html file | annotate | diff | comparison | revisions
eric7/DebugClients/Python/coverage/htmlfiles/keybd_closed.png file | annotate | diff | comparison | revisions
eric7/DebugClients/Python/coverage/htmlfiles/keybd_open.png file | annotate | diff | comparison | revisions
eric7/DebugClients/Python/coverage/htmlfiles/pyfile.html file | annotate | diff | comparison | revisions
eric7/DebugClients/Python/coverage/htmlfiles/style.css file | annotate | diff | comparison | revisions
eric7/DebugClients/Python/coverage/htmlfiles/style.scss 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/misc.py file | annotate | diff | comparison | revisions
eric7/DebugClients/Python/coverage/multiproc.py file | annotate | diff | comparison | revisions
eric7/DebugClients/Python/coverage/numbits.py file | annotate | diff | comparison | revisions
eric7/DebugClients/Python/coverage/parser.py file | annotate | diff | comparison | revisions
eric7/DebugClients/Python/coverage/phystokens.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/python.py file | annotate | diff | comparison | revisions
eric7/DebugClients/Python/coverage/pytracer.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/templite.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/eric7/DebugClients/Python/coverage/__init__.py	Fri Nov 19 19:28:47 2021 +0100
+++ b/eric7/DebugClients/Python/coverage/__init__.py	Sat Nov 20 16:47:38 2021 +0100
@@ -14,7 +14,7 @@
 
 from coverage.control import Coverage, process_startup
 from coverage.data import CoverageData
-from coverage.misc import CoverageException
+from coverage.exceptions import CoverageException
 from coverage.plugin import CoveragePlugin, FileTracer, FileReporter
 from coverage.pytracer import PyTracer
 
--- a/eric7/DebugClients/Python/coverage/annotate.py	Fri Nov 19 19:28:47 2021 +0100
+++ b/eric7/DebugClients/Python/coverage/annotate.py	Sat Nov 20 16:47:38 2021 +0100
@@ -3,7 +3,6 @@
 
 """Source file annotation for coverage.py."""
 
-import io
 import os
 import re
 
@@ -14,7 +13,7 @@
 os = isolate_module(os)
 
 
-class AnnotateReporter(object):
+class AnnotateReporter:
     """Generate annotated source files showing line coverage.
 
     This reporter creates annotated copies of the measured source files. Each
@@ -74,9 +73,8 @@
         else:
             dest_file = fr.filename + ",cover"
 
-        with io.open(dest_file, 'w', encoding='utf8') as dest:
-            i = 0
-            j = 0
+        with open(dest_file, 'w', encoding='utf-8') as dest:
+            i = j = 0
             covered = True
             source = fr.source()
             for lineno, line in enumerate(source.splitlines(True), start=1):
@@ -87,22 +85,20 @@
                 if i < len(statements) and statements[i] == lineno:
                     covered = j >= len(missing) or missing[j] > lineno
                 if self.blank_re.match(line):
-                    dest.write(u'  ')
+                    dest.write('  ')
                 elif self.else_re.match(line):
                     # Special logic for lines containing only 'else:'.
-                    if i >= len(statements) and j >= len(missing):
-                        dest.write(u'! ')
-                    elif i >= len(statements) or j >= len(missing):
-                        dest.write(u'> ')
+                    if j >= len(missing):
+                        dest.write('> ')
                     elif statements[i] == missing[j]:
-                        dest.write(u'! ')
+                        dest.write('! ')
                     else:
-                        dest.write(u'> ')
+                        dest.write('> ')
                 elif lineno in excluded:
-                    dest.write(u'- ')
+                    dest.write('- ')
                 elif covered:
-                    dest.write(u'> ')
+                    dest.write('> ')
                 else:
-                    dest.write(u'! ')
+                    dest.write('! ')
 
                 dest.write(line)
--- a/eric7/DebugClients/Python/coverage/backward.py	Fri Nov 19 19:28:47 2021 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,267 +0,0 @@
-# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
-# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt
-
-"""Add things to old Pythons so I can pretend they are newer."""
-
-# This file's purpose is to provide modules to be imported from here.
-# pylint: disable=unused-import
-
-import os
-import sys
-
-from datetime import datetime
-
-from coverage import env
-
-
-# Pythons 2 and 3 differ on where to get StringIO.
-try:
-    from cStringIO import StringIO
-except ImportError:
-    from io import StringIO
-
-# In py3, ConfigParser was renamed to the more-standard configparser.
-# But there's a py3 backport that installs "configparser" in py2, and I don't
-# want it because it has annoying deprecation warnings. So try the real py2
-# import first.
-try:
-    import ConfigParser as configparser
-except ImportError:
-    import configparser
-
-# What's a string called?
-try:
-    string_class = basestring
-except NameError:
-    string_class = str
-
-# What's a Unicode string called?
-try:
-    unicode_class = unicode
-except NameError:
-    unicode_class = str
-
-# range or xrange?
-try:
-    range = xrange      # pylint: disable=redefined-builtin
-except NameError:
-    range = range
-
-try:
-    from itertools import zip_longest
-except ImportError:
-    from itertools import izip_longest as zip_longest
-
-# Where do we get the thread id from?
-try:
-    from thread import get_ident as get_thread_id
-except ImportError:
-    from threading import get_ident as get_thread_id
-
-try:
-    os.PathLike
-except AttributeError:
-    # This is Python 2 and 3
-    path_types = (bytes, string_class, unicode_class)
-else:
-    # 3.6+
-    path_types = (bytes, str, os.PathLike)
-
-# shlex.quote is new, but there's an undocumented implementation in "pipes",
-# who knew!?
-try:
-    from shlex import quote as shlex_quote
-except ImportError:
-    # Useful function, available under a different (undocumented) name
-    # in Python versions earlier than 3.3.
-    from pipes import quote as shlex_quote
-
-try:
-    import reprlib
-except ImportError:             # pragma: not covered
-    # We need this on Python 2, but in testing environments, a backport is
-    # installed, so this import isn't used.
-    import repr as reprlib
-
-# A function to iterate listlessly over a dict's items, and one to get the
-# items as a list.
-try:
-    {}.iteritems
-except AttributeError:
-    # Python 3
-    def iitems(d):
-        """Produce the items from dict `d`."""
-        return d.items()
-
-    def litems(d):
-        """Return a list of items from dict `d`."""
-        return list(d.items())
-else:
-    # Python 2
-    def iitems(d):
-        """Produce the items from dict `d`."""
-        return d.iteritems()
-
-    def litems(d):
-        """Return a list of items from dict `d`."""
-        return d.items()
-
-# Getting the `next` function from an iterator is different in 2 and 3.
-try:
-    iter([]).next
-except AttributeError:
-    def iternext(seq):
-        """Get the `next` function for iterating over `seq`."""
-        return iter(seq).__next__
-else:
-    def iternext(seq):
-        """Get the `next` function for iterating over `seq`."""
-        return iter(seq).next
-
-# Python 3.x is picky about bytes and strings, so provide methods to
-# get them right, and make them no-ops in 2.x
-if env.PY3:
-    def to_bytes(s):
-        """Convert string `s` to bytes."""
-        return s.encode('utf8')
-
-    def to_string(b):
-        """Convert bytes `b` to string."""
-        return b.decode('utf8')
-
-    def binary_bytes(byte_values):
-        """Produce a byte string with the ints from `byte_values`."""
-        return bytes(byte_values)
-
-    def byte_to_int(byte):
-        """Turn a byte indexed from a bytes object into an int."""
-        return byte
-
-    def bytes_to_ints(bytes_value):
-        """Turn a bytes object into a sequence of ints."""
-        # In Python 3, iterating bytes gives ints.
-        return bytes_value
-
-else:
-    def to_bytes(s):
-        """Convert string `s` to bytes (no-op in 2.x)."""
-        return s
-
-    def to_string(b):
-        """Convert bytes `b` to string."""
-        return b
-
-    def binary_bytes(byte_values):
-        """Produce a byte string with the ints from `byte_values`."""
-        return "".join(chr(b) for b in byte_values)
-
-    def byte_to_int(byte):
-        """Turn a byte indexed from a bytes object into an int."""
-        return ord(byte)
-
-    def bytes_to_ints(bytes_value):
-        """Turn a bytes object into a sequence of ints."""
-        for byte in bytes_value:
-            yield ord(byte)
-
-
-try:
-    # In Python 2.x, the builtins were in __builtin__
-    BUILTINS = sys.modules['__builtin__']
-except KeyError:
-    # In Python 3.x, they're in builtins
-    BUILTINS = sys.modules['builtins']
-
-
-# imp was deprecated in Python 3.3
-try:
-    import importlib
-    import importlib.util
-    imp = None
-except ImportError:
-    importlib = None
-
-# We only want to use importlib if it has everything we need.
-try:
-    importlib_util_find_spec = importlib.util.find_spec
-except Exception:
-    import imp
-    importlib_util_find_spec = None
-
-# What is the .pyc magic number for this version of Python?
-try:
-    PYC_MAGIC_NUMBER = importlib.util.MAGIC_NUMBER
-except AttributeError:
-    PYC_MAGIC_NUMBER = imp.get_magic()
-
-
-def code_object(fn):
-    """Get the code object from a function."""
-    try:
-        return fn.func_code
-    except AttributeError:
-        return fn.__code__
-
-
-try:
-    from types import SimpleNamespace
-except ImportError:
-    # The code from https://docs.python.org/3/library/types.html#types.SimpleNamespace
-    class SimpleNamespace:
-        """Python implementation of SimpleNamespace, for Python 2."""
-        def __init__(self, **kwargs):
-            self.__dict__.update(kwargs)
-
-        def __repr__(self):
-            keys = sorted(self.__dict__)
-            items = ("{}={!r}".format(k, self.__dict__[k]) for k in keys)
-            return "{}({})".format(type(self).__name__, ", ".join(items))
-
-
-def format_local_datetime(dt):
-    """Return a string with local timezone representing the date.
-    If python version is lower than 3.6, the time zone is not included.
-    """
-    try:
-        return dt.astimezone().strftime('%Y-%m-%d %H:%M %z')
-    except (TypeError, ValueError):
-        # Datetime.astimezone in Python 3.5 can not handle naive datetime
-        return dt.strftime('%Y-%m-%d %H:%M')
-
-
-def invalidate_import_caches():
-    """Invalidate any import caches that may or may not exist."""
-    if importlib and hasattr(importlib, "invalidate_caches"):
-        importlib.invalidate_caches()
-
-
-def import_local_file(modname, modfile=None):
-    """Import a local file as a module.
-
-    Opens a file in the current directory named `modname`.py, imports it
-    as `modname`, and returns the module object.  `modfile` is the file to
-    import if it isn't in the current directory.
-
-    """
-    try:
-        import importlib.util as importlib_util
-    except ImportError:
-        importlib_util = None
-
-    if modfile is None:
-        modfile = modname + '.py'
-    if importlib_util:
-        spec = importlib_util.spec_from_file_location(modname, modfile)
-        mod = importlib_util.module_from_spec(spec)
-        sys.modules[modname] = mod
-        spec.loader.exec_module(mod)
-    else:
-        for suff in imp.get_suffixes():                 # pragma: part covered
-            if suff[0] == '.py':
-                break
-
-        with open(modfile, 'r') as f:
-            # pylint: disable=undefined-loop-variable
-            mod = imp.load_module(modname, f, modfile, suff)
-
-    return mod
--- a/eric7/DebugClients/Python/coverage/cmdline.py	Fri Nov 19 19:28:47 2021 +0100
+++ b/eric7/DebugClients/Python/coverage/cmdline.py	Sat Nov 20 16:47:38 2021 +0100
@@ -3,10 +3,10 @@
 
 """Command-line support for coverage.py."""
 
-from __future__ import print_function
 
 import glob
-import optparse
+import optparse     # pylint: disable=deprecated-module
+import os
 import os.path
 import shlex
 import sys
@@ -19,12 +19,13 @@
 from coverage.collector import CTracer
 from coverage.data import line_counts
 from coverage.debug import info_formatter, info_header, short_stack
+from coverage.exceptions import BaseCoverageException, ExceptionDuringRun, NoSource
 from coverage.execfile import PyRunner
-from coverage.misc import BaseCoverageException, ExceptionDuringRun, NoSource, output_encoding
-from coverage.results import should_fail_under
+from coverage.misc import human_sorted
+from coverage.results import Numbers, should_fail_under
 
 
-class Opts(object):
+class Opts:
     """A namespace class for individual options we'll build parsers from."""
 
     append = optparse.make_option(
@@ -46,14 +47,22 @@
         '', '--concurrency', action='store', metavar="LIB",
         choices=CONCURRENCY_CHOICES,
         help=(
-            "Properly measure code using a concurrency library. "
-            "Valid values are: %s."
-        ) % ", ".join(CONCURRENCY_CHOICES),
+            "Properly measure code using a concurrency library. " +
+            "Valid values are: {}."
+        ).format(", ".join(CONCURRENCY_CHOICES)),
     )
     context = optparse.make_option(
         '', '--context', action='store', metavar="LABEL",
         help="The context label to record for this coverage run.",
     )
+    contexts = optparse.make_option(
+        '', '--contexts', action='store',
+        metavar="REGEX1,REGEX2,...",
+        help=(
+            "Only display data from lines covered in the given contexts. " +
+            "Accepts Python regexes, which must be quoted."
+        ),
+    )
     debug = optparse.make_option(
         '', '--debug', action='store', metavar="OPTS",
         help="Debug options, separated by commas. [env: COVERAGE_DEBUG]",
@@ -78,58 +87,36 @@
         '', '--include', action='store',
         metavar="PAT1,PAT2,...",
         help=(
-            "Include only files whose paths match one of these patterns. "
+            "Include only files whose paths match one of these patterns. " +
             "Accepts shell-style wildcards, which must be quoted."
         ),
     )
     pylib = optparse.make_option(
         '-L', '--pylib', action='store_true',
         help=(
-            "Measure coverage even inside the Python installed library, "
+            "Measure coverage even inside the Python installed library, " +
             "which isn't done by default."
         ),
     )
-    sort = optparse.make_option(
-        '--sort', action='store', metavar='COLUMN',
-        help="Sort the report by the named column: name, stmts, miss, branch, brpart, or cover. "
-             "Default is name."
-    )
     show_missing = optparse.make_option(
         '-m', '--show-missing', action='store_true',
         help="Show line numbers of statements in each module that weren't executed.",
     )
-    skip_covered = optparse.make_option(
-        '--skip-covered', action='store_true',
-        help="Skip files with 100% coverage.",
-    )
-    no_skip_covered = optparse.make_option(
-        '--no-skip-covered', action='store_false', dest='skip_covered',
-        help="Disable --skip-covered.",
-    )
-    skip_empty = optparse.make_option(
-        '--skip-empty', action='store_true',
-        help="Skip files with no code.",
-    )
-    show_contexts = optparse.make_option(
-        '--show-contexts', action='store_true',
-        help="Show contexts for covered lines.",
+    module = optparse.make_option(
+        '-m', '--module', action='store_true',
+        help=(
+            "<pyfile> is an importable Python module, not a script path, " +
+            "to be run as 'python -m' would run it."
+        ),
     )
     omit = optparse.make_option(
         '', '--omit', action='store',
         metavar="PAT1,PAT2,...",
         help=(
-            "Omit files whose paths match one of these patterns. "
+            "Omit files whose paths match one of these patterns. " +
             "Accepts shell-style wildcards, which must be quoted."
         ),
     )
-    contexts = optparse.make_option(
-        '', '--contexts', action='store',
-        metavar="REGEX1,REGEX2,...",
-        help=(
-            "Only display data from lines covered in the given contexts. "
-            "Accepts Python regexes, which must be quoted."
-        ),
-    )
     output_xml = optparse.make_option(
         '-o', '', action='store', dest="outfile",
         metavar="OUTFILE",
@@ -147,41 +134,59 @@
     parallel_mode = optparse.make_option(
         '-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 "
+            "Append the machine name, process id and random number to the " +
+            ".coverage data file name to simplify collecting data from " +
             "many processes."
         ),
     )
-    module = optparse.make_option(
-        '-m', '--module', action='store_true',
-        help=(
-            "<pyfile> is an importable Python module, not a script path, "
-            "to be run as 'python -m' would run it."
-        ),
-    )
     precision = optparse.make_option(
         '', '--precision', action='store', metavar='N', type=int,
         help=(
-            "Number of digits after the decimal point to display for "
+            "Number of digits after the decimal point to display for " +
             "reported coverage percentages."
         ),
     )
+    quiet = optparse.make_option(
+        '-q', '--quiet', action='store_true',
+        help="Don't print messages about what is happening.",
+    )
     rcfile = optparse.make_option(
         '', '--rcfile', action='store',
         help=(
-            "Specify configuration file. "
-            "By default '.coveragerc', 'setup.cfg', 'tox.ini', and "
+            "Specify configuration file. " +
+            "By default '.coveragerc', 'setup.cfg', 'tox.ini', and " +
             "'pyproject.toml' are tried. [env: COVERAGE_RCFILE]"
         ),
     )
+    show_contexts = optparse.make_option(
+        '--show-contexts', action='store_true',
+        help="Show contexts for covered lines.",
+    )
+    skip_covered = optparse.make_option(
+        '--skip-covered', action='store_true',
+        help="Skip files with 100% coverage.",
+    )
+    no_skip_covered = optparse.make_option(
+        '--no-skip-covered', action='store_false', dest='skip_covered',
+        help="Disable --skip-covered.",
+    )
+    skip_empty = optparse.make_option(
+        '--skip-empty', action='store_true',
+        help="Skip files with no code.",
+    )
+    sort = optparse.make_option(
+        '--sort', action='store', metavar='COLUMN',
+        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,...",
-        help="A list of packages or directories of code to be measured.",
+        help="A list of directories or importable names of code to measure.",
     )
     timid = optparse.make_option(
         '', '--timid', action='store_true',
         help=(
-            "Use a simpler but slower trace method. Try this if you get "
+            "Use a simpler but slower trace method. Try this if you get " +
             "seemingly impossible results!"
         ),
     )
@@ -195,7 +200,7 @@
     )
 
 
-class CoverageOptionParser(optparse.OptionParser, object):
+class CoverageOptionParser(optparse.OptionParser):
     """Base OptionParser for coverage.py.
 
     Problems don't exit the program.
@@ -204,7 +209,7 @@
     """
 
     def __init__(self, *args, **kwargs):
-        super(CoverageOptionParser, self).__init__(
+        super().__init__(
             add_help_option=False, *args, **kwargs
             )
         self.set_defaults(
@@ -213,6 +218,7 @@
             branch=None,
             concurrency=None,
             context=None,
+            contexts=None,
             debug=None,
             directory=None,
             fail_under=None,
@@ -222,15 +228,15 @@
             keep=None,
             module=None,
             omit=None,
-            contexts=None,
             parallel_mode=None,
             precision=None,
             pylib=None,
+            quiet=None,
             rcfile=True,
+            show_contexts=None,
             show_missing=None,
             skip_covered=None,
             skip_empty=None,
-            show_contexts=None,
             sort=None,
             source=None,
             timid=None,
@@ -251,7 +257,7 @@
 
         """
         try:
-            options, args = super(CoverageOptionParser, self).parse_args(args, options)
+            options, args = super().parse_args(args, options)
         except self.OptionParserError:
             return False, None, None
         return True, options, args
@@ -266,7 +272,7 @@
     """Command-line parser for coverage.py global option arguments."""
 
     def __init__(self):
-        super(GlobalOptionParser, self).__init__()
+        super().__init__()
 
         self.add_options([
             Opts.help,
@@ -289,7 +295,7 @@
         """
         if usage:
             usage = "%prog " + usage
-        super(CmdOptionParser, self).__init__(
+        super().__init__(
             usage=usage,
             description=description,
         )
@@ -300,16 +306,16 @@
     def __eq__(self, other):
         # A convenience equality, so that I can put strings in unit test
         # results, and they will compare equal to objects.
-        return (other == "<CmdOptionParser:%s>" % self.cmd)
+        return (other == f"<CmdOptionParser:{self.cmd}>")
 
     __hash__ = None     # This object doesn't need to be hashed.
 
     def get_prog_name(self):
         """Override of an undocumented function in optparse.OptionParser."""
-        program_name = super(CmdOptionParser, self).get_prog_name()
+        program_name = super().get_prog_name()
 
         # Include the sub-command for this parser as part of the command.
-        return "{command} {subcommand}".format(command=program_name, subcommand=self.cmd)
+        return f"{program_name} {self.cmd}"
 
 
 GLOBAL_ARGS = [
@@ -329,7 +335,7 @@
             ] + GLOBAL_ARGS,
         usage="[options] [modules]",
         description=(
-            "Make annotated copies of the given files, marking statements that are executed "
+            "Make annotated copies of the given files, marking statements that are executed " +
             "with > and statements that are missed with !."
         ),
     ),
@@ -339,14 +345,15 @@
         [
             Opts.append,
             Opts.keep,
+            Opts.quiet,
             ] + GLOBAL_ARGS,
         usage="[options] <path1> <path2> ... <pathN>",
         description=(
-            "Combine data from multiple coverage files collected "
-            "with 'run -p'.  The combined results are written to a single "
-            "file representing the union of the data. The positional "
-            "arguments are data files or directories containing data files. "
-            "If no paths are provided, data files in the default data file's "
+            "Combine data from multiple coverage files collected " +
+            "with 'run -p'.  The combined results are written to a single " +
+            "file representing the union of the data. The positional " +
+            "arguments are data files or directories containing data files. " +
+            "If no paths are provided, data files in the default data file's " +
             "directory are combined."
         ),
     ),
@@ -355,12 +362,12 @@
         "debug", GLOBAL_ARGS,
         usage="<topic>",
         description=(
-            "Display information about the internals of coverage.py, "
-            "for diagnosing problems. "
-            "Topics are: "
-                "'data' to show a summary of the collected data; "
-                "'sys' to show installation information; "
-                "'config' to show the configuration; "
+            "Display information about the internals of coverage.py, " +
+            "for diagnosing problems. " +
+            "Topics are: " +
+                "'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."
         ),
     ),
@@ -386,6 +393,7 @@
             Opts.include,
             Opts.omit,
             Opts.precision,
+            Opts.quiet,
             Opts.show_contexts,
             Opts.skip_covered,
             Opts.no_skip_covered,
@@ -394,8 +402,8 @@
             ] + GLOBAL_ARGS,
         usage="[options] [modules]",
         description=(
-            "Create an HTML report of the coverage of the files.  "
-            "Each file gets its own page, with the source decorated to show "
+            "Create an HTML report of the coverage of the files.  " +
+            "Each file gets its own page, with the source decorated to show " +
             "executed, excluded, and missed lines."
         ),
     ),
@@ -410,6 +418,7 @@
             Opts.omit,
             Opts.output_json,
             Opts.json_pretty_print,
+            Opts.quiet,
             Opts.show_contexts,
             ] + GLOBAL_ARGS,
         usage="[options] [modules]",
@@ -462,6 +471,7 @@
             Opts.include,
             Opts.omit,
             Opts.output_xml,
+            Opts.quiet,
             Opts.skip_empty,
             ] + GLOBAL_ARGS,
         usage="[options] [modules]",
@@ -498,7 +508,7 @@
 
     if error:
         print(error, file=sys.stderr)
-        print("Use '%s help' for help." % (program_name,), file=sys.stderr)
+        print(f"Use '{program_name} help' for help.", file=sys.stderr)
     elif parser:
         print(parser.format_help().strip())
         print()
@@ -507,14 +517,14 @@
         if help_msg:
             print(help_msg.format(**help_params))
         else:
-            print("Don't know topic %r" % topic)
+            print(f"Don't know topic {topic!r}")
     print("Full documentation is at {__url__}".format(**help_params))
 
 
 OK, ERR, FAIL_UNDER = 0, 1, 2
 
 
-class CoverageScript(object):
+class CoverageScript:
     """The command-line interface to coverage.py."""
 
     def __init__(self):
@@ -542,7 +552,7 @@
         else:
             parser = CMDS.get(argv[0])
             if not parser:
-                show_help("Unknown command: '%s'" % argv[0])
+                show_help(f"Unknown command: {argv[0]!r}")
                 return ERR
             argv = argv[1:]
 
@@ -575,6 +585,7 @@
             concurrency=options.concurrency,
             check_preimported=True,
             context=options.context,
+            messages=not options.quiet,
             )
 
         if options.action == "debug":
@@ -646,6 +657,9 @@
                 show_contexts=options.show_contexts,
                 **report_args
             )
+        else:
+            # There are no other possible actions.
+            raise AssertionError
 
         if total is not None:
             # Apply the command line fail-under options, and then use the config
@@ -656,8 +670,10 @@
             fail_under = self.coverage.get_option("report:fail_under")
             precision = self.coverage.get_option("report:precision")
             if should_fail_under(total, fail_under, precision):
-                msg = "total of {total:.{p}f} is less than fail-under={fail_under:.{p}f}".format(
-                    total=total, fail_under=fail_under, p=precision,
+                msg = "total of {total} is less than fail-under={fail_under:.{p}f}".format(
+                    total=Numbers(precision=precision).display_covered(total),
+                    fail_under=fail_under,
+                    p=precision,
                 )
                 print("Coverage failure:", msg)
                 return FAIL_UNDER
@@ -708,7 +724,7 @@
             command_line = self.coverage.get_option("run:command_line")
             if command_line is not None:
                 args = shlex.split(command_line)
-                if args and args[0] == "-m":
+                if args and args[0] in {"-m", "--module"}:
                     options.module = True
                     args = args[1:]
         if not args:
@@ -727,12 +743,14 @@
                 # they will be None if they have not been specified.
                 if getattr(options, opt_name) is not None:
                     show_help(
-                        "Options affecting multiprocessing must only be specified "
-                        "in a configuration file.\n"
-                        "Remove --{} from the command line.".format(opt_name)
+                        "Options affecting multiprocessing must only be specified " +
+                        "in a configuration file.\n" +
+                        f"Remove --{opt_name} from the command line."
                     )
                     return ERR
 
+        os.environ["COVERAGE_RUN"] = "true"
+
         runner = PyRunner(args, as_module=bool(options.module))
         runner.prepare()
 
@@ -766,22 +784,22 @@
                 sys_info = self.coverage.sys_info()
                 print(info_header("sys"))
                 for line in info_formatter(sys_info):
-                    print(" %s" % line)
+                    print(f" {line}")
             elif info == 'data':
                 self.coverage.load()
                 data = self.coverage.get_data()
                 print(info_header("data"))
-                print("path: %s" % data.data_filename())
+                print(f"path: {data.data_filename()}")
                 if data:
-                    print("has_arcs: %r" % data.has_arcs())
+                    print(f"has_arcs: {data.has_arcs()!r}")
                     summary = line_counts(data, fullpath=True)
-                    filenames = sorted(summary.keys())
-                    print("\n%d files:" % len(filenames))
+                    filenames = human_sorted(summary.keys())
+                    print(f"\n{len(filenames)} files:")
                     for f in filenames:
-                        line = "%s: %d lines" % (f, summary[f])
+                        line = f"{f}: {summary[f]} lines"
                         plugin = data.file_tracer(f)
                         if plugin:
-                            line += " [%s]" % plugin
+                            line += f" [{plugin}]"
                         print(line)
                 else:
                     print("No data collected")
@@ -789,12 +807,12 @@
                 print(info_header("config"))
                 config_info = self.coverage.config.__dict__.items()
                 for line in info_formatter(config_info):
-                    print(" %s" % line)
+                    print(f" {line}")
             elif info == "premain":
                 print(info_header("premain"))
                 print(short_stack())
             else:
-                show_help("Don't know what you mean by %r" % info)
+                show_help(f"Don't know what you mean by {info!r}")
                 return ERR
 
         return OK
@@ -878,8 +896,6 @@
     except BaseCoverageException as err:
         # A controlled error inside coverage.py: print the message to the user.
         msg = err.args[0]
-        if env.PY2:
-            msg = msg.encode(output_encoding())
         print(msg)
         status = ERR
     except SystemExit as err:
--- a/eric7/DebugClients/Python/coverage/collector.py	Fri Nov 19 19:28:47 2021 +0100
+++ b/eric7/DebugClients/Python/coverage/collector.py	Sat Nov 20 16:47:38 2021 +0100
@@ -7,10 +7,10 @@
 import sys
 
 from coverage import env
-from coverage.backward import litems, range     # pylint: disable=redefined-builtin
 from coverage.debug import short_stack
 from coverage.disposition import FileDisposition
-from coverage.misc import CoverageException, isolate_module
+from coverage.exceptions import CoverageException
+from coverage.misc import human_sorted, isolate_module
 from coverage.pytracer import PyTracer
 
 os = isolate_module(os)
@@ -21,7 +21,7 @@
     from coverage.tracer import CTracer, CFileDisposition
 except ImportError:
     # Couldn't import the C extension, maybe it isn't built.
-    if os.getenv('COVERAGE_TEST_TRACER') == 'c':
+    if os.getenv('COVERAGE_TEST_TRACER') == 'c':        # pragma: part covered
         # During testing, we use the COVERAGE_TEST_TRACER environment variable
         # to indicate that we've fiddled with the environment to test this
         # fallback code.  If we thought we had a C tracer, but couldn't import
@@ -33,7 +33,7 @@
     CTracer = None
 
 
-class Collector(object):
+class Collector:
     """Collects trace data.
 
     Creates a Tracer object for each thread, since they track stack
@@ -116,7 +116,7 @@
         # 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("Conflicting concurrency settings: %s" % concurrency)
+            raise CoverageException(f"Conflicting concurrency settings: {concurrency}")
         self.concurrency = these_concurrencies.pop() if these_concurrencies else ''
 
         try:
@@ -136,13 +136,13 @@
                 import threading
                 self.threading = threading
             else:
-                raise CoverageException("Don't understand concurrency=%s" % concurrency)
-        except ImportError:
+                raise CoverageException(f"Don't understand concurrency={concurrency}")
+        except ImportError as ex:
             raise CoverageException(
-                "Couldn't trace with concurrency=%s, the module isn't installed." % (
+                "Couldn't trace with concurrency={}, the module isn't installed.".format(
                     self.concurrency,
                 )
-            )
+            ) from ex
 
         self.reset()
 
@@ -157,12 +157,14 @@
         if self._trace_class is CTracer:
             self.file_disposition_class = CFileDisposition
             self.supports_plugins = True
+            self.packed_arcs = True
         else:
             self.file_disposition_class = FileDisposition
             self.supports_plugins = False
+            self.packed_arcs = False
 
     def __repr__(self):
-        return "<Collector at 0x%x: %s>" % (id(self), self.tracer_name())
+        return f"<Collector at 0x{id(self):x}: {self.tracer_name()}>"
 
     def use_data(self, covdata, context):
         """Use `covdata` for recording data."""
@@ -244,7 +246,7 @@
             tracer.concur_id_func = self.concur_id_func
         elif self.concur_id_func:
             raise CoverageException(
-                "Can't support concurrency=%s with %s, only threads are supported" % (
+                "Can't support concurrency={} with {}, only threads are supported".format(
                     self.concurrency, self.tracer_name(),
                 )
             )
@@ -318,8 +320,8 @@
             (frame, event, arg), lineno = args
             try:
                 fn(frame, event, arg, lineno=lineno)
-            except TypeError:
-                raise Exception("fullcoverage must be run with the C trace function.")
+            except TypeError as ex:
+                raise Exception("fullcoverage must be run with the C trace function.") from ex
 
         # Install our installation tracer in threading, to jump-start other
         # threads.
@@ -332,9 +334,9 @@
         if self._collectors[-1] is not self:
             print("self._collectors:")
             for c in self._collectors:
-                print("  {!r}\n{}".format(c, c.origin))
+                print(f"  {c!r}\n{c.origin}")
         assert self._collectors[-1] is self, (
-            "Expected current collector to be %r, but it's %r" % (self, self._collectors[-1])
+            f"Expected current collector to be {self!r}, but it's {self._collectors[-1]!r}"
         )
 
         self.pause()
@@ -352,8 +354,8 @@
             stats = tracer.get_stats()
             if stats:
                 print("\nCoverage.py tracer stats:")
-                for k in sorted(stats.keys()):
-                    print("%20s: %s" % (k, stats[k]))
+                for k in human_sorted(stats.keys()):
+                    print(f"{k:>20}: {stats[k]}")
         if self.threading:
             self.threading.settrace(None)
 
@@ -390,7 +392,7 @@
         file_tracer = disposition.file_tracer
         plugin = file_tracer._coverage_plugin
         plugin_name = plugin._coverage_plugin_name
-        self.warn("Disabling plug-in {!r} due to previous exception".format(plugin_name))
+        self.warn(f"Disabling plug-in {plugin_name!r} due to previous exception")
         plugin._coverage_enabled = False
         disposition.trace = False
 
@@ -404,22 +406,22 @@
 
     def mapped_file_dict(self, d):
         """Return a dict like d, but with keys modified by file_mapper."""
-        # The call to litems() ensures that the GIL protects the dictionary
+        # The call to list(items()) ensures that the GIL protects the dictionary
         # iterator against concurrent modifications by tracers running
         # in other threads. We try three times in case of concurrent
         # access, hoping to get a clean copy.
         runtime_err = None
-        for _ in range(3):
+        for _ in range(3):                      # pragma: part covered
             try:
-                items = litems(d)
-            except RuntimeError as ex:
+                items = list(d.items())
+            except RuntimeError as ex:          # pragma: cant happen
                 runtime_err = ex
             else:
                 break
         else:
-            raise runtime_err
+            raise runtime_err                   # pragma: cant happen
 
-        return dict((self.cached_mapped_file(k), v) for k, v in items if v)
+        return {self.cached_mapped_file(k): v for k, v in items if v}
 
     def plugin_was_disabled(self, plugin):
         """Record that `plugin` was disabled during the run."""
@@ -437,7 +439,25 @@
             return False
 
         if self.branch:
-            self.covdata.add_arcs(self.mapped_file_dict(self.data))
+            if self.packed_arcs:
+                # Unpack the line number pairs packed into integers.  See
+                # tracer.c:CTracer_record_pair for the C code that creates
+                # these packed ints.
+                data = {}
+                for fname, packeds in self.data.items():
+                    tuples = []
+                    for packed in packeds:
+                        l1 = packed & 0xFFFFF
+                        l2 = (packed & (0xFFFFF << 20)) >> 20
+                        if packed & (1 << 40):
+                            l1 *= -1
+                        if packed & (1 << 41):
+                            l2 *= -1
+                        tuples.append((l1, l2))
+                    data[fname] = tuples
+            else:
+                data = self.data
+            self.covdata.add_arcs(self.mapped_file_dict(data))
         else:
             self.covdata.add_lines(self.mapped_file_dict(self.data))
 
--- a/eric7/DebugClients/Python/coverage/config.py	Fri Nov 19 19:28:47 2021 +0100
+++ b/eric7/DebugClients/Python/coverage/config.py	Sat Nov 20 16:47:38 2021 +0100
@@ -4,15 +4,14 @@
 """Config file for coverage.py"""
 
 import collections
+import configparser
 import copy
 import os
 import os.path
 import re
 
-from coverage import env
-from coverage.backward import configparser, iitems, string_class
-from coverage.misc import contract, CoverageException, isolate_module
-from coverage.misc import substitute_variables
+from coverage.exceptions import CoverageException
+from coverage.misc import contract, isolate_module, substitute_variables
 
 from coverage.tomlconfig import TomlConfigParser, TomlDecodeError
 
@@ -35,12 +34,9 @@
         if our_file:
             self.section_prefixes.append("")
 
-    def read(self, filenames, encoding=None):
+    def read(self, filenames, encoding_unused=None):
         """Read a file name as UTF-8 configuration data."""
-        kwargs = {}
-        if env.PYVERSION >= (3, 2):
-            kwargs['encoding'] = encoding or "utf-8"
-        return configparser.RawConfigParser.read(self, filenames, **kwargs)
+        return configparser.RawConfigParser.read(self, filenames, encoding="utf-8")
 
     def has_option(self, section, option):
         for section_prefix in self.section_prefixes:
@@ -128,8 +124,8 @@
                 re.compile(value)
             except re.error as e:
                 raise CoverageException(
-                    "Invalid [%s].%s value %r: %s" % (section, option, value, e)
-                )
+                    f"Invalid [{section}].{option} value {value!r}: {e}"
+                ) from e
             if value:
                 value_list.append(value)
         return value_list
@@ -154,7 +150,7 @@
 ]
 
 
-class CoverageConfig(object):
+class CoverageConfig:
     """Coverage.py configuration.
 
     The attributes of this class are the various settings that control the
@@ -245,14 +241,14 @@
 
     def from_args(self, **kwargs):
         """Read config values from `kwargs`."""
-        for k, v in iitems(kwargs):
+        for k, v in kwargs.items():
             if v is not None:
-                if k in self.MUST_BE_LIST and isinstance(v, string_class):
+                if k in self.MUST_BE_LIST and isinstance(v, str):
                     v = [v]
                 setattr(self, k, v)
 
     @contract(filename=str)
-    def from_file(self, filename, our_file):
+    def from_file(self, filename, warn, our_file):
         """Read configuration from a .rc file.
 
         `filename` is a file name to read.
@@ -276,7 +272,7 @@
         try:
             files_read = cp.read(filename)
         except (configparser.Error, TomlDecodeError) as err:
-            raise CoverageException("Couldn't read config file %s: %s" % (filename, err))
+            raise CoverageException(f"Couldn't read config file {filename}: {err}") from err
         if not files_read:
             return False
 
@@ -289,7 +285,7 @@
                 if was_set:
                     any_set = True
         except ValueError as err:
-            raise CoverageException("Couldn't read config file %s: %s" % (filename, err))
+            raise CoverageException(f"Couldn't read config file {filename}: {err}") from err
 
         # Check that there are no unrecognized options.
         all_options = collections.defaultdict(set)
@@ -297,12 +293,12 @@
             section, option = option_spec[1].split(":")
             all_options[section].add(option)
 
-        for section, options in iitems(all_options):
+        for section, options in all_options.items():
             real_section = cp.has_section(section)
             if real_section:
                 for unknown in set(cp.options(section)) - options:
-                    raise CoverageException(
-                        "Unrecognized option '[%s] %s=' in config file %s" % (
+                    warn(
+                        "Unrecognized option '[{}] {}=' in config file {}".format(
                             real_section, unknown, filename
                         )
                     )
@@ -447,7 +443,7 @@
             return
 
         # If we get here, we didn't find the option.
-        raise CoverageException("No such option: %r" % option_name)
+        raise CoverageException(f"No such option: {option_name!r}")
 
     def get_option(self, option_name):
         """Get an option from the configuration.
@@ -475,7 +471,7 @@
             return self.plugin_options.get(plugin_name, {}).get(key)
 
         # If we get here, we didn't find the option.
-        raise CoverageException("No such option: %r" % option_name)
+        raise CoverageException(f"No such option: {option_name!r}")
 
     def post_process_file(self, path):
         """Make final adjustments to a file path to make it usable."""
@@ -521,12 +517,13 @@
     return files_to_try
 
 
-def read_coverage_config(config_file, **kwargs):
+def read_coverage_config(config_file, warn, **kwargs):
     """Read the coverage.py configuration.
 
     Arguments:
         config_file: a boolean or string, see the `Coverage` class for the
             tricky details.
+        warn: a function to issue warnings.
         all others: keyword arguments from the `Coverage` class, used for
             setting values in the configuration.
 
@@ -545,11 +542,11 @@
         files_to_try = config_files_to_try(config_file)
 
         for fname, our_file, specified_file in files_to_try:
-            config_read = config.from_file(fname, our_file=our_file)
+            config_read = config.from_file(fname, warn, our_file=our_file)
             if config_read:
                 break
             if specified_file:
-                raise CoverageException("Couldn't read '%s' as a config file" % fname)
+                raise CoverageException(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/context.py	Fri Nov 19 19:28:47 2021 +0100
+++ b/eric7/DebugClients/Python/coverage/context.py	Sat Nov 20 16:47:38 2021 +0100
@@ -48,44 +48,18 @@
     fname = co.co_name
     method = None
     if co.co_argcount and co.co_varnames[0] == "self":
-        self = frame.f_locals["self"]
+        self = frame.f_locals.get("self", None)
         method = getattr(self, fname, None)
 
     if method is None:
         func = frame.f_globals.get(fname)
         if func is None:
             return None
-        return func.__module__ + '.' + fname
+        return func.__module__ + "." + fname
 
-    func = getattr(method, '__func__', None)
+    func = getattr(method, "__func__", None)
     if func is None:
         cls = self.__class__
-        return cls.__module__ + '.' + cls.__name__ + "." + fname
+        return cls.__module__ + "." + cls.__name__ + "." + fname
 
-    if hasattr(func, '__qualname__'):
-        qname = func.__module__ + '.' + func.__qualname__
-    else:
-        for cls in getattr(self.__class__, '__mro__', ()):
-            f = cls.__dict__.get(fname, None)
-            if f is None:
-                continue
-            if f is func:
-                qname = cls.__module__ + '.' + cls.__name__ + "." + fname
-                break
-        else:
-            # Support for old-style classes.
-            def mro(bases):
-                for base in bases:
-                    f = base.__dict__.get(fname, None)
-                    if f is func:
-                        return base.__module__ + '.' + base.__name__ + "." + fname
-                for base in bases:
-                    qname = mro(base.__bases__)
-                    if qname is not None:
-                        return qname
-                return None
-            qname = mro([self.__class__])
-            if qname is None:
-                qname = func.__module__ + '.' + fname
-
-    return qname
+    return func.__module__ + "." + func.__qualname__
--- a/eric7/DebugClients/Python/coverage/control.py	Fri Nov 19 19:28:47 2021 +0100
+++ b/eric7/DebugClients/Python/coverage/control.py	Sat Nov 20 16:47:38 2021 +0100
@@ -11,27 +11,28 @@
 import platform
 import sys
 import time
+import warnings
 
 from coverage import env
 from coverage.annotate import AnnotateReporter
-from coverage.backward import string_class, iitems
 from coverage.collector import Collector, CTracer
 from coverage.config import read_coverage_config
 from coverage.context import should_start_context_test_function, combine_context_switchers
 from coverage.data import CoverageData, combine_parallel_data
 from coverage.debug import DebugControl, short_stack, write_formatted_info
 from coverage.disposition import disposition_debug_msg
+from coverage.exceptions import CoverageException, CoverageWarning
 from coverage.files import PathAliases, abs_file, relative_filename, set_relative_directory
 from coverage.html import HtmlReporter
 from coverage.inorout import InOrOut
 from coverage.jsonreport import JsonReporter
-from coverage.misc import CoverageException, bool_or_none, join_regex
+from coverage.misc import bool_or_none, join_regex, human_sorted, human_sorted_items
 from coverage.misc import DefaultValue, ensure_dir_for_file, isolate_module
 from coverage.plugin import FileReporter
 from coverage.plugin_support import Plugins
 from coverage.python import PythonFileReporter
 from coverage.report import render_report
-from coverage.results import Analysis, Numbers
+from coverage.results import Analysis
 from coverage.summary import SummaryReporter
 from coverage.xmlreport import XmlReporter
 
@@ -61,7 +62,7 @@
 
 _DEFAULT_DATAFILE = DefaultValue("MISSING")
 
-class Coverage(object):
+class Coverage:
     """Programmatic access to coverage.py.
 
     To use::
@@ -102,6 +103,7 @@
         auto_data=False, timid=None, branch=None, config_file=True,
         source=None, source_pkgs=None, omit=None, include=None, debug=None,
         concurrency=None, check_preimported=False, context=None,
+        messages=False,
     ):  # pylint: disable=too-many-arguments
         """
         Many of these arguments duplicate and override values that can be
@@ -172,6 +174,9 @@
         `context` is a string to use as the :ref:`static context
         <static_contexts>` label for collected data.
 
+        If `messages` is true, some messages will be printed to stdout
+        indicating what is happening.
+
         .. versionadded:: 4.0
             The `concurrency` parameter.
 
@@ -184,6 +189,9 @@
         .. versionadded:: 5.3
             The `source_pkgs` parameter.
 
+        .. versionadded:: 6.0
+            The `messages` parameter.
+
         """
         # data_file=None means no disk file at all. data_file missing means
         # use the value from the config file.
@@ -191,15 +199,7 @@
         if data_file is _DEFAULT_DATAFILE:
             data_file = None
 
-        # Build our configuration from a number of sources.
-        self.config = read_coverage_config(
-            config_file=config_file,
-            data_file=data_file, cover_pylib=cover_pylib, timid=timid,
-            branch=branch, parallel=bool_or_none(data_suffix),
-            source=source, source_pkgs=source_pkgs, run_omit=omit, run_include=include, debug=debug,
-            report_omit=omit, report_include=include,
-            concurrency=concurrency, context=context,
-            )
+        self.config = None
 
         # This is injectable by tests.
         self._debug_file = None
@@ -212,6 +212,7 @@
         self._warn_unimported_source = True
         self._warn_preimported_source = check_preimported
         self._no_warn_slugs = None
+        self._messages = messages
 
         # A record of all the warnings that have been issued.
         self._warnings = []
@@ -234,6 +235,16 @@
         # Should we write the debug output?
         self._should_write_debug = True
 
+        # Build our configuration from a number of sources.
+        self.config = read_coverage_config(
+            config_file=config_file, warn=self._warn,
+            data_file=data_file, cover_pylib=cover_pylib, timid=timid,
+            branch=branch, parallel=bool_or_none(data_suffix),
+            source=source, source_pkgs=source_pkgs, run_omit=omit, run_include=include, debug=debug,
+            report_omit=omit, report_include=include,
+            concurrency=concurrency, context=context,
+            )
+
         # If we have sub-process measurement happening automatically, then we
         # want any explicit creation of a Coverage object to mean, this process
         # is already coverage-aware, so don't auto-measure it.  By now, the
@@ -291,14 +302,14 @@
         # '[run] _crash' will raise an exception if the value is close by in
         # the call stack, for testing error handling.
         if self.config._crash and self.config._crash in short_stack(limit=4):
-            raise Exception("Crashing because called by {}".format(self.config._crash))
+            raise Exception(f"Crashing because called by {self.config._crash}")
 
     def _write_startup_debug(self):
         """Write out debug info at startup if needed."""
         wrote_any = False
         with self._debug.without_callers():
             if self._debug.should('config'):
-                config_info = sorted(self.config.__dict__.items())
+                config_info = human_sorted_items(self.config.__dict__.items())
                 config_info = [(k, v) for k, v in config_info if not k.startswith('_')]
                 write_formatted_info(self._debug, "config", config_info)
                 wrote_any = True
@@ -334,9 +345,9 @@
         reason = self._inorout.check_include_omit_etc(filename, frame)
         if self._debug.should('trace'):
             if not reason:
-                msg = "Including %r" % (filename,)
+                msg = f"Including {filename!r}"
             else:
-                msg = "Not including %r: %s" % (filename, reason)
+                msg = f"Not including {filename!r}: {reason}"
             self._debug.write(msg)
 
         return not reason
@@ -351,22 +362,29 @@
 
         """
         if self._no_warn_slugs is None:
-            self._no_warn_slugs = list(self.config.disable_warnings)
+            if self.config is not None:
+                self._no_warn_slugs = list(self.config.disable_warnings)
 
-        if slug in self._no_warn_slugs:
-            # Don't issue the warning
-            return
+        if self._no_warn_slugs is not None:
+            if slug in self._no_warn_slugs:
+                # Don't issue the warning
+                return
 
         self._warnings.append(msg)
         if slug:
-            msg = "%s (%s)" % (msg, slug)
-        if self._debug.should('pid'):
-            msg = "[%d] %s" % (os.getpid(), msg)
-        sys.stderr.write("Coverage.py warning: %s\n" % msg)
+            msg = f"{msg} ({slug})"
+        if self._debug is not None and self._debug.should('pid'):
+            msg = f"[{os.getpid()}] {msg}"
+        warnings.warn(msg, category=CoverageWarning, stacklevel=2)
 
         if once:
             self._no_warn_slugs.append(slug)
 
+    def _message(self, msg):
+        """Write a message to the user, if configured to do so."""
+        if self._messages:
+            print(msg)
+
     def get_option(self, option_name):
         """Get an option from the configuration.
 
@@ -442,9 +460,7 @@
         elif dycon == "test_function":
             context_switchers = [should_start_context_test_function]
         else:
-            raise CoverageException(
-                "Don't understand dynamic_context setting: {!r}".format(dycon)
-            )
+            raise CoverageException(f"Don't understand dynamic_context setting: {dycon!r}")
 
         context_switchers.extend(
             plugin.dynamic_context for plugin in self._plugins.context_switchers
@@ -465,7 +481,7 @@
 
         suffix = self._data_suffix_specified
         if suffix or self.config.parallel:
-            if not isinstance(suffix, string_class):
+            if not isinstance(suffix, str):
                 # if data_suffix=True, use .machinename.pid.random
                 suffix = True
         else:
@@ -478,7 +494,7 @@
         # Early warning if we aren't going to be able to support plugins.
         if self._plugins.file_tracers and not self._collector.supports_plugins:
             self._warn(
-                "Plugin file tracers (%s) aren't supported with %s" % (
+                "Plugin file tracers ({}) aren't supported with {}".format(
                     ", ".join(
                         plugin._coverage_plugin_name
                             for plugin in self._plugins.file_tracers
@@ -562,7 +578,7 @@
     def _atexit(self):
         """Clean up on process shutdown."""
         if self._debug.should("process"):
-            self._debug.write("atexit: pid: {}, instance: {!r}".format(os.getpid(), self))
+            self._debug.write(f"atexit: pid: {os.getpid()}, instance: {self!r}")
         if self._started:
             self.stop()
         if self._auto_save:
@@ -598,9 +614,7 @@
 
         """
         if not self._started:                           # pragma: part started
-            raise CoverageException(
-                "Cannot switch context, coverage is not started"
-                )
+            raise CoverageException("Cannot switch context, coverage is not started")
 
         if self._collector.should_start_context:
             self._warn("Conflicting dynamic contexts", slug="dynamic-conflict", once=True)
@@ -692,7 +706,7 @@
 
         aliases = None
         if self.config.paths:
-            aliases = PathAliases()
+            aliases = PathAliases(relative=self.config.relative_files)
             for paths in self.config.paths.values():
                 result = paths[0]
                 for pattern in paths[1:]:
@@ -704,6 +718,7 @@
             data_paths=data_paths,
             strict=strict,
             keep=keep,
+            message=self._message,
         )
 
     def get_data(self):
@@ -798,21 +813,20 @@
         """
         # All reporting comes through here, so do reporting initialization.
         self._init()
-        Numbers.set_precision(self.config.precision)
         self._post_init()
 
         data = self.get_data()
         if not isinstance(it, FileReporter):
             it = self._get_file_reporter(it)
 
-        return Analysis(data, it, self._file_mapper)
+        return Analysis(data, self.config.precision, it, self._file_mapper)
 
     def _get_file_reporter(self, morf):
         """Get a FileReporter for a module or file name."""
         plugin = None
         file_reporter = "python"
 
-        if isinstance(morf, string_class):
+        if isinstance(morf, str):
             mapped_morf = self._file_mapper(morf)
             plugin_name = self._data.file_tracer(mapped_morf)
             if plugin_name:
@@ -822,7 +836,7 @@
                     file_reporter = plugin.file_reporter(mapped_morf)
                     if file_reporter is None:
                         raise CoverageException(
-                            "Plugin %r did not provide a file reporter for %r." % (
+                            "Plugin {!r} did not provide a file reporter for {!r}.".format(
                                 plugin._coverage_plugin_name, morf
                             )
                         )
@@ -918,6 +932,11 @@
     ):
         """Annotate a list of modules.
 
+        .. note::
+           This method has been obsoleted by more modern reporting tools,
+           including the :meth:`html_report` method.  It will be removed in a
+           future version.
+
         Each module in `morfs` is annotated.  The source is written to a new
         file, named with a ",cover" suffix, with each line prefixed with a
         marker to indicate the coverage of the line.  Covered lines have ">",
@@ -926,6 +945,9 @@
         See :meth:`report` for other arguments.
 
         """
+        print("The annotate command will be removed in a future version.")
+        print("Get in touch if you still use it: ned@nedbatchelder.com")
+
         with override_config(self,
             ignore_errors=ignore_errors, report_omit=omit,
             report_include=include, report_contexts=contexts,
@@ -969,7 +991,8 @@
             html_skip_empty=skip_empty, precision=precision,
         ):
             reporter = HtmlReporter(self)
-            return reporter.report(morfs)
+            ret = reporter.report(morfs)
+            return ret
 
     def xml_report(
         self, morfs=None, outfile=None, ignore_errors=None,
@@ -991,7 +1014,7 @@
             ignore_errors=ignore_errors, report_omit=omit, report_include=include,
             xml_output=outfile, report_contexts=contexts, skip_empty=skip_empty,
         ):
-            return render_report(self.config.xml_output, XmlReporter(self), morfs)
+            return render_report(self.config.xml_output, XmlReporter(self), morfs, self._message)
 
     def json_report(
         self, morfs=None, outfile=None, ignore_errors=None,
@@ -1015,7 +1038,7 @@
             json_output=outfile, report_contexts=contexts, json_pretty_print=pretty_print,
             json_show_contexts=show_contexts
         ):
-            return render_report(self.config.json_output, JsonReporter(self), morfs)
+            return render_report(self.config.json_output, JsonReporter(self), morfs, self._message)
 
     def sys_info(self):
         """Return a list of (key, value) pairs showing internal information."""
@@ -1036,8 +1059,8 @@
             return entries
 
         info = [
-            ('version', covmod.__version__),
-            ('coverage', covmod.__file__),
+            ('coverage_version', covmod.__version__),
+            ('coverage_module', covmod.__file__),
             ('tracer', self._collector.tracer_name() if self._collector else "-none-"),
             ('CTracer', 'available' if CTracer else "unavailable"),
             ('plugins.file_tracers', plugin_info(self._plugins.file_tracers)),
@@ -1061,10 +1084,13 @@
             ('pid', os.getpid()),
             ('cwd', os.getcwd()),
             ('path', sys.path),
-            ('environment', sorted(
-                ("%s = %s" % (k, v))
-                for k, v in iitems(os.environ)
-                if any(slug in k for slug in ("COV", "PY"))
+            ('environment', human_sorted(
+                f"{k} = {v}"
+                for k, v in os.environ.items()
+                if (
+                    any(slug in k for slug in ("COV", "PY")) or
+                    (k in ("HOME", "TEMP", "TMP"))
+                )
             )),
             ('command_line', " ".join(getattr(sys, 'argv', ['-none-']))),
             ]
--- a/eric7/DebugClients/Python/coverage/data.py	Fri Nov 19 19:28:47 2021 +0100
+++ b/eric7/DebugClients/Python/coverage/data.py	Sat Nov 20 16:47:38 2021 +0100
@@ -13,7 +13,8 @@
 import glob
 import os.path
 
-from coverage.misc import CoverageException, file_be_gone
+from coverage.exceptions import CoverageException
+from coverage.misc import file_be_gone
 from coverage.sqldata import CoverageData
 
 
@@ -52,7 +53,9 @@
     hasher.update(data.file_tracer(filename))
 
 
-def combine_parallel_data(data, aliases=None, data_paths=None, strict=False, keep=False):
+def combine_parallel_data(
+    data, aliases=None, data_paths=None, strict=False, keep=False, message=None,
+):
     """Combine a number of data files together.
 
     Treat `data.filename` as a file prefix, and combine the data from all
@@ -90,7 +93,7 @@
             pattern = os.path.join(os.path.abspath(p), localdot)
             files_to_combine.extend(glob.glob(pattern))
         else:
-            raise CoverageException("Couldn't combine from non-existent path '%s'" % (p,))
+            raise CoverageException(f"Couldn't combine from non-existent path '{p}'")
 
     if strict and not files_to_combine:
         raise CoverageException("No data to combine")
@@ -101,10 +104,10 @@
             # Sometimes we are combining into a file which is one of the
             # parallel files.  Skip that file.
             if data._debug.should('dataio'):
-                data._debug.write("Skipping combining ourself: %r" % (f,))
+                data._debug.write(f"Skipping combining ourself: {f!r}")
             continue
         if data._debug.should('dataio'):
-            data._debug.write("Combining data file %r" % (f,))
+            data._debug.write(f"Combining data file {f!r}")
         try:
             new_data = CoverageData(f, debug=data._debug)
             new_data.read()
@@ -116,9 +119,11 @@
         else:
             data.update(new_data, aliases=aliases)
             files_combined += 1
+            if message:
+                message(f"Combined data file {os.path.relpath(f)}")
             if not keep:
                 if data._debug.should('dataio'):
-                    data._debug.write("Deleting combined data file %r" % (f,))
+                    data._debug.write(f"Deleting combined data file {f!r}")
                 file_be_gone(f)
 
     if strict and not files_combined:
--- a/eric7/DebugClients/Python/coverage/debug.py	Fri Nov 19 19:28:47 2021 +0100
+++ b/eric7/DebugClients/Python/coverage/debug.py	Sat Nov 20 16:47:38 2021 +0100
@@ -6,16 +6,14 @@
 import contextlib
 import functools
 import inspect
+import io
 import itertools
 import os
 import pprint
+import reprlib
 import sys
-try:
-    import _thread
-except ImportError:
-    import thread as _thread
+import _thread
 
-from coverage.backward import reprlib, StringIO
 from coverage.misc import isolate_module
 
 os = isolate_module(os)
@@ -28,7 +26,7 @@
 FORCED_DEBUG_FILE = None
 
 
-class DebugControl(object):
+class DebugControl:
     """Control and output for debugging."""
 
     show_repr_attr = False      # For SimpleReprMixin
@@ -49,7 +47,7 @@
         self.raw_output = self.output.outfile
 
     def __repr__(self):
-        return "<DebugControl options=%r raw_output=%r>" % (self.options, self.raw_output)
+        return f"<DebugControl options={self.options!r} raw_output={self.raw_output!r}>"
 
     def should(self, option):
         """Decide whether to output debug information in category `option`."""
@@ -77,7 +75,7 @@
         if self.should('self'):
             caller_self = inspect.stack()[1][0].f_locals.get('self')
             if caller_self is not None:
-                self.output.write("self: {!r}\n".format(caller_self))
+                self.output.write(f"self: {caller_self!r}\n")
         if self.should('callers'):
             dump_stack_frames(out=self.output, skip=1)
         self.output.flush()
@@ -86,14 +84,14 @@
 class DebugControlString(DebugControl):
     """A `DebugControl` that writes to a StringIO, for testing."""
     def __init__(self, options):
-        super(DebugControlString, self).__init__(options, StringIO())
+        super().__init__(options, io.StringIO())
 
     def get_output(self):
         """Get the output text from the `DebugControl`."""
         return self.raw_output.getvalue()
 
 
-class NoDebugging(object):
+class NoDebugging:
     """A replacement for DebugControl that will never try to do anything."""
     def should(self, option):               # pylint: disable=unused-argument
         """Should we write debug messages?  Never."""
@@ -183,12 +181,12 @@
 def add_pid_and_tid(text):
     """A filter to add pid and tid to debug messages."""
     # Thread ids are useful, but too long. Make a shorter one.
-    tid = "{:04x}".format(short_id(_thread.get_ident()))
-    text = "{:5d}.{}: {}".format(os.getpid(), tid, text)
+    tid = f"{short_id(_thread.get_ident()):04x}"
+    text = f"{os.getpid():5d}.{tid}: {text}"
     return text
 
 
-class SimpleReprMixin(object):
+class SimpleReprMixin:
     """A mixin implementing a simple __repr__."""
     simple_repr_ignore = ['simple_repr_ignore', '$coverage.object_id']
 
@@ -202,7 +200,7 @@
         return "<{klass} @0x{id:x} {attrs}>".format(
             klass=self.__class__.__name__,
             id=id(self),
-            attrs=" ".join("{}={!r}".format(k, v) for k, v in show_attrs),
+            attrs=" ".join(f"{k}={v!r}" for k, v in show_attrs),
             )
 
 
@@ -245,7 +243,7 @@
     return text + ending
 
 
-class CwdTracker(object):                                   # pragma: debugging
+class CwdTracker:                                   # pragma: debugging
     """A class to add cwd info to debug messages."""
     def __init__(self):
         self.cwd = None
@@ -254,12 +252,12 @@
         """Add a cwd message for each new cwd."""
         cwd = os.getcwd()
         if cwd != self.cwd:
-            text = "cwd is now {!r}\n".format(cwd) + text
+            text = f"cwd is now {cwd!r}\n" + text
             self.cwd = cwd
         return text
 
 
-class DebugOutputFile(object):                              # pragma: debugging
+class DebugOutputFile:                              # pragma: debugging
     """A file-like object that includes pid and cwd information."""
     def __init__(self, outfile, show_process, filters):
         self.outfile = outfile
@@ -268,10 +266,10 @@
 
         if self.show_process:
             self.filters.insert(0, CwdTracker().filter)
-            self.write("New process: executable: %r\n" % (sys.executable,))
-            self.write("New process: cmd: %r\n" % (getattr(sys, 'argv', None),))
+            self.write(f"New process: executable: {sys.executable!r}\n")
+            self.write("New process: cmd: {!r}\n".format(getattr(sys, 'argv', None)))
             if hasattr(os, 'getppid'):
-                self.write("New process: pid: %r, parent pid: %r\n" % (os.getpid(), os.getppid()))
+                self.write(f"New process: pid: {os.getpid()!r}, parent pid: {os.getppid()!r}\n")
 
     SYS_MOD_NAME = '$coverage.debug.DebugOutputFile.the_one'
 
@@ -306,7 +304,9 @@
         if the_one is None or is_interim:
             if fileobj is None:
                 debug_file_name = os.environ.get("COVERAGE_DEBUG_FILE", FORCED_DEBUG_FILE)
-                if debug_file_name:
+                if debug_file_name in ("stdout", "stderr"):
+                    fileobj = getattr(sys, debug_file_name)
+                elif debug_file_name:
                     fileobj = open(debug_file_name, "a")
                 else:
                     fileobj = sys.stderr
@@ -370,7 +370,7 @@
         def _wrapper(self, *args, **kwargs):
             oid = getattr(self, OBJ_ID_ATTR, None)
             if oid is None:
-                oid = "{:08d} {:04d}".format(os.getpid(), next(OBJ_IDS))
+                oid = f"{os.getpid():08d} {next(OBJ_IDS):04d}"
                 setattr(self, OBJ_ID_ATTR, oid)
             extra = ""
             if show_args:
@@ -386,11 +386,11 @@
                 extra += " @ "
                 extra += "; ".join(_clean_stack_line(l) for l in short_stack().splitlines())
             callid = next(CALLS)
-            msg = "{} {:04d} {}{}\n".format(oid, callid, func.__name__, extra)
+            msg = f"{oid} {callid:04d} {func.__name__}{extra}\n"
             DebugOutputFile.get_one(interim=True).write(msg)
             ret = func(self, *args, **kwargs)
             if show_return:
-                msg = "{} {:04d} {} return {!r}\n".format(oid, callid, func.__name__, ret)
+                msg = f"{oid} {callid:04d} {func.__name__} return {ret!r}\n"
                 DebugOutputFile.get_one(interim=True).write(msg)
             return ret
         return _wrapper
--- a/eric7/DebugClients/Python/coverage/disposition.py	Fri Nov 19 19:28:47 2021 +0100
+++ b/eric7/DebugClients/Python/coverage/disposition.py	Sat Nov 20 16:47:38 2021 +0100
@@ -4,7 +4,7 @@
 """Simple value objects for tracking what to do with files."""
 
 
-class FileDisposition(object):
+class FileDisposition:
     """A simple value type for recording what to do with a file."""
     pass
 
@@ -29,9 +29,11 @@
 def disposition_debug_msg(disp):
     """Make a nice debug message of what the FileDisposition is doing."""
     if disp.trace:
-        msg = "Tracing %r" % (disp.original_filename,)
+        msg = f"Tracing {disp.original_filename!r}"
+        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
     else:
-        msg = "Not tracing %r: %s" % (disp.original_filename, disp.reason)
+        msg = f"Not tracing {disp.original_filename!r}: {disp.reason}"
     return msg
--- a/eric7/DebugClients/Python/coverage/doc/CHANGES.rst	Fri Nov 19 19:28:47 2021 +0100
+++ b/eric7/DebugClients/Python/coverage/doc/CHANGES.rst	Sat Nov 20 16:47:38 2021 +0100
@@ -9,22 +9,295 @@
 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.  If you
-want to know what's different in 5.0 since 4.5.x, see :ref:`whatsnew5x`.
-
+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:
     ..
     ..  .. _changes_981:
     ..
-    ..  Version 9.8.1 --- 2027-07-27
-    ..  ----------------------------
+    ..  Version 9.8.1 — 2027-07-27
+    ..  --------------------------
+
+.. _changes_612:
+
+Version 6.1.2 — 2021-11-10
+--------------------------
+
+- Python 3.11 is supported (tested with 3.11.0a2).  One still-open issue has to
+  do with `exits through with-statements <issue 1270_>`_.
+
+- Fix: When remapping file paths through the ``[paths]`` setting while
+  combining, the ``[run] relative_files`` setting was ignored, resulting in
+  absolute paths for remapped file names (`issue 1147`_).  This is now fixed.
+
+- Fix: Complex conditionals over excluded lines could have incorrectly reported
+  a missing branch (`issue 1271`_). This is now fixed.
+
+- Fix: More exceptions are now handled when trying to parse source files for
+  reporting.  Problems that used to terminate coverage.py can now be handled
+  with ``[report] ignore_errors``.  This helps with plugins failing to read
+  files (`django_coverage_plugin issue 78`_).
+
+- Fix: Removed another vestige of jQuery from the source tarball
+  (`issue 840`_).
+
+- Fix: Added a default value for a new-to-6.x argument of an internal class.
+  This unsupported class is being used by coveralls (`issue 1273`_). Although
+  I'd rather not "fix" unsupported interfaces, it's actually nicer with a
+  default value.
+
+.. _django_coverage_plugin issue 78: https://github.com/nedbat/django_coverage_plugin/issues/78
+.. _issue 1147: https://github.com/nedbat/coveragepy/issues/1147
+.. _issue 1270: https://github.com/nedbat/coveragepy/issues/1270
+.. _issue 1271: https://github.com/nedbat/coveragepy/issues/1271
+.. _issue 1273: https://github.com/nedbat/coveragepy/issues/1273
+
+
+.. _changes_611:
+
+Version 6.1.1 — 2021-10-31
+--------------------------
+
+- Fix: The sticky header on the HTML report didn't work unless you had branch
+  coverage enabled. This is now fixed: the sticky header works for everyone.
+  (Do people still use coverage without branch measurement!? j/k)
+
+- Fix: When using explicitly declared namespace packages, the "already imported
+  a file that will be measured" warning would be issued (`issue 888`_).  This
+  is now fixed.
+
+.. _issue 888: https://github.com/nedbat/coveragepy/issues/888
+
+
+.. _changes_61:
+
+Version 6.1 — 2021-10-30
+------------------------
+
+- Deprecated: The ``annotate`` command and the ``Coverage.annotate`` function
+  will be removed in a future version, unless people let me know that they are
+  using it.  Instead, the ``html`` command gives better-looking (and more
+  accurate) output, and the ``report -m`` command will tell you line numbers of
+  missing lines.  Please get in touch if you have a reason to use ``annotate``
+  over those better options: ned@nedbatchelder.com.
+
+- Feature: Coverage now sets an environment variable, ``COVERAGE_RUN`` when
+  running your code with the ``coverage run`` command.  The value is not
+  important, and may change in the future.  Closes `issue 553`_.
+
+- Feature: The HTML report pages for Python source files now have a sticky
+  header so the file name and controls are always visible.
+
+- Feature: The ``xml`` and ``json`` commands now describe what they wrote
+  where.
+
+- Feature: The ``html``, ``combine``, ``xml``, and ``json`` commands all accept
+  a ``-q/--quiet`` option to suppress the messages they write to stdout about
+  what they are doing (`issue 1254`_).
+
+- Feature: The ``html`` command writes a ``.gitignore`` file into the HTML
+  output directory, to prevent the report from being committed to git.  If you
+  want to commit it, you will need to delete that file.  Closes `issue 1244`_.
+
+- Feature: Added support for PyPy 3.8.
+
+- Fix: More generated code is now excluded from measurement.  Code such as
+  `attrs`_ boilerplate, or doctest code, was being measured though the
+  synthetic line numbers meant they were never reported.  Once Cython was
+  involved though, the generated .so files were parsed as Python, raising
+  syntax errors, as reported in `issue 1160`_.  This is now fixed.
+
+- Fix: When sorting human-readable names, numeric components are sorted
+  correctly: file10.py will appear after file9.py.  This applies to file names,
+  module names, environment variables, and test contexts.
+
+- Performance: Branch coverage measurement is faster, though you might only
+  notice on code that is executed many times, such as long-running loops.
+
+- Build: jQuery is no longer used or vendored (`issue 840`_ and `issue 1118`_).
+  Huge thanks to Nils Kattenbeck (septatrix) for the conversion to vanilla
+  JavaScript in `pull request 1248`_.
+
+.. _issue 553: https://github.com/nedbat/coveragepy/issues/553
+.. _issue 840: https://github.com/nedbat/coveragepy/issues/840
+.. _issue 1118: https://github.com/nedbat/coveragepy/issues/1118
+.. _issue 1160: https://github.com/nedbat/coveragepy/issues/1160
+.. _issue 1244: https://github.com/nedbat/coveragepy/issues/1244
+.. _pull request 1248: https://github.com/nedbat/coveragepy/pull/1248
+.. _issue 1254: https://github.com/nedbat/coveragepy/issues/1254
+.. _attrs: https://www.attrs.org/
+
+
+.. _changes_602:
+
+Version 6.0.2 — 2021-10-11
+--------------------------
+
+- Namespace packages being measured weren't properly handled by the new code
+  that ignores third-party packages. If the namespace package was installed, it
+  was ignored as a third-party package.  That problem (`issue 1231`_) is now
+  fixed.
+
+- Packages named as "source packages" (with ``source``, or ``source_pkgs``, or
+  pytest-cov's ``--cov``) might have been only partially measured.  Their
+  top-level statements could be marked as unexecuted, because they were
+  imported by coverage.py before measurement began (`issue 1232`_).  This is
+  now fixed, but the package will be imported twice, once by coverage.py, then
+  again by your test suite.  This could cause problems if importing the package
+  has side effects.
+
+- The :meth:`.CoverageData.contexts_by_lineno` method was documented to return
+  a dict, but was returning a defaultdict.  Now it returns a plain dict.  It
+  also no longer returns negative numbered keys.
+
+.. _issue 1231: https://github.com/nedbat/coveragepy/issues/1231
+.. _issue 1232: https://github.com/nedbat/coveragepy/issues/1232
+
+
+.. _changes_601:
+
+Version 6.0.1 — 2021-10-06
+--------------------------
+
+- In 6.0, the coverage.py exceptions moved from coverage.misc to
+  coverage.exceptions. These exceptions are not part of the public supported
+  API, CoverageException is. But a number of other third-party packages were
+  importing the exceptions from coverage.misc, so they are now available from
+  there again (`issue 1226`_).
+
+- Changed an internal detail of how tomli is imported, so that tomli can use
+  coverage.py for their own test suite (`issue 1228`_).
+
+- Defend against an obscure possibility under code obfuscation, where a
+  function can have an argument called "self", but no local named "self"
+  (`pull request 1210`_).  Thanks, Ben Carlsson.
+
+.. _pull request 1210: https://github.com/nedbat/coveragepy/pull/1210
+.. _issue 1226: https://github.com/nedbat/coveragepy/issues/1226
+.. _issue 1228: https://github.com/nedbat/coveragepy/issues/1228
+
+
+.. _changes_60:
+
+Version 6.0 — 2021-10-03
+------------------------
+
+- The ``coverage html`` command now prints a message indicating where the HTML
+  report was written.  Fixes `issue 1195`_.
+
+- The ``coverage combine`` command now prints messages indicating each data
+  file being combined.  Fixes `issue 1105`_.
+
+- The HTML report now includes a sentence about skipped files due to
+  ``skip_covered`` or ``skip_empty`` settings.  Fixes `issue 1163`_.
+
+- Unrecognized options in the configuration file are no longer errors. They are
+  now warnings, to ease the use of coverage across versions.  Fixes `issue
+  1035`_.
+
+- Fix handling of exceptions through context managers in Python 3.10. A missing
+  exception is no longer considered a missing branch from the with statement.
+  Fixes `issue 1205`_.
+
+- Fix another rarer instance of "Error binding parameter 0 - probably
+  unsupported type." (`issue 1010`_).
+
+- Creating a directory for the coverage data file now is safer against
+  conflicts when two coverage runs happen simultaneously (`pull 1220`_).
+  Thanks, Clément Pit-Claudel.
+
+.. _issue 1035: https://github.com/nedbat/coveragepy/issues/1035
+.. _issue 1105: https://github.com/nedbat/coveragepy/issues/1105
+.. _issue 1163: https://github.com/nedbat/coveragepy/issues/1163
+.. _issue 1195: https://github.com/nedbat/coveragepy/issues/1195
+.. _issue 1205: https://github.com/nedbat/coveragepy/issues/1205
+.. _pull 1220: https://github.com/nedbat/coveragepy/pull/1220
+
+
+.. _changes_60b1:
+
+Version 6.0b1 — 2021-07-18
+--------------------------
+
+- Dropped support for Python 2.7, PyPy 2, and Python 3.5.
+
+- Added support for the Python 3.10 ``match/case`` syntax.
+
+- Data collection is now thread-safe.  There may have been rare instances of
+  exceptions raised in multi-threaded programs.
+
+- Plugins (like the `Django coverage plugin`_) were generating "Already
+  imported a file that will be measured" warnings about Django itself.  These
+  have been fixed, closing `issue 1150`_.
+
+- Warnings generated by coverage.py are now real Python warnings.
+
+- Using ``--fail-under=100`` with coverage near 100% could result in the
+  self-contradictory message :code:`total of 100 is less than fail-under=100`.
+  This bug (`issue 1168`_) is now fixed.
+
+- The ``COVERAGE_DEBUG_FILE`` environment variable now accepts ``stdout`` and
+  ``stderr`` to write to those destinations.
+
+- TOML parsing now uses the `tomli`_ library.
+
+- Some minor changes to usually invisible details of the HTML report:
+
+  - Use a modern hash algorithm when fingerprinting, for high-security
+    environments (`issue 1189`_).  When generating the HTML report, we save the
+    hash of the data, to avoid regenerating an unchanged HTML page. We used to
+    use MD5 to generate the hash, and now use SHA-3-256.  This was never a
+    security concern, but security scanners would notice the MD5 algorithm and
+    raise a false alarm.
+
+  - Change how report file names are generated, to avoid leading underscores
+    (`issue 1167`_), to avoid rare file name collisions (`issue 584`_), and to
+    avoid file names becoming too long (`issue 580`_).
+
+.. _Django coverage plugin: https://pypi.org/project/django-coverage-plugin/
+.. _issue 580: https://github.com/nedbat/coveragepy/issues/580
+.. _issue 584: https://github.com/nedbat/coveragepy/issues/584
+.. _issue 1150: https://github.com/nedbat/coveragepy/issues/1150
+.. _issue 1167: https://github.com/nedbat/coveragepy/issues/1167
+.. _issue 1168: https://github.com/nedbat/coveragepy/issues/1168
+.. _issue 1189: https://github.com/nedbat/coveragepy/issues/1189
+.. _tomli: https://pypi.org/project/tomli/
+
+
+.. _changes_56b1:
+
+Version 5.6b1 — 2021-04-13
+--------------------------
+
+Note: 5.6 final was never released. These changes are part of 6.0.
+
+- Third-party packages are now ignored in coverage reporting.  This solves a
+  few problems:
+
+  - Coverage will no longer report about other people's code (`issue 876`_).
+    This is true even when using ``--source=.`` with a venv in the current
+    directory.
+
+  - Coverage will no longer generate "Already imported a file that will be
+    measured" warnings about coverage itself (`issue 905`_).
+
+- The HTML report uses j/k to move up and down among the highlighted chunks of
+  code.  They used to highlight the current chunk, but 5.0 broke that behavior.
+  Now the highlighting is working again.
+
+- The JSON report now includes ``percent_covered_display``, a string with the
+  total percentage, rounded to the same number of decimal places as the other
+  reports' totals.
+
+.. _issue 876: https://github.com/nedbat/coveragepy/issues/876
+.. _issue 905: https://github.com/nedbat/coveragepy/issues/905
+
 
 .. _changes_55:
 
-Version 5.5 --- 2021-02-28
---------------------------
+Version 5.5 — 2021-02-28
+------------------------
 
 - ``coverage combine`` has a new option, ``--keep`` to keep the original data
   files after combining them.  The default is still to delete the files after
@@ -59,8 +332,8 @@
 
 .. _changes_54:
 
-Version 5.4 --- 2021-01-24
---------------------------
+Version 5.4 — 2021-01-24
+------------------------
 
 - The text report produced by ``coverage report`` now always outputs a TOTAL
   line, even if only one Python file is reported.  This makes regex parsing
@@ -94,8 +367,8 @@
 
 .. _changes_531:
 
-Version 5.3.1 --- 2020-12-19
-----------------------------
+Version 5.3.1 — 2020-12-19
+--------------------------
 
 - When using ``--source`` on a large source tree, v5.x was slower than previous
   versions.  This performance regression is now fixed, closing `issue 1037`_.
@@ -120,8 +393,8 @@
 
 .. _changes_53:
 
-Version 5.3 --- 2020-09-13
---------------------------
+Version 5.3 — 2020-09-13
+------------------------
 
 - The ``source`` setting has always been interpreted as either a file path or a
   module, depending on which existed.  If both interpretations were valid, it
@@ -137,2782 +410,11 @@
 .. _issue 1011: https://github.com/nedbat/coveragepy/issues/1011
 
 
-.. _changes_521:
-
-Version 5.2.1 --- 2020-07-23
-----------------------------
-
-- The dark mode HTML report still used light colors for the context listing,
-  making them unreadable (`issue 1009`_).  This is now fixed.
-
-- The time stamp on the HTML report now includes the time zone. Thanks, Xie
-  Yanbo (`pull request 960`_).
-
-.. _pull request 960: https://github.com/nedbat/coveragepy/pull/960
-.. _issue 1009: https://github.com/nedbat/coveragepy/issues/1009
-
-
-.. _changes_52:
-
-Version 5.2 --- 2020-07-05
---------------------------
-
-- The HTML report has been redesigned by Vince Salvino.  There is now a dark
-  mode, the code text is larger, and system sans serif fonts are used, in
-  addition to other small changes (`issue 858`_ and `pull request 931`_).
-
-- The ``coverage report`` and ``coverage html`` commands now accept a
-  ``--precision`` option to control the number of decimal points displayed.
-  Thanks, Teake Nutma (`pull request 982`_).
-
-- The ``coverage report`` and ``coverage html`` commands now accept a
-  ``--no-skip-covered`` option to negate ``--skip-covered``.  Thanks, Anthony
-  Sottile (`issue 779`_ and `pull request 932`_).
-
-- The ``--skip-empty`` option is now available for the XML report, closing
-  `issue 976`_.
-
-- The ``coverage report`` command now accepts a ``--sort`` option to specify
-  how to sort the results.  Thanks, Jerin Peter George (`pull request 1005`_).
-
-- If coverage fails due to the coverage total not reaching the ``--fail-under``
-  value, it will now print a message making the condition clear.  Thanks,
-  Naveen Yadav (`pull request 977`_).
-
-- TOML configuration files with non-ASCII characters would cause errors on
-  Windows (`issue 990`_).  This is now fixed.
-
-- The output of ``--debug=trace`` now includes information about how the
-  ``--source`` option is being interpreted, and the module names being
-  considered.
-
-.. _pull request 931: https://github.com/nedbat/coveragepy/pull/931
-.. _pull request 932: https://github.com/nedbat/coveragepy/pull/932
-.. _pull request 977: https://github.com/nedbat/coveragepy/pull/977
-.. _pull request 982: https://github.com/nedbat/coveragepy/pull/982
-.. _pull request 1005: https://github.com/nedbat/coveragepy/pull/1005
-.. _issue 779: https://github.com/nedbat/coveragepy/issues/779
-.. _issue 858: https://github.com/nedbat/coveragepy/issues/858
-.. _issue 976: https://github.com/nedbat/coveragepy/issues/976
-.. _issue 990: https://github.com/nedbat/coveragepy/issues/990
-
-
-.. _changes_51:
-
-Version 5.1 --- 2020-04-12
---------------------------
-
-- The JSON report now includes counts of covered and missing branches. Thanks,
-  Salvatore Zagaria.
-
-- On Python 3.8, try-finally-return reported wrong branch coverage with
-  decorated async functions (`issue 964`_).  This is now fixed. Thanks, Kjell
-  Braden.
-
-- The :meth:`~coverage.Coverage.get_option` and
-  :meth:`~coverage.Coverage.set_option` methods can now manipulate the
-  ``[paths]`` configuration setting.  Thanks to Bernát Gábor for the fix for
-  `issue 967`_.
-
-.. _issue 964: https://github.com/nedbat/coveragepy/issues/964
-.. _issue 967: https://github.com/nedbat/coveragepy/issues/967
-
-
-.. _changes_504:
-
-Version 5.0.4 --- 2020-03-16
-----------------------------
-
-- If using the ``[run] relative_files`` setting, the XML report will use
-  relative files in the ``<source>`` elements indicating the location of source
-  code.  Closes `issue 948`_.
-
-- The textual summary report could report missing lines with negative line
-  numbers on PyPy3 7.1 (`issue 943`_).  This is now fixed.
-
-- Windows wheels for Python 3.8 were incorrectly built, but are now fixed.
-  (`issue 949`_)
-
-- Updated Python 3.9 support to 3.9a4.
-
-- HTML reports couldn't be sorted if localStorage wasn't available. This is now
-  fixed: sorting works even though the sorting setting isn't retained. (`issue
-  944`_ and `pull request 945`_). Thanks, Abdeali Kothari.
-
-.. _issue 943: https://github.com/nedbat/coveragepy/issues/943
-.. _issue 944: https://github.com/nedbat/coveragepy/issues/944
-.. _pull request 945: https://github.com/nedbat/coveragepy/pull/945
-.. _issue 948: https://github.com/nedbat/coveragepy/issues/948
-.. _issue 949: https://github.com/nedbat/coveragepy/issues/949
-
-
-.. _changes_503:
-
-Version 5.0.3 --- 2020-01-12
-----------------------------
-
-- A performance improvement in 5.0.2 didn't work for test suites that changed
-  directory before combining data, causing "Couldn't use data file: no such
-  table: meta" errors (`issue 916`_).  This is now fixed.
-
-- Coverage could fail to run your program with some form of "ModuleNotFound" or
-  "ImportError" trying to import from the current directory. This would happen
-  if coverage had been packaged into a zip file (for example, on Windows), or
-  was found indirectly (for example, by pyenv-virtualenv).  A number of
-  different scenarios were described in `issue 862`_ which is now fixed.  Huge
-  thanks to Agbonze O. Jeremiah for reporting it, and Alexander Waters and
-  George-Cristian Bîrzan for protracted debugging sessions.
-
-- Added the "premain" debug option.
-
-- Added SQLite compile-time options to the "debug sys" output.
-
-.. _issue 862: https://github.com/nedbat/coveragepy/issues/862
-.. _issue 916: https://github.com/nedbat/coveragepy/issues/916
-
-
-.. _changes_502:
-
-Version 5.0.2 --- 2020-01-05
-----------------------------
-
-- Programs that used multiprocessing and changed directories would fail under
-  coverage.  This is now fixed (`issue 890`_).  A side effect is that debug
-  information about the config files read now shows absolute paths to the
-  files.
-
-- When running programs as modules (``coverage run -m``) with ``--source``,
-  some measured modules were imported before coverage starts.  This resulted in
-  unwanted warnings ("Already imported a file that will be measured") and a
-  reduction in coverage totals (`issue 909`_).  This is now fixed.
-
-- If no data was collected, an exception about "No data to report" could happen
-  instead of a 0% report being created (`issue 884`_).  This is now fixed.
-
-- The handling of source files with non-encodable file names has changed.
-  Previously, if a file name could not be encoded as UTF-8, an error occurred,
-  as described in `issue 891`_.  Now, those files will not be measured, since
-  their data would not be recordable.
-
-- A new warning ("dynamic-conflict") is issued if two mechanisms are trying to
-  change the dynamic context.  Closes `issue 901`_.
-
-- ``coverage run --debug=sys`` would fail with an AttributeError. This is now
-  fixed (`issue 907`_).
-
-.. _issue 884: https://github.com/nedbat/coveragepy/issues/884
-.. _issue 890: https://github.com/nedbat/coveragepy/issues/890
-.. _issue 891: https://github.com/nedbat/coveragepy/issues/891
-.. _issue 901: https://github.com/nedbat/coveragepy/issues/901
-.. _issue 907: https://github.com/nedbat/coveragepy/issues/907
-.. _issue 909: https://github.com/nedbat/coveragepy/issues/909
-
-
-.. _changes_501:
-
-Version 5.0.1 --- 2019-12-22
-----------------------------
-
-- If a 4.x data file is the cause of a "file is not a database" error, then use
-  a more specific error message, "Looks like a coverage 4.x data file, are you
-  mixing versions of coverage?"  Helps diagnose the problems described in
-  `issue 886`_.
-
-- Measurement contexts and relative file names didn't work together, as
-  reported in `issue 899`_ and `issue 900`_.  This is now fixed, thanks to
-  David Szotten.
-
-- When using ``coverage run --concurrency=multiprocessing``, all data files
-  should be named with parallel-ready suffixes.  5.0 mistakenly named the main
-  process' file with no suffix when using ``--append``.  This is now fixed,
-  closing `issue 880`_.
-
-- Fixed a problem on Windows when the current directory is changed to a
-  different drive (`issue 895`_).  Thanks, Olivier Grisel.
-
-- Updated Python 3.9 support to 3.9a2.
-
-.. _issue 880: https://github.com/nedbat/coveragepy/issues/880
-.. _issue 886: https://github.com/nedbat/coveragepy/issues/886
-.. _issue 895: https://github.com/nedbat/coveragepy/issues/895
-.. _issue 899: https://github.com/nedbat/coveragepy/issues/899
-.. _issue 900: https://github.com/nedbat/coveragepy/issues/900
-
-
-.. _changes_50:
-
-Version 5.0 --- 2019-12-14
---------------------------
-
-Nothing new beyond 5.0b2.
-
-
-.. _changes_50b2:
-
-Version 5.0b2 --- 2019-12-08
-----------------------------
-
-- An experimental ``[run] relative_files`` setting tells coverage to store
-  relative file names in the data file. This makes it easier to run tests in
-  one (or many) environments, and then report in another.  It has not had much
-  real-world testing, so it may change in incompatible ways in the future.
-
-- When constructing a :class:`coverage.Coverage` object, `data_file` can be
-  specified as None to prevent writing any data file at all.  In previous
-  versions, an explicit `data_file=None` argument would use the default of
-  ".coverage". Fixes `issue 871`_.
-
-- Python files run with ``-m`` now have ``__spec__`` defined properly.  This
-  fixes `issue 745`_ (about not being able to run unittest tests that spawn
-  subprocesses), and `issue 838`_, which described the problem directly.
-
-- The ``[paths]`` configuration section is now ordered. If you specify more
-  than one list of patterns, the first one that matches will be used.  Fixes
-  `issue 649`_.
-
-- The :func:`.coverage.numbits.register_sqlite_functions` function now also
-  registers `numbits_to_nums` for use in SQLite queries.  Thanks, Simon
-  Willison.
-
-- Python 3.9a1 is supported.
-
-- Coverage.py has a mascot: :ref:`Sleepy Snake <sleepy>`.
-
-.. _issue 649: https://github.com/nedbat/coveragepy/issues/649
-.. _issue 745: https://github.com/nedbat/coveragepy/issues/745
-.. _issue 838: https://github.com/nedbat/coveragepy/issues/838
-.. _issue 871: https://github.com/nedbat/coveragepy/issues/871
-
-
-.. _changes_50b1:
-
-Version 5.0b1 --- 2019-11-11
-----------------------------
-
-- The HTML and textual reports now have a ``--skip-empty`` option that skips
-  files with no statements, notably ``__init__.py`` files.  Thanks, Reya B.
-
-- Configuration can now be read from `TOML`_ files.  This requires installing
-  coverage.py with the ``[toml]`` extra.  The standard "pyproject.toml" file
-  will be read automatically if no other configuration file is found, with
-  settings in the ``[tool.coverage.]`` namespace.  Thanks to Frazer McLean for
-  implementation and persistence.  Finishes `issue 664`_.
-
-- The ``[run] note`` setting has been deprecated. Using it will result in a
-  warning, and the note will not be written to the data file.  The
-  corresponding :class:`.CoverageData` methods have been removed.
-
-- The HTML report has been reimplemented (no more table around the source
-  code). This allowed for a better presentation of the context information,
-  hopefully resolving `issue 855`_.
-
-- Added sqlite3 module version information to ``coverage debug sys`` output.
-
-- Asking the HTML report to show contexts (``[html] show_contexts=True`` or
-  ``coverage html --show-contexts``) will issue a warning if there were no
-  contexts measured (`issue 851`_).
-
-.. _TOML: https://github.com/toml-lang/toml#readme
-.. _issue 664: https://github.com/nedbat/coveragepy/issues/664
-.. _issue 851: https://github.com/nedbat/coveragepy/issues/851
-.. _issue 855: https://github.com/nedbat/coveragepy/issues/855
-
-
-.. _changes_50a8:
-
-Version 5.0a8 --- 2019-10-02
-----------------------------
-
-- The :class:`.CoverageData` API has changed how queries are limited to
-  specific contexts.  Now you use :meth:`.CoverageData.set_query_context` to
-  set a single exact-match string, or :meth:`.CoverageData.set_query_contexts`
-  to set a list of regular expressions to match contexts.  This changes the
-  command-line ``--contexts`` option to use regular expressions instead of
-  filename-style wildcards.
-
-
-.. _changes_50a7:
-
-Version 5.0a7 --- 2019-09-21
-----------------------------
-
-- Data can now be "reported" in JSON format, for programmatic use, as requested
-  in `issue 720`_.  The new ``coverage json`` command writes raw and summarized
-  data to a JSON file.  Thanks, Matt Bachmann.
-
-- Dynamic contexts are now supported in the Python tracer, which is important
-  for PyPy users.  Closes `issue 846`_.
-
-- The compact line number representation introduced in 5.0a6 is called a
-  "numbits."  The :mod:`coverage.numbits` module provides functions for working
-  with them.
-
-- The reporting methods used to permanently apply their arguments to the
-  configuration of the Coverage object.  Now they no longer do.  The arguments
-  affect the operation of the method, but do not persist.
-
-- A class named "test_something" no longer confuses the ``test_function``
-  dynamic context setting.  Fixes `issue 829`_.
-
-- Fixed an unusual tokenizing issue with backslashes in comments.  Fixes
-  `issue 822`_.
-
-- ``debug=plugin`` didn't properly support configuration or dynamic context
-  plugins, but now it does, closing `issue 834`_.
-
-.. _issue 720: https://github.com/nedbat/coveragepy/issues/720
-.. _issue 822: https://github.com/nedbat/coveragepy/issues/822
-.. _issue 834: https://github.com/nedbat/coveragepy/issues/834
-.. _issue 829: https://github.com/nedbat/coveragepy/issues/829
-.. _issue 846: https://github.com/nedbat/coveragepy/issues/846
-
-
-.. _changes_50a6:
-
-Version 5.0a6 --- 2019-07-16
-----------------------------
-
-- Reporting on contexts. Big thanks to Stephan Richter and Albertas Agejevas
-  for the contribution.
-
-  - The ``--contexts`` option is available on the ``report`` and ``html``
-    commands.  It's a comma-separated list of shell-style wildcards, selecting
-    the contexts to report on.  Only contexts matching one of the wildcards
-    will be included in the report.
-
-  - The ``--show-contexts`` option for the ``html`` command adds context
-    information to each covered line.  Hovering over the "ctx" marker at the
-    end of the line reveals a list of the contexts that covered the line.
-
-- Database changes:
-
-  - Line numbers are now stored in a much more compact way.  For each file and
-    context, a single binary string is stored with a bit per line number.  This
-    greatly improves memory use, but makes ad-hoc use difficult.
-
-  - Dynamic contexts with no data are no longer written to the database.
-
-  - SQLite data storage is now faster.  There's no longer a reason to keep the
-    JSON data file code, so it has been removed.
-
-- Changes to the :class:`.CoverageData` interface:
-
-  - The new :meth:`.CoverageData.dumps` method serializes the data to a string,
-    and a corresponding :meth:`.CoverageData.loads` method reconstitutes this
-    data.  The format of the data string is subject to change at any time, and
-    so should only be used between two installations of the same version of
-    coverage.py.
-
-  - The :meth:`CoverageData constructor<.CoverageData.__init__>` has a new
-    argument, `no_disk` (default: False).  Setting it to True prevents writing
-    any data to the disk.  This is useful for transient data objects.
-
-- Added the classmethod :meth:`.Coverage.current` to get the latest started
-  Coverage instance.
-
-- Multiprocessing support in Python 3.8 was broken, but is now fixed.  Closes
-  `issue 828`_.
-
-- Error handling during reporting has changed slightly.  All reporting methods
-  now behave the same.  The ``--ignore-errors`` option keeps errors from
-  stopping the reporting, but files that couldn't parse as Python will always
-  be reported as warnings.  As with other warnings, you can suppress them with
-  the ``[run] disable_warnings`` configuration setting.
-
-- Coverage.py no longer fails if the user program deletes its current
-  directory. Fixes `issue 806`_.  Thanks, Dan Hemberger.
-
-- The scrollbar markers in the HTML report now accurately show the highlighted
-  lines, regardless of what categories of line are highlighted.
-
-- The hack to accommodate ShiningPanda_ looking for an obsolete internal data
-  file has been removed, since ShiningPanda 0.22 fixed it four years ago.
-
-- The deprecated `Reporter.file_reporters` property has been removed.
-
-.. _ShiningPanda: https://wiki.jenkins.io/display/JENKINS/ShiningPanda+Plugin
-.. _issue 806: https://github.com/nedbat/coveragepy/pull/806
-.. _issue 828: https://github.com/nedbat/coveragepy/issues/828
-
-
-.. _changes_50a5:
-
-Version 5.0a5 --- 2019-05-07
-----------------------------
-
-- Drop support for Python 3.4
-
-- Dynamic contexts can now be set two new ways, both thanks to Justas
-  Sadzevičius.
-
-  - A plugin can implement a ``dynamic_context`` method to check frames for
-    whether a new context should be started.  See
-    :ref:`dynamic_context_plugins` for more details.
-
-  - Another tool (such as a test runner) can use the new
-    :meth:`.Coverage.switch_context` method to explicitly change the context.
-
-- The ``dynamic_context = test_function`` setting now works with Python 2
-  old-style classes, though it only reports the method name, not the class it
-  was defined on.  Closes `issue 797`_.
-
-- ``fail_under`` values more than 100 are reported as errors.  Thanks to Mike
-  Fiedler for closing `issue 746`_.
-
-- The "missing" values in the text output are now sorted by line number, so
-  that missing branches are reported near the other lines they affect. The
-  values used to show all missing lines, and then all missing branches.
-
-- Access to the SQLite database used for data storage is now thread-safe.
-  Thanks, Stephan Richter. This closes `issue 702`_.
-
-- Combining data stored in SQLite is now about twice as fast, fixing `issue
-  761`_.  Thanks, Stephan Richter.
-
-- The ``filename`` attribute on :class:`.CoverageData` objects has been made
-  private.  You can use the ``data_filename`` method to get the actual file
-  name being used to store data, and the ``base_filename`` method to get the
-  original filename before parallelizing suffixes were added.  This is part of
-  fixing `issue 708`_.
-
-- Line numbers in the HTML report now align properly with source lines, even
-  when Chrome's minimum font size is set, fixing `issue 748`_.  Thanks Wen Ye.
-
-.. _issue 702: https://github.com/nedbat/coveragepy/issues/702
-.. _issue 708: https://github.com/nedbat/coveragepy/issues/708
-.. _issue 746: https://github.com/nedbat/coveragepy/issues/746
-.. _issue 748: https://github.com/nedbat/coveragepy/issues/748
-.. _issue 761: https://github.com/nedbat/coveragepy/issues/761
-.. _issue 797: https://github.com/nedbat/coveragepy/issues/797
-
-
-.. _changes_50a4:
-
-Version 5.0a4 --- 2018-11-25
-----------------------------
-
-- You can specify the command line to run your program with the ``[run]
-  command_line`` configuration setting, as requested in `issue 695`_.
-
-- Coverage will create directories as needed for the data file if they don't
-  exist, closing `issue 721`_.
-
-- The ``coverage run`` command has always adjusted the first entry in sys.path,
-  to properly emulate how Python runs your program.  Now this adjustment is
-  skipped if sys.path[0] is already different than Python's default.  This
-  fixes `issue 715`_.
-
-- Improvements to context support:
-
-  - The "no such table: meta" error is fixed.: `issue 716`_.
-
-  - Combining data files is now much faster.
-
-- Python 3.8 (as of today!) passes all tests.
-
-.. _issue 695: https://github.com/nedbat/coveragepy/issues/695
-.. _issue 715: https://github.com/nedbat/coveragepy/issues/715
-.. _issue 716: https://github.com/nedbat/coveragepy/issues/716
-.. _issue 721: https://github.com/nedbat/coveragepy/issues/721
-
-
-.. _changes_50a3:
-
-Version 5.0a3 --- 2018-10-06
-----------------------------
-
-- Context support: static contexts let you specify a label for a coverage run,
-  which is recorded in the data, and retained when you combine files.  See
-  :ref:`contexts` for more information.
-
-- Dynamic contexts: specifying ``[run] dynamic_context = test_function`` in the
-  config file will record the test function name as a dynamic context during
-  execution.  This is the core of "Who Tests What" (`issue 170`_).  Things to
-  note:
-
-  - There is no reporting support yet.  Use SQLite to query the .coverage file
-    for information.  Ideas are welcome about how reporting could be extended
-    to use this data.
-
-  - There's a noticeable slow-down before any test is run.
-
-  - Data files will now be roughly N times larger, where N is the number of
-    tests you have.  Combining data files is therefore also N times slower.
-
-  - No other values for ``dynamic_context`` are recognized yet.  Let me know
-    what else would be useful.  I'd like to use a pytest plugin to get better
-    information directly from pytest, for example.
-
-.. _issue 170: https://github.com/nedbat/coveragepy/issues/170
-
-- Environment variable substitution in configuration files now supports two
-  syntaxes for controlling the behavior of undefined variables: if ``VARNAME``
-  is not defined, ``${VARNAME?}`` will raise an error, and ``${VARNAME-default
-  value}`` will use "default value".
-
-- Partial support for Python 3.8, which has not yet released an alpha. Fixes
-  `issue 707`_ and `issue 714`_.
-
-.. _issue 707: https://github.com/nedbat/coveragepy/issues/707
-.. _issue 714: https://github.com/nedbat/coveragepy/issues/714
-
-
-.. _changes_50a2:
-
-Version 5.0a2 --- 2018-09-03
-----------------------------
-
-- Coverage's data storage has changed.  In version 4.x, .coverage files were
-  basically JSON.  Now, they are SQLite databases.  This means the data file
-  can be created earlier than it used to.  A large amount of code was
-  refactored to support this change.
-
-  - Because the data file is created differently than previous releases, you
-    may need ``parallel=true`` where you didn't before.
-
-  - The old data format is still available (for now) by setting the environment
-    variable COVERAGE_STORAGE=json. Please tell me if you think you need to
-    keep the JSON format.
-
-  - The database schema is guaranteed to change in the future, to support new
-    features.  I'm looking for opinions about making the schema part of the
-    public API to coverage.py or not.
-
-- Development moved from `Bitbucket`_ to `GitHub`_.
-
-- HTML files no longer have trailing and extra whitespace.
-
-- The sort order in the HTML report is stored in local storage rather than
-  cookies, closing `issue 611`_.  Thanks, Federico Bond.
-
-- pickle2json, for converting v3 data files to v4 data files, has been removed.
-
-.. _Bitbucket: https://bitbucket.org
-.. _GitHub: https://github.com/nedbat/coveragepy
-
-.. _issue 611: https://github.com/nedbat/coveragepy/issues/611
-
-
-.. _changes_50a1:
-
-Version 5.0a1 --- 2018-06-05
-----------------------------
-
-- Coverage.py no longer supports Python 2.6 or 3.3.
-
-- The location of the configuration file can now be specified with a
-  ``COVERAGE_RCFILE`` environment variable, as requested in `issue 650`_.
-
-- Namespace packages are supported on Python 3.7, where they used to cause
-  TypeErrors about path being None. Fixes `issue 700`_.
-
-- A new warning (``already-imported``) is issued if measurable files have
-  already been imported before coverage.py started measurement.  See
-  :ref:`cmd_warnings` for more information.
-
-- Running coverage many times for small runs in a single process should be
-  faster, closing `issue 625`_.  Thanks, David MacIver.
-
-- Large HTML report pages load faster.  Thanks, Pankaj Pandey.
-
-.. _issue 625: https://github.com/nedbat/coveragepy/issues/625
-.. _issue 650: https://github.com/nedbat/coveragepy/issues/650
-.. _issue 700: https://github.com/nedbat/coveragepy/issues/700
-
-
-.. _changes_454:
-
-Version 4.5.4 --- 2019-07-29
-----------------------------
-
-- Multiprocessing support in Python 3.8 was broken, but is now fixed.  Closes
-  `issue 828`_.
-
-.. _issue 828: https://github.com/nedbat/coveragepy/issues/828
-
-
-.. _changes_453:
-
-Version 4.5.3 --- 2019-03-09
-----------------------------
-
-- Only packaging metadata changes.
-
-
-.. _changes_452:
-
-Version 4.5.2 --- 2018-11-12
-----------------------------
-
-- Namespace packages are supported on Python 3.7, where they used to cause
-  TypeErrors about path being None. Fixes `issue 700`_.
-
-- Python 3.8 (as of today!) passes all tests.  Fixes `issue 707`_ and
-  `issue 714`_.
-
-- Development moved from `Bitbucket`_ to `GitHub`_.
-
-.. _issue 700: https://github.com/nedbat/coveragepy/issues/700
-.. _issue 707: https://github.com/nedbat/coveragepy/issues/707
-.. _issue 714: https://github.com/nedbat/coveragepy/issues/714
-
-.. _Bitbucket: https://bitbucket.org
-.. _GitHub: https://github.com/nedbat/coveragepy
-
-
-.. _changes_451:
-
-Version 4.5.1 --- 2018-02-10
-----------------------------
-
-- Now that 4.5 properly separated the ``[run] omit`` and ``[report] omit``
-  settings, an old bug has become apparent.  If you specified a package name
-  for ``[run] source``, then omit patterns weren't matched inside that package.
-  This bug (`issue 638`_) is now fixed.
-
-- On Python 3.7, reporting about a decorated function with no body other than a
-  docstring would crash coverage.py with an IndexError (`issue 640`_).  This is
-  now fixed.
-
-- Configurer plugins are now reported in the output of ``--debug=sys``.
-
-.. _issue 638: https://github.com/nedbat/coveragepy/issues/638
-.. _issue 640: https://github.com/nedbat/coveragepy/issues/640
-
-
-.. _changes_45:
-
-Version 4.5 --- 2018-02-03
---------------------------
-
-- A new kind of plugin is supported: configurers are invoked at start-up to
-  allow more complex configuration than the .coveragerc file can easily do.
-  See :ref:`api_plugin` for details.  This solves the complex configuration
-  problem described in `issue 563`_.
-
-- The ``fail_under`` option can now be a float.  Note that you must specify the
-  ``[report] precision`` configuration option for the fractional part to be
-  used.  Thanks to Lars Hupfeldt Nielsen for help with the implementation.
-  Fixes `issue 631`_.
-
-- The ``include`` and ``omit`` options can be specified for both the ``[run]``
-  and ``[report]`` phases of execution.  4.4.2 introduced some incorrect
-  interactions between those phases, where the options for one were confused
-  for the other.  This is now corrected, fixing `issue 621`_ and `issue 622`_.
-  Thanks to Daniel Hahler for seeing more clearly than I could.
-
-- The ``coverage combine`` command used to always overwrite the data file, even
-  when no data had been read from apparently combinable files.  Now, an error
-  is raised if we thought there were files to combine, but in fact none of them
-  could be used.  Fixes `issue 629`_.
-
-- The ``coverage combine`` command could get confused about path separators
-  when combining data collected on Windows with data collected on Linux, as
-  described in `issue 618`_.  This is now fixed: the result path always uses
-  the path separator specified in the ``[paths]`` result.
-
-- On Windows, the HTML report could fail when source trees are deeply nested,
-  due to attempting to create HTML filenames longer than the 250-character
-  maximum.  Now filenames will never get much larger than 200 characters,
-  fixing `issue 627`_.  Thanks to Alex Sandro for helping with the fix.
-
-.. _issue 563: https://github.com/nedbat/coveragepy/issues/563
-.. _issue 618: https://github.com/nedbat/coveragepy/issues/618
-.. _issue 621: https://github.com/nedbat/coveragepy/issues/621
-.. _issue 622: https://github.com/nedbat/coveragepy/issues/622
-.. _issue 627: https://github.com/nedbat/coveragepy/issues/627
-.. _issue 629: https://github.com/nedbat/coveragepy/issues/629
-.. _issue 631: https://github.com/nedbat/coveragepy/issues/631
-
-
-.. _changes_442:
-
-Version 4.4.2 --- 2017-11-05
-----------------------------
-
-- Support for Python 3.7.  In some cases, class and module docstrings are no
-  longer counted in statement totals, which could slightly change your total
-  results.
-
-- Specifying both ``--source`` and ``--include`` no longer silently ignores the
-  include setting, instead it displays a warning. Thanks, Loïc Dachary.  Closes
-  `issue 265`_ and `issue 101`_.
-
-- Fixed a race condition when saving data and multiple threads are tracing
-  (`issue 581`_). It could produce a "dictionary changed size during iteration"
-  RuntimeError.  I believe this mostly but not entirely fixes the race
-  condition.  A true fix would likely be too expensive.  Thanks, Peter Baughman
-  for the debugging, and Olivier Grisel for the fix with tests.
-
-- Configuration values which are file paths will now apply tilde-expansion,
-  closing `issue 589`_.
-
-- Now secondary config files like tox.ini and setup.cfg can be specified
-  explicitly, and prefixed sections like `[coverage:run]` will be read. Fixes
-  `issue 588`_.
-
-- Be more flexible about the command name displayed by help, fixing
-  `issue 600`_. Thanks, Ben Finney.
-
-.. _issue 101: https://github.com/nedbat/coveragepy/issues/101
-.. _issue 581: https://github.com/nedbat/coveragepy/issues/581
-.. _issue 588: https://github.com/nedbat/coveragepy/issues/588
-.. _issue 589: https://github.com/nedbat/coveragepy/issues/589
-.. _issue 600: https://github.com/nedbat/coveragepy/issues/600
-
-
-.. _changes_441:
-
-Version 4.4.1 --- 2017-05-14
-----------------------------
-
-- No code changes: just corrected packaging for Python 2.7 Linux wheels.
-
-
-.. _changes_44:
-
-Version 4.4 --- 2017-05-07
---------------------------
-
-- Reports could produce the wrong file names for packages, reporting ``pkg.py``
-  instead of the correct ``pkg/__init__.py``.  This is now fixed.  Thanks, Dirk
-  Thomas.
-
-- XML reports could produce ``<source>`` and ``<class>`` lines that together
-  didn't specify a valid source file path.  This is now fixed. (`issue 526`_)
-
-- Namespace packages are no longer warned as having no code. (`issue 572`_)
-
-- Code that uses ``sys.settrace(sys.gettrace())`` in a file that wasn't being
-  coverage-measured would prevent correct coverage measurement in following
-  code. An example of this was running doctests programmatically. This is now
-  fixed. (`issue 575`_)
-
-- Errors printed by the ``coverage`` command now go to stderr instead of
-  stdout.
-
-- Running ``coverage xml`` in a directory named with non-ASCII characters would
-  fail under Python 2. This is now fixed. (`issue 573`_)
-
-.. _issue 526: https://github.com/nedbat/coveragepy/issues/526
-.. _issue 572: https://github.com/nedbat/coveragepy/issues/572
-.. _issue 573: https://github.com/nedbat/coveragepy/issues/573
-.. _issue 575: https://github.com/nedbat/coveragepy/issues/575
-
-
-Version 4.4b1 --- 2017-04-04
-----------------------------
-
-- Some warnings can now be individually disabled.  Warnings that can be
-  disabled have a short name appended.  The ``[run] disable_warnings`` setting
-  takes a list of these warning names to disable. Closes both `issue 96`_ and
-  `issue 355`_.
-
-- The XML report now includes attributes from version 4 of the Cobertura XML
-  format, fixing `issue 570`_.
-
-- In previous versions, calling a method that used collected data would prevent
-  further collection.  For example, `save()`, `report()`, `html_report()`, and
-  others would all stop collection.  An explicit `start()` was needed to get it
-  going again.  This is no longer true.  Now you can use the collected data and
-  also continue measurement. Both `issue 79`_ and `issue 448`_ described this
-  problem, and have been fixed.
-
-- Plugins can now find unexecuted files if they choose, by implementing the
-  `find_executable_files` method.  Thanks, Emil Madsen.
-
-- Minimal IronPython support. You should be able to run IronPython programs
-  under ``coverage run``, though you will still have to do the reporting phase
-  with CPython.
-
-- Coverage.py has long had a special hack to support CPython's need to measure
-  the coverage of the standard library tests. This code was not installed by
-  kitted versions of coverage.py.  Now it is.
-
-.. _issue 79: https://github.com/nedbat/coveragepy/issues/79
-.. _issue 96: https://github.com/nedbat/coveragepy/issues/96
-.. _issue 355: https://github.com/nedbat/coveragepy/issues/355
-.. _issue 448: https://github.com/nedbat/coveragepy/issues/448
-.. _issue 570: https://github.com/nedbat/coveragepy/issues/570
-
-
-.. _changes_434:
-
-Version 4.3.4 --- 2017-01-17
-----------------------------
-
-- Fixing 2.6 in version 4.3.3 broke other things, because the too-tricky
-  exception wasn't properly derived from Exception, described in `issue 556`_.
-  A newb mistake; it hasn't been a good few days.
-
-.. _issue 556: https://github.com/nedbat/coveragepy/issues/556
-
-
-.. _changes_433:
-
-Version 4.3.3 --- 2017-01-17
-----------------------------
-
-- Python 2.6 support was broken due to a testing exception imported for the
-  benefit of the coverage.py test suite.  Properly conditionalizing it fixed
-  `issue 554`_ so that Python 2.6 works again.
-
-.. _issue 554: https://github.com/nedbat/coveragepy/issues/554
-
-
-.. _changes_432:
-
-Version 4.3.2 --- 2017-01-16
-----------------------------
-
-- Using the ``--skip-covered`` option on an HTML report with 100% coverage
-  would cause a "No data to report" error, as reported in `issue 549`_. This is
-  now fixed; thanks, Loïc Dachary.
-
-- If-statements can be optimized away during compilation, for example, `if 0:`
-  or `if __debug__:`.  Coverage.py had problems properly understanding these
-  statements which existed in the source, but not in the compiled bytecode.
-  This problem, reported in `issue 522`_, is now fixed.
-
-- If you specified ``--source`` as a directory, then coverage.py would look for
-  importable Python files in that directory, and could identify ones that had
-  never been executed at all.  But if you specified it as a package name, that
-  detection wasn't performed.  Now it is, closing `issue 426`_. Thanks to Loïc
-  Dachary for the fix.
-
-- If you started and stopped coverage measurement thousands of times in your
-  process, you could crash Python with a "Fatal Python error: deallocating
-  None" error.  This is now fixed.  Thanks to Alex Groce for the bug report.
-
-- On PyPy, measuring coverage in subprocesses could produce a warning: "Trace
-  function changed, measurement is likely wrong: None".  This was spurious, and
-  has been suppressed.
-
-- Previously, coverage.py couldn't start on Jython, due to that implementation
-  missing the multiprocessing module (`issue 551`_). This problem has now been
-  fixed. Also, `issue 322`_ about not being able to invoke coverage
-  conveniently, seems much better: ``jython -m coverage run myprog.py`` works
-  properly.
-
-- Let's say you ran the HTML report over and over again in the same output
-  directory, with ``--skip-covered``. And imagine due to your heroic
-  test-writing efforts, a file just achieved the goal of 100% coverage. With
-  coverage.py 4.3, the old HTML file with the less-than-100% coverage would be
-  left behind.  This file is now properly deleted.
-
-.. _issue 322: https://github.com/nedbat/coveragepy/issues/322
-.. _issue 426: https://github.com/nedbat/coveragepy/issues/426
-.. _issue 522: https://github.com/nedbat/coveragepy/issues/522
-.. _issue 549: https://github.com/nedbat/coveragepy/issues/549
-.. _issue 551: https://github.com/nedbat/coveragepy/issues/551
-
-
-.. _changes_431:
-
-Version 4.3.1 --- 2016-12-28
-----------------------------
-
-- Some environments couldn't install 4.3, as described in `issue 540`_. This is
-  now fixed.
-
-- The check for conflicting ``--source`` and ``--include`` was too simple in a
-  few different ways, breaking a few perfectly reasonable use cases, described
-  in `issue 541`_.  The check has been reverted while we re-think the fix for
-  `issue 265`_.
-
-.. _issue 540: https://github.com/nedbat/coveragepy/issues/540
-.. _issue 541: https://github.com/nedbat/coveragepy/issues/541
-
-
-.. _changes_43:
-
-Version 4.3 --- 2016-12-27
---------------------------
-
-Special thanks to **Loïc Dachary**, who took an extraordinary interest in
-coverage.py and contributed a number of improvements in this release.
-
-- Subprocesses that are measured with `automatic subprocess measurement`_ used
-  to read in any pre-existing data file.  This meant data would be incorrectly
-  carried forward from run to run.  Now those files are not read, so each
-  subprocess only writes its own data. Fixes `issue 510`_.
-
-- The ``coverage combine`` command will now fail if there are no data files to
-  combine. The combine changes in 4.2 meant that multiple combines could lose
-  data, leaving you with an empty .coverage data file. Fixes
-  `issue 525`_, `issue 412`_, `issue 516`_, and probably `issue 511`_.
-
-- Coverage.py wouldn't execute `sys.excepthook`_ when an exception happened in
-  your program.  Now it does, thanks to Andrew Hoos.  Closes `issue 535`_.
-
-- Branch coverage fixes:
-
-  - Branch coverage could misunderstand a finally clause on a try block that
-    never continued on to the following statement, as described in `issue
-    493`_.  This is now fixed. Thanks to Joe Doherty for the report and Loïc
-    Dachary for the fix.
-
-  - A while loop with a constant condition (while True) and a continue
-    statement would be mis-analyzed, as described in `issue 496`_. This is now
-    fixed, thanks to a bug report by Eli Skeggs and a fix by Loïc Dachary.
-
-  - While loops with constant conditions that were never executed could result
-    in a non-zero coverage report.  Artem Dayneko reported this in `issue
-    502`_, and Loïc Dachary provided the fix.
-
-- The HTML report now supports a ``--skip-covered`` option like the other
-  reporting commands.  Thanks, Loïc Dachary for the implementation, closing
-  `issue 433`_.
-
-- Options can now be read from a tox.ini file, if any. Like setup.cfg, sections
-  are prefixed with "coverage:", so ``[run]`` options will be read from the
-  ``[coverage:run]`` section of tox.ini. Implements part of `issue 519`_.
-  Thanks, Stephen Finucane.
-
-- Specifying both ``--source`` and ``--include`` no longer silently ignores the
-  include setting, instead it fails with a message. Thanks, Nathan Land and
-  Loïc Dachary. Closes `issue 265`_.
-
-- The ``Coverage.combine`` method has a new parameter, ``strict=False``, to
-  support failing if there are no data files to combine.
-
-- When forking subprocesses, the coverage data files would have the same random
-  number appended to the file name. This didn't cause problems, because the
-  file names had the process id also, making collisions (nearly) impossible.
-  But it was disconcerting.  This is now fixed.
-
-- The text report now properly sizes headers when skipping some files, fixing
-  `issue 524`_. Thanks, Anthony Sottile and Loïc Dachary.
-
-- Coverage.py can now search .pex files for source, just as it can .zip and
-  .egg.  Thanks, Peter Ebden.
-
-- Data files are now about 15% smaller.
-
-- Improvements in the ``[run] debug`` setting:
-
-  - The "dataio" debug setting now also logs when data files are deleted during
-    combining or erasing.
-
-  - A new debug option, "multiproc", for logging the behavior of
-    ``concurrency=multiprocessing``.
-
-  - If you used the debug options "config" and "callers" together, you'd get a
-    call stack printed for every line in the multi-line config output. This is
-    now fixed.
-
-- Fixed an unusual bug involving multiple coding declarations affecting code
-  containing code in multi-line strings: `issue 529`_.
-
-- Coverage.py will no longer be misled into thinking that a plain file is a
-  package when interpreting ``--source`` options.  Thanks, Cosimo Lupo.
-
-- If you try to run a non-Python file with coverage.py, you will now get a more
-  useful error message. `Issue 514`_.
-
-- The default pragma regex changed slightly, but this will only matter to you
-  if you are deranged and use mixed-case pragmas.
-
-- Deal properly with non-ASCII file names in an ASCII-only world, `issue 533`_.
-
-- Programs that set Unicode configuration values could cause UnicodeErrors when
-  generating HTML reports.  Pytest-cov is one example.  This is now fixed.
-
-- Prevented deprecation warnings from configparser that happened in some
-  circumstances, closing `issue 530`_.
-
-- Corrected the name of the jquery.ba-throttle-debounce.js library. Thanks,
-  Ben Finney.  Closes `issue 505`_.
-
-- Testing against PyPy 5.6 and PyPy3 5.5.
-
-- Switched to pytest from nose for running the coverage.py tests.
-
-- Renamed AUTHORS.txt to CONTRIBUTORS.txt, since there are other ways to
-  contribute than by writing code. Also put the count of contributors into the
-  author string in setup.py, though this might be too cute.
-
-.. _sys.excepthook: https://docs.python.org/3/library/sys.html#sys.excepthook
-.. _issue 265: https://github.com/nedbat/coveragepy/issues/265
-.. _issue 412: https://github.com/nedbat/coveragepy/issues/412
-.. _issue 433: https://github.com/nedbat/coveragepy/issues/433
-.. _issue 493: https://github.com/nedbat/coveragepy/issues/493
-.. _issue 496: https://github.com/nedbat/coveragepy/issues/496
-.. _issue 502: https://github.com/nedbat/coveragepy/issues/502
-.. _issue 505: https://github.com/nedbat/coveragepy/issues/505
-.. _issue 514: https://github.com/nedbat/coveragepy/issues/514
-.. _issue 510: https://github.com/nedbat/coveragepy/issues/510
-.. _issue 511: https://github.com/nedbat/coveragepy/issues/511
-.. _issue 516: https://github.com/nedbat/coveragepy/issues/516
-.. _issue 519: https://github.com/nedbat/coveragepy/issues/519
-.. _issue 524: https://github.com/nedbat/coveragepy/issues/524
-.. _issue 525: https://github.com/nedbat/coveragepy/issues/525
-.. _issue 529: https://github.com/nedbat/coveragepy/issues/529
-.. _issue 530: https://github.com/nedbat/coveragepy/issues/530
-.. _issue 533: https://github.com/nedbat/coveragepy/issues/533
-.. _issue 535: https://github.com/nedbat/coveragepy/issues/535
-
-
-.. _changes_42:
-
-Version 4.2 --- 2016-07-26
---------------------------
-
-- Since ``concurrency=multiprocessing`` uses subprocesses, options specified on
-  the coverage.py command line will not be communicated down to them.  Only
-  options in the configuration file will apply to the subprocesses.
-  Previously, the options didn't apply to the subprocesses, but there was no
-  indication.  Now it is an error to use ``--concurrency=multiprocessing`` and
-  other run-affecting options on the command line.  This prevents
-  failures like those reported in `issue 495`_.
-
-- Filtering the HTML report is now faster, thanks to Ville Skyttä.
-
-.. _issue 495: https://github.com/nedbat/coveragepy/issues/495
-
-
-Version 4.2b1 --- 2016-07-04
-----------------------------
-
-Work from the PyCon 2016 Sprints!
-
-- BACKWARD INCOMPATIBILITY: the ``coverage combine`` command now ignores an
-  existing ``.coverage`` data file.  It used to include that file in its
-  combining.  This caused confusing results, and extra tox "clean" steps.  If
-  you want the old behavior, use the new ``coverage combine --append`` option.
-
-- The ``concurrency`` option can now take multiple values, to support programs
-  using multiprocessing and another library such as eventlet.  This is only
-  possible in the configuration file, not from the command line. The
-  configuration file is the only way for sub-processes to all run with the same
-  options.  Fixes `issue 484`_.  Thanks to Josh Williams for prototyping.
-
-- Using a ``concurrency`` setting of ``multiprocessing`` now implies
-  ``--parallel`` so that the main program is measured similarly to the
-  sub-processes.
-
-- When using `automatic subprocess measurement`_, running coverage commands
-  would create spurious data files.  This is now fixed, thanks to diagnosis and
-  testing by Dan Riti.  Closes `issue 492`_.
-
-- A new configuration option, ``report:sort``, controls what column of the
-  text report is used to sort the rows.  Thanks to Dan Wandschneider, this
-  closes `issue 199`_.
-
-- The HTML report has a more-visible indicator for which column is being
-  sorted.  Closes `issue 298`_, thanks to Josh Williams.
-
-- If the HTML report cannot find the source for a file, the message now
-  suggests using the ``-i`` flag to allow the report to continue. Closes
-  `issue 231`_, thanks, Nathan Land.
-
-- When reports are ignoring errors, there's now a warning if a file cannot be
-  parsed, rather than being silently ignored.  Closes `issue 396`_. Thanks,
-  Matthew Boehm.
-
-- A new option for ``coverage debug`` is available: ``coverage debug config``
-  shows the current configuration.  Closes `issue 454`_, thanks to Matthew
-  Boehm.
-
-- Running coverage as a module (``python -m coverage``) no longer shows the
-  program name as ``__main__.py``.  Fixes `issue 478`_.  Thanks, Scott Belden.
-
-- The `test_helpers` module has been moved into a separate pip-installable
-  package: `unittest-mixins`_.
-
-.. _automatic subprocess measurement: https://coverage.readthedocs.io/en/latest/subprocess.html
-.. _issue 199: https://github.com/nedbat/coveragepy/issues/199
-.. _issue 231: https://github.com/nedbat/coveragepy/issues/231
-.. _issue 298: https://github.com/nedbat/coveragepy/issues/298
-.. _issue 396: https://github.com/nedbat/coveragepy/issues/396
-.. _issue 454: https://github.com/nedbat/coveragepy/issues/454
-.. _issue 478: https://github.com/nedbat/coveragepy/issues/478
-.. _issue 484: https://github.com/nedbat/coveragepy/issues/484
-.. _issue 492: https://github.com/nedbat/coveragepy/issues/492
-.. _unittest-mixins: https://pypi.org/project/unittest-mixins/
-
-
-.. _changes_41:
-
-Version 4.1 --- 2016-05-21
---------------------------
-
-- The internal attribute `Reporter.file_reporters` was removed in 4.1b3.  It
-  should have come has no surprise that there were third-party tools out there
-  using that attribute.  It has been restored, but with a deprecation warning.
-
-
-Version 4.1b3 --- 2016-05-10
-----------------------------
-
-- When running your program, execution can jump from an ``except X:`` line to
-  some other line when an exception other than ``X`` happens.  This jump is no
-  longer considered a branch when measuring branch coverage.
-
-- When measuring branch coverage, ``yield`` statements that were never resumed
-  were incorrectly marked as missing, as reported in `issue 440`_.  This is now
-  fixed.
-
-- During branch coverage of single-line callables like lambdas and generator
-  expressions, coverage.py can now distinguish between them never being called,
-  or being called but not completed.  Fixes `issue 90`_, `issue 460`_ and
-  `issue 475`_.
-
-- The HTML report now has a map of the file along the rightmost edge of the
-  page, giving an overview of where the missed lines are.  Thanks, Dmitry
-  Shishov.
-
-- The HTML report now uses different monospaced fonts, favoring Consolas over
-  Courier.  Along the way, `issue 472`_ about not properly handling one-space
-  indents was fixed.  The index page also has slightly different styling, to
-  try to make the clickable detail pages more apparent.
-
-- Missing branches reported with ``coverage report -m`` will now say ``->exit``
-  for missed branches to the exit of a function, rather than a negative number.
-  Fixes `issue 469`_.
-
-- ``coverage --help`` and ``coverage --version`` now mention which tracer is
-  installed, to help diagnose problems. The docs mention which features need
-  the C extension. (`issue 479`_)
-
-- Officially support PyPy 5.1, which required no changes, just updates to the
-  docs.
-
-- The `Coverage.report` function had two parameters with non-None defaults,
-  which have been changed.  `show_missing` used to default to True, but now
-  defaults to None.  If you had been calling `Coverage.report` without
-  specifying `show_missing`, you'll need to explicitly set it to True to keep
-  the same behavior.  `skip_covered` used to default to False. It is now None,
-  which doesn't change the behavior.  This fixes `issue 485`_.
-
-- It's never been possible to pass a namespace module to one of the analysis
-  functions, but now at least we raise a more specific error message, rather
-  than getting confused. (`issue 456`_)
-
-- The `coverage.process_startup` function now returns the `Coverage` instance
-  it creates, as suggested in `issue 481`_.
-
-- Make a small tweak to how we compare threads, to avoid buggy custom
-  comparison code in thread classes. (`issue 245`_)
-
-.. _issue 90: https://github.com/nedbat/coveragepy/issues/90
-.. _issue 245: https://github.com/nedbat/coveragepy/issues/245
-.. _issue 440: https://github.com/nedbat/coveragepy/issues/440
-.. _issue 456: https://github.com/nedbat/coveragepy/issues/456
-.. _issue 460: https://github.com/nedbat/coveragepy/issues/460
-.. _issue 469: https://github.com/nedbat/coveragepy/issues/469
-.. _issue 472: https://github.com/nedbat/coveragepy/issues/472
-.. _issue 475: https://github.com/nedbat/coveragepy/issues/475
-.. _issue 479: https://github.com/nedbat/coveragepy/issues/479
-.. _issue 481: https://github.com/nedbat/coveragepy/issues/481
-.. _issue 485: https://github.com/nedbat/coveragepy/issues/485
-
-
-Version 4.1b2 --- 2016-01-23
-----------------------------
-
-- Problems with the new branch measurement in 4.1 beta 1 were fixed:
-
-  - Class docstrings were considered executable.  Now they no longer are.
-
-  - ``yield from`` and ``await`` were considered returns from functions, since
-    they could transfer control to the caller.  This produced unhelpful
-    "missing branch" reports in a number of circumstances.  Now they no longer
-    are considered returns.
-
-  - In unusual situations, a missing branch to a negative number was reported.
-    This has been fixed, closing `issue 466`_.
-
-- The XML report now produces correct package names for modules found in
-  directories specified with ``source=``.  Fixes `issue 465`_.
-
-- ``coverage report`` won't produce trailing whitespace.
-
-.. _issue 465: https://github.com/nedbat/coveragepy/issues/465
-.. _issue 466: https://github.com/nedbat/coveragepy/issues/466
-
-
-Version 4.1b1 --- 2016-01-10
-----------------------------
-
-- Branch analysis has been rewritten: it used to be based on bytecode, but now
-  uses AST analysis.  This has changed a number of things:
-
-  - More code paths are now considered runnable, especially in
-    ``try``/``except`` structures.  This may mean that coverage.py will
-    identify more code paths as uncovered.  This could either raise or lower
-    your overall coverage number.
-
-  - Python 3.5's ``async`` and ``await`` keywords are properly supported,
-    fixing `issue 434`_.
-
-  - Some long-standing branch coverage bugs were fixed:
-
-    - `issue 129`_: functions with only a docstring for a body would
-      incorrectly report a missing branch on the ``def`` line.
-
-    - `issue 212`_: code in an ``except`` block could be incorrectly marked as
-      a missing branch.
-
-    - `issue 146`_: context managers (``with`` statements) in a loop or ``try``
-      block could confuse the branch measurement, reporting incorrect partial
-      branches.
-
-    - `issue 422`_: in Python 3.5, an actual partial branch could be marked as
-      complete.
-
-- Pragmas to disable coverage measurement can now be used on decorator lines,
-  and they will apply to the entire function or class being decorated.  This
-  implements the feature requested in `issue 131`_.
-
-- Multiprocessing support is now available on Windows.  Thanks, Rodrigue
-  Cloutier.
-
-- Files with two encoding declarations are properly supported, fixing
-  `issue 453`_. Thanks, Max Linke.
-
-- Non-ascii characters in regexes in the configuration file worked in 3.7, but
-  stopped working in 4.0.  Now they work again, closing `issue 455`_.
-
-- Form-feed characters would prevent accurate determination of the beginning of
-  statements in the rest of the file.  This is now fixed, closing `issue 461`_.
-
-.. _issue 129: https://github.com/nedbat/coveragepy/issues/129
-.. _issue 131: https://github.com/nedbat/coveragepy/issues/131
-.. _issue 146: https://github.com/nedbat/coveragepy/issues/146
-.. _issue 212: https://github.com/nedbat/coveragepy/issues/212
-.. _issue 422: https://github.com/nedbat/coveragepy/issues/422
-.. _issue 434: https://github.com/nedbat/coveragepy/issues/434
-.. _issue 453: https://github.com/nedbat/coveragepy/issues/453
-.. _issue 455: https://github.com/nedbat/coveragepy/issues/455
-.. _issue 461: https://github.com/nedbat/coveragepy/issues/461
-
-
-.. _changes_403:
-
-Version 4.0.3 --- 2015-11-24
-----------------------------
-
-- Fixed a mysterious problem that manifested in different ways: sometimes
-  hanging the process (`issue 420`_), sometimes making database connections
-  fail (`issue 445`_).
-
-- The XML report now has correct ``<source>`` elements when using a
-  ``--source=`` option somewhere besides the current directory.  This fixes
-  `issue 439`_. Thanks, Arcadiy Ivanov.
-
-- Fixed an unusual edge case of detecting source encodings, described in
-  `issue 443`_.
-
-- Help messages that mention the command to use now properly use the actual
-  command name, which might be different than "coverage".  Thanks to Ben
-  Finney, this closes `issue 438`_.
-
-.. _issue 420: https://github.com/nedbat/coveragepy/issues/420
-.. _issue 438: https://github.com/nedbat/coveragepy/issues/438
-.. _issue 439: https://github.com/nedbat/coveragepy/issues/439
-.. _issue 443: https://github.com/nedbat/coveragepy/issues/443
-.. _issue 445: https://github.com/nedbat/coveragepy/issues/445
-
-
-.. _changes_402:
-
-Version 4.0.2 --- 2015-11-04
-----------------------------
-
-- More work on supporting unusually encoded source. Fixed `issue 431`_.
-
-- Files or directories with non-ASCII characters are now handled properly,
-  fixing `issue 432`_.
-
-- Setting a trace function with sys.settrace was broken by a change in 4.0.1,
-  as reported in `issue 436`_.  This is now fixed.
-
-- Officially support PyPy 4.0, which required no changes, just updates to the
-  docs.
-
-.. _issue 431: https://github.com/nedbat/coveragepy/issues/431
-.. _issue 432: https://github.com/nedbat/coveragepy/issues/432
-.. _issue 436: https://github.com/nedbat/coveragepy/issues/436
-
-
-.. _changes_401:
-
-Version 4.0.1 --- 2015-10-13
-----------------------------
-
-- When combining data files, unreadable files will now generate a warning
-  instead of failing the command.  This is more in line with the older
-  coverage.py v3.7.1 behavior, which silently ignored unreadable files.
-  Prompted by `issue 418`_.
-
-- The --skip-covered option would skip reporting on 100% covered files, but
-  also skipped them when calculating total coverage.  This was wrong, it should
-  only remove lines from the report, not change the final answer.  This is now
-  fixed, closing `issue 423`_.
-
-- In 4.0, the data file recorded a summary of the system on which it was run.
-  Combined data files would keep all of those summaries.  This could lead to
-  enormous data files consisting of mostly repetitive useless information. That
-  summary is now gone, fixing `issue 415`_.  If you want summary information,
-  get in touch, and we'll figure out a better way to do it.
-
-- Test suites that mocked os.path.exists would experience strange failures, due
-  to coverage.py using their mock inadvertently.  This is now fixed, closing
-  `issue 416`_.
-
-- Importing a ``__init__`` module explicitly would lead to an error:
-  ``AttributeError: 'module' object has no attribute '__path__'``, as reported
-  in `issue 410`_.  This is now fixed.
-
-- Code that uses ``sys.settrace(sys.gettrace())`` used to incur a more than 2x
-  speed penalty.  Now there's no penalty at all. Fixes `issue 397`_.
-
-- Pyexpat C code will no longer be recorded as a source file, fixing
-  `issue 419`_.
-
-- The source kit now contains all of the files needed to have a complete source
-  tree, re-fixing `issue 137`_ and closing `issue 281`_.
-
-.. _issue 281: https://github.com/nedbat/coveragepy/issues/281
-.. _issue 397: https://github.com/nedbat/coveragepy/issues/397
-.. _issue 410: https://github.com/nedbat/coveragepy/issues/410
-.. _issue 415: https://github.com/nedbat/coveragepy/issues/415
-.. _issue 416: https://github.com/nedbat/coveragepy/issues/416
-.. _issue 418: https://github.com/nedbat/coveragepy/issues/418
-.. _issue 419: https://github.com/nedbat/coveragepy/issues/419
-.. _issue 423: https://github.com/nedbat/coveragepy/issues/423
-
-
-.. _changes_40:
-
-Version 4.0 --- 2015-09-20
---------------------------
-
-No changes from 4.0b3
-
-
-Version 4.0b3 --- 2015-09-07
-----------------------------
-
-- Reporting on an unmeasured file would fail with a traceback.  This is now
-  fixed, closing `issue 403`_.
-
-- The Jenkins ShiningPanda_ plugin looks for an obsolete file name to find the
-  HTML reports to publish, so it was failing under coverage.py 4.0.  Now we
-  create that file if we are running under Jenkins, to keep things working
-  smoothly. `issue 404`_.
-
-- Kits used to include tests and docs, but didn't install them anywhere, or
-  provide all of the supporting tools to make them useful.  Kits no longer
-  include tests and docs.  If you were using them from the older packages, get
-  in touch and help me understand how.
-
-.. _issue 403: https://github.com/nedbat/coveragepy/issues/403
-.. _issue 404: https://github.com/nedbat/coveragepy/issues/404
-
-
-Version 4.0b2 --- 2015-08-22
-----------------------------
-
-- 4.0b1 broke ``--append`` creating new data files.  This is now fixed, closing
-  `issue 392`_.
-
-- ``py.test --cov`` can write empty data, then touch files due to ``--source``,
-  which made coverage.py mistakenly force the data file to record lines instead
-  of arcs.  This would lead to a "Can't combine line data with arc data" error
-  message.  This is now fixed, and changed some method names in the
-  CoverageData interface.  Fixes `issue 399`_.
-
-- `CoverageData.read_fileobj` and `CoverageData.write_fileobj` replace the
-  `.read` and `.write` methods, and are now properly inverses of each other.
+.. endchangesinclude
 
-- When using ``report --skip-covered``, a message will now be included in the
-  report output indicating how many files were skipped, and if all files are
-  skipped, coverage.py won't accidentally scold you for having no data to
-  report.  Thanks, Krystian Kichewko.
-
-- A new conversion utility has been added:  ``python -m coverage.pickle2json``
-  will convert v3.x pickle data files to v4.x JSON data files.  Thanks,
-  Alexander Todorov.  Closes `issue 395`_.
-
-- A new version identifier is available, `coverage.version_info`, a plain tuple
-  of values similar to `sys.version_info`_.
-
-.. _issue 392: https://github.com/nedbat/coveragepy/issues/392
-.. _issue 395: https://github.com/nedbat/coveragepy/issues/395
-.. _issue 399: https://github.com/nedbat/coveragepy/issues/399
-.. _sys.version_info: https://docs.python.org/3/library/sys.html#sys.version_info
-
-
-Version 4.0b1 --- 2015-08-02
-----------------------------
-
-- Coverage.py is now licensed under the Apache 2.0 license.  See NOTICE.txt for
-  details.  Closes `issue 313`_.
-
-- The data storage has been completely revamped.  The data file is now
-  JSON-based instead of a pickle, closing `issue 236`_.  The `CoverageData`
-  class is now a public supported documented API to the data file.
-
-- A new configuration option, ``[run] note``, lets you set a note that will be
-  stored in the `runs` section of the data file.  You can use this to annotate
-  the data file with any information you like.
-
-- Unrecognized configuration options will now print an error message and stop
-  coverage.py.  This should help prevent configuration mistakes from passing
-  silently.  Finishes `issue 386`_.
-
-- In parallel mode, ``coverage erase`` will now delete all of the data files,
-  fixing `issue 262`_.
-
-- Coverage.py now accepts a directory name for ``coverage run`` and will run a
-  ``__main__.py`` found there, just like Python will.  Fixes `issue 252`_.
-  Thanks, Dmitry Trofimov.
-
-- The XML report now includes a ``missing-branches`` attribute.  Thanks, Steve
-  Peak.  This is not a part of the Cobertura DTD, so the XML report no longer
-  references the DTD.
-
-- Missing branches in the HTML report now have a bit more information in the
-  right-hand annotations.  Hopefully this will make their meaning clearer.
-
-- All the reporting functions now behave the same if no data had been
-  collected, exiting with a status code of 1.  Fixed ``fail_under`` to be
-  applied even when the report is empty.  Thanks, Ionel Cristian Mărieș.
-
-- Plugins are now initialized differently.  Instead of looking for a class
-  called ``Plugin``, coverage.py looks for a function called ``coverage_init``.
-
-- A file-tracing plugin can now ask to have built-in Python reporting by
-  returning `"python"` from its `file_reporter()` method.
-
-- Code that was executed with `exec` would be mis-attributed to the file that
-  called it.  This is now fixed, closing `issue 380`_.
-
-- The ability to use item access on `Coverage.config` (introduced in 4.0a2) has
-  been changed to a more explicit `Coverage.get_option` and
-  `Coverage.set_option` API.
-
-- The ``Coverage.use_cache`` method is no longer supported.
-
-- The private method ``Coverage._harvest_data`` is now called
-  ``Coverage.get_data``, and returns the ``CoverageData`` containing the
-  collected data.
-
-- The project is consistently referred to as "coverage.py" throughout the code
-  and the documentation, closing `issue 275`_.
-
-- Combining data files with an explicit configuration file was broken in 4.0a6,
-  but now works again, closing `issue 385`_.
-
-- ``coverage combine`` now accepts files as well as directories.
-
-- The speed is back to 3.7.1 levels, after having slowed down due to plugin
-  support, finishing up `issue 387`_.
-
-.. _issue 236: https://github.com/nedbat/coveragepy/issues/236
-.. _issue 252: https://github.com/nedbat/coveragepy/issues/252
-.. _issue 262: https://github.com/nedbat/coveragepy/issues/262
-.. _issue 275: https://github.com/nedbat/coveragepy/issues/275
-.. _issue 313: https://github.com/nedbat/coveragepy/issues/313
-.. _issue 380: https://github.com/nedbat/coveragepy/issues/380
-.. _issue 385: https://github.com/nedbat/coveragepy/issues/385
-.. _issue 386: https://github.com/nedbat/coveragepy/issues/386
-.. _issue 387: https://github.com/nedbat/coveragepy/issues/387
-
-.. 40 issues closed in 4.0 below here
-
-
-Version 4.0a6 --- 2015-06-21
-----------------------------
-
-- Python 3.5b2 and PyPy 2.6.0 are supported.
-
-- The original module-level function interface to coverage.py is no longer
-  supported.  You must now create a ``coverage.Coverage`` object, and use
-  methods on it.
-
-- The ``coverage combine`` command now accepts any number of directories as
-  arguments, and will combine all the data files from those directories.  This
-  means you don't have to copy the files to one directory before combining.
-  Thanks, Christine Lytwynec.  Finishes `issue 354`_.
-
-- Branch coverage couldn't properly handle certain extremely long files. This
-  is now fixed (`issue 359`_).
-
-- Branch coverage didn't understand yield statements properly.  Mickie Betz
-  persisted in pursuing this despite Ned's pessimism.  Fixes `issue 308`_ and
-  `issue 324`_.
-
-- The COVERAGE_DEBUG environment variable can be used to set the
-  ``[run] debug`` configuration option to control what internal operations are
-  logged.
-
-- HTML reports were truncated at formfeed characters.  This is now fixed
-  (`issue 360`_).  It's always fun when the problem is due to a `bug in the
-  Python standard library <http://bugs.python.org/issue19035>`_.
-
-- Files with incorrect encoding declaration comments are no longer ignored by
-  the reporting commands, fixing `issue 351`_.
-
-- HTML reports now include a timestamp in the footer, closing `issue 299`_.
-  Thanks, Conrad Ho.
-
-- HTML reports now begrudgingly use double-quotes rather than single quotes,
-  because there are "software engineers" out there writing tools that read HTML
-  and somehow have no idea that single quotes exist.  Capitulates to the absurd
-  `issue 361`_.  Thanks, Jon Chappell.
-
-- The ``coverage annotate`` command now handles non-ASCII characters properly,
-  closing `issue 363`_.  Thanks, Leonardo Pistone.
-
-- Drive letters on Windows were not normalized correctly, now they are. Thanks,
-  Ionel Cristian Mărieș.
-
-- Plugin support had some bugs fixed, closing `issue 374`_ and `issue 375`_.
-  Thanks, Stefan Behnel.
-
-.. _issue 299: https://github.com/nedbat/coveragepy/issues/299
-.. _issue 308: https://github.com/nedbat/coveragepy/issues/308
-.. _issue 324: https://github.com/nedbat/coveragepy/issues/324
-.. _issue 351: https://github.com/nedbat/coveragepy/issues/351
-.. _issue 354: https://github.com/nedbat/coveragepy/issues/354
-.. _issue 359: https://github.com/nedbat/coveragepy/issues/359
-.. _issue 360: https://github.com/nedbat/coveragepy/issues/360
-.. _issue 361: https://github.com/nedbat/coveragepy/issues/361
-.. _issue 363: https://github.com/nedbat/coveragepy/issues/363
-.. _issue 374: https://github.com/nedbat/coveragepy/issues/374
-.. _issue 375: https://github.com/nedbat/coveragepy/issues/375
-
-
-Version 4.0a5 --- 2015-02-16
-----------------------------
-
-- Plugin support is now implemented in the C tracer instead of the Python
-  tracer. This greatly improves the speed of tracing projects using plugins.
-
-- Coverage.py now always adds the current directory to sys.path, so that
-  plugins can import files in the current directory (`issue 358`_).
-
-- If the `config_file` argument to the Coverage constructor is specified as
-  ".coveragerc", it is treated as if it were True.  This means setup.cfg is
-  also examined, and a missing file is not considered an error (`issue 357`_).
-
-- Wildly experimental: support for measuring processes started by the
-  multiprocessing module.  To use, set ``--concurrency=multiprocessing``,
-  either on the command line or in the .coveragerc file (`issue 117`_). Thanks,
-  Eduardo Schettino.  Currently, this does not work on Windows.
-
-- A new warning is possible, if a desired file isn't measured because it was
-  imported before coverage.py was started (`issue 353`_).
-
-- The `coverage.process_startup` function now will start coverage measurement
-  only once, no matter how many times it is called.  This fixes problems due
-  to unusual virtualenv configurations (`issue 340`_).
-
-- Added 3.5.0a1 to the list of supported CPython versions.
-
-.. _issue 117: https://github.com/nedbat/coveragepy/issues/117
-.. _issue 340: https://github.com/nedbat/coveragepy/issues/340
-.. _issue 353: https://github.com/nedbat/coveragepy/issues/353
-.. _issue 357: https://github.com/nedbat/coveragepy/issues/357
-.. _issue 358: https://github.com/nedbat/coveragepy/issues/358
-
-
-Version 4.0a4 --- 2015-01-25
-----------------------------
-
-- Plugins can now provide sys_info for debugging output.
-
-- Started plugins documentation.
-
-- Prepared to move the docs to readthedocs.org.
-
-
-Version 4.0a3 --- 2015-01-20
-----------------------------
-
-- Reports now use file names with extensions.  Previously, a report would
-  describe a/b/c.py as "a/b/c".  Now it is shown as "a/b/c.py".  This allows
-  for better support of non-Python files, and also fixed `issue 69`_.
-
-- The XML report now reports each directory as a package again.  This was a bad
-  regression, I apologize.  This was reported in `issue 235`_, which is now
-  fixed.
-
-- A new configuration option for the XML report: ``[xml] package_depth``
-  controls which directories are identified as packages in the report.
-  Directories deeper than this depth are not reported as packages.
-  The default is that all directories are reported as packages.
-  Thanks, Lex Berezhny.
-
-- When looking for the source for a frame, check if the file exists. On
-  Windows, .pyw files are no longer recorded as .py files. Along the way, this
-  fixed `issue 290`_.
-
-- Empty files are now reported as 100% covered in the XML report, not 0%
-  covered (`issue 345`_).
-
-- Regexes in the configuration file are now compiled as soon as they are read,
-  to provide error messages earlier (`issue 349`_).
-
-.. _issue 69: https://github.com/nedbat/coveragepy/issues/69
-.. _issue 235: https://github.com/nedbat/coveragepy/issues/235
-.. _issue 290: https://github.com/nedbat/coveragepy/issues/290
-.. _issue 345: https://github.com/nedbat/coveragepy/issues/345
-.. _issue 349: https://github.com/nedbat/coveragepy/issues/349
-
-
-Version 4.0a2 --- 2015-01-14
-----------------------------
-
-- Officially support PyPy 2.4, and PyPy3 2.4.  Drop support for
-  CPython 3.2 and older versions of PyPy.  The code won't work on CPython 3.2.
-  It will probably still work on older versions of PyPy, but I'm not testing
-  against them.
-
-- Plugins!
-
-- The original command line switches (`-x` to run a program, etc) are no
-  longer supported.
-
-- A new option: `coverage report --skip-covered` will reduce the number of
-  files reported by skipping files with 100% coverage.  Thanks, Krystian
-  Kichewko.  This means that empty `__init__.py` files will be skipped, since
-  they are 100% covered, closing `issue 315`_.
-
-- You can now specify the ``--fail-under`` option in the ``.coveragerc`` file
-  as the ``[report] fail_under`` option.  This closes `issue 314`_.
-
-- The ``COVERAGE_OPTIONS`` environment variable is no longer supported.  It was
-  a hack for ``--timid`` before configuration files were available.
-
-- The HTML report now has filtering.  Type text into the Filter box on the
-  index page, and only modules with that text in the name will be shown.
-  Thanks, Danny Allen.
-
-- The textual report and the HTML report used to report partial branches
-  differently for no good reason.  Now the text report's "missing branches"
-  column is a "partial branches" column so that both reports show the same
-  numbers.  This closes `issue 342`_.
-
-- If you specify a ``--rcfile`` that cannot be read, you will get an error
-  message.  Fixes `issue 343`_.
-
-- The ``--debug`` switch can now be used on any command.
-
-- You can now programmatically adjust the configuration of coverage.py by
-  setting items on `Coverage.config` after construction.
-
-- A module run with ``-m`` can be used as the argument to ``--source``, fixing
-  `issue 328`_.  Thanks, Buck Evan.
-
-- The regex for matching exclusion pragmas has been fixed to allow more kinds
-  of whitespace, fixing `issue 334`_.
-
-- Made some PyPy-specific tweaks to improve speed under PyPy.  Thanks, Alex
-  Gaynor.
-
-- In some cases, with a source file missing a final newline, coverage.py would
-  count statements incorrectly.  This is now fixed, closing `issue 293`_.
-
-- The status.dat file that HTML reports use to avoid re-creating files that
-  haven't changed is now a JSON file instead of a pickle file.  This obviates
-  `issue 287`_ and `issue 237`_.
-
-.. _issue 237: https://github.com/nedbat/coveragepy/issues/237
-.. _issue 287: https://github.com/nedbat/coveragepy/issues/287
-.. _issue 293: https://github.com/nedbat/coveragepy/issues/293
-.. _issue 314: https://github.com/nedbat/coveragepy/issues/314
-.. _issue 315: https://github.com/nedbat/coveragepy/issues/315
-.. _issue 328: https://github.com/nedbat/coveragepy/issues/328
-.. _issue 334: https://github.com/nedbat/coveragepy/issues/334
-.. _issue 342: https://github.com/nedbat/coveragepy/issues/342
-.. _issue 343: https://github.com/nedbat/coveragepy/issues/343
-
-
-Version 4.0a1 --- 2014-09-27
-----------------------------
-
-- Python versions supported are now CPython 2.6, 2.7, 3.2, 3.3, and 3.4, and
-  PyPy 2.2.
-
-- Gevent, eventlet, and greenlet are now supported, closing `issue 149`_.
-  The ``concurrency`` setting specifies the concurrency library in use.  Huge
-  thanks to Peter Portante for initial implementation, and to Joe Jevnik for
-  the final insight that completed the work.
-
-- Options are now also read from a setup.cfg file, if any.  Sections are
-  prefixed with "coverage:", so the ``[run]`` options will be read from the
-  ``[coverage:run]`` section of setup.cfg.  Finishes `issue 304`_.
-
-- The ``report -m`` command can now show missing branches when reporting on
-  branch coverage.  Thanks, Steve Leonard. Closes `issue 230`_.
-
-- The XML report now contains a <source> element, fixing `issue 94`_.  Thanks
-  Stan Hu.
-
-- The class defined in the coverage module is now called ``Coverage`` instead
-  of ``coverage``, though the old name still works, for backward compatibility.
-
-- The ``fail-under`` value is now rounded the same as reported results,
-  preventing paradoxical results, fixing `issue 284`_.
-
-- The XML report will now create the output directory if need be, fixing
-  `issue 285`_.  Thanks, Chris Rose.
-
-- HTML reports no longer raise UnicodeDecodeError if a Python file has
-  undecodable characters, fixing `issue 303`_ and `issue 331`_.
-
-- The annotate command will now annotate all files, not just ones relative to
-  the current directory, fixing `issue 57`_.
-
-- The coverage module no longer causes deprecation warnings on Python 3.4 by
-  importing the imp module, fixing `issue 305`_.
-
-- Encoding declarations in source files are only considered if they are truly
-  comments.  Thanks, Anthony Sottile.
-
-.. _issue 57: https://github.com/nedbat/coveragepy/issues/57
-.. _issue 94: https://github.com/nedbat/coveragepy/issues/94
-.. _issue 149: https://github.com/nedbat/coveragepy/issues/149
-.. _issue 230: https://github.com/nedbat/coveragepy/issues/230
-.. _issue 284: https://github.com/nedbat/coveragepy/issues/284
-.. _issue 285: https://github.com/nedbat/coveragepy/issues/285
-.. _issue 303: https://github.com/nedbat/coveragepy/issues/303
-.. _issue 304: https://github.com/nedbat/coveragepy/issues/304
-.. _issue 305: https://github.com/nedbat/coveragepy/issues/305
-.. _issue 331: https://github.com/nedbat/coveragepy/issues/331
-
-
-.. _changes_371:
-
-Version 3.7.1 --- 2013-12-13
-----------------------------
-
-- Improved the speed of HTML report generation by about 20%.
-
-- Fixed the mechanism for finding OS-installed static files for the HTML report
-  so that it will actually find OS-installed static files.
-
-
-.. _changes_37:
-
-Version 3.7 --- 2013-10-06
---------------------------
-
-- Added the ``--debug`` switch to ``coverage run``.  It accepts a list of
-  options indicating the type of internal activity to log to stderr.
-
-- Improved the branch coverage facility, fixing `issue 92`_ and `issue 175`_.
-
-- Running code with ``coverage run -m`` now behaves more like Python does,
-  setting sys.path properly, which fixes `issue 207`_ and `issue 242`_.
-
-- Coverage.py can now run .pyc files directly, closing `issue 264`_.
-
-- Coverage.py properly supports .pyw files, fixing `issue 261`_.
-
-- Omitting files within a tree specified with the ``source`` option would
-  cause them to be incorrectly marked as unexecuted, as described in
-  `issue 218`_.  This is now fixed.
-
-- When specifying paths to alias together during data combining, you can now
-  specify relative paths, fixing `issue 267`_.
-
-- Most file paths can now be specified with username expansion (``~/src``, or
-  ``~build/src``, for example), and with environment variable expansion
-  (``build/$BUILDNUM/src``).
-
-- Trying to create an XML report with no files to report on, would cause a
-  ZeroDivideError, but no longer does, fixing `issue 250`_.
-
-- When running a threaded program under the Python tracer, coverage.py no
-  longer issues a spurious warning about the trace function changing: "Trace
-  function changed, measurement is likely wrong: None."  This fixes `issue
-  164`_.
-
-- Static files necessary for HTML reports are found in system-installed places,
-  to ease OS-level packaging of coverage.py.  Closes `issue 259`_.
-
-- Source files with encoding declarations, but a blank first line, were not
-  decoded properly.  Now they are.  Thanks, Roger Hu.
-
-- The source kit now includes the ``__main__.py`` file in the root coverage
-  directory, fixing `issue 255`_.
-
-.. _issue 92: https://github.com/nedbat/coveragepy/issues/92
-.. _issue 164: https://github.com/nedbat/coveragepy/issues/164
-.. _issue 175: https://github.com/nedbat/coveragepy/issues/175
-.. _issue 207: https://github.com/nedbat/coveragepy/issues/207
-.. _issue 242: https://github.com/nedbat/coveragepy/issues/242
-.. _issue 218: https://github.com/nedbat/coveragepy/issues/218
-.. _issue 250: https://github.com/nedbat/coveragepy/issues/250
-.. _issue 255: https://github.com/nedbat/coveragepy/issues/255
-.. _issue 259: https://github.com/nedbat/coveragepy/issues/259
-.. _issue 261: https://github.com/nedbat/coveragepy/issues/261
-.. _issue 264: https://github.com/nedbat/coveragepy/issues/264
-.. _issue 267: https://github.com/nedbat/coveragepy/issues/267
-
-
-.. _changes_36:
-
-Version 3.6 --- 2013-01-05
---------------------------
-
-- Added a page to the docs about troublesome situations, closing `issue 226`_,
-  and added some info to the TODO file, closing `issue 227`_.
-
-.. _issue 226: https://github.com/nedbat/coveragepy/issues/226
-.. _issue 227: https://github.com/nedbat/coveragepy/issues/227
-
-
-Version 3.6b3 --- 2012-12-29
-----------------------------
-
-- Beta 2 broke the nose plugin. It's fixed again, closing `issue 224`_.
-
-.. _issue 224: https://github.com/nedbat/coveragepy/issues/224
-
-
-Version 3.6b2 --- 2012-12-23
-----------------------------
-
-- Coverage.py runs on Python 2.3 and 2.4 again. It was broken in 3.6b1.
-
-- The C extension is optionally compiled using a different more widely-used
-  technique, taking another stab at fixing `issue 80`_ once and for all.
-
-- Combining data files would create entries for phantom files if used with
-  ``source`` and path aliases.  It no longer does.
-
-- ``debug sys`` now shows the configuration file path that was read.
-
-- If an oddly-behaved package claims that code came from an empty-string
-  file name, coverage.py no longer associates it with the directory name,
-  fixing `issue 221`_.
-
-.. _issue 221: https://github.com/nedbat/coveragepy/issues/221
-
-
-Version 3.6b1 --- 2012-11-28
-----------------------------
-
-- Wildcards in ``include=`` and ``omit=`` arguments were not handled properly
-  in reporting functions, though they were when running.  Now they are handled
-  uniformly, closing `issue 143`_ and `issue 163`_.  **NOTE**: it is possible
-  that your configurations may now be incorrect.  If you use ``include`` or
-  ``omit`` during reporting, whether on the command line, through the API, or
-  in a configuration file, please check carefully that you were not relying on
-  the old broken behavior.
-
-- The **report**, **html**, and **xml** commands now accept a ``--fail-under``
-  switch that indicates in the exit status whether the coverage percentage was
-  less than a particular value.  Closes `issue 139`_.
-
-- The reporting functions coverage.report(), coverage.html_report(), and
-  coverage.xml_report() now all return a float, the total percentage covered
-  measurement.
-
-- The HTML report's title can now be set in the configuration file, with the
-  ``--title`` switch on the command line, or via the API.
-
-- Configuration files now support substitution of environment variables, using
-  syntax like ``${WORD}``.  Closes `issue 97`_.
-
-- Embarrassingly, the ``[xml] output=`` setting in the .coveragerc file simply
-  didn't work.  Now it does.
-
-- The XML report now consistently uses file names for the file name attribute,
-  rather than sometimes using module names.  Fixes `issue 67`_.
-  Thanks, Marcus Cobden.
-
-- Coverage percentage metrics are now computed slightly differently under
-  branch coverage.  This means that completely unexecuted files will now
-  correctly have 0% coverage, fixing `issue 156`_.  This also means that your
-  total coverage numbers will generally now be lower if you are measuring
-  branch coverage.
-
-- When installing, now in addition to creating a "coverage" command, two new
-  aliases are also installed.  A "coverage2" or "coverage3" command will be
-  created, depending on whether you are installing in Python 2.x or 3.x.
-  A "coverage-X.Y" command will also be created corresponding to your specific
-  version of Python.  Closes `issue 111`_.
-
-- The coverage.py installer no longer tries to bootstrap setuptools or
-  Distribute.  You must have one of them installed first, as `issue 202`_
-  recommended.
-
-- The coverage.py kit now includes docs (closing `issue 137`_) and tests.
-
-- On Windows, files are now reported in their correct case, fixing `issue 89`_
-  and `issue 203`_.
-
-- If a file is missing during reporting, the path shown in the error message
-  is now correct, rather than an incorrect path in the current directory.
-  Fixes `issue 60`_.
-
-- Running an HTML report in Python 3 in the same directory as an old Python 2
-  HTML report would fail with a UnicodeDecodeError. This issue (`issue 193`_)
-  is now fixed.
-
-- Fixed yet another error trying to parse non-Python files as Python, this
-  time an IndentationError, closing `issue 82`_ for the fourth time...
-
-- If `coverage xml` fails because there is no data to report, it used to
-  create a zero-length XML file.  Now it doesn't, fixing `issue 210`_.
-
-- Jython files now work with the ``--source`` option, fixing `issue 100`_.
-
-- Running coverage.py under a debugger is unlikely to work, but it shouldn't
-  fail with "TypeError: 'NoneType' object is not iterable".  Fixes `issue
-  201`_.
-
-- On some Linux distributions, when installed with the OS package manager,
-  coverage.py would report its own code as part of the results.  Now it won't,
-  fixing `issue 214`_, though this will take some time to be repackaged by the
-  operating systems.
-
-- Docstrings for the legacy singleton methods are more helpful.  Thanks Marius
-  Gedminas.  Closes `issue 205`_.
-
-- The pydoc tool can now show documentation for the class `coverage.coverage`.
-  Closes `issue 206`_.
-
-- Added a page to the docs about contributing to coverage.py, closing
-  `issue 171`_.
-
-- When coverage.py ended unsuccessfully, it may have reported odd errors like
-  ``'NoneType' object has no attribute 'isabs'``.  It no longer does,
-  so kiss `issue 153`_ goodbye.
-
-.. _issue 60: https://github.com/nedbat/coveragepy/issues/60
-.. _issue 67: https://github.com/nedbat/coveragepy/issues/67
-.. _issue 89: https://github.com/nedbat/coveragepy/issues/89
-.. _issue 97: https://github.com/nedbat/coveragepy/issues/97
-.. _issue 100: https://github.com/nedbat/coveragepy/issues/100
-.. _issue 111: https://github.com/nedbat/coveragepy/issues/111
-.. _issue 137: https://github.com/nedbat/coveragepy/issues/137
-.. _issue 139: https://github.com/nedbat/coveragepy/issues/139
-.. _issue 143: https://github.com/nedbat/coveragepy/issues/143
-.. _issue 153: https://github.com/nedbat/coveragepy/issues/153
-.. _issue 156: https://github.com/nedbat/coveragepy/issues/156
-.. _issue 163: https://github.com/nedbat/coveragepy/issues/163
-.. _issue 171: https://github.com/nedbat/coveragepy/issues/171
-.. _issue 193: https://github.com/nedbat/coveragepy/issues/193
-.. _issue 201: https://github.com/nedbat/coveragepy/issues/201
-.. _issue 202: https://github.com/nedbat/coveragepy/issues/202
-.. _issue 203: https://github.com/nedbat/coveragepy/issues/203
-.. _issue 205: https://github.com/nedbat/coveragepy/issues/205
-.. _issue 206: https://github.com/nedbat/coveragepy/issues/206
-.. _issue 210: https://github.com/nedbat/coveragepy/issues/210
-.. _issue 214: https://github.com/nedbat/coveragepy/issues/214
-
-
-.. _changes_353:
-
-Version 3.5.3 --- 2012-09-29
-----------------------------
-
-- Line numbers in the HTML report line up better with the source lines, fixing
-  `issue 197`_, thanks Marius Gedminas.
-
-- When specifying a directory as the source= option, the directory itself no
-  longer needs to have a ``__init__.py`` file, though its sub-directories do,
-  to be considered as source files.
-
-- Files encoded as UTF-8 with a BOM are now properly handled, fixing
-  `issue 179`_.  Thanks, Pablo Carballo.
-
-- Fixed more cases of non-Python files being reported as Python source, and
-  then not being able to parse them as Python.  Closes `issue 82`_ (again).
-  Thanks, Julian Berman.
-
-- Fixed memory leaks under Python 3, thanks, Brett Cannon. Closes `issue 147`_.
-
-- Optimized .pyo files may not have been handled correctly, `issue 195`_.
-  Thanks, Marius Gedminas.
-
-- Certain unusually named file paths could have been mangled during reporting,
-  `issue 194`_.  Thanks, Marius Gedminas.
-
-- Try to do a better job of the impossible task of detecting when we can't
-  build the C extension, fixing `issue 183`_.
-
-- Testing is now done with `tox`_, thanks, Marc Abramowitz.
-
-.. _issue 147: https://github.com/nedbat/coveragepy/issues/147
-.. _issue 179: https://github.com/nedbat/coveragepy/issues/179
-.. _issue 183: https://github.com/nedbat/coveragepy/issues/183
-.. _issue 194: https://github.com/nedbat/coveragepy/issues/194
-.. _issue 195: https://github.com/nedbat/coveragepy/issues/195
-.. _issue 197: https://github.com/nedbat/coveragepy/issues/197
-.. _tox: https://tox.readthedocs.io/
-
-
-.. _changes_352:
-
-Version 3.5.2 --- 2012-05-04
-----------------------------
-
-No changes since 3.5.2.b1
-
-
-Version 3.5.2b1 --- 2012-04-29
-------------------------------
-
-- The HTML report has slightly tweaked controls: the buttons at the top of
-  the page are color-coded to the source lines they affect.
-
-- Custom CSS can be applied to the HTML report by specifying a CSS file as
-  the ``extra_css`` configuration value in the ``[html]`` section.
-
-- Source files with custom encodings declared in a comment at the top are now
-  properly handled during reporting on Python 2.  Python 3 always handled them
-  properly.  This fixes `issue 157`_.
-
-- Backup files left behind by editors are no longer collected by the source=
-  option, fixing `issue 168`_.
-
-- If a file doesn't parse properly as Python, we don't report it as an error
-  if the file name seems like maybe it wasn't meant to be Python.  This is a
-  pragmatic fix for `issue 82`_.
-
-- The ``-m`` switch on ``coverage report``, which includes missing line numbers
-  in the summary report, can now be specified as ``show_missing`` in the
-  config file.  Closes `issue 173`_.
-
-- When running a module with ``coverage run -m <modulename>``, certain details
-  of the execution environment weren't the same as for
-  ``python -m <modulename>``.  This had the unfortunate side-effect of making
-  ``coverage run -m unittest discover`` not work if you had tests in a
-  directory named "test".  This fixes `issue 155`_ and `issue 142`_.
-
-- Now the exit status of your product code is properly used as the process
-  status when running ``python -m coverage run ...``.  Thanks, JT Olds.
-
-- When installing into pypy, we no longer attempt (and fail) to compile
-  the C tracer function, closing `issue 166`_.
-
-.. _issue 142: https://github.com/nedbat/coveragepy/issues/142
-.. _issue 155: https://github.com/nedbat/coveragepy/issues/155
-.. _issue 157: https://github.com/nedbat/coveragepy/issues/157
-.. _issue 166: https://github.com/nedbat/coveragepy/issues/166
-.. _issue 168: https://github.com/nedbat/coveragepy/issues/168
-.. _issue 173: https://github.com/nedbat/coveragepy/issues/173
-
-
-.. _changes_351:
-
-Version 3.5.1 --- 2011-09-23
-----------------------------
-
-- The ``[paths]`` feature unfortunately didn't work in real world situations
-  where you wanted to, you know, report on the combined data.  Now all paths
-  stored in the combined file are canonicalized properly.
-
-
-Version 3.5.1b1 --- 2011-08-28
-------------------------------
-
-- When combining data files from parallel runs, you can now instruct
-  coverage.py about which directories are equivalent on different machines.  A
-  ``[paths]`` section in the configuration file lists paths that are to be
-  considered equivalent.  Finishes `issue 17`_.
+Older changes
+-------------
 
-- for-else constructs are understood better, and don't cause erroneous partial
-  branch warnings.  Fixes `issue 122`_.
-
-- Branch coverage for ``with`` statements is improved, fixing `issue 128`_.
-
-- The number of partial branches reported on the HTML summary page was
-  different than the number reported on the individual file pages.  This is
-  now fixed.
-
-- An explicit include directive to measure files in the Python installation
-  wouldn't work because of the standard library exclusion.  Now the include
-  directive takes precedence, and the files will be measured.  Fixes
-  `issue 138`_.
-
-- The HTML report now handles Unicode characters in Python source files
-  properly.  This fixes `issue 124`_ and `issue 144`_. Thanks, Devin
-  Jeanpierre.
-
-- In order to help the core developers measure the test coverage of the
-  standard library, Brandon Rhodes devised an aggressive hack to trick Python
-  into running some coverage.py code before anything else in the process.
-  See the coverage/fullcoverage directory if you are interested.
-
-.. _issue 17: https://github.com/nedbat/coveragepy/issues/17
-.. _issue 122: https://github.com/nedbat/coveragepy/issues/122
-.. _issue 124: https://github.com/nedbat/coveragepy/issues/124
-.. _issue 128: https://github.com/nedbat/coveragepy/issues/128
-.. _issue 138: https://github.com/nedbat/coveragepy/issues/138
-.. _issue 144: https://github.com/nedbat/coveragepy/issues/144
-
-
-.. _changes_35:
-
-Version 3.5 --- 2011-06-29
---------------------------
-
-- The HTML report hotkeys now behave slightly differently when the current
-  chunk isn't visible at all:  a chunk on the screen will be selected,
-  instead of the old behavior of jumping to the literal next chunk.
-  The hotkeys now work in Google Chrome.  Thanks, Guido van Rossum.
-
-
-Version 3.5b1 --- 2011-06-05
-----------------------------
-
-- The HTML report now has hotkeys.  Try ``n``, ``s``, ``m``, ``x``, ``b``,
-  ``p``, and ``c`` on the overview page to change the column sorting.
-  On a file page, ``r``, ``m``, ``x``, and ``p`` toggle the run, missing,
-  excluded, and partial line markings.  You can navigate the highlighted
-  sections of code by using the ``j`` and ``k`` keys for next and previous.
-  The ``1`` (one) key jumps to the first highlighted section in the file,
-  and ``0`` (zero) scrolls to the top of the file.
-
-- The ``--omit`` and ``--include`` switches now interpret their values more
-  usefully.  If the value starts with a wildcard character, it is used as-is.
-  If it does not, it is interpreted relative to the current directory.
-  Closes `issue 121`_.
-
-- Partial branch warnings can now be pragma'd away.  The configuration option
-  ``partial_branches`` is a list of regular expressions.  Lines matching any of
-  those expressions will never be marked as a partial branch.  In addition,
-  there's a built-in list of regular expressions marking statements which
-  should never be marked as partial.  This list includes ``while True:``,
-  ``while 1:``, ``if 1:``, and ``if 0:``.
-
-- The ``coverage()`` constructor accepts single strings for the ``omit=`` and
-  ``include=`` arguments, adapting to a common error in programmatic use.
-
-- Modules can now be run directly using ``coverage run -m modulename``, to
-  mirror Python's ``-m`` flag.  Closes `issue 95`_, thanks, Brandon Rhodes.
-
-- ``coverage run`` didn't emulate Python accurately in one small detail: the
-  current directory inserted into ``sys.path`` was relative rather than
-  absolute. This is now fixed.
-
-- HTML reporting is now incremental: a record is kept of the data that
-  produced the HTML reports, and only files whose data has changed will
-  be generated.  This should make most HTML reporting faster.
-
-- Pathological code execution could disable the trace function behind our
-  backs, leading to incorrect code measurement.  Now if this happens,
-  coverage.py will issue a warning, at least alerting you to the problem.
-  Closes `issue 93`_.  Thanks to Marius Gedminas for the idea.
-
-- The C-based trace function now behaves properly when saved and restored
-  with ``sys.gettrace()`` and ``sys.settrace()``.  This fixes `issue 125`_
-  and `issue 123`_.  Thanks, Devin Jeanpierre.
-
-- Source files are now opened with Python 3.2's ``tokenize.open()`` where
-  possible, to get the best handling of Python source files with encodings.
-  Closes `issue 107`_, thanks, Brett Cannon.
-
-- Syntax errors in supposed Python files can now be ignored during reporting
-  with the ``-i`` switch just like other source errors.  Closes `issue 115`_.
-
-- Installation from source now succeeds on machines without a C compiler,
-  closing `issue 80`_.
-
-- Coverage.py can now be run directly from a working tree by specifying
-  the directory name to python:  ``python coverage_py_working_dir run ...``.
-  Thanks, Brett Cannon.
-
-- A little bit of Jython support: `coverage run` can now measure Jython
-  execution by adapting when $py.class files are traced. Thanks, Adi Roiban.
-  Jython still doesn't provide the Python libraries needed to make
-  coverage reporting work, unfortunately.
-
-- Internally, files are now closed explicitly, fixing `issue 104`_.  Thanks,
-  Brett Cannon.
-
-.. _issue 80: https://github.com/nedbat/coveragepy/issues/80
-.. _issue 93: https://github.com/nedbat/coveragepy/issues/93
-.. _issue 95: https://github.com/nedbat/coveragepy/issues/95
-.. _issue 104: https://github.com/nedbat/coveragepy/issues/104
-.. _issue 107: https://github.com/nedbat/coveragepy/issues/107
-.. _issue 115: https://github.com/nedbat/coveragepy/issues/115
-.. _issue 121: https://github.com/nedbat/coveragepy/issues/121
-.. _issue 123: https://github.com/nedbat/coveragepy/issues/123
-.. _issue 125: https://github.com/nedbat/coveragepy/issues/125
-
-
-.. _changes_34:
-
-Version 3.4 --- 2010-09-19
---------------------------
-
-- The XML report is now sorted by package name, fixing `issue 88`_.
-
-- Programs that exited with ``sys.exit()`` with no argument weren't handled
-  properly, producing a coverage.py stack trace.  That is now fixed.
-
-.. _issue 88: https://github.com/nedbat/coveragepy/issues/88
-
-
-Version 3.4b2 --- 2010-09-06
-----------------------------
-
-- Completely unexecuted files can now be included in coverage results, reported
-  as 0% covered.  This only happens if the --source option is specified, since
-  coverage.py needs guidance about where to look for source files.
-
-- The XML report output now properly includes a percentage for branch coverage,
-  fixing `issue 65`_ and `issue 81`_.
-
-- Coverage percentages are now displayed uniformly across reporting methods.
-  Previously, different reports could round percentages differently.  Also,
-  percentages are only reported as 0% or 100% if they are truly 0 or 100, and
-  are rounded otherwise.  Fixes `issue 41`_ and `issue 70`_.
-
-- The precision of reported coverage percentages can be set with the
-  ``[report] precision`` config file setting.  Completes `issue 16`_.
-
-- Threads derived from ``threading.Thread`` with an overridden `run` method
-  would report no coverage for the `run` method.  This is now fixed, closing
-  `issue 85`_.
-
-.. _issue 16: https://github.com/nedbat/coveragepy/issues/16
-.. _issue 41: https://github.com/nedbat/coveragepy/issues/41
-.. _issue 65: https://github.com/nedbat/coveragepy/issues/65
-.. _issue 70: https://github.com/nedbat/coveragepy/issues/70
-.. _issue 81: https://github.com/nedbat/coveragepy/issues/81
-.. _issue 85: https://github.com/nedbat/coveragepy/issues/85
-
-
-Version 3.4b1 --- 2010-08-21
-----------------------------
-
-- BACKWARD INCOMPATIBILITY: the ``--omit`` and ``--include`` switches now take
-  file patterns rather than file prefixes, closing `issue 34`_ and `issue 36`_.
-
-- BACKWARD INCOMPATIBILITY: the `omit_prefixes` argument is gone throughout
-  coverage.py, replaced with `omit`, a list of file name patterns suitable for
-  `fnmatch`.  A parallel argument `include` controls what files are included.
-
-- The run command now has a ``--source`` switch, a list of directories or
-  module names.  If provided, coverage.py will only measure execution in those
-  source files.
-
-- Various warnings are printed to stderr for problems encountered during data
-  measurement: if a ``--source`` module has no Python source to measure, or is
-  never encountered at all, or if no data is collected.
-
-- The reporting commands (report, annotate, html, and xml) now have an
-  ``--include`` switch to restrict reporting to modules matching those file
-  patterns, similar to the existing ``--omit`` switch. Thanks, Zooko.
-
-- The run command now supports ``--include`` and ``--omit`` to control what
-  modules it measures. This can speed execution and reduce the amount of data
-  during reporting. Thanks Zooko.
-
-- Since coverage.py 3.1, using the Python trace function has been slower than
-  it needs to be.  A cache of tracing decisions was broken, but has now been
-  fixed.
-
-- Python 2.7 and 3.2 have introduced new opcodes that are now supported.
-
-- Python files with no statements, for example, empty ``__init__.py`` files,
-  are now reported as having zero statements instead of one.  Fixes `issue 1`_.
-
-- Reports now have a column of missed line counts rather than executed line
-  counts, since developers should focus on reducing the missed lines to zero,
-  rather than increasing the executed lines to varying targets.  Once
-  suggested, this seemed blindingly obvious.
-
-- Line numbers in HTML source pages are clickable, linking directly to that
-  line, which is highlighted on arrival.  Added a link back to the index page
-  at the bottom of each HTML page.
-
-- Programs that call ``os.fork`` will properly collect data from both the child
-  and parent processes.  Use ``coverage run -p`` to get two data files that can
-  be combined with ``coverage combine``.  Fixes `issue 56`_.
-
-- Coverage.py is now runnable as a module: ``python -m coverage``.  Thanks,
-  Brett Cannon.
-
-- When measuring code running in a virtualenv, most of the system library was
-  being measured when it shouldn't have been.  This is now fixed.
-
-- Doctest text files are no longer recorded in the coverage data, since they
-  can't be reported anyway.  Fixes `issue 52`_ and `issue 61`_.
-
-- Jinja HTML templates compile into Python code using the HTML file name,
-  which confused coverage.py.  Now these files are no longer traced, fixing
-  `issue 82`_.
-
-- Source files can have more than one dot in them (foo.test.py), and will be
-  treated properly while reporting.  Fixes `issue 46`_.
-
-- Source files with DOS line endings are now properly tokenized for syntax
-  coloring on non-DOS machines.  Fixes `issue 53`_.
-
-- Unusual code structure that confused exits from methods with exits from
-  classes is now properly analyzed.  See `issue 62`_.
-
-- Asking for an HTML report with no files now shows a nice error message rather
-  than a cryptic failure ('int' object is unsubscriptable). Fixes `issue 59`_.
-
-.. _issue 1:  https://github.com/nedbat/coveragepy/issues/1
-.. _issue 34: https://github.com/nedbat/coveragepy/issues/34
-.. _issue 36: https://github.com/nedbat/coveragepy/issues/36
-.. _issue 46: https://github.com/nedbat/coveragepy/issues/46
-.. _issue 53: https://github.com/nedbat/coveragepy/issues/53
-.. _issue 52: https://github.com/nedbat/coveragepy/issues/52
-.. _issue 56: https://github.com/nedbat/coveragepy/issues/56
-.. _issue 61: https://github.com/nedbat/coveragepy/issues/61
-.. _issue 62: https://github.com/nedbat/coveragepy/issues/62
-.. _issue 59: https://github.com/nedbat/coveragepy/issues/59
-.. _issue 82: https://github.com/nedbat/coveragepy/issues/82
-
-
-.. _changes_331:
-
-Version 3.3.1 --- 2010-03-06
-----------------------------
-
-- Using `parallel=True` in .coveragerc file prevented reporting, but now does
-  not, fixing `issue 49`_.
-
-- When running your code with "coverage run", if you call `sys.exit()`,
-  coverage.py will exit with that status code, fixing `issue 50`_.
-
-.. _issue 49: https://github.com/nedbat/coveragepy/issues/49
-.. _issue 50: https://github.com/nedbat/coveragepy/issues/50
-
-
-.. _changes_33:
-
-Version 3.3 --- 2010-02-24
---------------------------
-
-- Settings are now read from a .coveragerc file.  A specific file can be
-  specified on the command line with --rcfile=FILE.  The name of the file can
-  be programmatically set with the `config_file` argument to the coverage()
-  constructor, or reading a config file can be disabled with
-  `config_file=False`.
-
-- Fixed a problem with nested loops having their branch possibilities
-  mischaracterized: `issue 39`_.
-
-- Added coverage.process_start to enable coverage measurement when Python
-  starts.
-
-- Parallel data file names now have a random number appended to them in
-  addition to the machine name and process id.
-
-- Parallel data files combined with "coverage combine" are deleted after
-  they're combined, to clean up unneeded files.  Fixes `issue 40`_.
-
-- Exceptions thrown from product code run with "coverage run" are now displayed
-  without internal coverage.py frames, so the output is the same as when the
-  code is run without coverage.py.
-
-- The `data_suffix` argument to the coverage constructor is now appended with
-  an added dot rather than simply appended, so that .coveragerc files will not
-  be confused for data files.
-
-- Python source files that don't end with a newline can now be executed, fixing
-  `issue 47`_.
-
-- Added an AUTHORS.txt file.
-
-.. _issue 39: https://github.com/nedbat/coveragepy/issues/39
-.. _issue 40: https://github.com/nedbat/coveragepy/issues/40
-.. _issue 47: https://github.com/nedbat/coveragepy/issues/47
-
-
-.. _changes_32:
-
-Version 3.2 --- 2009-12-05
---------------------------
-
-- Added a ``--version`` option on the command line.
-
-
-Version 3.2b4 --- 2009-12-01
-----------------------------
-
-- Branch coverage improvements:
-
-  - The XML report now includes branch information.
-
-- Click-to-sort HTML report columns are now persisted in a cookie.  Viewing
-  a report will sort it first the way you last had a coverage report sorted.
-  Thanks, `Chris Adams`_.
-
-- On Python 3.x, setuptools has been replaced by `Distribute`_.
-
-.. _Distribute: https://pypi.org/project/distribute/
-
-
-Version 3.2b3 --- 2009-11-23
-----------------------------
-
-- Fixed a memory leak in the C tracer that was introduced in 3.2b1.
-
-- Branch coverage improvements:
-
-  - Branches to excluded code are ignored.
-
-- The table of contents in the HTML report is now sortable: click the headers
-  on any column.  Thanks, `Chris Adams`_.
-
-.. _Chris Adams: http://chris.improbable.org
-
-
-Version 3.2b2 --- 2009-11-19
-----------------------------
+The complete history is available in the `coverage.py docs`__.
 
-- Branch coverage improvements:
-
-  - Classes are no longer incorrectly marked as branches: `issue 32`_.
-
-  - "except" clauses with types are no longer incorrectly marked as branches:
-    `issue 35`_.
-
-- Fixed some problems syntax coloring sources with line continuations and
-  source with tabs: `issue 30`_ and `issue 31`_.
-
-- The --omit option now works much better than before, fixing `issue 14`_ and
-  `issue 33`_.  Thanks, Danek Duvall.
-
-.. _issue 14: https://github.com/nedbat/coveragepy/issues/14
-.. _issue 30: https://github.com/nedbat/coveragepy/issues/30
-.. _issue 31: https://github.com/nedbat/coveragepy/issues/31
-.. _issue 32: https://github.com/nedbat/coveragepy/issues/32
-.. _issue 33: https://github.com/nedbat/coveragepy/issues/33
-.. _issue 35: https://github.com/nedbat/coveragepy/issues/35
-
-
-Version 3.2b1 --- 2009-11-10
-----------------------------
-
-- Branch coverage!
-
-- XML reporting has file paths that let Cobertura find the source code.
-
-- The tracer code has changed, it's a few percent faster.
-
-- Some exceptions reported by the command line interface have been cleaned up
-  so that tracebacks inside coverage.py aren't shown.  Fixes `issue 23`_.
-
-.. _issue 23: https://github.com/nedbat/coveragepy/issues/23
-
-
-.. _changes_31:
-
-Version 3.1 --- 2009-10-04
---------------------------
-
-- Source code can now be read from eggs.  Thanks, Ross Lawley.  Fixes
-  `issue 25`_.
-
-.. _issue 25: https://github.com/nedbat/coveragepy/issues/25
-
-
-Version 3.1b1 --- 2009-09-27
-----------------------------
-
-- Python 3.1 is now supported.
-
-- Coverage.py has a new command line syntax with sub-commands.  This expands
-  the possibilities for adding features and options in the future.  The old
-  syntax is still supported.  Try "coverage help" to see the new commands.
-  Thanks to Ben Finney for early help.
-
-- Added an experimental "coverage xml" command for producing coverage reports
-  in a Cobertura-compatible XML format.  Thanks, Bill Hart.
-
-- Added the --timid option to enable a simpler slower trace function that works
-  for DecoratorTools projects, including TurboGears.  Fixed `issue 12`_ and
-  `issue 13`_.
-
-- HTML reports show modules from other directories.  Fixed `issue 11`_.
-
-- HTML reports now display syntax-colored Python source.
-
-- Programs that change directory will still write .coverage files in the
-  directory where execution started.  Fixed `issue 24`_.
-
-- Added a "coverage debug" command for getting diagnostic information about the
-  coverage.py installation.
-
-.. _issue 11: https://github.com/nedbat/coveragepy/issues/11
-.. _issue 12: https://github.com/nedbat/coveragepy/issues/12
-.. _issue 13: https://github.com/nedbat/coveragepy/issues/13
-.. _issue 24: https://github.com/nedbat/coveragepy/issues/24
-
-
-.. _changes_301:
-
-Version 3.0.1 --- 2009-07-07
-----------------------------
-
-- Removed the recursion limit in the tracer function.  Previously, code that
-  ran more than 500 frames deep would crash. Fixed `issue 9`_.
-
-- Fixed a bizarre problem involving pyexpat, whereby lines following XML parser
-  invocations could be overlooked.  Fixed `issue 10`_.
-
-- On Python 2.3, coverage.py could mis-measure code with exceptions being
-  raised.  This is now fixed.
-
-- The coverage.py code itself will now not be measured by coverage.py, and no
-  coverage.py modules will be mentioned in the nose --with-cover plug-in.
-  Fixed `issue 8`_.
-
-- When running source files, coverage.py now opens them in universal newline
-  mode just like Python does.  This lets it run Windows files on Mac, for
-  example.
-
-.. _issue 9: https://github.com/nedbat/coveragepy/issues/9
-.. _issue 10: https://github.com/nedbat/coveragepy/issues/10
-.. _issue 8: https://github.com/nedbat/coveragepy/issues/8
-
-
-.. _changes_30:
-
-Version 3.0 --- 2009-06-13
---------------------------
-
-- Fixed the way the Python library was ignored.  Too much code was being
-  excluded the old way.
-
-- Tabs are now properly converted in HTML reports.  Previously indentation was
-  lost.  Fixed `issue 6`_.
-
-- Nested modules now get a proper flat_rootname.  Thanks, Christian Heimes.
-
-.. _issue 6: https://github.com/nedbat/coveragepy/issues/6
-
-
-Version 3.0b3 --- 2009-05-16
-----------------------------
-
-- Added parameters to coverage.__init__ for options that had been set on the
-  coverage object itself.
-
-- Added clear_exclude() and get_exclude_list() methods for programmatic
-  manipulation of the exclude regexes.
-
-- Added coverage.load() to read previously-saved data from the data file.
-
-- Improved the finding of code files.  For example, .pyc files that have been
-  installed after compiling are now located correctly.  Thanks, Detlev
-  Offenbach.
-
-- When using the object API (that is, constructing a coverage() object), data
-  is no longer saved automatically on process exit.  You can re-enable it with
-  the auto_data=True parameter on the coverage() constructor. The module-level
-  interface still uses automatic saving.
-
-
-Version 3.0b --- 2009-04-30
----------------------------
-
-HTML reporting, and continued refactoring.
-
-- HTML reports and annotation of source files: use the new -b (browser) switch.
-  Thanks to George Song for code, inspiration and guidance.
-
-- Code in the Python standard library is not measured by default.  If you need
-  to measure standard library code, use the -L command-line switch during
-  execution, or the cover_pylib=True argument to the coverage() constructor.
-
-- Source annotation into a directory (-a -d) behaves differently.  The
-  annotated files are named with their hierarchy flattened so that same-named
-  files from different directories no longer collide.  Also, only files in the
-  current tree are included.
-
-- coverage.annotate_file is no longer available.
-
-- Programs executed with -x now behave more as they should, for example,
-  __file__ has the correct value.
-
-- .coverage data files have a new pickle-based format designed for better
-  extensibility.
-
-- Removed the undocumented cache_file argument to coverage.usecache().
-
-
-Version 3.0b1 --- 2009-03-07
-----------------------------
-
-Major overhaul.
-
-- Coverage.py is now a package rather than a module.  Functionality has been
-  split into classes.
-
-- The trace function is implemented in C for speed.  Coverage.py runs are now
-  much faster.  Thanks to David Christian for productive micro-sprints and
-  other encouragement.
-
-- Executable lines are identified by reading the line number tables in the
-  compiled code, removing a great deal of complicated analysis code.
-
-- Precisely which lines are considered executable has changed in some cases.
-  Therefore, your coverage stats may also change slightly.
-
-- The singleton coverage object is only created if the module-level functions
-  are used.  This maintains the old interface while allowing better
-  programmatic use of coverage.py.
-
-- The minimum supported Python version is 2.3.
-
-
-Version 2.85 --- 2008-09-14
----------------------------
-
-- Add support for finding source files in eggs. Don't check for
-  morf's being instances of ModuleType, instead use duck typing so that
-  pseudo-modules can participate. Thanks, Imri Goldberg.
-
-- Use os.realpath as part of the fixing of file names so that symlinks won't
-  confuse things. Thanks, Patrick Mezard.
-
-
-Version 2.80 --- 2008-05-25
----------------------------
-
-- Open files in rU mode to avoid line ending craziness. Thanks, Edward Loper.
-
-
-Version 2.78 --- 2007-09-30
----------------------------
-
-- Don't try to predict whether a file is Python source based on the extension.
-  Extension-less files are often Pythons scripts. Instead, simply parse the
-  file and catch the syntax errors. Hat tip to Ben Finney.
-
-
-Version 2.77 --- 2007-07-29
----------------------------
-
-- Better packaging.
-
-
-Version 2.76 --- 2007-07-23
----------------------------
-
-- Now Python 2.5 is *really* fully supported: the body of the new with
-  statement is counted as executable.
-
-
-Version 2.75 --- 2007-07-22
----------------------------
-
-- Python 2.5 now fully supported. The method of dealing with multi-line
-  statements is now less sensitive to the exact line that Python reports during
-  execution. Pass statements are handled specially so that their disappearance
-  during execution won't throw off the measurement.
-
-
-Version 2.7 --- 2007-07-21
---------------------------
-
-- "#pragma: nocover" is excluded by default.
-
-- Properly ignore docstrings and other constant expressions that appear in the
-  middle of a function, a problem reported by Tim Leslie.
-
-- coverage.erase() shouldn't clobber the exclude regex. Change how parallel
-  mode is invoked, and fix erase() so that it erases the cache when called
-  programmatically.
-
-- In reports, ignore code executed from strings, since we can't do anything
-  useful with it anyway.
-
-- Better file handling on Linux, thanks Guillaume Chazarain.
-
-- Better shell support on Windows, thanks Noel O'Boyle.
-
-- Python 2.2 support maintained, thanks Catherine Proulx.
-
-- Minor changes to avoid lint warnings.
-
-
-Version 2.6 --- 2006-08-23
---------------------------
-
-- Applied Joseph Tate's patch for function decorators.
-
-- Applied Sigve Tjora and Mark van der Wal's fixes for argument handling.
-
-- Applied Geoff Bache's parallel mode patch.
-
-- Refactorings to improve testability. Fixes to command-line logic for parallel
-  mode and collect.
-
-
-Version 2.5 --- 2005-12-04
---------------------------
-
-- Call threading.settrace so that all threads are measured. Thanks Martin
-  Fuzzey.
-
-- Add a file argument to report so that reports can be captured to a different
-  destination.
-
-- Coverage.py can now measure itself.
-
-- Adapted Greg Rogers' patch for using relative file names, and sorting and
-  omitting files to report on.
-
-
-Version 2.2 --- 2004-12-31
---------------------------
-
-- Allow for keyword arguments in the module global functions. Thanks, Allen.
-
-
-Version 2.1 --- 2004-12-14
---------------------------
-
-- Return 'analysis' to its original behavior and add 'analysis2'. Add a global
-  for 'annotate', and factor it, adding 'annotate_file'.
-
-
-Version 2.0 --- 2004-12-12
---------------------------
-
-Significant code changes.
-
-- Finding executable statements has been rewritten so that docstrings and
-  other quirks of Python execution aren't mistakenly identified as missing
-  lines.
-
-- Lines can be excluded from consideration, even entire suites of lines.
-
-- The file system cache of covered lines can be disabled programmatically.
-
-- Modernized the code.
-
-
-Earlier History
----------------
-
-2001-12-04 GDR Created.
-
-2001-12-06 GDR Added command-line interface and source code annotation.
-
-2001-12-09 GDR Moved design and interface to separate documents.
-
-2001-12-10 GDR Open cache file as binary on Windows. Allow simultaneous -e and
--x, or -a and -r.
-
-2001-12-12 GDR Added command-line help. Cache analysis so that it only needs to
-be done once when you specify -a and -r.
-
-2001-12-13 GDR Improved speed while recording. Portable between Python 1.5.2
-and 2.1.1.
-
-2002-01-03 GDR Module-level functions work correctly.
-
-2002-01-07 GDR Update sys.path when running a file with the -x option, so that
-it matches the value the program would get if it were run on its own.
+__ https://coverage.readthedocs.io/en/latest/changes.html
--- a/eric7/DebugClients/Python/coverage/doc/CONTRIBUTORS.txt	Fri Nov 19 19:28:47 2021 +0100
+++ b/eric7/DebugClients/Python/coverage/doc/CONTRIBUTORS.txt	Sat Nov 20 16:47:38 2021 +0100
@@ -20,6 +20,7 @@
 Aron Griffis
 Artem Dayneko
 Arthur Deygin
+Ben Carlsson
 Ben Finney
 Bernát Gábor
 Bill Hart
@@ -37,6 +38,7 @@
 Christian Heimes
 Christine Lytwynec
 Christoph Zwerschke
+Clément Pit-Claudel
 Conrad Ho
 Cosimo Lupo
 Dan Hemberger
@@ -106,6 +108,7 @@
 Mike Fiedler
 Naveen Yadav
 Nathan Land
+Nils Kattenbeck
 Noel O'Boyle
 Olivier Grisel
 Ori Avtalion
@@ -124,6 +127,7 @@
 Sandra Martocchia
 Scott Belden
 Sebastián Ramírez
+Sergey B Kirpichev
 Sigve Tjora
 Simon Willison
 Stan Hu
--- a/eric7/DebugClients/Python/coverage/doc/README.rst	Fri Nov 19 19:28:47 2021 +0100
+++ b/eric7/DebugClients/Python/coverage/doc/README.rst	Sat Nov 20 16:47:38 2021 +0100
@@ -9,7 +9,7 @@
 
 |  |license| |versions| |status|
 |  |test-status| |quality-status| |docs| |codecov|
-|  |kit| |format| |repos| |downloads|
+|  |kit| |downloads| |format| |repos|
 |  |stars| |forks| |contributors|
 |  |tidelift| |twitter-coveragepy| |twitter-nedbat|
 
@@ -17,11 +17,10 @@
 the code analysis tools and tracing hooks provided in the Python standard
 library to determine which lines are executable, and which have been executed.
 
-Coverage.py runs on many versions of Python:
+Coverage.py runs on these versions of Python:
 
-* CPython 2.7.
-* CPython 3.5 through 3.10 alpha.
-* PyPy2 7.3.3 and PyPy3 7.3.3.
+* CPython 3.6 through 3.11.
+* PyPy3 7.3.7.
 
 Documentation is on `Read the Docs`_.  Code repository and issue tracker are on
 `GitHub`_.
@@ -30,8 +29,8 @@
 .. _GitHub: https://github.com/nedbat/coveragepy
 
 
-**New in 5.x:** SQLite data storage, JSON report, contexts, relative filenames,
-dropped support for Python 2.6, 3.3 and 3.4.
+**New in 6.x:** dropped support for Python 2.7 and 3.5; added support for 3.10
+match/case statements.
 
 
 For Enterprise
@@ -104,9 +103,6 @@
 .. |docs| image:: https://readthedocs.org/projects/coverage/badge/?version=latest&style=flat
     :target: https://coverage.readthedocs.io/
     :alt: Documentation
-.. |reqs| image:: https://requires.io/github/nedbat/coveragepy/requirements.svg?branch=master
-    :target: https://requires.io/github/nedbat/coveragepy/requirements/?branch=master
-    :alt: Requirements status
 .. |kit| image:: https://badge.fury.io/py/coverage.svg
     :target: https://pypi.org/project/coverage/
     :alt: PyPI status
@@ -129,7 +125,7 @@
     :target: https://codecov.io/github/nedbat/coveragepy?branch=master
     :alt: Coverage!
 .. |repos| image:: https://repology.org/badge/tiny-repos/python:coverage.svg
-    :target: https://repology.org/metapackage/python:coverage/versions
+    :target: https://repology.org/project/python:coverage/versions
     :alt: Packaging status
 .. |tidelift| image:: https://tidelift.com/badges/package/pypi/coverage
     :target: https://tidelift.com/subscription/pkg/pypi-coverage?utm_source=pypi-coverage&utm_medium=referral&utm_campaign=readme
--- a/eric7/DebugClients/Python/coverage/env.py	Fri Nov 19 19:28:47 2021 +0100
+++ b/eric7/DebugClients/Python/coverage/env.py	Sat Nov 20 16:47:38 2021 +0100
@@ -20,26 +20,21 @@
 # Python versions. We amend version_info with one more value, a zero if an
 # official version, or 1 if built from source beyond an official version.
 PYVERSION = sys.version_info + (int(platform.python_version()[-1] == "+"),)
-PY2 = PYVERSION < (3, 0)
-PY3 = PYVERSION >= (3, 0)
 
 if PYPY:
     PYPYVERSION = sys.pypy_version_info
 
-PYPY2 = PYPY and PY2
-PYPY3 = PYPY and PY3
-
 # Python behavior.
-class PYBEHAVIOR(object):
+class PYBEHAVIOR:
     """Flags indicating this Python's behavior."""
 
+    # Does Python conform to PEP626, Precise line numbers for debugging and other tools.
+    # https://www.python.org/dev/peps/pep-0626
     pep626 = CPYTHON and (PYVERSION > (3, 10, 0, 'alpha', 4))
 
     # Is "if __debug__" optimized away?
-    if PYPY3:
+    if PYPY:
         optimize_if_debug = True
-    elif PYPY2:
-        optimize_if_debug = False
     else:
         optimize_if_debug = not pep626
 
@@ -47,7 +42,7 @@
     optimize_if_not_debug = (not PYPY) and (PYVERSION >= (3, 7, 0, 'alpha', 4))
     if pep626:
         optimize_if_not_debug = False
-    if PYPY3:
+    if PYPY:
         optimize_if_not_debug = True
 
     # Is "if not __debug__" optimized away even better?
@@ -55,23 +50,11 @@
     if pep626:
         optimize_if_not_debug2 = False
 
-    # Do we have yield-from?
-    yield_from = (PYVERSION >= (3, 3))
-
-    # Do we have PEP 420 namespace packages?
-    namespaces_pep420 = (PYVERSION >= (3, 3))
-
-    # Do .pyc files have the source file size recorded in them?
-    size_in_pyc = (PYVERSION >= (3, 3))
-
-    # Do we have async and await syntax?
-    async_syntax = (PYVERSION >= (3, 5))
-
-    # PEP 448 defined additional unpacking generalizations
-    unpackings_pep448 = (PYVERSION >= (3, 5))
+    # Yet another way to optimize "if not __debug__"?
+    optimize_if_not_debug3 = (PYPY and PYVERSION >= (3, 8))
 
     # Can co_lnotab have negative deltas?
-    negative_lnotab = (PYVERSION >= (3, 6)) and not (PYPY and PYPYVERSION < (7, 2))
+    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))
@@ -80,7 +63,10 @@
     # 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))
+    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))
@@ -94,7 +80,7 @@
     # 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 = (PYVERSION >= (3, 8))
+    trace_decorated_def = (CPYTHON and PYVERSION >= (3, 8))
 
     # Are while-true loops optimized into absolute jumps with no loop setup?
     nix_while_true = (PYVERSION >= (3, 8))
@@ -116,6 +102,16 @@
     # Are "if 0:" lines (and similar) kept in the compiled code?
     keep_constant_test = pep626
 
+    # When leaving a with-block, do we visit the with-line again for the exit?
+    exit_through_with = (PYVERSION >= (3, 10, 0, 'beta'))
+
+    # Match-case construct.
+    match_case = (PYVERSION >= (3, 10))
+
+    # Some words are keywords in some places, identifiers in other places.
+    soft_keywords = (PYVERSION >= (3, 10))
+
+
 # Coverage.py specifics.
 
 # Are we using the C-implemented trace function?
@@ -128,3 +124,8 @@
 # Even when running tests, you can use COVERAGE_TESTING=0 to disable the
 # test-specific behavior like contracts.
 TESTING = os.getenv('COVERAGE_TESTING', '') == 'True'
+
+# 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)))
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/eric7/DebugClients/Python/coverage/exceptions.py	Sat Nov 20 16:47:38 2021 +0100
@@ -0,0 +1,53 @@
+# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
+# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt
+
+"""Exceptions coverage.py can raise."""
+
+
+class BaseCoverageException(Exception):
+    """The base of all Coverage exceptions."""
+    pass
+
+
+class CoverageException(BaseCoverageException):
+    """An exception raised by a coverage.py function."""
+    pass
+
+
+class NoSource(CoverageException):
+    """We couldn't find the source for a module."""
+    pass
+
+
+class NoCode(NoSource):
+    """We couldn't find any code at all."""
+    pass
+
+
+class NotPython(CoverageException):
+    """A source file turned out not to be parsable Python."""
+    pass
+
+
+class ExceptionDuringRun(CoverageException):
+    """An exception happened while running customer code.
+
+    Construct it with three arguments, the values from `sys.exc_info`.
+
+    """
+    pass
+
+
+class StopEverything(BaseCoverageException):
+    """An exception that means everything should stop.
+
+    The CoverageTest class converts these to SkipTest, so that when running
+    tests, raising this exception will automatically skip the test.
+
+    """
+    pass
+
+
+class CoverageWarning(Warning):
+    """A warning from Coverage.py."""
+    pass
--- a/eric7/DebugClients/Python/coverage/execfile.py	Fri Nov 19 19:28:47 2021 +0100
+++ b/eric7/DebugClients/Python/coverage/execfile.py	Sat Nov 20 16:47:38 2021 +0100
@@ -3,6 +3,8 @@
 
 """Execute files of Python code."""
 
+import importlib.machinery
+import importlib.util
 import inspect
 import marshal
 import os
@@ -11,17 +13,18 @@
 import types
 
 from coverage import env
-from coverage.backward import BUILTINS
-from coverage.backward import PYC_MAGIC_NUMBER, imp, importlib_util_find_spec
+from coverage.exceptions import CoverageException, ExceptionDuringRun, NoCode, NoSource
 from coverage.files import canonical_filename, python_reported_file
-from coverage.misc import CoverageException, ExceptionDuringRun, NoCode, NoSource, isolate_module
+from coverage.misc import isolate_module
 from coverage.phystokens import compile_unicode
 from coverage.python import get_python_source
 
 os = isolate_module(os)
 
 
-class DummyLoader(object):
+PYC_MAGIC_NUMBER = importlib.util.MAGIC_NUMBER
+
+class DummyLoader:
     """A shim for the pep302 __loader__, emulating pkgutil.ImpLoader.
 
     Currently only implements the .fullname attribute
@@ -30,79 +33,35 @@
         self.fullname = fullname
 
 
-if importlib_util_find_spec:
-    def find_module(modulename):
-        """Find the module named `modulename`.
+def find_module(modulename):
+    """Find the module named `modulename`.
 
-        Returns the file path of the module, the name of the enclosing
-        package, and the spec.
-        """
-        try:
-            spec = importlib_util_find_spec(modulename)
-        except ImportError as err:
-            raise NoSource(str(err))
+    Returns the file path of the module, the name of the enclosing
+    package, and the spec.
+    """
+    try:
+        spec = importlib.util.find_spec(modulename)
+    except ImportError as err:
+        raise NoSource(str(err)) from err
+    if not spec:
+        raise NoSource(f"No module named {modulename!r}")
+    pathname = spec.origin
+    packagename = spec.name
+    if spec.submodule_search_locations:
+        mod_main = modulename + ".__main__"
+        spec = importlib.util.find_spec(mod_main)
         if not spec:
-            raise NoSource("No module named %r" % (modulename,))
+            raise NoSource(
+                f"No module named {mod_main}; " +
+                f"{modulename!r} is a package and cannot be directly executed"
+            )
         pathname = spec.origin
         packagename = spec.name
-        if spec.submodule_search_locations:
-            mod_main = modulename + ".__main__"
-            spec = importlib_util_find_spec(mod_main)
-            if not spec:
-                raise NoSource(
-                    "No module named %s; "
-                    "%r is a package and cannot be directly executed"
-                    % (mod_main, modulename)
-                )
-            pathname = spec.origin
-            packagename = spec.name
-        packagename = packagename.rpartition(".")[0]
-        return pathname, packagename, spec
-else:
-    def find_module(modulename):
-        """Find the module named `modulename`.
-
-        Returns the file path of the module, the name of the enclosing
-        package, and None (where a spec would have been).
-        """
-        openfile = None
-        glo, loc = globals(), locals()
-        try:
-            # Search for the module - inside its parent package, if any - using
-            # standard import mechanics.
-            if '.' in modulename:
-                packagename, name = modulename.rsplit('.', 1)
-                package = __import__(packagename, glo, loc, ['__path__'])
-                searchpath = package.__path__
-            else:
-                packagename, name = None, modulename
-                searchpath = None  # "top-level search" in imp.find_module()
-            openfile, pathname, _ = imp.find_module(name, searchpath)
-
-            # Complain if this is a magic non-file module.
-            if openfile is None and pathname is None:
-                raise NoSource(
-                    "module does not live in a file: %r" % modulename
-                    )
-
-            # If `modulename` is actually a package, not a mere module, then we
-            # pretend to be Python 2.7 and try running its __main__.py script.
-            if openfile is None:
-                packagename = modulename
-                name = '__main__'
-                package = __import__(packagename, glo, loc, ['__path__'])
-                searchpath = package.__path__
-                openfile, pathname, _ = imp.find_module(name, searchpath)
-        except ImportError as err:
-            raise NoSource(str(err))
-        finally:
-            if openfile:
-                openfile.close()
-
-        return pathname, packagename, None
+    packagename = packagename.rpartition(".")[0]
+    return pathname, packagename, spec
 
 
-class PyRunner(object):
+class PyRunner:
     """Multi-stage execution of Python code.
 
     This is meant to emulate real Python execution as closely as possible.
@@ -176,29 +135,25 @@
             # directory.
             for ext in [".py", ".pyc", ".pyo"]:
                 try_filename = os.path.join(self.arg0, "__main__" + ext)
+                # 3.8.10 changed how files are reported when running a
+                # directory.  But I'm not sure how far this change is going to
+                # spread, so I'll just hard-code it here for now.
+                if env.PYVERSION >= (3, 8, 10):
+                    try_filename = os.path.abspath(try_filename)
                 if os.path.exists(try_filename):
                     self.arg0 = try_filename
                     break
             else:
                 raise NoSource("Can't find '__main__' module in '%s'" % self.arg0)
 
-            if env.PY2:
-                self.arg0 = os.path.abspath(self.arg0)
-
             # Make a spec. I don't know if this is the right way to do it.
-            try:
-                import importlib.machinery
-            except ImportError:
-                pass
-            else:
-                try_filename = python_reported_file(try_filename)
-                self.spec = importlib.machinery.ModuleSpec("__main__", None, origin=try_filename)
-                self.spec.has_location = True
+            try_filename = python_reported_file(try_filename)
+            self.spec = importlib.machinery.ModuleSpec("__main__", None, origin=try_filename)
+            self.spec.has_location = True
             self.package = ""
             self.loader = DummyLoader("__main__")
         else:
-            if env.PY3:
-                self.loader = DummyLoader("__main__")
+            self.loader = DummyLoader("__main__")
 
         self.arg0 = python_reported_file(self.arg0)
 
@@ -220,7 +175,7 @@
         if self.spec is not None:
             main_mod.__spec__ = self.spec
 
-        main_mod.__builtins__ = BUILTINS
+        main_mod.__builtins__ = sys.modules['builtins']
 
         sys.modules['__main__'] = main_mod
 
@@ -236,8 +191,8 @@
         except CoverageException:
             raise
         except Exception as exc:
-            msg = "Couldn't run '{filename}' as Python code: {exc.__class__.__name__}: {exc}"
-            raise CoverageException(msg.format(filename=self.arg0, exc=exc))
+            msg = f"Couldn't run '{self.arg0}' as Python code: {exc.__class__.__name__}: {exc}"
+            raise CoverageException(msg) from exc
 
         # Execute the code object.
         # Return to the original directory in case the test code exits in
@@ -265,22 +220,20 @@
 
             # Call the excepthook.
             try:
-                if hasattr(err, "__traceback__"):
-                    err.__traceback__ = err.__traceback__.tb_next
+                err.__traceback__ = err.__traceback__.tb_next
                 sys.excepthook(typ, err, tb.tb_next)
             except SystemExit:                      # pylint: disable=try-except-raise
                 raise
-            except Exception:
+            except Exception as exc:
                 # Getting the output right in the case of excepthook
                 # shenanigans is kind of involved.
                 sys.stderr.write("Error in sys.excepthook:\n")
                 typ2, err2, tb2 = sys.exc_info()
                 err2.__suppress_context__ = True
-                if hasattr(err2, "__traceback__"):
-                    err2.__traceback__ = err2.__traceback__.tb_next
+                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)
+                raise ExceptionDuringRun(typ, err, tb.tb_next) from exc
             else:
                 sys.exit(1)
         finally:
@@ -321,8 +274,8 @@
     # Open the source file.
     try:
         source = get_python_source(filename)
-    except (IOError, NoSource):
-        raise NoSource("No file to run: '%s'" % filename)
+    except (OSError, NoSource) as exc:
+        raise NoSource(f"No file to run: '{filename}'") from exc
 
     code = compile_unicode(source, filename, "exec")
     return code
@@ -332,15 +285,15 @@
     """Get a code object from a .pyc file."""
     try:
         fpyc = open(filename, "rb")
-    except IOError:
-        raise NoCode("No file to run: '%s'" % filename)
+    except OSError as exc:
+        raise NoCode(f"No file to run: '{filename}'") from exc
 
     with fpyc:
         # First four bytes are a version-specific magic number.  It has to
         # match or we won't run the file.
         magic = fpyc.read(4)
         if magic != PYC_MAGIC_NUMBER:
-            raise NoCode("Bad magic number in .pyc file: {} != {}".format(magic, PYC_MAGIC_NUMBER))
+            raise NoCode(f"Bad magic number in .pyc file: {magic} != {PYC_MAGIC_NUMBER}")
 
         date_based = True
         if env.PYBEHAVIOR.hashed_pyc_pep552:
@@ -352,9 +305,8 @@
         if date_based:
             # Skip the junk in the header that we don't need.
             fpyc.read(4)            # Skip the moddate.
-            if env.PYBEHAVIOR.size_in_pyc:
-                # 3.3 added another long to the header (size), skip it.
-                fpyc.read(4)
+            # 3.3 added another long to the header (size), skip it.
+            fpyc.read(4)
 
         # The rest of the file is the code object we want.
         code = marshal.load(fpyc)
--- a/eric7/DebugClients/Python/coverage/files.py	Fri Nov 19 19:28:47 2021 +0100
+++ b/eric7/DebugClients/Python/coverage/files.py	Sat Nov 20 16:47:38 2021 +0100
@@ -13,8 +13,8 @@
 import sys
 
 from coverage import env
-from coverage.backward import unicode_class
-from coverage.misc import contract, CoverageException, join_regex, isolate_module
+from coverage.exceptions import CoverageException
+from coverage.misc import contract, human_sorted, isolate_module, join_regex
 
 
 os = isolate_module(os)
@@ -48,7 +48,7 @@
     fnorm = os.path.normcase(filename)
     if fnorm.startswith(RELATIVE_DIR):
         filename = filename[len(RELATIVE_DIR):]
-    return unicode_filename(filename)
+    return filename
 
 
 @contract(returns='unicode')
@@ -77,7 +77,7 @@
     return CANONICAL_FILENAME_CACHE[filename]
 
 
-MAX_FLAT = 200
+MAX_FLAT = 100
 
 @contract(filename='unicode', returns='unicode')
 def flat_rootname(filename):
@@ -87,15 +87,16 @@
     the same directory, but need to differentiate same-named files from
     different directories.
 
-    For example, the file a/b/c.py will return 'a_b_c_py'
+    For example, the file a/b/c.py will return 'd_86bbcbe134d28fd2_c_py'
 
     """
-    name = ntpath.splitdrive(filename)[1]
-    name = re.sub(r"[\\/.:]", "_", name)
-    if len(name) > MAX_FLAT:
-        h = hashlib.sha1(name.encode('UTF-8')).hexdigest()
-        name = name[-(MAX_FLAT-len(h)-1):] + '_' + h
-    return name
+    dirname, basename = ntpath.split(filename)
+    if dirname:
+        fp = hashlib.new("sha3_256", dirname.encode("UTF-8")).hexdigest()[:16]
+        prefix = f"d_{fp}_"
+    else:
+        prefix = ""
+    return prefix + basename.replace(".", "_")
 
 
 if env.WINDOWS:
@@ -105,8 +106,6 @@
 
     def actual_path(path):
         """Get the actual path of `path`, including the correct case."""
-        if env.PY2 and isinstance(path, unicode_class):
-            path = path.encode(sys.getfilesystemencoding())
         if path in _ACTUAL_PATH_CACHE:
             return _ACTUAL_PATH_CACHE[path]
 
@@ -143,21 +142,6 @@
         return filename
 
 
-if env.PY2:
-    @contract(returns='unicode')
-    def unicode_filename(filename):
-        """Return a Unicode version of `filename`."""
-        if isinstance(filename, str):
-            encoding = sys.getfilesystemencoding() or sys.getdefaultencoding()
-            filename = filename.decode(encoding, "replace")
-        return filename
-else:
-    @contract(filename='unicode', returns='unicode')
-    def unicode_filename(filename):
-        """Return a Unicode version of `filename`."""
-        return filename
-
-
 @contract(returns='unicode')
 def abs_file(path):
     """Return the absolute normalized form of `path`."""
@@ -167,7 +151,6 @@
         pass
     path = os.path.abspath(path)
     path = actual_path(path)
-    path = unicode_filename(path)
     return path
 
 
@@ -207,7 +190,7 @@
     return prepped
 
 
-class TreeMatcher(object):
+class TreeMatcher:
     """A matcher for files in a tree.
 
     Construct with a list of paths, either files or directories. Paths match
@@ -215,18 +198,21 @@
     somewhere in a subtree rooted at one of the directories.
 
     """
-    def __init__(self, paths):
-        self.paths = list(paths)
+    def __init__(self, paths, name="unknown"):
+        self.original_paths = human_sorted(paths)
+        self.paths = list(map(os.path.normcase, paths))
+        self.name = name
 
     def __repr__(self):
-        return "<TreeMatcher %r>" % self.paths
+        return f"<TreeMatcher {self.name} {self.original_paths!r}>"
 
     def info(self):
         """A list of strings for displaying when dumping state."""
-        return self.paths
+        return self.original_paths
 
     def match(self, fpath):
         """Does `fpath` indicate a file in one of our trees?"""
+        fpath = os.path.normcase(fpath)
         for p in self.paths:
             if fpath.startswith(p):
                 if fpath == p:
@@ -238,13 +224,14 @@
         return False
 
 
-class ModuleMatcher(object):
+class ModuleMatcher:
     """A matcher for modules in a tree."""
-    def __init__(self, module_names):
+    def __init__(self, module_names, name="unknown"):
         self.modules = list(module_names)
+        self.name = name
 
     def __repr__(self):
-        return "<ModuleMatcher %r>" % (self.modules)
+        return f"<ModuleMatcher {self.name} {self.modules!r}>"
 
     def info(self):
         """A list of strings for displaying when dumping state."""
@@ -266,14 +253,15 @@
         return False
 
 
-class FnmatchMatcher(object):
+class FnmatchMatcher:
     """A matcher for files by file name pattern."""
-    def __init__(self, pats):
+    def __init__(self, pats, name="unknown"):
         self.pats = list(pats)
         self.re = fnmatches_to_regex(self.pats, case_insensitive=env.WINDOWS)
+        self.name = name
 
     def __repr__(self):
-        return "<FnmatchMatcher %r>" % self.pats
+        return f"<FnmatchMatcher {self.name} {self.pats!r}>"
 
     def info(self):
         """A list of strings for displaying when dumping state."""
@@ -327,7 +315,7 @@
     return compiled
 
 
-class PathAliases(object):
+class PathAliases:
     """A collection of aliases for paths.
 
     When combining data files from remote machines, often the paths to source
@@ -338,13 +326,15 @@
     map a path through those aliases to produce a unified path.
 
     """
-    def __init__(self):
+    def __init__(self, relative=False):
         self.aliases = []
+        self.relative = relative
 
     def pprint(self):       # pragma: debugging
         """Dump the important parts of the PathAliases, for debugging."""
+        print(f"Aliases (relative={self.relative}):")
         for regex, result in self.aliases:
-            print("{!r} --> {!r}".format(regex.pattern, result))
+            print(f"{regex.pattern!r} --> {result!r}")
 
     def add(self, pattern, result):
         """Add the `pattern`/`result` pair to the list of aliases.
@@ -405,7 +395,8 @@
             if m:
                 new = path.replace(m.group(0), result)
                 new = new.replace(sep(path), sep(result))
-                new = canonical_filename(new)
+                if not self.relative:
+                    new = canonical_filename(new)
                 return new
         return path
 
--- a/eric7/DebugClients/Python/coverage/html.py	Fri Nov 19 19:28:47 2021 +0100
+++ b/eric7/DebugClients/Python/coverage/html.py	Sat Nov 20 16:47:38 2021 +0100
@@ -8,13 +8,14 @@
 import os
 import re
 import shutil
+import types
 
 import coverage
-from coverage import env
-from coverage.backward import iitems, SimpleNamespace, format_local_datetime
 from coverage.data import add_data_to_hash
+from coverage.exceptions import CoverageException
 from coverage.files import flat_rootname
-from coverage.misc import CoverageException, ensure_dir, file_be_gone, Hasher, isolate_module
+from coverage.misc import ensure_dir, file_be_gone, Hasher, isolate_module, format_local_datetime
+from coverage.misc import human_sorted
 from coverage.report import get_analysis_to_report
 from coverage.results import Numbers
 from coverage.templite import Templite
@@ -22,42 +23,12 @@
 os = isolate_module(os)
 
 
-# Static files are looked for in a list of places.
-STATIC_PATH = [
-    # The place Debian puts system Javascript libraries.
-    "/usr/share/javascript",
-
-    # Our htmlfiles directory.
-    os.path.join(os.path.dirname(__file__), "htmlfiles"),
-]
-
-
-def data_filename(fname, pkgdir=""):
-    """Return the path to a data file of ours.
-
-    The file is searched for on `STATIC_PATH`, and the first place it's found,
-    is returned.
-
-    Each directory in `STATIC_PATH` is searched as-is, and also, if `pkgdir`
-    is provided, at that sub-directory.
-
+def data_filename(fname):
+    """Return the path to an "htmlfiles" data file of ours.
     """
-    tried = []
-    for static_dir in STATIC_PATH:
-        static_filename = os.path.join(static_dir, fname)
-        if os.path.exists(static_filename):
-            return static_filename
-        else:
-            tried.append(static_filename)
-        if pkgdir:
-            static_filename = os.path.join(static_dir, pkgdir, fname)
-            if os.path.exists(static_filename):
-                return static_filename
-            else:
-                tried.append(static_filename)
-    raise CoverageException(
-        "Couldn't find static file %r from %r, tried: %r" % (fname, os.getcwd(), tried)
-    )
+    static_dir = os.path.join(os.path.dirname(__file__), "htmlfiles")
+    static_filename = os.path.join(static_dir, fname)
+    return static_filename
 
 
 def read_data(fname):
@@ -73,7 +44,7 @@
         fout.write(html.encode('ascii', 'xmlcharrefreplace'))
 
 
-class HtmlDataGeneration(object):
+class HtmlDataGeneration:
     """Generate structured data to be turned into HTML reports."""
 
     EMPTY = "(empty)"
@@ -123,14 +94,14 @@
             contexts = contexts_label = None
             context_list = None
             if category and self.config.show_contexts:
-                contexts = sorted(c or self.EMPTY for c in contexts_by_lineno[lineno])
+                contexts = human_sorted(c or self.EMPTY for c in contexts_by_lineno.get(lineno, ()))
                 if contexts == [self.EMPTY]:
                     contexts_label = self.EMPTY
                 else:
-                    contexts_label = "{} ctx".format(len(contexts))
+                    contexts_label = f"{len(contexts)} ctx"
                     context_list = contexts
 
-            lines.append(SimpleNamespace(
+            lines.append(types.SimpleNamespace(
                 tokens=tokens,
                 number=lineno,
                 category=category,
@@ -142,7 +113,7 @@
                 long_annotations=long_annotations,
             ))
 
-        file_data = SimpleNamespace(
+        file_data = types.SimpleNamespace(
             relative_filename=fr.relative_filename(),
             nums=analysis.numbers,
             lines=lines,
@@ -151,22 +122,17 @@
         return file_data
 
 
-class HtmlReporter(object):
+class HtmlReporter:
     """HTML reporting."""
 
     # These files will be copied from the htmlfiles directory to the output
     # directory.
     STATIC_FILES = [
-        ("style.css", ""),
-        ("jquery.min.js", "jquery"),
-        ("jquery.ba-throttle-debounce.min.js", "jquery-throttle-debounce"),
-        ("jquery.hotkeys.js", "jquery-hotkeys"),
-        ("jquery.isonscreen.js", "jquery-isonscreen"),
-        ("jquery.tablesorter.min.js", "jquery-tablesorter"),
-        ("coverage_html.js", ""),
-        ("keybd_closed.png", ""),
-        ("keybd_open.png", ""),
-        ("favicon_32.png", ""),
+        "style.css",
+        "coverage_html.js",
+        "keybd_closed.png",
+        "keybd_open.png",
+        "favicon_32.png",
     ]
 
     def __init__(self, cov):
@@ -179,11 +145,11 @@
             self.skip_covered = self.config.skip_covered
         self.skip_empty = self.config.html_skip_empty
         if self.skip_empty is None:
-            self.skip_empty= self.config.skip_empty
+            self.skip_empty = self.config.skip_empty
+        self.skipped_covered_count = 0
+        self.skipped_empty_count = 0
 
         title = self.config.html_title
-        if env.PY2:
-            title = title.decode("utf8")
 
         if self.config.extra_css:
             self.extra_css = os.path.basename(self.config.extra_css)
@@ -197,7 +163,7 @@
         self.all_files_nums = []
         self.incr = IncrementalChecker(self.directory)
         self.datagen = HtmlDataGeneration(self.coverage)
-        self.totals = Numbers()
+        self.totals = Numbers(precision=self.config.precision)
 
         self.template_globals = {
             # Functions available in the templates.
@@ -255,18 +221,17 @@
     def make_local_static_report_files(self):
         """Make local instances of static files for HTML report."""
         # The files we provide must always be copied.
-        for static, pkgdir in self.STATIC_FILES:
-            shutil.copyfile(
-                data_filename(static, pkgdir),
-                os.path.join(self.directory, static)
-            )
+        for static in self.STATIC_FILES:
+            shutil.copyfile(data_filename(static), os.path.join(self.directory, static))
+
+        # .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")
 
         # The user may have extra CSS they want copied.
         if self.extra_css:
-            shutil.copyfile(
-                self.config.extra_css,
-                os.path.join(self.directory, self.extra_css)
-            )
+            shutil.copyfile(self.config.extra_css, os.path.join(self.directory, self.extra_css))
 
     def html_file(self, fr, analysis):
         """Generate an HTML file for one source file."""
@@ -286,12 +251,14 @@
             if no_missing_lines and no_missing_branches:
                 # If there's an existing file, remove it.
                 file_be_gone(html_path)
+                self.skipped_covered_count += 1
                 return
 
         if self.skip_empty:
             # Don't report on empty files.
             if nums.n_statements == 0:
                 file_be_gone(html_path)
+                self.skipped_empty_count += 1
                 return
 
         # Find out if the file on disk is already correct.
@@ -310,15 +277,15 @@
                 else:
                     tok_html = escape(tok_text) or '&nbsp;'
                     html.append(
-                        u'<span class="{}">{}</span>'.format(tok_type, tok_html)
+                        f'<span class="{tok_type}">{tok_html}</span>'
                     )
             ldata.html = ''.join(html)
 
             if ldata.short_annotations:
                 # 202F is NARROW NO-BREAK SPACE.
                 # 219B is RIGHTWARDS ARROW WITH STROKE.
-                ldata.annotate = u",&nbsp;&nbsp; ".join(
-                    u"{}&#x202F;&#x219B;&#x202F;{}".format(ldata.number, d)
+                ldata.annotate = ",&nbsp;&nbsp; ".join(
+                    f"{ldata.number}&#x202F;&#x219B;&#x202F;{d}"
                     for d in ldata.short_annotations
                     )
             else:
@@ -329,10 +296,10 @@
                 if len(longs) == 1:
                     ldata.annotate_long = longs[0]
                 else:
-                    ldata.annotate_long = u"{:d} missed branches: {}".format(
+                    ldata.annotate_long = "{:d} missed branches: {}".format(
                         len(longs),
-                        u", ".join(
-                            u"{:d}) {}".format(num, ann_long)
+                        ", ".join(
+                            f"{num:d}) {ann_long}"
                             for num, ann_long in enumerate(longs, start=1)
                             ),
                     )
@@ -360,18 +327,36 @@
         """Write the index.html file for this report."""
         index_tmpl = Templite(read_data("index.html"), self.template_globals)
 
+        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",
+            )
+        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",
+            )
+
         html = index_tmpl.render({
             'files': self.file_summaries,
             'totals': self.totals,
+            'skipped_covered_msg': skipped_covered_msg,
+            'skipped_empty_msg': skipped_empty_msg,
         })
 
-        write_html(os.path.join(self.directory, "index.html"), html)
+        index_file = os.path.join(self.directory, "index.html")
+        write_html(index_file, html)
+        self.coverage._message(f"Wrote HTML report to {index_file}")
 
         # Write the latest hashes for next time.
         self.incr.write()
 
 
-class IncrementalChecker(object):
+class IncrementalChecker:
     """Logic and data to support incremental reporting."""
 
     STATUS_FILE = "status.json"
@@ -421,7 +406,7 @@
             status_file = os.path.join(self.directory, self.STATUS_FILE)
             with open(status_file) as fstatus:
                 status = json.load(fstatus)
-        except (IOError, ValueError):
+        except (OSError, ValueError):
             usable = False
         else:
             usable = True
@@ -432,7 +417,7 @@
 
         if usable:
             self.files = {}
-            for filename, fileinfo in iitems(status['files']):
+            for filename, fileinfo in status['files'].items():
                 fileinfo['index']['nums'] = Numbers(*fileinfo['index']['nums'])
                 self.files[filename] = fileinfo
             self.globals = status['globals']
@@ -443,7 +428,7 @@
         """Write the current status."""
         status_file = os.path.join(self.directory, self.STATUS_FILE)
         files = {}
-        for filename, fileinfo in iitems(self.files):
+        for filename, fileinfo in self.files.items():
             fileinfo['index']['nums'] = fileinfo['index']['nums'].init_args()
             files[filename] = fileinfo
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/eric7/DebugClients/Python/coverage/htmlfiles/coverage_html.js	Sat Nov 20 16:47:38 2021 +0100
@@ -0,0 +1,575 @@
+// Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
+// For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt
+
+// Coverage.py HTML report browser code.
+/*jslint browser: true, sloppy: true, vars: true, plusplus: true, maxerr: 50, indent: 4 */
+/*global coverage: true, document, window, $ */
+
+coverage = {};
+
+// General helpers
+function debounce(callback, wait) {
+    let timeoutId = null;
+    return function(...args) {
+        clearTimeout(timeoutId);
+        timeoutId = setTimeout(() => {
+            callback.apply(this, args);
+        }, wait);
+    };
+};
+
+function checkVisible(element) {
+    const rect = element.getBoundingClientRect();
+    const viewBottom = Math.max(document.documentElement.clientHeight, window.innerHeight);
+    const viewTop = 30;
+    return !(rect.bottom < viewTop || rect.top >= viewBottom);
+}
+
+// Helpers for table sorting
+function getCellValue(row, column = 0) {
+    const cell = row.cells[column]
+    if (cell.childElementCount == 1) {
+        const child = cell.firstElementChild
+        if (child instanceof HTMLTimeElement && child.dateTime) {
+            return child.dateTime
+        } else if (child instanceof HTMLDataElement && child.value) {
+            return child.value
+        }
+    }
+    return cell.innerText || cell.textContent;
+}
+
+function rowComparator(rowA, rowB, column = 0) {
+    let valueA = getCellValue(rowA, column);
+    let valueB = getCellValue(rowB, column);
+    if (!isNaN(valueA) && !isNaN(valueB)) {
+        return valueA - valueB
+    }
+    return valueA.localeCompare(valueB, undefined, {numeric: true});
+}
+
+function sortColumn(th) {
+    // Get the current sorting direction of the selected header,
+    // clear state on other headers and then set the new sorting direction
+    const currentSortOrder = th.getAttribute("aria-sort");
+    [...th.parentElement.cells].forEach(header => header.setAttribute("aria-sort", "none"));
+    if (currentSortOrder === "none") {
+        th.setAttribute("aria-sort", th.dataset.defaultSortOrder || "ascending");
+    } else {
+        th.setAttribute("aria-sort", currentSortOrder === "ascending" ? "descending" : "ascending");
+    }
+
+    const column = [...th.parentElement.cells].indexOf(th)
+
+    // Sort all rows and afterwards append them in order to move them in the DOM
+    Array.from(th.closest("table").querySelectorAll("tbody tr"))
+        .sort((rowA, rowB) => rowComparator(rowA, rowB, column) * (th.getAttribute("aria-sort") === "ascending" ? 1 : -1))
+        .forEach(tr => tr.parentElement.appendChild(tr) );
+}
+
+// Find all the elements with data-shortcut attribute, and use them to assign a shortcut key.
+coverage.assign_shortkeys = function () {
+    document.querySelectorAll("[data-shortcut]").forEach(element => {
+        document.addEventListener("keypress", event => {
+            if (event.target.tagName.toLowerCase() === "input") {
+                return; // ignore keypress from search filter
+            }
+            if (event.key === element.dataset.shortcut) {
+                element.click();
+            }
+        });
+    });
+};
+
+// Create the events for the filter box.
+coverage.wire_up_filter = function () {
+    // Cache elements.
+    const table = document.querySelector("table.index");
+    const table_body_rows = table.querySelectorAll("tbody tr");
+    const no_rows = document.getElementById("no_rows");
+
+    // Observe filter keyevents.
+    document.getElementById("filter").addEventListener("input", debounce(event => {
+        // Keep running total of each metric, first index contains number of shown rows
+        const totals = new Array(table.rows[0].cells.length).fill(0);
+        // Accumulate the percentage as fraction
+        totals[totals.length - 1] = { "numer": 0, "denom": 0 };
+
+        // Hide / show elements.
+        table_body_rows.forEach(row => {
+            if (!row.cells[0].textContent.includes(event.target.value)) {
+                // hide
+                row.classList.add("hidden");
+                return;
+            }
+
+            // show
+            row.classList.remove("hidden");
+            totals[0]++;
+
+            for (let column = 1; column < totals.length; column++) {
+                // Accumulate dynamic totals
+                cell = row.cells[column]
+                if (column === totals.length - 1) {
+                    // Last column contains percentage
+                    const [numer, denom] = cell.dataset.ratio.split(" ");
+                    totals[column]["numer"] += parseInt(numer, 10);
+                    totals[column]["denom"] += parseInt(denom, 10);
+                } else {
+                    totals[column] += parseInt(cell.textContent, 10);
+                }
+            }
+        });
+
+        // Show placeholder if no rows will be displayed.
+        if (!totals[0]) {
+            // Show placeholder, hide table.
+            no_rows.style.display = "block";
+            table.style.display = "none";
+            return;
+        }
+
+        // Hide placeholder, show table.
+        no_rows.style.display = null;
+        table.style.display = null;
+
+        const footer = table.tFoot.rows[0];
+        // Calculate new dynamic sum values based on visible rows.
+        for (let column = 1; column < totals.length; column++) {
+            // Get footer cell element.
+            const cell = footer.cells[column];
+
+            // Set value into dynamic footer cell element.
+            if (column === totals.length - 1) {
+                // Percentage column uses the numerator and denominator,
+                // and adapts to the number of decimal places.
+                const match = /\.([0-9]+)/.exec(cell.textContent);
+                const places = match ? match[1].length : 0;
+                const { numer, denom } = totals[column];
+                cell.dataset.ratio = `${numer} ${denom}`;
+                // Check denom to prevent NaN if filtered files contain no statements
+                cell.textContent = denom
+                    ? `${(numer * 100 / denom).toFixed(places)}%`
+                    : `${(100).toFixed(places)}%`;
+            } else {
+                cell.textContent = totals[column];
+            }
+        }
+    }));
+
+    // Trigger change event on setup, to force filter on page refresh
+    // (filter value may still be present).
+    document.getElementById("filter").dispatchEvent(new Event("change"));
+};
+
+coverage.INDEX_SORT_STORAGE = "COVERAGE_INDEX_SORT_2";
+
+// Loaded on index.html
+coverage.index_ready = function () {
+    coverage.assign_shortkeys();
+    coverage.wire_up_filter();
+    document.querySelectorAll("[data-sortable] th[aria-sort]").forEach(
+        th => th.addEventListener("click", e => sortColumn(e.target))
+    );
+
+    // Look for a localStorage item containing previous sort settings:
+    const stored_list = localStorage.getItem(coverage.INDEX_SORT_STORAGE);
+
+    if (stored_list) {
+        const {column, direction} = JSON.parse(stored_list);
+        const th = document.querySelector("[data-sortable]").tHead.rows[0].cells[column];
+        th.setAttribute("aria-sort", direction === "ascending" ? "descending" : "ascending");
+        th.click()
+    }
+
+    // Watch for page unload events so we can save the final sort settings:
+    window.addEventListener("unload", function () {
+        const th = document.querySelector('[data-sortable] th[aria-sort="ascending"], [data-sortable] [aria-sort="descending"]');
+        if (!th) {
+            return;
+        }
+        localStorage.setItem(coverage.INDEX_SORT_STORAGE, JSON.stringify({
+            column: [...th.parentElement.cells].indexOf(th),
+            direction: th.getAttribute("aria-sort"),
+        }));
+    });
+};
+
+// -- pyfile stuff --
+
+coverage.LINE_FILTERS_STORAGE = "COVERAGE_LINE_FILTERS";
+
+coverage.pyfile_ready = function () {
+    // If we're directed to a particular line number, highlight the line.
+    var frag = location.hash;
+    if (frag.length > 2 && frag[1] === 't') {
+        document.querySelector(frag).closest(".n").classList.add("highlight");
+        coverage.set_sel(parseInt(frag.substr(2), 10));
+    } else {
+        coverage.set_sel(0);
+    }
+
+    const on_click = function(sel, fn) {
+        const elt = document.querySelector(sel);
+        if (elt) {
+            elt.addEventListener("click", fn);
+        }
+    }
+    on_click(".button_toggle_run", coverage.toggle_lines);
+    on_click(".button_toggle_mis", coverage.toggle_lines);
+    on_click(".button_toggle_exc", coverage.toggle_lines);
+    on_click(".button_toggle_par", coverage.toggle_lines);
+
+    on_click(".button_next_chunk", coverage.to_next_chunk_nicely);
+    on_click(".button_prev_chunk", coverage.to_prev_chunk_nicely);
+    on_click(".button_top_of_page", coverage.to_top);
+    on_click(".button_first_chunk", coverage.to_first_chunk);
+
+    coverage.filters = undefined;
+    try {
+        coverage.filters = localStorage.getItem(coverage.LINE_FILTERS_STORAGE);
+    } catch(err) {}
+
+    if (coverage.filters) {
+        coverage.filters = JSON.parse(coverage.filters);
+    }
+    else {
+        coverage.filters = {run: false, exc: true, mis: true, par: true};
+    }
+
+    for (cls in coverage.filters) {
+        coverage.set_line_visibilty(cls, coverage.filters[cls]);
+    }
+
+    coverage.assign_shortkeys();
+    coverage.init_scroll_markers();
+    coverage.wire_up_sticky_header();
+
+    // Rebuild scroll markers when the window height changes.
+    window.addEventListener("resize", coverage.build_scroll_markers);
+};
+
+coverage.toggle_lines = function (event) {
+    const btn = event.target.closest("button");
+    const category = btn.value
+    const show = !btn.classList.contains("show_" + category);
+    coverage.set_line_visibilty(category, show);
+    coverage.build_scroll_markers();
+    coverage.filters[category] = show;
+    try {
+        localStorage.setItem(coverage.LINE_FILTERS_STORAGE, JSON.stringify(coverage.filters));
+    } catch(err) {}
+};
+
+coverage.set_line_visibilty = function (category, should_show) {
+    const cls = "show_" + category;
+    const btn = document.querySelector(".button_toggle_" + category);
+    if (btn) {
+        if (should_show) {
+            document.querySelectorAll("#source ." + category).forEach(e => e.classList.add(cls));
+            btn.classList.add(cls);
+        }
+        else {
+            document.querySelectorAll("#source ." + category).forEach(e => e.classList.remove(cls));
+            btn.classList.remove(cls);
+        }
+    }
+};
+
+// Return the nth line div.
+coverage.line_elt = function (n) {
+    return document.getElementById("t" + n)?.closest("p");
+};
+
+// Set the selection.  b and e are line numbers.
+coverage.set_sel = function (b, e) {
+    // The first line selected.
+    coverage.sel_begin = b;
+    // The next line not selected.
+    coverage.sel_end = (e === undefined) ? b+1 : e;
+};
+
+coverage.to_top = function () {
+    coverage.set_sel(0, 1);
+    coverage.scroll_window(0);
+};
+
+coverage.to_first_chunk = function () {
+    coverage.set_sel(0, 1);
+    coverage.to_next_chunk();
+};
+
+// Return a string indicating what kind of chunk this line belongs to,
+// or null if not a chunk.
+coverage.chunk_indicator = function (line_elt) {
+    const classes = line_elt?.className;
+    if (!classes) {
+        return null;
+    }
+    const match = classes.match(/\bshow_\w+\b/);
+    if (!match) {
+        return null;
+    }
+    return match[0];
+};
+
+coverage.to_next_chunk = function () {
+    const c = coverage;
+
+    // Find the start of the next colored chunk.
+    var probe = c.sel_end;
+    var chunk_indicator, probe_line;
+    while (true) {
+        probe_line = c.line_elt(probe);
+        if (!probe_line) {
+            return;
+        }
+        chunk_indicator = c.chunk_indicator(probe_line);
+        if (chunk_indicator) {
+            break;
+        }
+        probe++;
+    }
+
+    // There's a next chunk, `probe` points to it.
+    var begin = probe;
+
+    // Find the end of this chunk.
+    var next_indicator = chunk_indicator;
+    while (next_indicator === chunk_indicator) {
+        probe++;
+        probe_line = c.line_elt(probe);
+        next_indicator = c.chunk_indicator(probe_line);
+    }
+    c.set_sel(begin, probe);
+    c.show_selection();
+};
+
+coverage.to_prev_chunk = function () {
+    const c = coverage;
+
+    // Find the end of the prev colored chunk.
+    var probe = c.sel_begin-1;
+    var probe_line = c.line_elt(probe);
+    if (!probe_line) {
+        return;
+    }
+    var chunk_indicator = c.chunk_indicator(probe_line);
+    while (probe > 1 && !chunk_indicator) {
+        probe--;
+        probe_line = c.line_elt(probe);
+        if (!probe_line) {
+            return;
+        }
+        chunk_indicator = c.chunk_indicator(probe_line);
+    }
+
+    // There's a prev chunk, `probe` points to its last line.
+    var end = probe+1;
+
+    // Find the beginning of this chunk.
+    var prev_indicator = chunk_indicator;
+    while (prev_indicator === chunk_indicator) {
+        probe--;
+        if (probe <= 0) {
+            return;
+        }
+        probe_line = c.line_elt(probe);
+        prev_indicator = c.chunk_indicator(probe_line);
+    }
+    c.set_sel(probe+1, end);
+    c.show_selection();
+};
+
+// Returns 0, 1, or 2: how many of the two ends of the selection are on
+// the screen right now?
+coverage.selection_ends_on_screen = function () {
+    if (coverage.sel_begin === 0) {
+        return 0;
+    }
+
+    const begin = coverage.line_elt(coverage.sel_begin);
+    const end = coverage.line_elt(coverage.sel_end-1);
+
+    return (
+        (checkVisible(begin) ? 1 : 0)
+        + (checkVisible(end) ? 1 : 0)
+    );
+};
+
+coverage.to_next_chunk_nicely = function () {
+    if (coverage.selection_ends_on_screen() === 0) {
+        // The selection is entirely off the screen:
+        // Set the top line on the screen as selection.
+
+        // This will select the top-left of the viewport
+        // As this is most likely the span with the line number we take the parent
+        const line = document.elementFromPoint(0, 0).parentElement;
+        if (line.parentElement !== document.getElementById("source")) {
+            // The element is not a source line but the header or similar
+            coverage.select_line_or_chunk(1);
+        } else {
+            // We extract the line number from the id
+            coverage.select_line_or_chunk(parseInt(line.id.substring(1), 10));
+        }
+    }
+    coverage.to_next_chunk();
+};
+
+coverage.to_prev_chunk_nicely = function () {
+    if (coverage.selection_ends_on_screen() === 0) {
+        // The selection is entirely off the screen:
+        // Set the lowest line on the screen as selection.
+
+        // This will select the bottom-left of the viewport
+        // As this is most likely the span with the line number we take the parent
+        const line = document.elementFromPoint(document.documentElement.clientHeight-1, 0).parentElement;
+        if (line.parentElement !== document.getElementById("source")) {
+            // The element is not a source line but the header or similar
+            coverage.select_line_or_chunk(coverage.lines_len);
+        } else {
+            // We extract the line number from the id
+            coverage.select_line_or_chunk(parseInt(line.id.substring(1), 10));
+        }
+    }
+    coverage.to_prev_chunk();
+};
+
+// Select line number lineno, or if it is in a colored chunk, select the
+// entire chunk
+coverage.select_line_or_chunk = function (lineno) {
+    var c = coverage;
+    var probe_line = c.line_elt(lineno);
+    if (!probe_line) {
+        return;
+    }
+    var the_indicator = c.chunk_indicator(probe_line);
+    if (the_indicator) {
+        // The line is in a highlighted chunk.
+        // Search backward for the first line.
+        var probe = lineno;
+        var indicator = the_indicator;
+        while (probe > 0 && indicator === the_indicator) {
+            probe--;
+            probe_line = c.line_elt(probe);
+            if (!probe_line) {
+                break;
+            }
+            indicator = c.chunk_indicator(probe_line);
+        }
+        var begin = probe + 1;
+
+        // Search forward for the last line.
+        probe = lineno;
+        indicator = the_indicator;
+        while (indicator === the_indicator) {
+            probe++;
+            probe_line = c.line_elt(probe);
+            indicator = c.chunk_indicator(probe_line);
+        }
+
+        coverage.set_sel(begin, probe);
+    }
+    else {
+        coverage.set_sel(lineno);
+    }
+};
+
+coverage.show_selection = function () {
+    // Highlight the lines in the chunk
+    document.querySelectorAll("#source .highlight").forEach(e => e.classList.remove("highlight"));
+    for (let probe = coverage.sel_begin; probe < coverage.sel_end; probe++) {
+        coverage.line_elt(probe).querySelector(".n").classList.add("highlight");
+    }
+
+    coverage.scroll_to_selection();
+};
+
+coverage.scroll_to_selection = function () {
+    // Scroll the page if the chunk isn't fully visible.
+    if (coverage.selection_ends_on_screen() < 2) {
+        const element = coverage.line_elt(coverage.sel_begin);
+        coverage.scroll_window(element.offsetTop - 60);
+    }
+};
+
+coverage.scroll_window = function (to_pos) {
+    window.scroll({top: to_pos, behavior: "smooth"});
+};
+
+coverage.init_scroll_markers = function () {
+    // Init some variables
+    coverage.lines_len = document.querySelectorAll('#source > p').length;
+
+    // Build html
+    coverage.build_scroll_markers();
+};
+
+coverage.build_scroll_markers = function () {
+    const temp_scroll_marker = document.getElementById('scroll_marker')
+    if (temp_scroll_marker) temp_scroll_marker.remove();
+    // Don't build markers if the window has no scroll bar.
+    if (document.body.scrollHeight <= window.innerHeight) {
+        return;
+    }
+
+    const marker_scale = window.innerHeight / document.body.scrollHeight;
+    const line_height = Math.min(Math.max(3, window.innerHeight / coverage.lines_len), 10);
+
+    let previous_line = -99, last_mark, last_top;
+
+    const scroll_marker = document.createElement("div");
+    scroll_marker.id = "scroll_marker";
+    document.getElementById('source').querySelectorAll(
+        'p.show_run, p.show_mis, p.show_exc, p.show_exc, p.show_par'
+    ).forEach(element => {
+        const line_top = Math.floor(element.offsetTop * marker_scale);
+        const line_number = parseInt(element.id.substr(1));
+
+        if (line_number === previous_line + 1) {
+            // If this solid missed block just make previous mark higher.
+            last_mark.style.height = `${line_top + line_height - last_top}px`;
+        } else {
+            // Add colored line in scroll_marker block.
+            last_mark = document.createElement("div");
+            last_mark.id = `m${line_number}`;
+            last_mark.classList.add("marker");
+            last_mark.style.height = `${line_height}px`;
+            last_mark.style.top = `${line_top}px`;
+            scroll_marker.append(last_mark);
+            last_top = line_top;
+        }
+
+        previous_line = line_number;
+    });
+
+    // Append last to prevent layout calculation
+    document.body.append(scroll_marker);
+};
+
+coverage.wire_up_sticky_header = function () {
+    const header = document.querySelector('header');
+    const header_bottom = (
+        header.querySelector('.content h2').getBoundingClientRect().top -
+        header.getBoundingClientRect().top
+    );
+
+    function updateHeader() {
+        if (window.scrollY > header_bottom) {
+            header.classList.add('sticky');
+        } else {
+            header.classList.remove('sticky');
+        }
+    }
+
+    window.addEventListener('scroll', updateHeader);
+    updateHeader();
+};
+
+document.addEventListener("DOMContentLoaded", () => {
+    if (document.body.classList.contains("indexfile")) {
+        coverage.index_ready();
+    } else {
+        coverage.pyfile_ready();
+    }
+});
Binary file eric7/DebugClients/Python/coverage/htmlfiles/favicon_32.png has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/eric7/DebugClients/Python/coverage/htmlfiles/index.html	Sat Nov 20 16:47:38 2021 +0100
@@ -0,0 +1,121 @@
+{# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 #}
+{# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt #}
+
+<!DOCTYPE html>
+<html>
+<head>
+    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+    <title>{{ title|escape }}</title>
+    <link rel="icon" sizes="32x32" href="favicon_32.png">
+    <link rel="stylesheet" href="style.css" type="text/css">
+    {% if extra_css %}
+        <link rel="stylesheet" href="{{ extra_css }}" type="text/css">
+    {% endif %}
+    <script type="text/javascript" src="coverage_html.js" defer></script>
+</head>
+<body class="indexfile">
+
+<header>
+    <div class="content">
+        <h1>{{ title|escape }}:
+            <span class="pc_cov">{{totals.pc_covered_str}}%</span>
+        </h1>
+
+        <div id="help_panel_wrapper">
+            <input id="help_panel_state" type="checkbox">
+            <label for="help_panel_state">
+                <img id="keyboard_icon" src="keybd_closed.png" alt="Show/hide keyboard shortcuts" />
+            </label>
+            <div id="help_panel">
+                <p class="legend">Shortcuts on this page</p>
+                <div>
+                    <p class="keyhelp">
+                        <kbd>n</kbd>
+                        <kbd>s</kbd>
+                        <kbd>m</kbd>
+                        <kbd>x</kbd>
+                        {% if has_arcs %}
+                        <kbd>b</kbd>
+                        <kbd>p</kbd>
+                        {% endif %}
+                        <kbd>c</kbd> &nbsp; change column sorting
+                    </p>
+                </div>
+            </div>
+        </div>
+
+        <form id="filter_container">
+            <input id="filter" type="text" value="" placeholder="filter..." />
+        </form>
+    </div>
+</header>
+
+<main id="index">
+    <table class="index" data-sortable>
+        <thead>
+            {# The title="" attr doesn"t work in Safari. #}
+            <tr class="tablehead" title="Click to sort">
+                <th class="name left" aria-sort="none" data-shortcut="n">Module</th>
+                <th aria-sort="none" data-default-sort-order="descending" data-shortcut="s">statements</th>
+                <th aria-sort="none" data-default-sort-order="descending" data-shortcut="m">missing</th>
+                <th aria-sort="none" data-default-sort-order="descending" data-shortcut="x">excluded</th>
+                {% if has_arcs %}
+                <th aria-sort="none" data-default-sort-order="descending" data-shortcut="b">branches</th>
+                <th aria-sort="none" data-default-sort-order="descending" data-shortcut="p">partial</th>
+                {% endif %}
+                <th class="right" aria-sort="none" data-shortcut="c">coverage</th>
+            </tr>
+        </thead>
+        <tbody>
+            {% for file in files %}
+            <tr class="file">
+                <td class="name left"><a href="{{file.html_filename}}">{{file.relative_filename}}</a></td>
+                <td>{{file.nums.n_statements}}</td>
+                <td>{{file.nums.n_missing}}</td>
+                <td>{{file.nums.n_excluded}}</td>
+                {% if has_arcs %}
+                <td>{{file.nums.n_branches}}</td>
+                <td>{{file.nums.n_partial_branches}}</td>
+                {% endif %}
+                <td class="right" data-ratio="{{file.nums.ratio_covered|pair}}">{{file.nums.pc_covered_str}}%</td>
+            </tr>
+            {% endfor %}
+        </tbody>
+        <tfoot>
+            <tr class="total">
+                <td class="name left">Total</td>
+                <td>{{totals.n_statements}}</td>
+                <td>{{totals.n_missing}}</td>
+                <td>{{totals.n_excluded}}</td>
+                {% if has_arcs %}
+                <td>{{totals.n_branches}}</td>
+                <td>{{totals.n_partial_branches}}</td>
+                {% endif %}
+                <td class="right" data-ratio="{{totals.ratio_covered|pair}}">{{totals.pc_covered_str}}%</td>
+            </tr>
+        </tfoot>
+    </table>
+
+    <p id="no_rows">
+        No items found using the specified filter.
+    </p>
+
+    {% if skipped_covered_msg %}
+        <p>{{ skipped_covered_msg }}</p>
+    {% endif %}
+    {% if skipped_empty_msg %}
+        <p>{{ skipped_empty_msg }}</p>
+    {% endif %}
+</main>
+
+<footer>
+    <div class="content">
+        <p>
+            <a class="nav" href="{{__url__}}">coverage.py v{{__version__}}</a>,
+            created at {{ time_stamp }}
+        </p>
+    </div>
+</footer>
+
+</body>
+</html>
Binary file eric7/DebugClients/Python/coverage/htmlfiles/keybd_closed.png has changed
Binary file eric7/DebugClients/Python/coverage/htmlfiles/keybd_open.png has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/eric7/DebugClients/Python/coverage/htmlfiles/pyfile.html	Sat Nov 20 16:47:38 2021 +0100
@@ -0,0 +1,120 @@
+{# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 #}
+{# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt #}
+
+<!DOCTYPE html>
+<html>
+<head>
+    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+    {# IE8 rounds line-height incorrectly, and adding this emulateIE7 line makes it right! #}
+    {# http://social.msdn.microsoft.com/Forums/en-US/iewebdevelopment/thread/7684445e-f080-4d8f-8529-132763348e21 #}
+    <meta http-equiv="X-UA-Compatible" content="IE=emulateIE7" />
+    <title>Coverage for {{relative_filename|escape}}: {{nums.pc_covered_str}}%</title>
+    <link rel="icon" sizes="32x32" href="favicon_32.png">
+    <link rel="stylesheet" href="style.css" type="text/css">
+    {% if extra_css %}
+        <link rel="stylesheet" href="{{ extra_css }}" type="text/css">
+    {% endif %}
+    <script type="text/javascript" src="coverage_html.js" defer></script>
+</head>
+<body class="pyfile">
+
+<header>
+    <div class="content">
+        <h1>
+            <span class="text">Coverage for </span><b>{{relative_filename|escape}}</b>:
+            <span class="pc_cov">{{nums.pc_covered_str}}%</span>
+        </h1>
+
+        <div id="help_panel_wrapper">
+            <input id="help_panel_state" type="checkbox">
+            <label for="help_panel_state">
+                <img id="keyboard_icon" src="keybd_closed.png" alt="Show/hide keyboard shortcuts" />
+            </label>
+            <div id="help_panel">
+                <p class="legend">Shortcuts on this page</p>
+                <div>
+                    <p class="keyhelp">
+                        <kbd>r</kbd>
+                        <kbd>m</kbd>
+                        <kbd>x</kbd>
+                        {% if has_arcs %}
+                        <kbd>p</kbd>
+                        {% endif %}
+                        &nbsp; toggle line displays
+                    </p>
+                    <p class="keyhelp">
+                        <kbd>j</kbd>
+                        <kbd>k</kbd> &nbsp; next/prev highlighted chunk
+                    </p>
+                    <p class="keyhelp">
+                        <kbd>0</kbd> &nbsp; (zero) top of page
+                    </p>
+                    <p class="keyhelp">
+                        <kbd>1</kbd> &nbsp; (one) first highlighted chunk
+                    </p>
+                </div>
+            </div>
+        </div>
+
+        <h2>
+            <span class="text">{{nums.n_statements}} statements &nbsp;</span>
+            <button type="button" class="{{category.run}} button_toggle_run" value="run" data-shortcut="r" title="Toggle lines run">{{nums.n_executed}}<span class="text"> run</span></button>
+            <button type="button" class="{{category.mis}} button_toggle_mis" value="mis" data-shortcut="m" title="Toggle lines missing">{{nums.n_missing}}<span class="text"> missing</span></button>
+            <button type="button" class="{{category.exc}} button_toggle_exc" value="exc" data-shortcut="x" title="Toggle lines excluded">{{nums.n_excluded}}<span class="text"> excluded</span></button>
+            {% if has_arcs %}
+            <button type="button" class="{{category.par}} button_toggle_par" value="par" data-shortcut="p" title="Toggle lines partially run">{{nums.n_partial_branches}}<span class="text"> partial</span></button>
+            {% endif %}
+        </h2>
+
+        <div style="display: none;">
+            <button type="button" class="button_next_chunk" data-shortcut="j">Next highlighted chunk</button>
+            <button type="button" class="button_prev_chunk" data-shortcut="k">Previous highlighted chunk</button>
+            <button type="button" class="button_top_of_page" data-shortcut="0">Goto top of page</button>
+            <button type="button" class="button_first_chunk" data-shortcut="1">Goto first highlighted chunk</button>
+        </div>
+    </div>
+</header>
+
+<main id="source">
+    {% for line in lines -%}
+        {% joined %}
+        <p class="{{line.css_class}}">
+            <span class="n"><a id="t{{line.number}}" href="#t{{line.number}}">{{line.number}}</a></span>
+            <span class="t">{{line.html}}&nbsp;</span>
+            {% if line.context_list %}
+                <input type="checkbox" id="ctxs{{line.number}}" />
+            {% endif %}
+            {# Things that should float right in the line. #}
+            <span class="r">
+                {% if line.annotate %}
+                    <span class="annotate short">{{line.annotate}}</span>
+                    <span class="annotate long">{{line.annotate_long}}</span>
+                {% endif %}
+                {% if line.contexts %}
+                    <label for="ctxs{{line.number}}" class="ctx">{{ line.contexts_label }}</label>
+                {% endif %}
+            </span>
+            {# Things that should appear below the line. #}
+            {% if line.context_list %}
+                <span class="ctxs">
+                    {% for context in line.context_list %}
+                        <span>{{context}}</span>
+                    {% endfor %}
+                </span>
+            {% endif %}
+        </p>
+        {% endjoined %}
+    {% endfor %}
+</main>
+
+<footer>
+    <div class="content">
+        <p>
+            <a class="nav" href="index.html">&#xab; index</a> &nbsp; &nbsp; <a class="nav" href="{{__url__}}">coverage.py v{{__version__}}</a>,
+            created at {{ time_stamp }}
+        </p>
+    </div>
+</footer>
+
+</body>
+</html>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/eric7/DebugClients/Python/coverage/htmlfiles/style.css	Sat Nov 20 16:47:38 2021 +0100
@@ -0,0 +1,307 @@
+@charset "UTF-8";
+/* Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 */
+/* For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt */
+/* Don't edit this .css file. Edit the .scss file instead! */
+html, body, h1, h2, h3, p, table, td, th { margin: 0; padding: 0; border: 0; font-weight: inherit; font-style: inherit; font-size: 100%; font-family: inherit; vertical-align: baseline; }
+
+body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; font-size: 1em; background: #fff; color: #000; }
+
+@media (prefers-color-scheme: dark) { body { background: #1e1e1e; } }
+
+@media (prefers-color-scheme: dark) { body { color: #eee; } }
+
+html > body { font-size: 16px; }
+
+a:active, a:focus { outline: 2px dashed #007acc; }
+
+p { font-size: .875em; line-height: 1.4em; }
+
+table { border-collapse: collapse; }
+
+td { vertical-align: top; }
+
+table tr.hidden { display: none !important; }
+
+p#no_rows { display: none; font-size: 1.2em; }
+
+a.nav { text-decoration: none; color: inherit; }
+
+a.nav:hover { text-decoration: underline; color: inherit; }
+
+header { background: #f8f8f8; width: 100%; z-index: 2; border-bottom: 1px solid #ccc; }
+
+@media (prefers-color-scheme: dark) { header { background: black; } }
+
+@media (prefers-color-scheme: dark) { header { border-color: #333; } }
+
+header .content { padding: 1rem 3.5rem; }
+
+header h2 { margin-top: .5em; font-size: 1em; }
+
+header.sticky { position: fixed; left: 0; right: 0; height: 2.5em; }
+
+header.sticky .text { display: none; }
+
+header.sticky h1, header.sticky h2 { font-size: 1em; margin-top: 0; display: inline-block; }
+
+header.sticky .content { padding: 0.5rem 3.5rem; }
+
+header.sticky .content p { font-size: 1em; }
+
+header.sticky ~ #source { padding-top: 6.5em; }
+
+main { position: relative; z-index: 1; }
+
+.indexfile footer { margin: 1rem 3.5rem; }
+
+.pyfile footer { margin: 1rem 1rem; }
+
+footer .content { padding: 0; color: #666; font-style: italic; }
+
+@media (prefers-color-scheme: dark) { footer .content { color: #aaa; } }
+
+#index { margin: 1rem 0 0 3.5rem; }
+
+h1 { font-size: 1.25em; display: inline-block; }
+
+#filter_container { float: right; margin: 0 2em 0 0; }
+
+#filter_container input { width: 10em; padding: 0.2em 0.5em; border: 2px solid #ccc; background: #fff; color: #000; }
+
+@media (prefers-color-scheme: dark) { #filter_container input { border-color: #444; } }
+
+@media (prefers-color-scheme: dark) { #filter_container input { background: #1e1e1e; } }
+
+@media (prefers-color-scheme: dark) { #filter_container input { color: #eee; } }
+
+#filter_container input:focus { border-color: #007acc; }
+
+header button { font-family: inherit; font-size: inherit; border: 1px solid; border-radius: .2em; color: inherit; padding: .1em .5em; margin: 1px calc(.1em + 1px); cursor: pointer; border-color: #ccc; }
+
+@media (prefers-color-scheme: dark) { header button { border-color: #444; } }
+
+header button:active, header button:focus { outline: 2px dashed #007acc; }
+
+header button.run { background: #eeffee; }
+
+@media (prefers-color-scheme: dark) { header button.run { background: #373d29; } }
+
+header button.run.show_run { background: #dfd; border: 2px solid #00dd00; margin: 0 .1em; }
+
+@media (prefers-color-scheme: dark) { header button.run.show_run { background: #373d29; } }
+
+header button.mis { background: #ffeeee; }
+
+@media (prefers-color-scheme: dark) { header button.mis { background: #4b1818; } }
+
+header button.mis.show_mis { background: #fdd; border: 2px solid #ff0000; margin: 0 .1em; }
+
+@media (prefers-color-scheme: dark) { header button.mis.show_mis { background: #4b1818; } }
+
+header button.exc { background: #f7f7f7; }
+
+@media (prefers-color-scheme: dark) { header button.exc { background: #333; } }
+
+header button.exc.show_exc { background: #eee; border: 2px solid #808080; margin: 0 .1em; }
+
+@media (prefers-color-scheme: dark) { header button.exc.show_exc { background: #333; } }
+
+header button.par { background: #ffffd5; }
+
+@media (prefers-color-scheme: dark) { header button.par { background: #650; } }
+
+header button.par.show_par { background: #ffa; border: 2px solid #bbbb00; margin: 0 .1em; }
+
+@media (prefers-color-scheme: dark) { header button.par.show_par { background: #650; } }
+
+#help_panel, #source p .annotate.long { display: none; position: absolute; z-index: 999; background: #ffffcc; border: 1px solid #888; border-radius: .2em; color: #333; padding: .25em .5em; }
+
+#source p .annotate.long { white-space: normal; float: right; top: 1.75em; right: 1em; height: auto; }
+
+#help_panel_wrapper { float: right; position: relative; }
+
+#keyboard_icon { margin: 5px; }
+
+#help_panel_state { display: none; }
+
+#help_panel { top: 25px; right: 0; padding: .75em; border: 1px solid #883; }
+
+#help_panel .legend { font-style: italic; margin-bottom: 1em; }
+
+.indexfile #help_panel { width: 25em; }
+
+.pyfile #help_panel { width: 18em; }
+
+#help_panel_state:checked ~ #help_panel { display: block; }
+
+.keyhelp { margin-top: .75em; }
+
+kbd { border: 1px solid black; border-color: #888 #333 #333 #888; padding: .1em .35em; font-family: SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-weight: bold; background: #eee; border-radius: 3px; }
+
+#source { padding: 1em 0 1em 3.5rem; font-family: SFMono-Regular, Menlo, Monaco, Consolas, monospace; }
+
+#source p { position: relative; white-space: pre; }
+
+#source p * { box-sizing: border-box; }
+
+#source p .n { float: left; text-align: right; width: 3.5rem; box-sizing: border-box; margin-left: -3.5rem; padding-right: 1em; color: #999; }
+
+@media (prefers-color-scheme: dark) { #source p .n { color: #777; } }
+
+#source p .n.highlight { background: #ffdd00; }
+
+#source p .n a { margin-top: -4em; padding-top: 4em; text-decoration: none; color: #999; }
+
+@media (prefers-color-scheme: dark) { #source p .n a { color: #777; } }
+
+#source p .n a:hover { text-decoration: underline; color: #999; }
+
+@media (prefers-color-scheme: dark) { #source p .n a:hover { color: #777; } }
+
+#source p .t { display: inline-block; width: 100%; box-sizing: border-box; margin-left: -.5em; padding-left: 0.3em; border-left: 0.2em solid #fff; }
+
+@media (prefers-color-scheme: dark) { #source p .t { border-color: #1e1e1e; } }
+
+#source p .t:hover { background: #f2f2f2; }
+
+@media (prefers-color-scheme: dark) { #source p .t:hover { background: #282828; } }
+
+#source p .t:hover ~ .r .annotate.long { display: block; }
+
+#source p .t .com { color: #008000; font-style: italic; line-height: 1px; }
+
+@media (prefers-color-scheme: dark) { #source p .t .com { color: #6a9955; } }
+
+#source p .t .key { font-weight: bold; line-height: 1px; }
+
+#source p .t .str { color: #0451a5; }
+
+@media (prefers-color-scheme: dark) { #source p .t .str { color: #9cdcfe; } }
+
+#source p.mis .t { border-left: 0.2em solid #ff0000; }
+
+#source p.mis.show_mis .t { background: #fdd; }
+
+@media (prefers-color-scheme: dark) { #source p.mis.show_mis .t { background: #4b1818; } }
+
+#source p.mis.show_mis .t:hover { background: #f2d2d2; }
+
+@media (prefers-color-scheme: dark) { #source p.mis.show_mis .t:hover { background: #532323; } }
+
+#source p.run .t { border-left: 0.2em solid #00dd00; }
+
+#source p.run.show_run .t { background: #dfd; }
+
+@media (prefers-color-scheme: dark) { #source p.run.show_run .t { background: #373d29; } }
+
+#source p.run.show_run .t:hover { background: #d2f2d2; }
+
+@media (prefers-color-scheme: dark) { #source p.run.show_run .t:hover { background: #404633; } }
+
+#source p.exc .t { border-left: 0.2em solid #808080; }
+
+#source p.exc.show_exc .t { background: #eee; }
+
+@media (prefers-color-scheme: dark) { #source p.exc.show_exc .t { background: #333; } }
+
+#source p.exc.show_exc .t:hover { background: #e2e2e2; }
+
+@media (prefers-color-scheme: dark) { #source p.exc.show_exc .t:hover { background: #3c3c3c; } }
+
+#source p.par .t { border-left: 0.2em solid #bbbb00; }
+
+#source p.par.show_par .t { background: #ffa; }
+
+@media (prefers-color-scheme: dark) { #source p.par.show_par .t { background: #650; } }
+
+#source p.par.show_par .t:hover { background: #f2f2a2; }
+
+@media (prefers-color-scheme: dark) { #source p.par.show_par .t:hover { background: #6d5d0c; } }
+
+#source p .r { position: absolute; top: 0; right: 2.5em; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; }
+
+#source p .annotate { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; color: #666; padding-right: .5em; }
+
+@media (prefers-color-scheme: dark) { #source p .annotate { color: #ddd; } }
+
+#source p .annotate.short:hover ~ .long { display: block; }
+
+#source p .annotate.long { width: 30em; right: 2.5em; }
+
+#source p input { display: none; }
+
+#source p input ~ .r label.ctx { cursor: pointer; border-radius: .25em; }
+
+#source p input ~ .r label.ctx::before { content: "▶ "; }
+
+#source p input ~ .r label.ctx:hover { background: #e8f4ff; color: #666; }
+
+@media (prefers-color-scheme: dark) { #source p input ~ .r label.ctx:hover { background: #0f3a42; } }
+
+@media (prefers-color-scheme: dark) { #source p input ~ .r label.ctx:hover { color: #aaa; } }
+
+#source p input:checked ~ .r label.ctx { background: #d0e8ff; color: #666; border-radius: .75em .75em 0 0; padding: 0 .5em; margin: -.25em 0; }
+
+@media (prefers-color-scheme: dark) { #source p input:checked ~ .r label.ctx { background: #056; } }
+
+@media (prefers-color-scheme: dark) { #source p input:checked ~ .r label.ctx { color: #aaa; } }
+
+#source p input:checked ~ .r label.ctx::before { content: "▼ "; }
+
+#source p input:checked ~ .ctxs { padding: .25em .5em; overflow-y: scroll; max-height: 10.5em; }
+
+#source p label.ctx { color: #999; display: inline-block; padding: 0 .5em; font-size: .8333em; }
+
+@media (prefers-color-scheme: dark) { #source p label.ctx { color: #777; } }
+
+#source p .ctxs { display: block; max-height: 0; overflow-y: hidden; transition: all .2s; padding: 0 .5em; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; white-space: nowrap; background: #d0e8ff; border-radius: .25em; margin-right: 1.75em; }
+
+@media (prefers-color-scheme: dark) { #source p .ctxs { background: #056; } }
+
+#source p .ctxs span { display: block; text-align: right; }
+
+#index { font-family: SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-size: 0.875em; }
+
+#index table.index { margin-left: -.5em; }
+
+#index td, #index th { text-align: right; width: 5em; padding: .25em .5em; border-bottom: 1px solid #eee; }
+
+@media (prefers-color-scheme: dark) { #index td, #index th { border-color: #333; } }
+
+#index td.name, #index th.name { text-align: left; width: auto; }
+
+#index th { font-style: italic; color: #333; cursor: pointer; }
+
+@media (prefers-color-scheme: dark) { #index th { color: #ddd; } }
+
+#index th:hover { background: #eee; }
+
+@media (prefers-color-scheme: dark) { #index th:hover { background: #333; } }
+
+#index th[aria-sort="ascending"], #index th[aria-sort="descending"] { white-space: nowrap; background: #eee; padding-left: .5em; }
+
+@media (prefers-color-scheme: dark) { #index th[aria-sort="ascending"], #index th[aria-sort="descending"] { background: #333; } }
+
+#index th[aria-sort="ascending"]::after { font-family: sans-serif; content: " ↑"; }
+
+#index th[aria-sort="descending"]::after { font-family: sans-serif; content: " ↓"; }
+
+#index td.name a { text-decoration: none; color: inherit; }
+
+#index tr.total td, #index tr.total_dynamic td { font-weight: bold; border-top: 1px solid #ccc; border-bottom: none; }
+
+#index tr.file:hover { background: #eee; }
+
+@media (prefers-color-scheme: dark) { #index tr.file:hover { background: #333; } }
+
+#index tr.file:hover td.name { text-decoration: underline; color: inherit; }
+
+#scroll_marker { position: fixed; z-index: 3; right: 0; top: 0; width: 16px; height: 100%; background: #fff; border-left: 1px solid #eee; will-change: transform; }
+
+@media (prefers-color-scheme: dark) { #scroll_marker { background: #1e1e1e; } }
+
+@media (prefers-color-scheme: dark) { #scroll_marker { border-color: #333; } }
+
+#scroll_marker .marker { background: #ccc; position: absolute; min-height: 3px; width: 100%; }
+
+@media (prefers-color-scheme: dark) { #scroll_marker .marker { background: #444; } }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/eric7/DebugClients/Python/coverage/htmlfiles/style.scss	Sat Nov 20 16:47:38 2021 +0100
@@ -0,0 +1,712 @@
+/* Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 */
+/* For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt */
+
+// CSS styles for coverage.py HTML reports.
+
+// When you edit this file, you need to run "make css" to get the CSS file
+// generated, and then check in both the .scss and the .css files.
+
+// When working on the file, this command is useful:
+//      sass --watch --style=compact --sourcemap=none --no-cache coverage/htmlfiles/style.scss:htmlcov/style.css
+//
+// OR you can process sass purely in python with `pip install pysass`, then:
+//      pysassc --style=compact coverage/htmlfiles/style.scss coverage/htmlfiles/style.css
+
+// Ignore this comment, it's for the CSS output file:
+/* Don't edit this .css file. Edit the .scss file instead! */
+
+// Dimensions
+$left-gutter: 3.5rem;
+
+//
+// Declare colors and variables
+//
+
+$font-normal:             -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
+$font-code:               SFMono-Regular, Menlo, Monaco, Consolas, monospace;
+
+$off-button-lighten:      50%;
+$hover-dark-amt:          95%;
+
+$focus-color:             #007acc;
+
+$mis-color:               #ff0000;
+$run-color:               #00dd00;
+$exc-color:               #808080;
+$par-color:               #bbbb00;
+
+$light-bg:                #fff;
+$light-fg:                #000;
+$light-gray1:             #f8f8f8;
+$light-gray2:             #eee;
+$light-gray3:             #ccc;
+$light-gray4:             #999;
+$light-gray5:             #666;
+$light-gray6:             #333;
+$light-pln-bg:            $light-bg;
+$light-mis-bg:            #fdd;
+$light-run-bg:            #dfd;
+$light-exc-bg:            $light-gray2;
+$light-par-bg:            #ffa;
+$light-token-com:         #008000;
+$light-token-str:         #0451a5;
+$light-context-bg-color:  #d0e8ff;
+
+$dark-bg:                 #1e1e1e;
+$dark-fg:                 #eee;
+$dark-gray1:              #222;
+$dark-gray2:              #333;
+$dark-gray3:              #444;
+$dark-gray4:              #777;
+$dark-gray5:              #aaa;
+$dark-gray6:              #ddd;
+$dark-pln-bg:             $dark-bg;
+$dark-mis-bg:             #4b1818;
+$dark-run-bg:             #373d29;
+$dark-exc-bg:             $dark-gray2;
+$dark-par-bg:             #650;
+$dark-token-com:          #6a9955;
+$dark-token-str:          #9cdcfe;
+$dark-context-bg-color:   #056;
+
+//
+// Mixins and utilities
+//
+
+@mixin background-dark($color) {
+    @media (prefers-color-scheme: dark) {
+        background: $color;
+    }
+}
+@mixin color-dark($color) {
+    @media (prefers-color-scheme: dark) {
+        color: $color;
+    }
+}
+@mixin border-color-dark($color) {
+    @media (prefers-color-scheme: dark) {
+        border-color: $color;
+    }
+}
+
+// Add visual outline to navigable elements on focus improve accessibility.
+@mixin focus-border {
+    &:active, &:focus {
+        outline: 2px dashed $focus-color;
+    }
+}
+
+// Page-wide styles
+html, body, h1, h2, h3, p, table, td, th {
+    margin: 0;
+    padding: 0;
+    border: 0;
+    font-weight: inherit;
+    font-style: inherit;
+    font-size: 100%;
+    font-family: inherit;
+    vertical-align: baseline;
+}
+
+// Set baseline grid to 16 pt.
+body {
+    font-family: $font-normal;
+    font-size: 1em;
+    background: $light-bg;
+    color: $light-fg;
+    @include background-dark($dark-bg);
+    @include color-dark($dark-fg);
+}
+
+html>body {
+    font-size: 16px;
+}
+
+a {
+    @include focus-border;
+}
+
+p {
+    font-size: .875em;
+    line-height: 1.4em;
+}
+
+table {
+    border-collapse: collapse;
+}
+td {
+    vertical-align: top;
+}
+table tr.hidden {
+    display: none !important;
+}
+
+p#no_rows {
+    display: none;
+    font-size: 1.2em;
+}
+
+a.nav {
+    text-decoration: none;
+    color: inherit;
+
+    &:hover {
+        text-decoration: underline;
+        color: inherit;
+    }
+}
+
+// Page structure
+header {
+    background: $light-gray1;
+    @include background-dark(black);
+    width: 100%;
+    z-index: 2;
+    border-bottom: 1px solid $light-gray3;
+    @include border-color-dark($dark-gray2);
+
+    .content {
+        padding: 1rem $left-gutter;
+    }
+
+    h2 {
+        margin-top: .5em;
+        font-size: 1em;
+    }
+
+    &.sticky {
+        position: fixed;
+        left: 0;
+        right: 0;
+        height: 2.5em;
+
+        .text {
+            display: none;
+        }
+
+        h1, h2 {
+            font-size: 1em;
+            margin-top: 0;
+            display: inline-block;
+        }
+
+        .content {
+            padding: .5rem $left-gutter;
+            p {
+                font-size: 1em;
+            }
+        }
+
+        & ~ #source {
+            padding-top: 6.5em;
+        }
+    }
+}
+
+main {
+    position: relative;
+    z-index: 1;
+}
+
+.indexfile footer {
+    margin: 1rem $left-gutter;
+}
+
+.pyfile footer {
+    margin: 1rem 1rem;
+}
+
+footer .content {
+    padding: 0;
+    color: $light-gray5;
+    @include color-dark($dark-gray5);
+    font-style: italic;
+}
+
+#index {
+    margin: 1rem 0 0 $left-gutter;
+}
+
+// Header styles
+
+h1 {
+    font-size: 1.25em;
+    display: inline-block;
+}
+
+#filter_container {
+    float: right;
+    margin: 0 2em 0 0;
+
+    input {
+        width: 10em;
+        padding: 0.2em 0.5em;
+        border: 2px solid $light-gray3;
+        background: $light-bg;
+        color: $light-fg;
+        @include border-color-dark($dark-gray3);
+        @include background-dark($dark-bg);
+        @include color-dark($dark-fg);
+        &:focus {
+            border-color: $focus-color;
+        }
+    }
+}
+
+header button {
+    font-family: inherit;
+    font-size: inherit;
+    border: 1px solid;
+    border-radius: .2em;
+    color: inherit;
+    padding: .1em .5em;
+    margin: 1px calc(.1em + 1px);
+    cursor: pointer;
+    border-color: $light-gray3;
+    @include border-color-dark($dark-gray3);
+    @include focus-border;
+
+    &.run {
+        background: mix($light-run-bg, $light-bg, $off-button-lighten);
+        @include background-dark($dark-run-bg);
+        &.show_run {
+            background: $light-run-bg;
+            @include background-dark($dark-run-bg);
+            border: 2px solid $run-color;
+            margin: 0 .1em;
+        }
+    }
+    &.mis {
+        background: mix($light-mis-bg, $light-bg, $off-button-lighten);
+        @include background-dark($dark-mis-bg);
+        &.show_mis {
+            background: $light-mis-bg;
+            @include background-dark($dark-mis-bg);
+            border: 2px solid $mis-color;
+            margin: 0 .1em;
+        }
+    }
+    &.exc {
+        background: mix($light-exc-bg, $light-bg, $off-button-lighten);
+        @include background-dark($dark-exc-bg);
+        &.show_exc {
+            background: $light-exc-bg;
+            @include background-dark($dark-exc-bg);
+            border: 2px solid $exc-color;
+            margin: 0 .1em;
+        }
+    }
+    &.par {
+        background: mix($light-par-bg, $light-bg, $off-button-lighten);
+        @include background-dark($dark-par-bg);
+        &.show_par {
+            background: $light-par-bg;
+            @include background-dark($dark-par-bg);
+            border: 2px solid $par-color;
+            margin: 0 .1em;
+        }
+    }
+}
+
+// Yellow post-it things.
+%popup {
+    display: none;
+    position: absolute;
+    z-index: 999;
+    background: #ffffcc;
+    border: 1px solid #888;
+    border-radius: .2em;
+    color: #333;
+    padding: .25em .5em;
+}
+
+// Yellow post-it's in the text listings.
+%in-text-popup {
+    @extend %popup;
+    white-space: normal;
+    float: right;
+    top: 1.75em;
+    right: 1em;
+    height: auto;
+}
+
+// Help panel
+#help_panel_wrapper {
+    float: right;
+    position: relative;
+}
+
+#keyboard_icon {
+    margin: 5px;
+}
+
+#help_panel_state {
+    display: none;
+}
+
+#help_panel {
+    @extend %popup;
+    top: 25px;
+    right: 0;
+    padding: .75em;
+    border: 1px solid #883;
+
+    .legend {
+        font-style: italic;
+        margin-bottom: 1em;
+    }
+
+    .indexfile & {
+        width: 25em;
+        //min-height: 4em;
+    }
+
+    .pyfile & {
+        width: 18em;
+        //min-height: 8em;
+    }
+
+    #help_panel_state:checked ~ & {
+        display: block;
+    }
+}
+
+.keyhelp {
+    margin-top: .75em;
+}
+
+kbd {
+    border: 1px solid black;
+    border-color: #888 #333 #333 #888;
+    padding: .1em .35em;
+    font-family: $font-code;
+    font-weight: bold;
+    background: #eee;
+    border-radius: 3px;
+}
+
+// Source file styles
+
+// The slim bar at the left edge of the source lines, colored by coverage.
+$border-indicator-width: .2em;
+
+#source {
+    padding: 1em 0 1em $left-gutter;
+    font-family: $font-code;
+
+    p {
+        // position relative makes position:absolute pop-ups appear in the right place.
+        position: relative;
+        white-space: pre;
+
+        * {
+            box-sizing: border-box;
+        }
+
+        .n {
+            float: left;
+            text-align: right;
+            width: $left-gutter;
+            box-sizing: border-box;
+            margin-left: -$left-gutter;
+            padding-right: 1em;
+            color: $light-gray4;
+            @include color-dark($dark-gray4);
+
+            &.highlight {
+                background: #ffdd00;
+            }
+
+            a {
+                // These two lines make anchors to the line scroll the line to be
+                // visible beneath the fixed-position header.
+                margin-top: -4em;
+                padding-top: 4em;
+
+                text-decoration: none;
+                color: $light-gray4;
+                @include color-dark($dark-gray4);
+                &:hover {
+                    text-decoration: underline;
+                    color: $light-gray4;
+                    @include color-dark($dark-gray4);
+                }
+            }
+        }
+
+        .t {
+            display: inline-block;
+            width: 100%;
+            box-sizing: border-box;
+            margin-left: -.5em;
+            padding-left: .5em - $border-indicator-width;
+            border-left: $border-indicator-width solid $light-bg;
+            @include border-color-dark($dark-bg);
+
+            &:hover {
+                background: mix($light-pln-bg, $light-fg, $hover-dark-amt);
+                @include background-dark(mix($dark-pln-bg, $dark-fg, $hover-dark-amt));
+
+                & ~ .r .annotate.long {
+                    display: block;
+                }
+            }
+
+            // Syntax coloring
+            .com {
+                color: $light-token-com;
+                @include color-dark($dark-token-com);
+                font-style: italic;
+                line-height: 1px;
+            }
+            .key {
+                font-weight: bold;
+                line-height: 1px;
+            }
+            .str {
+                color: $light-token-str;
+                @include color-dark($dark-token-str);
+            }
+        }
+
+        &.mis {
+            .t {
+                border-left: $border-indicator-width solid $mis-color;
+            }
+
+            &.show_mis .t {
+                background: $light-mis-bg;
+                @include background-dark($dark-mis-bg);
+
+                &:hover {
+                    background: mix($light-mis-bg, $light-fg, $hover-dark-amt);
+                    @include background-dark(mix($dark-mis-bg, $dark-fg, $hover-dark-amt));
+                }
+            }
+        }
+
+        &.run {
+            .t {
+                border-left: $border-indicator-width solid $run-color;
+            }
+
+            &.show_run .t {
+                background: $light-run-bg;
+                @include background-dark($dark-run-bg);
+
+                &:hover {
+                    background: mix($light-run-bg, $light-fg, $hover-dark-amt);
+                    @include background-dark(mix($dark-run-bg, $dark-fg, $hover-dark-amt));
+                }
+            }
+        }
+
+        &.exc {
+            .t {
+                border-left: $border-indicator-width solid $exc-color;
+            }
+
+            &.show_exc .t {
+                background: $light-exc-bg;
+                @include background-dark($dark-exc-bg);
+
+                &:hover {
+                    background: mix($light-exc-bg, $light-fg, $hover-dark-amt);
+                    @include background-dark(mix($dark-exc-bg, $dark-fg, $hover-dark-amt));
+                }
+            }
+        }
+
+        &.par {
+            .t {
+                border-left: $border-indicator-width solid $par-color;
+            }
+
+            &.show_par .t {
+                background: $light-par-bg;
+                @include background-dark($dark-par-bg);
+
+                &:hover {
+                    background: mix($light-par-bg, $light-fg, $hover-dark-amt);
+                    @include background-dark(mix($dark-par-bg, $dark-fg, $hover-dark-amt));
+                }
+            }
+
+        }
+
+        .r {
+            position: absolute;
+            top: 0;
+            right: 2.5em;
+            font-family: $font-normal;
+        }
+
+        .annotate {
+            font-family: $font-normal;
+            color: $light-gray5;
+            @include color-dark($dark-gray6);
+            padding-right: .5em;
+
+            &.short:hover ~ .long {
+                display: block;
+            }
+
+            &.long {
+                @extend %in-text-popup;
+                width: 30em;
+                right: 2.5em;
+            }
+        }
+
+        input {
+            display: none;
+
+            & ~ .r label.ctx {
+                cursor: pointer;
+                border-radius: .25em;
+                &::before {
+                    content: "▶ ";
+                }
+                &:hover {
+                    background: mix($light-context-bg-color, $light-bg, $off-button-lighten);
+                    @include background-dark(mix($dark-context-bg-color, $dark-bg, $off-button-lighten));
+                    color: $light-gray5;
+                    @include color-dark($dark-gray5);
+                }
+            }
+
+            &:checked ~ .r label.ctx {
+                background: $light-context-bg-color;
+                @include background-dark($dark-context-bg-color);
+                color: $light-gray5;
+                @include color-dark($dark-gray5);
+                border-radius: .75em .75em 0 0;
+                padding: 0 .5em;
+                margin: -.25em 0;
+                &::before {
+                    content: "▼ ";
+                }
+            }
+
+            &:checked ~ .ctxs {
+                padding: .25em .5em;
+                overflow-y: scroll;
+                max-height: 10.5em;
+            }
+        }
+
+        label.ctx {
+            color: $light-gray4;
+            @include color-dark($dark-gray4);
+            display: inline-block;
+            padding: 0 .5em;
+            font-size: .8333em;   // 10/12
+        }
+
+        .ctxs {
+            display: block;
+            max-height: 0;
+            overflow-y: hidden;
+            transition: all .2s;
+            padding: 0 .5em;
+            font-family: $font-normal;
+            white-space: nowrap;
+            background: $light-context-bg-color;
+            @include background-dark($dark-context-bg-color);
+            border-radius: .25em;
+            margin-right: 1.75em;
+            span {
+                display: block;
+                text-align: right;
+            }
+        }
+    }
+}
+
+
+// index styles
+#index {
+    font-family: $font-code;
+    font-size: 0.875em;
+
+    table.index {
+        margin-left: -.5em;
+    }
+    td, th {
+        text-align: right;
+        width: 5em;
+        padding: .25em .5em;
+        border-bottom: 1px solid $light-gray2;
+        @include border-color-dark($dark-gray2);
+        &.name {
+            text-align: left;
+            width: auto;
+        }
+    }
+    th {
+        font-style: italic;
+        color: $light-gray6;
+        @include color-dark($dark-gray6);
+        cursor: pointer;
+        &:hover {
+            background: $light-gray2;
+            @include background-dark($dark-gray2);
+        }
+        &[aria-sort="ascending"], &[aria-sort="descending"] {
+            white-space: nowrap;
+            background: $light-gray2;
+            @include background-dark($dark-gray2);
+            padding-left: .5em;
+        }
+        &[aria-sort="ascending"]::after {
+            font-family: sans-serif;
+            content: " ↑";
+        }
+        &[aria-sort="descending"]::after {
+            font-family: sans-serif;
+            content: " ↓";
+        }
+    }
+    td.name a {
+        text-decoration: none;
+        color: inherit;
+    }
+
+    tr.total td,
+    tr.total_dynamic td {
+        font-weight: bold;
+        border-top: 1px solid #ccc;
+        border-bottom: none;
+    }
+    tr.file:hover {
+        background: $light-gray2;
+        @include background-dark($dark-gray2);
+        td.name {
+            text-decoration: underline;
+            color: inherit;
+        }
+    }
+}
+
+// scroll marker styles
+#scroll_marker {
+    position: fixed;
+    z-index: 3;
+    right: 0;
+    top: 0;
+    width: 16px;
+    height: 100%;
+    background: $light-bg;
+    border-left: 1px solid $light-gray2;
+    @include background-dark($dark-bg);
+    @include border-color-dark($dark-gray2);
+    will-change: transform; // for faster scrolling of fixed element in Chrome
+
+    .marker {
+        background: $light-gray3;
+        @include background-dark($dark-gray3);
+        position: absolute;
+        min-height: 3px;
+        width: 100%;
+    }
+}
--- a/eric7/DebugClients/Python/coverage/inorout.py	Fri Nov 19 19:28:47 2021 +0100
+++ b/eric7/DebugClients/Python/coverage/inorout.py	Sat Nov 20 16:47:38 2021 +0100
@@ -3,22 +3,22 @@
 
 """Determining whether files are being measured/reported or not."""
 
-# For finding the stdlib
-import atexit
+import importlib.util
 import inspect
 import itertools
 import os
 import platform
 import re
 import sys
+import sysconfig
 import traceback
 
 from coverage import env
-from coverage.backward import code_object
 from coverage.disposition import FileDisposition, disposition_init
+from coverage.exceptions import CoverageException
 from coverage.files import TreeMatcher, FnmatchMatcher, ModuleMatcher
 from coverage.files import prep_patterns, find_python_files, canonical_filename
-from coverage.misc import CoverageException
+from coverage.misc import sys_modules_saved
 from coverage.python import source_for_file, source_for_morf
 
 
@@ -108,7 +108,86 @@
     return os.path.exists(mod__file__)
 
 
-class InOrOut(object):
+def file_and_path_for_module(modulename):
+    """Find the file and search path for `modulename`.
+
+    Returns:
+        filename: The filename of the module, or None.
+        path: A list (possibly empty) of directories to find submodules in.
+
+    """
+    filename = None
+    path = []
+    try:
+        spec = importlib.util.find_spec(modulename)
+    except ImportError:
+        pass
+    else:
+        if spec is not None:
+            if spec.origin != "namespace":
+                filename = spec.origin
+            path = list(spec.submodule_search_locations or ())
+    return filename, path
+
+
+def add_stdlib_paths(paths):
+    """Add paths where the stdlib can be found to the set `paths`."""
+    # Look at where some standard modules are located. That's the
+    # indication for "installed with the interpreter". In some
+    # environments (virtualenv, for example), these modules may be
+    # spread across a few locations. Look at all the candidate modules
+    # we've imported, and take all the different ones.
+    modules_we_happen_to_have = [
+        inspect, itertools, os, platform, re, sysconfig, traceback,
+        _pypy_irc_topic, _structseq,
+    ]
+    for m in modules_we_happen_to_have:
+        if m is not None and hasattr(m, "__file__"):
+            paths.add(canonical_path(m, directory=True))
+
+    if _structseq and not hasattr(_structseq, '__file__'):
+        # PyPy 2.4 has no __file__ in the builtin modules, but the code
+        # objects still have the file names.  So dig into one to find
+        # the path to exclude.  The "filename" might be synthetic,
+        # don't be fooled by those.
+        structseq_file = _structseq.structseq_new.__code__.co_filename
+        if not structseq_file.startswith("<"):
+            paths.add(canonical_path(structseq_file))
+
+
+def add_third_party_paths(paths):
+    """Add locations for third-party packages to the set `paths`."""
+    # Get the paths that sysconfig knows about.
+    scheme_names = set(sysconfig.get_scheme_names())
+
+    for scheme in scheme_names:
+        # https://foss.heptapod.net/pypy/pypy/-/issues/3433
+        better_scheme = "pypy_posix" if scheme == "pypy" else scheme
+        if os.name in better_scheme.split("_"):
+            config_paths = sysconfig.get_paths(scheme)
+            for path_name in ["platlib", "purelib", "scripts"]:
+                paths.add(config_paths[path_name])
+
+
+def add_coverage_paths(paths):
+    """Add paths where coverage.py code can be found to the set `paths`."""
+    cover_path = canonical_path(__file__, directory=True)
+    paths.add(cover_path)
+    if env.TESTING:
+        # Don't include our own test code.
+        paths.add(os.path.join(cover_path, "tests"))
+
+        # When testing, we use PyContracts, which should be considered
+        # part of coverage.py, and it uses six. Exclude those directories
+        # just as we exclude ourselves.
+        if env.USE_CONTRACTS:
+            import contracts
+            import six
+            for mod in [contracts, six]:
+                paths.add(canonical_path(mod))
+
+
+class InOrOut:
     """Machinery for determining what files to measure."""
 
     def __init__(self, warn, debug):
@@ -118,8 +197,8 @@
         # The matchers for should_trace.
         self.source_match = None
         self.source_pkgs_match = None
-        self.pylib_paths = self.cover_paths = None
-        self.pylib_match = self.cover_match = None
+        self.pylib_paths = self.cover_paths = self.third_paths = None
+        self.pylib_match = self.cover_match = self.third_match = None
         self.include_match = self.omit_match = None
         self.plugins = []
         self.disp_class = FileDisposition
@@ -130,6 +209,9 @@
         self.source_pkgs_unmatched = []
         self.omit = self.include = None
 
+        # Is the source inside a third-party area?
+        self.source_in_third = False
+
     def configure(self, config):
         """Apply the configuration to get ready for decision-time."""
         self.source_pkgs.extend(config.source_pkgs)
@@ -146,38 +228,16 @@
         # The directories for files considered "installed with the interpreter".
         self.pylib_paths = set()
         if not config.cover_pylib:
-            # Look at where some standard modules are located. That's the
-            # indication for "installed with the interpreter". In some
-            # environments (virtualenv, for example), these modules may be
-            # spread across a few locations. Look at all the candidate modules
-            # we've imported, and take all the different ones.
-            for m in (atexit, inspect, os, platform, _pypy_irc_topic, re, _structseq, traceback):
-                if m is not None and hasattr(m, "__file__"):
-                    self.pylib_paths.add(canonical_path(m, directory=True))
-
-            if _structseq and not hasattr(_structseq, '__file__'):
-                # PyPy 2.4 has no __file__ in the builtin modules, but the code
-                # objects still have the file names.  So dig into one to find
-                # the path to exclude.  The "filename" might be synthetic,
-                # don't be fooled by those.
-                structseq_file = code_object(_structseq.structseq_new).co_filename
-                if not structseq_file.startswith("<"):
-                    self.pylib_paths.add(canonical_path(structseq_file))
+            add_stdlib_paths(self.pylib_paths)
 
         # To avoid tracing the coverage.py code itself, we skip anything
         # located where we are.
-        self.cover_paths = [canonical_path(__file__, directory=True)]
-        if env.TESTING:
-            # Don't include our own test code.
-            self.cover_paths.append(os.path.join(self.cover_paths[0], "tests"))
+        self.cover_paths = set()
+        add_coverage_paths(self.cover_paths)
 
-            # When testing, we use PyContracts, which should be considered
-            # part of coverage.py, and it uses six. Exclude those directories
-            # just as we exclude ourselves.
-            import contracts
-            import six
-            for mod in [contracts, six]:
-                self.cover_paths.append(canonical_path(mod))
+        # Find where third-party packages are installed.
+        self.third_paths = set()
+        add_third_party_paths(self.third_paths)
 
         def debug(msg):
             if self.debug:
@@ -187,25 +247,58 @@
         if self.source or self.source_pkgs:
             against = []
             if self.source:
-                self.source_match = TreeMatcher(self.source)
-                against.append("trees {!r}".format(self.source_match))
+                self.source_match = TreeMatcher(self.source, "source")
+                against.append(f"trees {self.source_match!r}")
             if self.source_pkgs:
-                self.source_pkgs_match = ModuleMatcher(self.source_pkgs)
-                against.append("modules {!r}".format(self.source_pkgs_match))
+                self.source_pkgs_match = ModuleMatcher(self.source_pkgs, "source_pkgs")
+                against.append(f"modules {self.source_pkgs_match!r}")
             debug("Source matching against " + " and ".join(against))
         else:
-            if self.cover_paths:
-                self.cover_match = TreeMatcher(self.cover_paths)
-                debug("Coverage code matching: {!r}".format(self.cover_match))
             if self.pylib_paths:
-                self.pylib_match = TreeMatcher(self.pylib_paths)
-                debug("Python stdlib matching: {!r}".format(self.pylib_match))
+                self.pylib_match = TreeMatcher(self.pylib_paths, "pylib")
+                debug(f"Python stdlib matching: {self.pylib_match!r}")
         if self.include:
-            self.include_match = FnmatchMatcher(self.include)
-            debug("Include matching: {!r}".format(self.include_match))
+            self.include_match = FnmatchMatcher(self.include, "include")
+            debug(f"Include matching: {self.include_match!r}")
         if self.omit:
-            self.omit_match = FnmatchMatcher(self.omit)
-            debug("Omit matching: {!r}".format(self.omit_match))
+            self.omit_match = FnmatchMatcher(self.omit, "omit")
+            debug(f"Omit matching: {self.omit_match!r}")
+
+        self.cover_match = TreeMatcher(self.cover_paths, "coverage")
+        debug(f"Coverage code matching: {self.cover_match!r}")
+
+        self.third_match = TreeMatcher(self.third_paths, "third")
+        debug(f"Third-party lib matching: {self.third_match!r}")
+
+        # Check if the source we want to measure has been installed as a
+        # third-party package.
+        with sys_modules_saved():
+            for pkg in self.source_pkgs:
+                try:
+                    modfile, path = file_and_path_for_module(pkg)
+                    debug(f"Imported source package {pkg!r} as {modfile!r}")
+                except CoverageException as exc:
+                    debug(f"Couldn't import source package {pkg!r}: {exc}")
+                    continue
+                if modfile:
+                    if self.third_match.match(modfile):
+                        debug(
+                            f"Source is in third-party because of source_pkg {pkg!r} at {modfile!r}"
+                        )
+                        self.source_in_third = True
+                else:
+                    for pathdir in path:
+                        if self.third_match.match(pathdir):
+                            debug(
+                                f"Source is in third-party because of {pkg!r} path directory " +
+                                f"at {pathdir!r}"
+                            )
+                            self.source_in_third = True
+
+        for src in self.source:
+            if self.third_match.match(src):
+                debug(f"Source is in third-party because of source directory {src!r}")
+                self.source_in_third = True
 
     def should_trace(self, filename, frame=None):
         """Decide whether to trace execution in `filename`, with a reason.
@@ -225,6 +318,9 @@
             disp.reason = reason
             return disp
 
+        if original_filename.startswith('<'):
+            return nope(disp, "not a real original file name")
+
         if frame is not None:
             # Compiled Python files have two file names: frame.f_code.co_filename is
             # the file name at the time the .pyc was compiled.  The second name is
@@ -257,11 +353,6 @@
             # can't do anything with the data later anyway.
             return nope(disp, "not a real file name")
 
-        # pyexpat does a dumb thing, calling the trace function explicitly from
-        # C code with a C file name.
-        if re.search(r"[/\\]Modules[/\\]pyexpat.c", filename):
-            return nope(disp, "pyexpat lies about itself")
-
         # Jython reports the .class file to the tracer, use the source file.
         if filename.endswith("$py.class"):
             filename = filename[:-9] + ".py"
@@ -289,10 +380,9 @@
                         )
                     break
             except Exception:
-                self.warn(
-                    "Disabling plug-in %r due to an exception:" % (plugin._coverage_plugin_name)
-                )
-                traceback.print_exc()
+                plugin_name = plugin._coverage_plugin_name
+                tb = traceback.format_exc()
+                self.warn(f"Disabling plug-in {plugin_name!r} due to an exception:\n{tb}")
                 plugin._coverage_enabled = False
                 continue
         else:
@@ -303,8 +393,7 @@
         if not disp.has_dynamic_filename:
             if not disp.source_filename:
                 raise CoverageException(
-                    "Plugin %r didn't set source_filename for %r" %
-                    (plugin, disp.original_filename)
+                    f"Plugin {plugin!r} didn't set source_filename for '{disp.original_filename}'"
                 )
             reason = self.check_include_omit_etc(disp.source_filename, frame)
             if reason:
@@ -334,25 +423,32 @@
                     if modulename in self.source_pkgs_unmatched:
                         self.source_pkgs_unmatched.remove(modulename)
                 else:
-                    extra = "module {!r} ".format(modulename)
+                    extra = f"module {modulename!r} "
             if not ok and self.source_match:
                 if self.source_match.match(filename):
                     ok = True
             if not ok:
                 return extra + "falls outside the --source spec"
+            if not self.source_in_third:
+                if self.third_match.match(filename):
+                    return "inside --source, but is third-party"
         elif self.include_match:
             if not self.include_match.match(filename):
                 return "falls outside the --include trees"
         else:
+            # We exclude the coverage.py code itself, since a little of it
+            # will be measured otherwise.
+            if self.cover_match.match(filename):
+                return "is part of coverage.py"
+
             # If we aren't supposed to trace installed code, then check if this
             # is near the Python standard library and skip it if so.
             if self.pylib_match and self.pylib_match.match(filename):
                 return "is in the stdlib"
 
-            # We exclude the coverage.py code itself, since a little of it
-            # will be measured otherwise.
-            if self.cover_match and self.cover_match.match(filename):
-                return "is part of coverage.py"
+            # Exclude anything in the third-party installation areas.
+            if self.third_match.match(filename):
+                return "is a third-party module"
 
         # Check the file against the omit pattern.
         if self.omit_match and self.omit_match.match(filename):
@@ -360,7 +456,7 @@
 
         # No point tracing a file we can't later write to SQLite.
         try:
-            filename.encode("utf8")
+            filename.encode("utf-8")
         except UnicodeEncodeError:
             return "non-encodable filename"
 
@@ -384,11 +480,26 @@
                 if filename in warned:
                     continue
 
+                if len(getattr(mod, "__path__", ())) > 1:
+                    # A namespace package, which confuses this code, so ignore it.
+                    continue
+
                 disp = self.should_trace(filename)
+                if disp.has_dynamic_filename:
+                    # A plugin with dynamic filenames: the Python file
+                    # shouldn't cause a warning, since it won't be the subject
+                    # of tracing anyway.
+                    continue
                 if disp.trace:
-                    msg = "Already imported a file that will be measured: {}".format(filename)
+                    msg = f"Already imported a file that will be measured: {filename}"
                     self.warn(msg, slug="already-imported")
                     warned.add(filename)
+                elif self.debug and self.debug.should('trace'):
+                    self.debug.write(
+                        "Didn't trace already imported file {!r}: {}".format(
+                            disp.original_filename, disp.reason
+                        )
+                    )
 
     def warn_unimported_source(self):
         """Warn about source packages that were of interest, but never traced."""
@@ -403,7 +514,7 @@
         """
         mod = sys.modules.get(pkg)
         if mod is None:
-            self.warn("Module %s was never imported." % pkg, slug="module-not-imported")
+            self.warn(f"Module {pkg} was never imported.", slug="module-not-imported")
             return
 
         if module_is_namespace(mod):
@@ -412,16 +523,14 @@
             return
 
         if not module_has_file(mod):
-            self.warn("Module %s has no Python source." % pkg, slug="module-not-python")
+            self.warn(f"Module {pkg} has no Python source.", slug="module-not-python")
             return
 
         # The module was in sys.modules, and seems like a module with code, but
         # we never measured it. I guess that means it was imported before
         # coverage even started.
-        self.warn(
-            "Module %s was previously imported, but not measured" % pkg,
-            slug="module-not-measured",
-        )
+        msg = f"Module {pkg} was previously imported, but not measured"
+        self.warn(msg, slug="module-not-measured")
 
     def find_possibly_unexecuted_files(self):
         """Find files in the areas of interest that might be untraced.
@@ -433,12 +542,10 @@
                 not module_has_file(sys.modules[pkg])):
                 continue
             pkg_file = source_for_file(sys.modules[pkg].__file__)
-            for ret in self._find_executable_files(canonical_path(pkg_file)):
-                yield ret
+            yield from self._find_executable_files(canonical_path(pkg_file))
 
         for src in self.source:
-            for ret in self._find_executable_files(src):
-                yield ret
+            yield from self._find_executable_files(src)
 
     def _find_plugin_files(self, src_dir):
         """Get executable files from the plugins."""
@@ -473,14 +580,15 @@
         Returns a list of (key, value) pairs.
         """
         info = [
-            ('cover_paths', self.cover_paths),
-            ('pylib_paths', self.pylib_paths),
+            ("coverage_paths", self.cover_paths),
+            ("stdlib_paths", self.pylib_paths),
+            ("third_party_paths", self.third_paths),
         ]
 
         matcher_names = [
             'source_match', 'source_pkgs_match',
             'include_match', 'omit_match',
-            'cover_match', 'pylib_match',
+            'cover_match', 'pylib_match', 'third_match',
             ]
 
         for matcher_name in matcher_names:
--- a/eric7/DebugClients/Python/coverage/jsonreport.py	Fri Nov 19 19:28:47 2021 +0100
+++ b/eric7/DebugClients/Python/coverage/jsonreport.py	Sat Nov 20 16:47:38 2021 +0100
@@ -1,4 +1,3 @@
-# coding: utf-8
 # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
 # For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt
 
@@ -12,13 +11,15 @@
 from coverage.results import Numbers
 
 
-class JsonReporter(object):
+class JsonReporter:
     """A reporter for writing JSON coverage results."""
 
+    report_type = "JSON report"
+
     def __init__(self, coverage):
         self.coverage = coverage
         self.config = self.coverage.config
-        self.total = Numbers()
+        self.total = Numbers(self.config.precision)
         self.report_data = {}
 
     def report(self, morfs, outfile=None):
@@ -52,6 +53,7 @@
             'covered_lines': self.total.n_executed,
             'num_statements': self.total.n_statements,
             'percent_covered': self.total.pc_covered,
+            'percent_covered_display': self.total.pc_covered_str,
             'missing_lines': self.total.n_missing,
             'excluded_lines': self.total.n_excluded,
         }
@@ -80,6 +82,7 @@
             'covered_lines': nums.n_executed,
             'num_statements': nums.n_statements,
             'percent_covered': nums.pc_covered,
+            'percent_covered_display': nums.pc_covered_str,
             'missing_lines': nums.n_missing,
             'excluded_lines': nums.n_excluded,
         }
@@ -87,12 +90,10 @@
             'executed_lines': sorted(analysis.executed),
             'summary': summary,
             'missing_lines': sorted(analysis.missing),
-            'excluded_lines': sorted(analysis.excluded)
+            'excluded_lines': sorted(analysis.excluded),
         }
         if self.config.json_show_contexts:
-            reported_file['contexts'] = analysis.data.contexts_by_lineno(
-                analysis.filename,
-            )
+            reported_file['contexts'] = analysis.data.contexts_by_lineno(analysis.filename)
         if coverage_data.has_arcs():
             reported_file['summary'].update({
                 'num_branches': nums.n_branches,
--- a/eric7/DebugClients/Python/coverage/misc.py	Fri Nov 19 19:28:47 2021 +0100
+++ b/eric7/DebugClients/Python/coverage/misc.py	Sat Nov 20 16:47:38 2021 +0100
@@ -3,8 +3,11 @@
 
 """Miscellaneous stuff for coverage.py."""
 
+import contextlib
 import errno
 import hashlib
+import importlib
+import importlib.util
 import inspect
 import locale
 import os
@@ -16,7 +19,12 @@
 import types
 
 from coverage import env
-from coverage.backward import to_bytes, unicode_class
+from coverage.exceptions import CoverageException
+
+# In 6.0, the exceptions moved from misc.py to exceptions.py.  But a number of
+# other packages were importing the exceptions from misc, so import them here.
+# pylint: disable=unused-wildcard-import
+from coverage.exceptions import *   # pylint: disable=wildcard-import
 
 ISOLATED_MODULES = {}
 
@@ -42,6 +50,49 @@
 os = isolate_module(os)
 
 
+class SysModuleSaver:
+    """Saves the contents of sys.modules, and removes new modules later."""
+    def __init__(self):
+        self.old_modules = set(sys.modules)
+
+    def restore(self):
+        """Remove any modules imported since this object started."""
+        new_modules = set(sys.modules) - self.old_modules
+        for m in new_modules:
+            del sys.modules[m]
+
+
+@contextlib.contextmanager
+def sys_modules_saved():
+    """A context manager to remove any modules imported during a block."""
+    saver = SysModuleSaver()
+    try:
+        yield
+    finally:
+        saver.restore()
+
+
+def import_third_party(modname):
+    """Import a third-party module we need, but might not be installed.
+
+    This also cleans out the module after the import, so that coverage won't
+    appear to have imported it.  This lets the third party use coverage for
+    their own tests.
+
+    Arguments:
+        modname (str): the name of the module to import.
+
+    Returns:
+        The imported module, or None if the module couldn't be imported.
+
+    """
+    with sys_modules_saved():
+        try:
+            return importlib.import_module(modname)
+        except ImportError:
+            return None
+
+
 def dummy_decorator_with_args(*args_unused, **kwargs_unused):
     """Dummy no-op implementation of a decorator with arguments."""
     def _decorator(func):
@@ -49,14 +100,9 @@
     return _decorator
 
 
-# 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 = env.TESTING and not bool(int(os.environ.get("COVERAGE_NO_CONTRACTS", 0)))
-
 # Use PyContracts for assertion testing on parameters and returns, but only if
 # we are running our own test suite.
-if USE_CONTRACTS:
+if env.USE_CONTRACTS:
     from contracts import contract              # pylint: disable=unused-import
     from contracts import new_contract as raw_new_contract
 
@@ -71,8 +117,7 @@
 
     # Define contract words that PyContract doesn't have.
     new_contract('bytes', lambda v: isinstance(v, bytes))
-    if env.PY3:
-        new_contract('unicode', lambda v: isinstance(v, unicode_class))
+    new_contract('unicode', lambda v: isinstance(v, str))
 
     def one_of(argnames):
         """Ensure that only one of the argnames is non-None."""
@@ -121,7 +166,7 @@
 
         def _wrapper(self):
             if hasattr(self, attr):
-                raise AssertionError("Shouldn't have called %s more than once" % fn.__name__)
+                raise AssertionError(f"Shouldn't have called {fn.__name__} more than once")
             setattr(self, attr, True)
             return fn(self)
         return _wrapper
@@ -156,8 +201,8 @@
 
     If `directory` is None or empty, do nothing.
     """
-    if directory and not os.path.isdir(directory):
-        os.makedirs(directory)
+    if directory:
+        os.makedirs(directory, exist_ok=True)
 
 
 def ensure_dir_for_file(path):
@@ -197,22 +242,22 @@
     return suffix
 
 
-class Hasher(object):
-    """Hashes Python data into md5."""
+class Hasher:
+    """Hashes Python data for fingerprinting."""
     def __init__(self):
-        self.md5 = hashlib.md5()
+        self.hash = hashlib.new("sha3_256")
 
     def update(self, v):
         """Add `v` to the hash, recursively if needed."""
-        self.md5.update(to_bytes(str(type(v))))
-        if isinstance(v, unicode_class):
-            self.md5.update(v.encode('utf8'))
+        self.hash.update(str(type(v)).encode("utf-8"))
+        if isinstance(v, str):
+            self.hash.update(v.encode("utf-8"))
         elif isinstance(v, bytes):
-            self.md5.update(v)
+            self.hash.update(v)
         elif v is None:
             pass
         elif isinstance(v, (int, float)):
-            self.md5.update(to_bytes(str(v)))
+            self.hash.update(str(v).encode("utf-8"))
         elif isinstance(v, (tuple, list)):
             for e in v:
                 self.update(e)
@@ -230,11 +275,11 @@
                     continue
                 self.update(k)
                 self.update(a)
-        self.md5.update(b'.')
+        self.hash.update(b'.')
 
     def hexdigest(self):
         """Retrieve the hex digest of the hash."""
-        return self.md5.hexdigest()
+        return self.hash.hexdigest()[:32]
 
 
 def _needs_to_implement(that, func_name):
@@ -245,16 +290,14 @@
     else:
         thing = "Class"
         klass = that.__class__
-        name = "{klass.__module__}.{klass.__name__}".format(klass=klass)
+        name = f"{klass.__module__}.{klass.__name__}"
 
     raise NotImplementedError(
-        "{thing} {name!r} needs to implement {func_name}()".format(
-            thing=thing, name=name, func_name=func_name
-            )
+        f"{thing} {name!r} needs to implement {func_name}()"
         )
 
 
-class DefaultValue(object):
+class DefaultValue:
     """A sentinel object to use for unusual default-value needs.
 
     Construct with a string that will be used as the repr, for display in help
@@ -299,16 +342,18 @@
         )
         """
 
+    dollar_groups = ('dollar', 'word1', 'word2')
+
     def dollar_replace(match):
         """Called for each $replacement."""
         # Only one of the groups will have matched, just get its text.
-        word = next(g for g in match.group('dollar', 'word1', 'word2') if g)
+        word = next(g for g in match.group(*dollar_groups) if g)    # pragma: always breaks
         if word == "$":
             return "$"
         elif word in variables:
             return variables[word]
         elif match.group('strict'):
-            msg = "Variable {} is undefined: {!r}".format(word, text)
+            msg = f"Variable {word} is undefined: {text!r}"
             raise CoverageException(msg)
         else:
             return match.group('defval')
@@ -317,45 +362,56 @@
     return text
 
 
-class BaseCoverageException(Exception):
-    """The base of all Coverage exceptions."""
-    pass
+def format_local_datetime(dt):
+    """Return a string with local timezone representing the date.
+    """
+    return dt.astimezone().strftime('%Y-%m-%d %H:%M %z')
 
 
-class CoverageException(BaseCoverageException):
-    """An exception raised by a coverage.py function."""
-    pass
+def import_local_file(modname, modfile=None):
+    """Import a local file as a module.
 
+    Opens a file in the current directory named `modname`.py, imports it
+    as `modname`, and returns the module object.  `modfile` is the file to
+    import if it isn't in the current directory.
 
-class NoSource(CoverageException):
-    """We couldn't find the source for a module."""
-    pass
+    """
+    if modfile is None:
+        modfile = modname + '.py'
+    spec = importlib.util.spec_from_file_location(modname, modfile)
+    mod = importlib.util.module_from_spec(spec)
+    sys.modules[modname] = mod
+    spec.loader.exec_module(mod)
 
-
-class NoCode(NoSource):
-    """We couldn't find any code at all."""
-    pass
+    return mod
 
 
-class NotPython(CoverageException):
-    """A source file turned out not to be parsable Python."""
-    pass
-
+def human_key(s):
+    """Turn a string into a list of string and number chunks.
+        "z23a" -> ["z", 23, "a"]
+    """
+    def tryint(s):
+        """If `s` is a number, return an int, else `s` unchanged."""
+        try:
+            return int(s)
+        except ValueError:
+            return s
 
-class ExceptionDuringRun(CoverageException):
-    """An exception happened while running customer code.
+    return [tryint(c) for c in re.split(r"(\d+)", s)]
 
-    Construct it with three arguments, the values from `sys.exc_info`.
+def human_sorted(strings):
+    """Sort the given iterable of strings the way that humans expect.
+
+    Numeric components in the strings are sorted as numbers.
+
+    Returns the sorted list.
 
     """
-    pass
-
-
-class StopEverything(BaseCoverageException):
-    """An exception that means everything should stop.
+    return sorted(strings, key=human_key)
 
-    The CoverageTest class converts these to SkipTest, so that when running
-    tests, raising this exception will automatically skip the test.
+def human_sorted_items(items, reverse=False):
+    """Sort the (string, value) items the way humans expect.
 
+    Returns the sorted list of items.
     """
-    pass
+    return sorted(items, key=lambda pair: (human_key(pair[0]), pair[1]), reverse=reverse)
--- a/eric7/DebugClients/Python/coverage/multiproc.py	Fri Nov 19 19:28:47 2021 +0100
+++ b/eric7/DebugClients/Python/coverage/multiproc.py	Sat Nov 20 16:47:38 2021 +0100
@@ -10,7 +10,6 @@
 import sys
 import traceback
 
-from coverage import env
 from coverage.misc import contract
 
 # An attribute that will be set on the module to indicate that it has been
@@ -18,11 +17,7 @@
 PATCHED_MARKER = "_coverage$patched"
 
 
-if env.PYVERSION >= (3, 4):
-    OriginalProcess = multiprocessing.process.BaseProcess
-else:
-    OriginalProcess = multiprocessing.Process
-
+OriginalProcess = multiprocessing.process.BaseProcess
 original_bootstrap = OriginalProcess._bootstrap
 
 class ProcessWithCoverage(OriginalProcess):         # pylint: disable=abstract-method
@@ -53,7 +48,7 @@
             if debug.should("multiproc"):
                 debug.write("Saved multiprocessing data")
 
-class Stowaway(object):
+class Stowaway:
     """An object to pickle, so when it is unpickled, it can apply the monkey-patch."""
     def __init__(self, rcfile):
         self.rcfile = rcfile
@@ -79,10 +74,7 @@
     if hasattr(multiprocessing, PATCHED_MARKER):
         return
 
-    if env.PYVERSION >= (3, 4):
-        OriginalProcess._bootstrap = ProcessWithCoverage._bootstrap
-    else:
-        multiprocessing.Process = ProcessWithCoverage
+    OriginalProcess._bootstrap = ProcessWithCoverage._bootstrap
 
     # Set the value in ProcessWithCoverage that will be pickled into the child
     # process.
--- a/eric7/DebugClients/Python/coverage/numbits.py	Fri Nov 19 19:28:47 2021 +0100
+++ b/eric7/DebugClients/Python/coverage/numbits.py	Sat Nov 20 16:47:38 2021 +0100
@@ -15,22 +15,15 @@
 """
 import json
 
-from coverage import env
-from coverage.backward import byte_to_int, bytes_to_ints, binary_bytes, zip_longest
+from itertools import zip_longest
+
 from coverage.misc import contract, new_contract
 
-if env.PY3:
-    def _to_blob(b):
-        """Convert a bytestring into a type SQLite will accept for a blob."""
-        return b
+def _to_blob(b):
+    """Convert a bytestring into a type SQLite will accept for a blob."""
+    return b
 
-    new_contract('blob', lambda v: isinstance(v, bytes))
-else:
-    def _to_blob(b):
-        """Convert a bytestring into a type SQLite will accept for a blob."""
-        return buffer(b)                                    # pylint: disable=undefined-variable
-
-    new_contract('blob', lambda v: isinstance(v, buffer))   # pylint: disable=undefined-variable
+new_contract('blob', lambda v: isinstance(v, bytes))
 
 
 @contract(nums='Iterable', returns='blob')
@@ -69,7 +62,7 @@
 
     """
     nums = []
-    for byte_i, byte in enumerate(bytes_to_ints(numbits)):
+    for byte_i, byte in enumerate(numbits):
         for bit_i in range(8):
             if (byte & (1 << bit_i)):
                 nums.append(byte_i * 8 + bit_i)
@@ -83,8 +76,8 @@
     Returns:
         A new numbits, the union of `numbits1` and `numbits2`.
     """
-    byte_pairs = zip_longest(bytes_to_ints(numbits1), bytes_to_ints(numbits2), fillvalue=0)
-    return _to_blob(binary_bytes(b1 | b2 for b1, b2 in byte_pairs))
+    byte_pairs = zip_longest(numbits1, numbits2, fillvalue=0)
+    return _to_blob(bytes(b1 | b2 for b1, b2 in byte_pairs))
 
 
 @contract(numbits1='blob', numbits2='blob', returns='blob')
@@ -94,8 +87,8 @@
     Returns:
         A new numbits, the intersection `numbits1` and `numbits2`.
     """
-    byte_pairs = zip_longest(bytes_to_ints(numbits1), bytes_to_ints(numbits2), fillvalue=0)
-    intersection_bytes = binary_bytes(b1 & b2 for b1, b2 in byte_pairs)
+    byte_pairs = zip_longest(numbits1, numbits2, fillvalue=0)
+    intersection_bytes = bytes(b1 & b2 for b1, b2 in byte_pairs)
     return _to_blob(intersection_bytes.rstrip(b'\0'))
 
 
@@ -109,7 +102,7 @@
     Returns:
         A bool, True if there is any number in both `numbits1` and `numbits2`.
     """
-    byte_pairs = zip_longest(bytes_to_ints(numbits1), bytes_to_ints(numbits2), fillvalue=0)
+    byte_pairs = zip_longest(numbits1, numbits2, fillvalue=0)
     return any(b1 & b2 for b1, b2 in byte_pairs)
 
 
@@ -123,7 +116,7 @@
     nbyte, nbit = divmod(num, 8)
     if nbyte >= len(numbits):
         return False
-    return bool(byte_to_int(numbits[nbyte]) & (1 << nbit))
+    return bool(numbits[nbyte] & (1 << nbit))
 
 
 def register_sqlite_functions(connection):
--- a/eric7/DebugClients/Python/coverage/parser.py	Fri Nov 19 19:28:47 2021 +0100
+++ b/eric7/DebugClients/Python/coverage/parser.py	Sat Nov 20 16:47:38 2021 +0100
@@ -11,16 +11,14 @@
 import tokenize
 
 from coverage import env
-from coverage.backward import range    # pylint: disable=redefined-builtin
-from coverage.backward import bytes_to_ints, string_class
 from coverage.bytecode import code_objects
 from coverage.debug import short_stack
+from coverage.exceptions import NoSource, NotPython, StopEverything
 from coverage.misc import contract, join_regex, new_contract, nice_pair, one_of
-from coverage.misc import NoSource, NotPython, StopEverything
 from coverage.phystokens import compile_unicode, generate_tokens, neuter_encoding_declaration
 
 
-class PythonParser(object):
+class PythonParser:
     """Parse code to find executable lines, excluded lines, etc.
 
     This information is all based on static analysis: no code execution is
@@ -42,10 +40,8 @@
             from coverage.python import get_python_source
             try:
                 self.text = get_python_source(self.filename)
-            except IOError as err:
-                raise NoSource(
-                    "No source for code: '%s': %s" % (self.filename, err)
-                )
+            except OSError as err:
+                raise NoSource(f"No source for code: '{self.filename}': {err}") from err
 
         self.exclude = exclude
 
@@ -84,18 +80,10 @@
         # multi-line statements.
         self._multiline = {}
 
-        # Lazily-created ByteParser, arc data, and missing arc descriptions.
-        self._byte_parser = None
+        # Lazily-created arc data, and missing arc descriptions.
         self._all_arcs = None
         self._missing_arc_fragments = None
 
-    @property
-    def byte_parser(self):
-        """Create a ByteParser on demand."""
-        if not self._byte_parser:
-            self._byte_parser = ByteParser(self.text, filename=self.filename)
-        return self._byte_parser
-
     def lines_matching(self, *regexes):
         """Find the lines matching one of a list of regexes.
 
@@ -105,8 +93,6 @@
 
         """
         combined = join_regex(regexes)
-        if env.PY2:
-            combined = combined.decode("utf8")
         regex_c = re.compile(combined)
         matches = set()
         for i, ltext in enumerate(self.lines, start=1):
@@ -203,7 +189,8 @@
 
         # Find the starts of the executable statements.
         if not empty:
-            self.raw_statements.update(self.byte_parser._find_statements())
+            byte_parser = ByteParser(self.text, filename=self.filename)
+            self.raw_statements.update(byte_parser._find_statements())
 
         # The first line of modules can lie and say 1 always, even if the first
         # line of code is later. If so, map 1 to the actual first line of the
@@ -251,10 +238,9 @@
             else:
                 lineno = err.args[1][0]     # TokenError
             raise NotPython(
-                u"Couldn't parse '%s' as Python source: '%s' at line %d" % (
-                    self.filename, err.args[0], lineno
-                )
-            )
+                f"Couldn't parse '{self.filename}' as Python source: " +
+                f"{err.args[0]!r} at line {lineno}"
+            ) from err
 
         self.excluded = self.first_lines(self.raw_excluded)
 
@@ -349,16 +335,16 @@
                     emsg = "didn't jump to line {lineno}"
             emsg = emsg.format(lineno=end)
 
-            msg = "line {start} {emsg}".format(start=actual_start, emsg=emsg)
+            msg = f"line {actual_start} {emsg}"
             if smsg is not None:
-                msg += ", because {smsg}".format(smsg=smsg.format(lineno=actual_start))
+                msg += f", because {smsg.format(lineno=actual_start)}"
 
             msgs.append(msg)
 
         return " or ".join(msgs)
 
 
-class ByteParser(object):
+class ByteParser:
     """Parse bytecode to understand the structure of code."""
 
     @contract(text='unicode')
@@ -371,17 +357,17 @@
                 self.code = compile_unicode(text, filename, "exec")
             except SyntaxError as synerr:
                 raise NotPython(
-                    u"Couldn't parse '%s' as Python source: '%s' at line %d" % (
+                    "Couldn't parse '%s' as Python source: '%s' at line %d" % (
                         filename, synerr.msg, synerr.lineno
                     )
-                )
+                ) from synerr
 
         # Alternative Python implementations don't always provide all the
         # 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
-                    "This implementation of Python doesn't support code analysis.\n"
+                    "This implementation of Python doesn't support code analysis.\n" +
                     "Run coverage.py under another Python for this command."
                 )
 
@@ -405,8 +391,8 @@
                     yield line
         else:
             # Adapted from dis.py in the standard library.
-            byte_increments = bytes_to_ints(self.code.co_lnotab[0::2])
-            line_increments = bytes_to_ints(self.code.co_lnotab[1::2])
+            byte_increments = self.code.co_lnotab[0::2]
+            line_increments = self.code.co_lnotab[1::2]
 
             last_line_num = None
             line_num = self.code.co_firstlineno
@@ -432,15 +418,45 @@
         """
         for bp in self.child_parsers():
             # Get all of the lineno information from this code.
-            for l in bp._line_numbers():
-                yield l
+            yield from bp._line_numbers()
 
 
 #
 # AST analysis
 #
 
-class LoopBlock(object):
+class BlockBase:
+    """
+    Blocks need to handle various exiting statements in their own ways.
+
+    All of these methods take a list of exits, and a callable `add_arc`
+    function that they can use to add arcs if needed.  They return True if the
+    exits are handled, or False if the search should continue up the block
+    stack.
+    """
+    # pylint: disable=unused-argument
+    def process_break_exits(self, exits, add_arc):
+        """Process break exits."""
+        # Because break can only appear in loops, and most subclasses
+        # implement process_break_exits, this function is never reached.
+        raise AssertionError
+
+    def process_continue_exits(self, exits, add_arc):
+        """Process continue exits."""
+        # Because continue can only appear in loops, and most subclasses
+        # implement process_continue_exits, this function is never reached.
+        raise AssertionError
+
+    def process_raise_exits(self, exits, add_arc):
+        """Process raise exits."""
+        return False
+
+    def process_return_exits(self, exits, add_arc):
+        """Process return exits."""
+        return False
+
+
+class LoopBlock(BlockBase):
     """A block on the block stack representing a `for` or `while` loop."""
     @contract(start=int)
     def __init__(self, start):
@@ -449,8 +465,17 @@
         # A set of ArcStarts, the arcs from break statements exiting this loop.
         self.break_exits = set()
 
+    def process_break_exits(self, exits, add_arc):
+        self.break_exits.update(exits)
+        return True
 
-class FunctionBlock(object):
+    def process_continue_exits(self, exits, add_arc):
+        for xit in exits:
+            add_arc(xit.lineno, self.start, xit.cause)
+        return True
+
+
+class FunctionBlock(BlockBase):
     """A block on the block stack representing a function definition."""
     @contract(start=int, name=str)
     def __init__(self, start, name):
@@ -459,8 +484,24 @@
         # The name of the function.
         self.name = name
 
+    def process_raise_exits(self, exits, add_arc):
+        for xit in exits:
+            add_arc(
+                xit.lineno, -self.start, xit.cause,
+                f"didn't except from function {self.name!r}",
+            )
+        return True
 
-class TryBlock(object):
+    def process_return_exits(self, exits, add_arc):
+        for xit in exits:
+            add_arc(
+                xit.lineno, -self.start, xit.cause,
+                f"didn't return from function {self.name!r}",
+            )
+        return True
+
+
+class TryBlock(BlockBase):
     """A block on the block stack representing a `try` block."""
     @contract(handler_start='int|None', final_start='int|None')
     def __init__(self, handler_start, final_start):
@@ -473,8 +514,73 @@
         # that need to route through the "finally:" clause.
         self.break_from = set()
         self.continue_from = set()
+        self.raise_from = set()
         self.return_from = set()
-        self.raise_from = set()
+
+    def process_break_exits(self, exits, add_arc):
+        if self.final_start is not None:
+            self.break_from.update(exits)
+            return True
+        return False
+
+    def process_continue_exits(self, exits, add_arc):
+        if self.final_start is not None:
+            self.continue_from.update(exits)
+            return True
+        return False
+
+    def process_raise_exits(self, exits, add_arc):
+        if self.handler_start is not None:
+            for xit in exits:
+                add_arc(xit.lineno, self.handler_start, xit.cause)
+        else:
+            assert self.final_start is not None
+            self.raise_from.update(exits)
+        return True
+
+    def process_return_exits(self, exits, add_arc):
+        if self.final_start is not None:
+            self.return_from.update(exits)
+            return True
+        return False
+
+
+class WithBlock(BlockBase):
+    """A block on the block stack representing a `with` block."""
+    @contract(start=int)
+    def __init__(self, start):
+        # We only ever use this block if it is needed, so that we don't have to
+        # check this setting in all the methods.
+        assert env.PYBEHAVIOR.exit_through_with
+
+        # The line number of the with statement.
+        self.start = start
+
+        # The ArcStarts for breaks/continues/returns/raises inside the "with:"
+        # that need to go through the with-statement while exiting.
+        self.break_from = set()
+        self.continue_from = set()
+        self.return_from = set()
+
+    def _process_exits(self, exits, add_arc, from_set=None):
+        """Helper to process the four kinds of exits."""
+        for xit in exits:
+            add_arc(xit.lineno, self.start, xit.cause)
+        if from_set is not None:
+            from_set.update(exits)
+        return True
+
+    def process_break_exits(self, exits, add_arc):
+        return self._process_exits(exits, add_arc, self.break_from)
+
+    def process_continue_exits(self, exits, add_arc):
+        return self._process_exits(exits, add_arc, self.continue_from)
+
+    def process_raise_exits(self, exits, add_arc):
+        return self._process_exits(exits, add_arc)
+
+    def process_return_exits(self, exits, add_arc):
+        return self._process_exits(exits, add_arc, self.return_from)
 
 
 class ArcStart(collections.namedtuple("Arc", "lineno, cause")):
@@ -490,7 +596,7 @@
 
     """
     def __new__(cls, lineno, cause=None):
-        return super(ArcStart, cls).__new__(cls, lineno, cause)
+        return super().__new__(cls, lineno, cause)
 
 
 # Define contract words that PyContract doesn't have.
@@ -498,11 +604,7 @@
 new_contract('ArcStarts', lambda seq: all(isinstance(x, ArcStart) for x in seq))
 
 
-# Turn on AST dumps with an environment variable.
-# $set_env.py: COVERAGE_AST_DUMP - Dump the AST nodes when parsing code.
-AST_DUMP = bool(int(os.environ.get("COVERAGE_AST_DUMP", 0)))
-
-class NodeList(object):
+class NodeList:
     """A synthetic fictitious node, containing a sequence of nodes.
 
     This is used when collapsing optimized if-statements, to represent the
@@ -513,25 +615,33 @@
         self.body = body
         self.lineno = body[0].lineno
 
-
 # TODO: some add_arcs methods here don't add arcs, they return them. Rename them.
 # TODO: the cause messages have too many commas.
 # TODO: Shouldn't the cause messages join with "and" instead of "or"?
 
-class AstArcAnalyzer(object):
+def ast_parse(text):
+    """How we create an AST parse."""
+    return ast.parse(neuter_encoding_declaration(text))
+
+
+class AstArcAnalyzer:
     """Analyze source text with an AST to find executable code paths."""
 
     @contract(text='unicode', statements=set)
     def __init__(self, text, statements, multiline):
-        self.root_node = ast.parse(neuter_encoding_declaration(text))
+        self.root_node = ast_parse(text)
         # TODO: I think this is happening in too many places.
         self.statements = {multiline.get(l, l) for l in statements}
         self.multiline = multiline
 
-        if AST_DUMP:                                # pragma: debugging
+        # Turn on AST dumps with an environment variable.
+        # $set_env.py: COVERAGE_AST_DUMP - Dump the AST nodes when parsing code.
+        dump_ast = bool(int(os.environ.get("COVERAGE_AST_DUMP", 0)))
+
+        if dump_ast:                                # pragma: debugging
             # Dump the AST so that failing tests have helpful output.
-            print("Statements: {}".format(self.statements))
-            print("Multiline map: {}".format(self.multiline))
+            print(f"Statements: {self.statements}")
+            print(f"Multiline map: {self.multiline}")
             ast_dump(self.root_node)
 
         self.arcs = set()
@@ -564,7 +674,7 @@
     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("\nAdding arc: ({}, {}): {!r}, {!r}".format(start, end, smsg, emsg))
+            print(f"\nAdding arc: ({start}, {end}): {smsg!r}, {emsg!r}")
             print(short_stack(limit=6))
         self.arcs.add((start, end))
 
@@ -603,8 +713,7 @@
     _line__ClassDef = _line_decorated
 
     def _line__Dict(self, node):
-        # Python 3.5 changed how dict literals are made.
-        if env.PYVERSION >= (3, 5) and node.keys:
+        if node.keys:
             if node.keys[0] is not None:
                 return node.keys[0].lineno
             else:
@@ -634,8 +743,8 @@
 
     # The node types that just flow to the next node with no complications.
     OK_TO_DEFAULT = {
-        "Assign", "Assert", "AugAssign", "Delete", "Exec", "Expr", "Global",
-        "Import", "ImportFrom", "Nonlocal", "Pass", "Print",
+        "AnnAssign", "Assign", "Assert", "AugAssign", "Delete", "Expr", "Global",
+        "Import", "ImportFrom", "Nonlocal", "Pass",
     }
 
     @contract(returns='ArcStarts')
@@ -661,11 +770,10 @@
             return handler(node)
         else:
             # No handler: either it's something that's ok to default (a simple
-            # statement), or it's something we overlooked. Change this 0 to 1
-            # to see if it's overlooked.
-            if 0:
+            # statement), or it's something we overlooked.
+            if env.TESTING:
                 if node_name not in self.OK_TO_DEFAULT:
-                    print("*** Unhandled: {}".format(node))
+                    raise Exception(f"*** Unhandled: {node}")       # pragma: only failure
 
             # Default for simple statements: one exit from this node.
             return {ArcStart(self.line_for_node(node))}
@@ -799,61 +907,30 @@
     @contract(exits='ArcStarts')
     def process_break_exits(self, exits):
         """Add arcs due to jumps from `exits` being breaks."""
-        for block in self.nearest_blocks():
-            if isinstance(block, LoopBlock):
-                block.break_exits.update(exits)
-                break
-            elif isinstance(block, TryBlock) and block.final_start is not None:
-                block.break_from.update(exits)
+        for block in self.nearest_blocks():                         # pragma: always breaks
+            if block.process_break_exits(exits, self.add_arc):
                 break
 
     @contract(exits='ArcStarts')
     def process_continue_exits(self, exits):
         """Add arcs due to jumps from `exits` being continues."""
-        for block in self.nearest_blocks():
-            if isinstance(block, LoopBlock):
-                for xit in exits:
-                    self.add_arc(xit.lineno, block.start, xit.cause)
-                break
-            elif isinstance(block, TryBlock) and block.final_start is not None:
-                block.continue_from.update(exits)
+        for block in self.nearest_blocks():                         # pragma: always breaks
+            if block.process_continue_exits(exits, self.add_arc):
                 break
 
     @contract(exits='ArcStarts')
     def process_raise_exits(self, exits):
         """Add arcs due to jumps from `exits` being raises."""
         for block in self.nearest_blocks():
-            if isinstance(block, TryBlock):
-                if block.handler_start is not None:
-                    for xit in exits:
-                        self.add_arc(xit.lineno, block.handler_start, xit.cause)
-                    break
-                elif block.final_start is not None:
-                    block.raise_from.update(exits)
-                    break
-            elif isinstance(block, FunctionBlock):
-                for xit in exits:
-                    self.add_arc(
-                        xit.lineno, -block.start, xit.cause,
-                        "didn't except from function {!r}".format(block.name),
-                    )
+            if block.process_raise_exits(exits, self.add_arc):
                 break
 
     @contract(exits='ArcStarts')
     def process_return_exits(self, exits):
         """Add arcs due to jumps from `exits` being returns."""
-        for block in self.nearest_blocks():
-            if isinstance(block, TryBlock) and block.final_start is not None:
-                block.return_from.update(exits)
+        for block in self.nearest_blocks():                         # pragma: always breaks
+            if block.process_return_exits(exits, self.add_arc):
                 break
-            elif isinstance(block, FunctionBlock):
-                for xit in exits:
-                    self.add_arc(
-                        xit.lineno, -block.start, xit.cause,
-                        "didn't return from function {!r}".format(block.name),
-                    )
-                break
-
 
     # Handlers: _handle__*
     #
@@ -862,6 +939,9 @@
     # also call self.add_arc to record arcs they find.  These functions mirror
     # the Python semantics of each syntactic construct.  See the docstring
     # for add_arcs to understand the concept of exits from a node.
+    #
+    # Every node type that represents a statement should have a handler, or it
+    # should be listed in OK_TO_DEFAULT.
 
     @contract(returns='ArcStarts')
     def _handle__Break(self, node):
@@ -943,6 +1023,24 @@
         return exits
 
     @contract(returns='ArcStarts')
+    def _handle__Match(self, node):
+        start = self.line_for_node(node)
+        last_start = start
+        exits = set()
+        had_wildcard = False
+        for case in node.cases:
+            case_start = self.line_for_node(case.pattern)
+            if isinstance(case.pattern, ast.MatchAs):
+                had_wildcard = True
+            self.add_arc(last_start, case_start, "the pattern on line {lineno} always matched")
+            from_start = ArcStart(case_start, cause="the pattern on line {lineno} never matched")
+            exits |= self.add_body_arcs(case.body, from_start=from_start)
+            last_start = case_start
+        if not had_wildcard:
+            exits.add(from_start)
+        return exits
+
+    @contract(returns='ArcStarts')
     def _handle__NodeList(self, node):
         start = self.line_for_node(node)
         exits = self.add_body_arcs(node.body, from_start=ArcStart(start))
@@ -976,6 +1074,9 @@
         else:
             final_start = None
 
+        # This is true by virtue of Python syntax: have to have either except
+        # or finally, or both.
+        assert handler_start is not None or final_start is not None
         try_block = TryBlock(handler_start, final_start)
         self.block_stack.append(try_block)
 
@@ -1090,36 +1191,11 @@
         return exits
 
     @contract(returns='ArcStarts')
-    def _handle__TryExcept(self, node):
-        # Python 2.7 uses separate TryExcept and TryFinally nodes. If we get
-        # TryExcept, it means there was no finally, so fake it, and treat as
-        # a general Try node.
-        node.finalbody = []
-        return self._handle__Try(node)
-
-    @contract(returns='ArcStarts')
-    def _handle__TryFinally(self, node):
-        # Python 2.7 uses separate TryExcept and TryFinally nodes. If we get
-        # TryFinally, see if there's a TryExcept nested inside. If so, merge
-        # them. Otherwise, fake fields to complete a Try node.
-        node.handlers = []
-        node.orelse = []
-
-        first = node.body[0]
-        if first.__class__.__name__ == "TryExcept" and node.lineno == first.lineno:
-            assert len(node.body) == 1
-            node.body = first.body
-            node.handlers = first.handlers
-            node.orelse = first.orelse
-
-        return self._handle__Try(node)
-
-    @contract(returns='ArcStarts')
     def _handle__While(self, node):
         start = to_top = self.line_for_node(node.test)
         constant_test = self.is_constant_expr(node.test)
         top_is_body0 = False
-        if constant_test and (env.PY3 or constant_test == "Num"):
+        if constant_test:
             top_is_body0 = True
         if env.PYBEHAVIOR.keep_constant_test:
             top_is_body0 = False
@@ -1146,11 +1222,37 @@
     @contract(returns='ArcStarts')
     def _handle__With(self, node):
         start = self.line_for_node(node)
+        if env.PYBEHAVIOR.exit_through_with:
+            self.block_stack.append(WithBlock(start=start))
         exits = self.add_body_arcs(node.body, from_start=ArcStart(start))
+        if env.PYBEHAVIOR.exit_through_with:
+            with_block = self.block_stack.pop()
+            with_exit = {ArcStart(start)}
+            if exits:
+                for xit in exits:
+                    self.add_arc(xit.lineno, start)
+                exits = with_exit
+            if with_block.break_from:
+                self.process_break_exits(
+                    self._combine_finally_starts(with_block.break_from, with_exit)
+                    )
+            if with_block.continue_from:
+                self.process_continue_exits(
+                    self._combine_finally_starts(with_block.continue_from, with_exit)
+                    )
+            if with_block.return_from:
+                self.process_return_exits(
+                    self._combine_finally_starts(with_block.return_from, with_exit)
+                    )
         return exits
 
     _handle__AsyncWith = _handle__With
 
+    # Code object dispatchers: _code_object__*
+    #
+    # These methods are used by analyze() as the start of the analysis.
+    # There is one for each construct with a code object.
+
     def _code_object__Module(self, node):
         start = self.line_for_node(node)
         if node.body:
@@ -1178,86 +1280,85 @@
         for xit in exits:
             self.add_arc(
                 xit.lineno, -start, xit.cause,
-                "didn't exit the body of class {!r}".format(node.name),
+                f"didn't exit the body of class {node.name!r}",
             )
 
-    def _make_oneline_code_method(noun):     # pylint: disable=no-self-argument
-        """A function to make methods for online callable _code_object__ methods."""
-        def _code_object__oneline_callable(self, node):
+    def _make_expression_code_method(noun):                     # pylint: disable=no-self-argument
+        """A function to make methods for expression-based callable _code_object__ methods."""
+        def _code_object__expression_callable(self, node):
             start = self.line_for_node(node)
-            self.add_arc(-start, start, None, "didn't run the {} on line {}".format(noun, start))
-            self.add_arc(
-                start, -start, None,
-                "didn't finish the {} on line {}".format(noun, start),
-            )
-        return _code_object__oneline_callable
+            self.add_arc(-start, start, None, f"didn't run the {noun} on line {start}")
+            self.add_arc(start, -start, None, f"didn't finish the {noun} on line {start}")
+        return _code_object__expression_callable
 
-    _code_object__Lambda = _make_oneline_code_method("lambda")
-    _code_object__GeneratorExp = _make_oneline_code_method("generator expression")
-    _code_object__DictComp = _make_oneline_code_method("dictionary comprehension")
-    _code_object__SetComp = _make_oneline_code_method("set comprehension")
-    if env.PY3:
-        _code_object__ListComp = _make_oneline_code_method("list comprehension")
+    _code_object__Lambda = _make_expression_code_method("lambda")
+    _code_object__GeneratorExp = _make_expression_code_method("generator expression")
+    _code_object__DictComp = _make_expression_code_method("dictionary comprehension")
+    _code_object__SetComp = _make_expression_code_method("set comprehension")
+    _code_object__ListComp = _make_expression_code_method("list comprehension")
 
 
-if AST_DUMP:            # pragma: debugging
-    # Code only used when dumping the AST for debugging.
+# Code only used when dumping the AST for debugging.
 
-    SKIP_DUMP_FIELDS = ["ctx"]
+SKIP_DUMP_FIELDS = ["ctx"]
 
-    def _is_simple_value(value):
-        """Is `value` simple enough to be displayed on a single line?"""
-        return (
-            value in [None, [], (), {}, set()] or
-            isinstance(value, (string_class, int, float))
-        )
+def _is_simple_value(value):
+    """Is `value` simple enough to be displayed on a single line?"""
+    return (
+        value in [None, [], (), {}, set()] or
+        isinstance(value, (bytes, int, float, str))
+    )
 
-    def ast_dump(node, depth=0):
-        """Dump the AST for `node`.
+def ast_dump(node, depth=0, print=print):   # pylint: disable=redefined-builtin
+    """Dump the AST for `node`.
 
-        This recursively walks the AST, printing a readable version.
+    This recursively walks the AST, printing a readable version.
 
-        """
-        indent = " " * depth
-        if not isinstance(node, ast.AST):
-            print("{}<{} {!r}>".format(indent, node.__class__.__name__, node))
-            return
-
-        lineno = getattr(node, "lineno", None)
-        if lineno is not None:
-            linemark = " @ {}".format(node.lineno)
-        else:
-            linemark = ""
-        head = "{}<{}{}".format(indent, node.__class__.__name__, linemark)
+    """
+    indent = " " * depth
+    lineno = getattr(node, "lineno", None)
+    if lineno is not None:
+        linemark = f" @ {node.lineno},{node.col_offset}"
+        if hasattr(node, "end_lineno"):
+            linemark += ":"
+            if node.end_lineno != node.lineno:
+                linemark += f"{node.end_lineno},"
+            linemark += f"{node.end_col_offset}"
+    else:
+        linemark = ""
+    head = f"{indent}<{node.__class__.__name__}{linemark}"
 
-        named_fields = [
-            (name, value)
-            for name, value in ast.iter_fields(node)
-            if name not in SKIP_DUMP_FIELDS
-        ]
-        if not named_fields:
-            print("{}>".format(head))
-        elif len(named_fields) == 1 and _is_simple_value(named_fields[0][1]):
-            field_name, value = named_fields[0]
-            print("{} {}: {!r}>".format(head, field_name, value))
-        else:
-            print(head)
-            if 0:
-                print("{}# mro: {}".format(
-                    indent, ", ".join(c.__name__ for c in node.__class__.__mro__[1:]),
-                ))
-            next_indent = indent + "    "
-            for field_name, value in named_fields:
-                prefix = "{}{}:".format(next_indent, field_name)
-                if _is_simple_value(value):
-                    print("{} {!r}".format(prefix, value))
-                elif isinstance(value, list):
-                    print("{} [".format(prefix))
-                    for n in value:
-                        ast_dump(n, depth + 8)
-                    print("{}]".format(next_indent))
-                else:
-                    print(prefix)
-                    ast_dump(value, depth + 8)
+    named_fields = [
+        (name, value)
+        for name, value in ast.iter_fields(node)
+        if name not in SKIP_DUMP_FIELDS
+    ]
+    if not named_fields:
+        print(f"{head}>")
+    elif len(named_fields) == 1 and _is_simple_value(named_fields[0][1]):
+        field_name, value = named_fields[0]
+        print(f"{head} {field_name}: {value!r}>")
+    else:
+        print(head)
+        if 0:
+            print("{}# mro: {}".format(
+                indent, ", ".join(c.__name__ for c in node.__class__.__mro__[1:]),
+            ))
+        next_indent = indent + "    "
+        for field_name, value in named_fields:
+            prefix = f"{next_indent}{field_name}:"
+            if _is_simple_value(value):
+                print(f"{prefix} {value!r}")
+            elif isinstance(value, list):
+                print(f"{prefix} [")
+                for n in value:
+                    if _is_simple_value(n):
+                        print(f"{next_indent}    {n!r}")
+                    else:
+                        ast_dump(n, depth + 8, print=print)
+                print(f"{next_indent}]")
+            else:
+                print(prefix)
+                ast_dump(value, depth + 8, print=print)
 
-            print("{}>".format(indent))
+        print(f"{indent}>")
--- a/eric7/DebugClients/Python/coverage/phystokens.py	Fri Nov 19 19:28:47 2021 +0100
+++ b/eric7/DebugClients/Python/coverage/phystokens.py	Sat Nov 20 16:47:38 2021 +0100
@@ -3,15 +3,13 @@
 
 """Better tokenizing for coverage.py."""
 
-import codecs
+import ast
 import keyword
 import re
-import sys
 import token
 import tokenize
 
 from coverage import env
-from coverage.backward import iternext, unicode_class
 from coverage.misc import contract
 
 
@@ -70,6 +68,21 @@
         last_lineno = elineno
 
 
+class MatchCaseFinder(ast.NodeVisitor):
+    """Helper for finding match/case lines."""
+    def __init__(self, source):
+        # This will be the set of line numbers that start match or case statements.
+        self.match_case_lines = set()
+        self.visit(ast.parse(source))
+
+    def visit_Match(self, node):
+        """Invoked by ast.NodeVisitor.visit"""
+        self.match_case_lines.add(node.lineno)
+        for case in node.cases:
+            self.match_case_lines.add(case.pattern.lineno)
+        self.generic_visit(node)
+
+
 @contract(source='unicode')
 def source_token_lines(source):
     """Generate a series of lines, one for each line in `source`.
@@ -94,7 +107,10 @@
     source = source.expandtabs(8).replace('\r\n', '\n')
     tokgen = generate_tokens(source)
 
-    for ttype, ttext, (_, scol), (_, ecol), _ in phys_tokens(tokgen):
+    if env.PYBEHAVIOR.soft_keywords:
+        match_case_lines = MatchCaseFinder(source).match_case_lines
+
+    for ttype, ttext, (sline, scol), (_, ecol), _ in phys_tokens(tokgen):
         mark_start = True
         for part in re.split('(\n)', ttext):
             if part == '\n':
@@ -108,11 +124,24 @@
                 mark_end = False
             else:
                 if mark_start and scol > col:
-                    line.append(("ws", u" " * (scol - col)))
+                    line.append(("ws", " " * (scol - col)))
                     mark_start = False
                 tok_class = tokenize.tok_name.get(ttype, 'xx').lower()[:3]
-                if ttype == token.NAME and keyword.iskeyword(ttext):
-                    tok_class = "key"
+                if ttype == token.NAME:
+                    if keyword.iskeyword(ttext):
+                        # Hard keywords are always keywords.
+                        tok_class = "key"
+                    elif env.PYBEHAVIOR.soft_keywords and keyword.issoftkeyword(ttext):
+                        # Soft keywords appear at the start of the line, on lines that start
+                        # match or case statements.
+                        if len(line) == 0:
+                            is_start_of_line = True
+                        elif (len(line) == 1) and line[0][0] == "ws":
+                            is_start_of_line = True
+                        else:
+                            is_start_of_line = False
+                        if is_start_of_line and sline in match_case_lines:
+                            tok_class = "key"
                 line.append((tok_class, part))
                 mark_end = True
             scol = 0
@@ -123,7 +152,7 @@
         yield line
 
 
-class CachedTokenizer(object):
+class CachedTokenizer:
     """A one-element cache around tokenize.generate_tokens.
 
     When reporting, coverage.py tokenizes files twice, once to find the
@@ -143,7 +172,7 @@
         """A stand-in for `tokenize.generate_tokens`."""
         if text != self.last_text:
             self.last_text = text
-            readline = iternext(text.splitlines(True))
+            readline = iter(text.splitlines(True)).__next__
             self.last_tokens = list(tokenize.generate_tokens(readline))
         return self.last_tokens
 
@@ -154,102 +183,7 @@
 COOKIE_RE = re.compile(r"^[ \t]*#.*coding[:=][ \t]*([-\w.]+)", flags=re.MULTILINE)
 
 @contract(source='bytes')
-def _source_encoding_py2(source):
-    """Determine the encoding for `source`, according to PEP 263.
-
-    `source` is a byte string, the text of the program.
-
-    Returns a string, the name of the encoding.
-
-    """
-    assert isinstance(source, bytes)
-
-    # Do this so the detect_encode code we copied will work.
-    readline = iternext(source.splitlines(True))
-
-    # This is mostly code adapted from Py3.2's tokenize module.
-
-    def _get_normal_name(orig_enc):
-        """Imitates get_normal_name in tokenizer.c."""
-        # Only care about the first 12 characters.
-        enc = orig_enc[:12].lower().replace("_", "-")
-        if re.match(r"^utf-8($|-)", enc):
-            return "utf-8"
-        if re.match(r"^(latin-1|iso-8859-1|iso-latin-1)($|-)", enc):
-            return "iso-8859-1"
-        return orig_enc
-
-    # From detect_encode():
-    # It detects the encoding from the presence of a UTF-8 BOM or an encoding
-    # cookie as specified in PEP-0263.  If both a BOM and a cookie are present,
-    # but disagree, a SyntaxError will be raised.  If the encoding cookie is an
-    # invalid charset, raise a SyntaxError.  Note that if a UTF-8 BOM is found,
-    # 'utf-8-sig' is returned.
-
-    # If no encoding is specified, then the default will be returned.
-    default = 'ascii'
-
-    bom_found = False
-    encoding = None
-
-    def read_or_stop():
-        """Get the next source line, or ''."""
-        try:
-            return readline()
-        except StopIteration:
-            return ''
-
-    def find_cookie(line):
-        """Find an encoding cookie in `line`."""
-        try:
-            line_string = line.decode('ascii')
-        except UnicodeDecodeError:
-            return None
-
-        matches = COOKIE_RE.findall(line_string)
-        if not matches:
-            return None
-        encoding = _get_normal_name(matches[0])
-        try:
-            codec = codecs.lookup(encoding)
-        except LookupError:
-            # This behavior mimics the Python interpreter
-            raise SyntaxError("unknown encoding: " + encoding)
-
-        if bom_found:
-            # codecs in 2.3 were raw tuples of functions, assume the best.
-            codec_name = getattr(codec, 'name', encoding)
-            if codec_name != 'utf-8':
-                # This behavior mimics the Python interpreter
-                raise SyntaxError('encoding problem: utf-8')
-            encoding += '-sig'
-        return encoding
-
-    first = read_or_stop()
-    if first.startswith(codecs.BOM_UTF8):
-        bom_found = True
-        first = first[3:]
-        default = 'utf-8-sig'
-    if not first:
-        return default
-
-    encoding = find_cookie(first)
-    if encoding:
-        return encoding
-
-    second = read_or_stop()
-    if not second:
-        return default
-
-    encoding = find_cookie(second)
-    if encoding:
-        return encoding
-
-    return default
-
-
-@contract(source='bytes')
-def _source_encoding_py3(source):
+def source_encoding(source):
     """Determine the encoding for `source`, according to PEP 263.
 
     `source` is a byte string: the text of the program.
@@ -257,31 +191,23 @@
     Returns a string, the name of the encoding.
 
     """
-    readline = iternext(source.splitlines(True))
+    readline = iter(source.splitlines(True)).__next__
     return tokenize.detect_encoding(readline)[0]
 
 
-if env.PY3:
-    source_encoding = _source_encoding_py3
-else:
-    source_encoding = _source_encoding_py2
-
-
 @contract(source='unicode')
 def compile_unicode(source, filename, mode):
     """Just like the `compile` builtin, but works on any Unicode string.
 
     Python 2's compile() builtin has a stupid restriction: if the source string
     is Unicode, then it may not have a encoding declaration in it.  Why not?
-    Who knows!  It also decodes to utf8, and then tries to interpret those utf8
-    bytes according to the encoding declaration.  Why? Who knows!
+    Who knows!  It also decodes to utf-8, and then tries to interpret those
+    utf-8 bytes according to the encoding declaration.  Why? Who knows!
 
     This function neuters the coding declaration, and compiles it.
 
     """
     source = neuter_encoding_declaration(source)
-    if env.PY2 and isinstance(filename, unicode_class):
-        filename = filename.encode(sys.getfilesystemencoding(), "replace")
     code = compile(source, filename, mode)
     return code
 
--- a/eric7/DebugClients/Python/coverage/plugin.py	Fri Nov 19 19:28:47 2021 +0100
+++ b/eric7/DebugClients/Python/coverage/plugin.py	Sat Nov 20 16:47:38 2021 +0100
@@ -116,7 +116,7 @@
 from coverage.misc import contract, _needs_to_implement
 
 
-class CoveragePlugin(object):
+class CoveragePlugin:
     """Base class for coverage.py plug-ins."""
 
     def file_tracer(self, filename):        # pylint: disable=unused-argument
@@ -232,7 +232,7 @@
         return []
 
 
-class FileTracer(object):
+class FileTracer:
     """Support needed for files during the execution phase.
 
     File tracer plug-ins implement subclasses of FileTracer to return from
@@ -315,7 +315,7 @@
         return lineno, lineno
 
 
-class FileReporter(object):
+class FileReporter:
     """Support needed for files during the analysis and reporting phases.
 
     File tracer plug-ins implement a subclass of `FileReporter`, and return
@@ -359,12 +359,12 @@
         Returns a Unicode string.
 
         The base implementation simply reads the `self.filename` file and
-        decodes it as UTF8.  Override this method if your file isn't readable
+        decodes it as UTF-8.  Override this method if your file isn't readable
         as a text file, or if you need other encoding support.
 
         """
         with open(self.filename, "rb") as f:
-            return f.read().decode("utf8")
+            return f.read().decode("utf-8")
 
     def lines(self):
         """Get the executable lines in this file.
@@ -476,7 +476,7 @@
         to {end}".
 
         """
-        return "Line {start} didn't jump to line {end}".format(start=start, end=end)
+        return f"Line {start} didn't jump to line {end}"
 
     def source_token_lines(self):
         """Generate a series of tokenized lines, one for each line in `source`.
--- a/eric7/DebugClients/Python/coverage/plugin_support.py	Fri Nov 19 19:28:47 2021 +0100
+++ b/eric7/DebugClients/Python/coverage/plugin_support.py	Sat Nov 20 16:47:38 2021 +0100
@@ -7,13 +7,14 @@
 import os.path
 import sys
 
-from coverage.misc import CoverageException, isolate_module
+from coverage.exceptions import CoverageException
+from coverage.misc import isolate_module
 from coverage.plugin import CoveragePlugin, FileTracer, FileReporter
 
 os = isolate_module(os)
 
 
-class Plugins(object):
+class Plugins:
     """The currently loaded collection of coverage.py plugins."""
 
     def __init__(self):
@@ -44,7 +45,7 @@
             coverage_init = getattr(mod, "coverage_init", None)
             if not coverage_init:
                 raise CoverageException(
-                    "Plugin module %r didn't define a coverage_init function" % module
+                    f"Plugin module {module!r} didn't define a coverage_init function"
                 )
 
             options = config.get_plugin_options(module)
@@ -95,10 +96,10 @@
         is a list to append the plugin to.
 
         """
-        plugin_name = "%s.%s" % (self.current_module, plugin.__class__.__name__)
+        plugin_name = f"{self.current_module}.{plugin.__class__.__name__}"
         if self.debug and self.debug.should('plugin'):
-            self.debug.write("Loaded plugin %r: %r" % (self.current_module, plugin))
-            labelled = LabelledDebug("plugin %r" % (self.current_module,), self.debug)
+            self.debug.write(f"Loaded plugin {self.current_module!r}: {plugin!r}")
+            labelled = LabelledDebug(f"plugin {self.current_module!r}", self.debug)
             plugin = DebugPluginWrapper(plugin, labelled)
 
         # pylint: disable=attribute-defined-outside-init
@@ -122,7 +123,7 @@
         return self.names[plugin_name]
 
 
-class LabelledDebug(object):
+class LabelledDebug:
     """A Debug writer, but with labels for prepending to the messages."""
 
     def __init__(self, label, debug, prev_labels=()):
@@ -140,45 +141,45 @@
 
     def write(self, message):
         """Write `message`, but with the labels prepended."""
-        self.debug.write("%s%s" % (self.message_prefix(), message))
+        self.debug.write(f"{self.message_prefix()}{message}")
 
 
 class DebugPluginWrapper(CoveragePlugin):
     """Wrap a plugin, and use debug to report on what it's doing."""
 
     def __init__(self, plugin, debug):
-        super(DebugPluginWrapper, self).__init__()
+        super().__init__()
         self.plugin = plugin
         self.debug = debug
 
     def file_tracer(self, filename):
         tracer = self.plugin.file_tracer(filename)
-        self.debug.write("file_tracer(%r) --> %r" % (filename, tracer))
+        self.debug.write(f"file_tracer({filename!r}) --> {tracer!r}")
         if tracer:
-            debug = self.debug.add_label("file %r" % (filename,))
+            debug = self.debug.add_label(f"file {filename!r}")
             tracer = DebugFileTracerWrapper(tracer, debug)
         return tracer
 
     def file_reporter(self, filename):
         reporter = self.plugin.file_reporter(filename)
-        self.debug.write("file_reporter(%r) --> %r" % (filename, reporter))
+        self.debug.write(f"file_reporter({filename!r}) --> {reporter!r}")
         if reporter:
-            debug = self.debug.add_label("file %r" % (filename,))
+            debug = self.debug.add_label(f"file {filename!r}")
             reporter = DebugFileReporterWrapper(filename, reporter, debug)
         return reporter
 
     def dynamic_context(self, frame):
         context = self.plugin.dynamic_context(frame)
-        self.debug.write("dynamic_context(%r) --> %r" % (frame, context))
+        self.debug.write(f"dynamic_context({frame!r}) --> {context!r}")
         return context
 
     def find_executable_files(self, src_dir):
         executable_files = self.plugin.find_executable_files(src_dir)
-        self.debug.write("find_executable_files(%r) --> %r" % (src_dir, executable_files))
+        self.debug.write(f"find_executable_files({src_dir!r}) --> {executable_files!r}")
         return executable_files
 
     def configure(self, config):
-        self.debug.write("configure(%r)" % (config,))
+        self.debug.write(f"configure({config!r})")
         self.plugin.configure(config)
 
     def sys_info(self):
@@ -201,24 +202,24 @@
 
     def source_filename(self):
         sfilename = self.tracer.source_filename()
-        self.debug.write("source_filename() --> %r" % (sfilename,))
+        self.debug.write(f"source_filename() --> {sfilename!r}")
         return sfilename
 
     def has_dynamic_source_filename(self):
         has = self.tracer.has_dynamic_source_filename()
-        self.debug.write("has_dynamic_source_filename() --> %r" % (has,))
+        self.debug.write(f"has_dynamic_source_filename() --> {has!r}")
         return has
 
     def dynamic_source_filename(self, filename, frame):
         dyn = self.tracer.dynamic_source_filename(filename, frame)
-        self.debug.write("dynamic_source_filename(%r, %s) --> %r" % (
+        self.debug.write("dynamic_source_filename({!r}, {}) --> {!r}".format(
             filename, self._show_frame(frame), dyn,
         ))
         return dyn
 
     def line_number_range(self, frame):
         pair = self.tracer.line_number_range(frame)
-        self.debug.write("line_number_range(%s) --> %r" % (self._show_frame(frame), pair))
+        self.debug.write(f"line_number_range({self._show_frame(frame)}) --> {pair!r}")
         return pair
 
 
@@ -226,48 +227,48 @@
     """A debugging `FileReporter`."""
 
     def __init__(self, filename, reporter, debug):
-        super(DebugFileReporterWrapper, self).__init__(filename)
+        super().__init__(filename)
         self.reporter = reporter
         self.debug = debug
 
     def relative_filename(self):
         ret = self.reporter.relative_filename()
-        self.debug.write("relative_filename() --> %r" % (ret,))
+        self.debug.write(f"relative_filename() --> {ret!r}")
         return ret
 
     def lines(self):
         ret = self.reporter.lines()
-        self.debug.write("lines() --> %r" % (ret,))
+        self.debug.write(f"lines() --> {ret!r}")
         return ret
 
     def excluded_lines(self):
         ret = self.reporter.excluded_lines()
-        self.debug.write("excluded_lines() --> %r" % (ret,))
+        self.debug.write(f"excluded_lines() --> {ret!r}")
         return ret
 
     def translate_lines(self, lines):
         ret = self.reporter.translate_lines(lines)
-        self.debug.write("translate_lines(%r) --> %r" % (lines, ret))
+        self.debug.write(f"translate_lines({lines!r}) --> {ret!r}")
         return ret
 
     def translate_arcs(self, arcs):
         ret = self.reporter.translate_arcs(arcs)
-        self.debug.write("translate_arcs(%r) --> %r" % (arcs, ret))
+        self.debug.write(f"translate_arcs({arcs!r}) --> {ret!r}")
         return ret
 
     def no_branch_lines(self):
         ret = self.reporter.no_branch_lines()
-        self.debug.write("no_branch_lines() --> %r" % (ret,))
+        self.debug.write(f"no_branch_lines() --> {ret!r}")
         return ret
 
     def exit_counts(self):
         ret = self.reporter.exit_counts()
-        self.debug.write("exit_counts() --> %r" % (ret,))
+        self.debug.write(f"exit_counts() --> {ret!r}")
         return ret
 
     def arcs(self):
         ret = self.reporter.arcs()
-        self.debug.write("arcs() --> %r" % (ret,))
+        self.debug.write(f"arcs() --> {ret!r}")
         return ret
 
     def source(self):
--- a/eric7/DebugClients/Python/coverage/python.py	Fri Nov 19 19:28:47 2021 +0100
+++ b/eric7/DebugClients/Python/coverage/python.py	Sat Nov 20 16:47:38 2021 +0100
@@ -7,9 +7,10 @@
 import types
 import zipimport
 
-from coverage import env, files
+from coverage import env
+from coverage.exceptions import CoverageException, NoSource
+from coverage.files import canonical_filename, relative_filename
 from coverage.misc import contract, expensive, isolate_module, join_regex
-from coverage.misc import CoverageException, NoSource
 from coverage.parser import PythonParser
 from coverage.phystokens import source_token_lines, source_encoding
 from coverage.plugin import FileReporter
@@ -56,9 +57,7 @@
             break
     else:
         # Couldn't find source.
-        exc_msg = "No source for code: '%s'.\n" % (filename,)
-        exc_msg += "Aborting report output, consider using -i."
-        raise NoSource(exc_msg)
+        raise NoSource(f"No source for code: '{filename}'.")
 
     # Replace \f because of http://bugs.python.org/issue19035
     source = source.replace(b'\f', b' ')
@@ -90,7 +89,7 @@
                 continue
             try:
                 data = zi.get_data(parts[1])
-            except IOError:
+            except OSError:
                 continue
             return data
     return None
@@ -136,11 +135,11 @@
     elif isinstance(morf, types.ModuleType):
         # A module should have had .__file__, otherwise we can't use it.
         # This could be a PEP-420 namespace package.
-        raise CoverageException("Module {} has no file".format(morf))
+        raise CoverageException(f"Module {morf} has no file")
     else:
         filename = morf
 
-    filename = source_for_file(files.unicode_filename(filename))
+    filename = source_for_file(filename)
     return filename
 
 
@@ -152,16 +151,15 @@
 
         filename = source_for_morf(morf)
 
-        super(PythonFileReporter, self).__init__(files.canonical_filename(filename))
+        super().__init__(canonical_filename(filename))
 
         if hasattr(morf, '__name__'):
             name = morf.__name__.replace(".", os.sep)
             if os.path.basename(filename).startswith('__init__.'):
                 name += os.sep + "__init__"
             name += ".py"
-            name = files.unicode_filename(name)
         else:
-            name = files.relative_filename(filename)
+            name = relative_filename(filename)
         self.relname = name
 
         self._source = None
@@ -169,7 +167,7 @@
         self._excluded = None
 
     def __repr__(self):
-        return "<PythonFileReporter {!r}>".format(self.filename)
+        return f"<PythonFileReporter {self.filename!r}>"
 
     @contract(returns='unicode')
     def relative_filename(self):
--- a/eric7/DebugClients/Python/coverage/pytracer.py	Fri Nov 19 19:28:47 2021 +0100
+++ b/eric7/DebugClients/Python/coverage/pytracer.py	Sat Nov 20 16:47:38 2021 +0100
@@ -11,8 +11,6 @@
 
 # We need the YIELD_VALUE opcode below, in a comparison-friendly form.
 YIELD_VALUE = dis.opmap['YIELD_VALUE']
-if env.PY2:
-    YIELD_VALUE = chr(YIELD_VALUE)
 
 # When running meta-coverage, this file can try to trace itself, which confuses
 # everything.  Don't trace ourselves.
@@ -20,7 +18,7 @@
 THIS_FILE = __file__.rstrip("co")
 
 
-class PyTracer(object):
+class PyTracer:
     """Python implementation of the raw data tracer."""
 
     # Because of poor implementations of trace-function-manipulating tools,
@@ -50,15 +48,13 @@
         # The threading module to use, if any.
         self.threading = None
 
-        self.cur_file_dict = None
+        self.cur_file_data = None
         self.last_line = 0          # int, but uninitialized.
         self.cur_file_name = None
         self.context = None
         self.started_context = False
 
         self.data_stack = []
-        self.last_exc_back = None
-        self.last_exc_firstlineno = 0
         self.thread = None
         self.stopped = False
         self._activity = False
@@ -85,7 +81,7 @@
             if 0:
                 f.write(".{:x}.{:x}".format(
                     self.thread.ident,
-                    self.threading.currentThread().ident,
+                    self.threading.current_thread().ident,
                 ))
             f.write(" {}".format(" ".join(map(str, args))))
             if 0:
@@ -115,22 +111,11 @@
                     self.log(">", f.f_code.co_filename, f.f_lineno, f.f_code.co_name, f.f_trace)
                     f = f.f_back
             sys.settrace(None)
-            self.cur_file_dict, self.cur_file_name, self.last_line, self.started_context = (
+            self.cur_file_data, self.cur_file_name, self.last_line, self.started_context = (
                 self.data_stack.pop()
             )
             return None
 
-        if self.last_exc_back:
-            if frame == self.last_exc_back:
-                # Someone forgot a return event.
-                if self.trace_arcs and self.cur_file_dict:
-                    pair = (self.last_line, -self.last_exc_firstlineno)
-                    self.cur_file_dict[pair] = None
-                self.cur_file_dict, self.cur_file_name, self.last_line, self.started_context = (
-                    self.data_stack.pop()
-                )
-            self.last_exc_back = None
-
         # if event != 'call' and frame.f_code.co_filename != self.cur_file_name:
         #     self.log("---\n*", frame.f_code.co_filename, self.cur_file_name, frame.f_lineno)
 
@@ -152,7 +137,7 @@
             self._activity = True
             self.data_stack.append(
                 (
-                    self.cur_file_dict,
+                    self.cur_file_data,
                     self.cur_file_name,
                     self.last_line,
                     self.started_context,
@@ -165,12 +150,12 @@
                 disp = self.should_trace(filename, frame)
                 self.should_trace_cache[filename] = disp
 
-            self.cur_file_dict = None
+            self.cur_file_data = None
             if disp.trace:
                 tracename = disp.source_filename
                 if tracename not in self.data:
-                    self.data[tracename] = {}
-                self.cur_file_dict = self.data[tracename]
+                    self.data[tracename] = set()
+                self.cur_file_data = self.data[tracename]
             # The call event is really a "start frame" event, and happens for
             # function calls and re-entering generators.  The f_lasti field is
             # -1 for calls, and a real offset for generators.  Use <0 as the
@@ -181,34 +166,31 @@
                 self.last_line = frame.f_lineno
         elif event == 'line':
             # Record an executed line.
-            if self.cur_file_dict is not None:
+            if self.cur_file_data is not None:
                 lineno = frame.f_lineno
 
                 if self.trace_arcs:
-                    self.cur_file_dict[(self.last_line, lineno)] = None
+                    self.cur_file_data.add((self.last_line, lineno))
                 else:
-                    self.cur_file_dict[lineno] = None
+                    self.cur_file_data.add(lineno)
                 self.last_line = lineno
         elif event == 'return':
-            if self.trace_arcs and self.cur_file_dict:
+            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:
                     first = frame.f_code.co_firstlineno
-                    self.cur_file_dict[(self.last_line, -first)] = None
+                    self.cur_file_data.add((self.last_line, -first))
             # Leaving this function, pop the filename stack.
-            self.cur_file_dict, self.cur_file_name, self.last_line, self.started_context = (
+            self.cur_file_data, self.cur_file_name, self.last_line, self.started_context = (
                 self.data_stack.pop()
             )
             # Leaving a context?
             if self.started_context:
                 self.context = None
                 self.switch_context(None)
-        elif event == 'exception':
-            self.last_exc_back = frame.f_back
-            self.last_exc_firstlineno = frame.f_code.co_firstlineno
         return self._trace
 
     def start(self):
@@ -220,9 +202,9 @@
         self.stopped = False
         if self.threading:
             if self.thread is None:
-                self.thread = self.threading.currentThread()
+                self.thread = self.threading.current_thread()
             else:
-                if self.thread.ident != self.threading.currentThread().ident:
+                if self.thread.ident != self.threading.current_thread().ident:
                     # Re-starting from a different thread!? Don't set the trace
                     # function, but we are marked as running again, so maybe it
                     # will be ok?
@@ -243,7 +225,7 @@
         # right thread.
         self.stopped = True
 
-        if self.threading and self.thread.ident != self.threading.currentThread().ident:
+        if self.threading and self.thread.ident != self.threading.current_thread().ident:
             # Called on a different thread than started us: we can't unhook
             # ourselves, but we've set the flag that we should stop, so we
             # won't do any more tracing.
@@ -256,10 +238,8 @@
             # 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
-                self.warn(
-                    "Trace function changed, measurement is likely wrong: %r" % (tf,),
-                    slug="trace-changed",
-                )
+                msg = f"Trace function changed, measurement is likely wrong: {tf!r}"
+                self.warn(msg, slug="trace-changed")
 
     def activity(self):
         """Has there been any activity?"""
--- a/eric7/DebugClients/Python/coverage/report.py	Fri Nov 19 19:28:47 2021 +0100
+++ b/eric7/DebugClients/Python/coverage/report.py	Sat Nov 20 16:47:38 2021 +0100
@@ -2,35 +2,35 @@
 # For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt
 
 """Reporter foundation for coverage.py."""
+
 import sys
 
-from coverage import env
+from coverage.exceptions import CoverageException, NotPython
 from coverage.files import prep_patterns, FnmatchMatcher
-from coverage.misc import CoverageException, NoSource, NotPython, ensure_dir_for_file, file_be_gone
+from coverage.misc import ensure_dir_for_file, file_be_gone
 
 
-def render_report(output_path, reporter, morfs):
-    """Run the provided reporter ensuring any required setup and cleanup is done
+def render_report(output_path, reporter, morfs, msgfn):
+    """Run a one-file report generator, managing the output file.
 
-    At a high level this method ensures the output file is ready to be written to. Then writes the
-    report to it. Then closes the file and deletes any garbage created if necessary.
+    This function ensures the output file is ready to be written to. Then writes
+    the report to it. Then closes the file and cleans up.
+
     """
     file_to_close = None
     delete_file = False
-    if output_path:
-        if output_path == '-':
-            outfile = sys.stdout
-        else:
-            # Ensure that the output directory is created; done here
-            # because this report pre-opens the output file.
-            # HTMLReport does this using the Report plumbing because
-            # its task is more complex, being multiple files.
-            ensure_dir_for_file(output_path)
-            open_kwargs = {}
-            if env.PY3:
-                open_kwargs['encoding'] = 'utf8'
-            outfile = open(output_path, "w", **open_kwargs)
-            file_to_close = outfile
+
+    if output_path == "-":
+        outfile = sys.stdout
+    else:
+        # Ensure that the output directory is created; done here
+        # because this report pre-opens the output file.
+        # HTMLReport does this using the Report plumbing because
+        # its task is more complex, being multiple files.
+        ensure_dir_for_file(output_path)
+        outfile = open(output_path, "w", encoding="utf-8")
+        file_to_close = outfile
+
     try:
         return reporter.report(morfs, outfile=outfile)
     except CoverageException:
@@ -40,7 +40,9 @@
         if file_to_close:
             file_to_close.close()
             if delete_file:
-                file_be_gone(output_path)
+                file_be_gone(output_path)           # pragma: part covered (doesn't return)
+            else:
+                msgfn(f"Wrote {reporter.report_type} to {output_path}")
 
 
 def get_analysis_to_report(coverage, morfs):
@@ -55,11 +57,11 @@
     config = coverage.config
 
     if config.report_include:
-        matcher = FnmatchMatcher(prep_patterns(config.report_include))
+        matcher = FnmatchMatcher(prep_patterns(config.report_include), "report_include")
         file_reporters = [fr for fr in file_reporters if matcher.match(fr.filename)]
 
     if config.report_omit:
-        matcher = FnmatchMatcher(prep_patterns(config.report_omit))
+        matcher = FnmatchMatcher(prep_patterns(config.report_omit), "report_omit")
         file_reporters = [fr for fr in file_reporters if not matcher.match(fr.filename)]
 
     if not file_reporters:
@@ -68,9 +70,6 @@
     for fr in sorted(file_reporters):
         try:
             analysis = coverage._analyze(fr)
-        except NoSource:
-            if not config.ignore_errors:
-                raise
         except NotPython:
             # Only report errors for .py files, and only if we didn't
             # explicitly suppress those errors.
@@ -78,9 +77,15 @@
             # should_be_python() method.
             if fr.should_be_python():
                 if config.ignore_errors:
-                    msg = "Couldn't parse Python file '{}'".format(fr.filename)
+                    msg = f"Couldn't parse Python file '{fr.filename}'"
                     coverage._warn(msg, slug="couldnt-parse")
                 else:
                     raise
+        except Exception as exc:
+            if config.ignore_errors:
+                msg = f"Couldn't parse '{fr.filename}': {exc}".rstrip()
+                coverage._warn(msg, slug="couldnt-parse")
+            else:
+                raise
         else:
             yield (fr, analysis)
--- a/eric7/DebugClients/Python/coverage/results.py	Fri Nov 19 19:28:47 2021 +0100
+++ b/eric7/DebugClients/Python/coverage/results.py	Sat Nov 20 16:47:38 2021 +0100
@@ -5,15 +5,15 @@
 
 import collections
 
-from coverage.backward import iitems
 from coverage.debug import SimpleReprMixin
-from coverage.misc import contract, CoverageException, nice_pair
+from coverage.exceptions import CoverageException
+from coverage.misc import contract, nice_pair
 
 
-class Analysis(object):
+class Analysis:
     """The results of analyzing a FileReporter."""
 
-    def __init__(self, data, file_reporter, file_mapper):
+    def __init__(self, data, precision, file_reporter, file_mapper):
         self.data = data
         self.file_reporter = file_reporter
         self.filename = file_mapper(self.file_reporter.filename)
@@ -32,8 +32,8 @@
             self.no_branch = self.file_reporter.no_branch_lines()
             n_branches = self._total_branches()
             mba = self.missing_branch_arcs()
-            n_partial_branches = sum(len(v) for k,v in iitems(mba) if k not in self.missing)
-            n_missing_branches = sum(len(v) for k,v in iitems(mba))
+            n_partial_branches = sum(len(v) for k,v in mba.items() if k not in self.missing)
+            n_missing_branches = sum(len(v) for k,v in mba.items())
         else:
             self._arc_possibilities = []
             self.exit_counts = {}
@@ -41,6 +41,7 @@
             n_branches = n_partial_branches = n_missing_branches = 0
 
         self.numbers = Numbers(
+            precision=precision,
             n_files=1,
             n_statements=len(self.statements),
             n_excluded=len(self.excluded),
@@ -59,7 +60,7 @@
 
         """
         if branches and self.has_arcs():
-            arcs = iitems(self.missing_branch_arcs())
+            arcs = self.missing_branch_arcs().items()
         else:
             arcs = None
 
@@ -83,13 +84,14 @@
 
     @contract(returns='list(tuple(int, int))')
     def arcs_missing(self):
-        """Returns a sorted list of the arcs in the code not executed."""
+        """Returns a sorted list of the unexecuted arcs in the code."""
         possible = self.arc_possibilities()
         executed = self.arcs_executed()
         missing = (
             p for p in possible
                 if p not in executed
                     and p[0] not in self.no_branch
+                    and p[1] not in self.excluded
         )
         return sorted(missing)
 
@@ -113,7 +115,7 @@
 
     def _branch_lines(self):
         """Returns a list of line numbers that have more than one exit."""
-        return [l1 for l1,count in iitems(self.exit_counts) if count > 1]
+        return [l1 for l1,count in self.exit_counts.items() if count > 1]
 
     def _total_branches(self):
         """How many total branches are there?"""
@@ -158,15 +160,16 @@
     up statistics across files.
 
     """
-    # A global to determine the precision on coverage percentages, the number
-    # of decimal places.
-    _precision = 0
-    _near0 = 1.0              # These will change when _precision is changed.
-    _near100 = 99.0
 
-    def __init__(self, n_files=0, n_statements=0, n_excluded=0, n_missing=0,
-                    n_branches=0, n_partial_branches=0, n_missing_branches=0
-                    ):
+    def __init__(self,
+            precision=0,
+            n_files=0, n_statements=0, n_excluded=0, n_missing=0,
+            n_branches=0, n_partial_branches=0, n_missing_branches=0
+            ):
+        assert 0 <= precision < 10
+        self._precision = precision
+        self._near0 = 1.0 / 10**precision
+        self._near100 = 100.0 - self._near0
         self.n_files = n_files
         self.n_statements = n_statements
         self.n_excluded = n_excluded
@@ -178,18 +181,11 @@
     def init_args(self):
         """Return a list for __init__(*args) to recreate this object."""
         return [
+            self._precision,
             self.n_files, self.n_statements, self.n_excluded, self.n_missing,
             self.n_branches, self.n_partial_branches, self.n_missing_branches,
         ]
 
-    @classmethod
-    def set_precision(cls, precision):
-        """Set the number of decimal places used to report percentages."""
-        assert 0 <= precision < 10
-        cls._precision = precision
-        cls._near0 = 1.0 / 10**precision
-        cls._near100 = 100.0 - cls._near0
-
     @property
     def n_executed(self):
         """Returns the number of executed statements."""
@@ -219,7 +215,16 @@
         result in either "0" or "100".
 
         """
-        pc = self.pc_covered
+        return self.display_covered(self.pc_covered)
+
+    def display_covered(self, pc):
+        """Return a displayable total percentage, as a string.
+
+        Note that "0" is only returned when the value is truly zero, and "100"
+        is only returned when the value is truly 100.  Rounding can never
+        result in either "0" or "100".
+
+        """
         if 0 < pc < self._near0:
             pc = self._near0
         elif self._near100 < pc < 100:
@@ -228,12 +233,11 @@
             pc = round(pc, self._precision)
         return "%.*f" % (self._precision, pc)
 
-    @classmethod
-    def pc_str_width(cls):
+    def pc_str_width(self):
         """How many characters wide can pc_covered_str be?"""
         width = 3   # "100"
-        if cls._precision > 0:
-            width += 1 + cls._precision
+        if self._precision > 0:
+            width += 1 + self._precision
         return width
 
     @property
@@ -244,7 +248,7 @@
         return numerator, denominator
 
     def __add__(self, other):
-        nums = Numbers()
+        nums = Numbers(precision=self._precision)
         nums.n_files = self.n_files + other.n_files
         nums.n_statements = self.n_statements + other.n_statements
         nums.n_excluded = self.n_excluded + other.n_excluded
@@ -260,9 +264,8 @@
 
     def __radd__(self, other):
         # Implementing 0+Numbers allows us to sum() a list of Numbers.
-        if other == 0:
-            return self
-        return NotImplemented       # pragma: not covered (we never call it this way)
+        assert other == 0   # we only ever call it this way.
+        return self
 
 
 def _line_ranges(statements, lines):
@@ -333,7 +336,7 @@
     """
     # We can never achieve higher than 100% coverage, or less than zero.
     if not (0 <= fail_under <= 100.0):
-        msg = "fail_under={} is invalid. Must be between 0 and 100.".format(fail_under)
+        msg = f"fail_under={fail_under} is invalid. Must be between 0 and 100."
         raise CoverageException(msg)
 
     # Special case for fail_under=100, it must really be 100.
--- a/eric7/DebugClients/Python/coverage/sqldata.py	Fri Nov 19 19:28:47 2021 +0100
+++ b/eric7/DebugClients/Python/coverage/sqldata.py	Sat Nov 20 16:47:38 2021 +0100
@@ -8,19 +8,20 @@
 
 import collections
 import datetime
+import functools
 import glob
 import itertools
 import os
 import re
 import sqlite3
 import sys
+import threading
 import zlib
 
-from coverage import env
-from coverage.backward import get_thread_id, iitems, to_bytes, to_string
 from coverage.debug import NoDebugging, SimpleReprMixin, clipped_repr
+from coverage.exceptions import CoverageException
 from coverage.files import PathAliases
-from coverage.misc import CoverageException, contract, file_be_gone, filename_suffix, isolate_module
+from coverage.misc import contract, file_be_gone, filename_suffix, isolate_module
 from coverage.numbits import numbits_to_nums, numbits_union, nums_to_numbits
 from coverage.version import __version__
 
@@ -179,6 +180,10 @@
     Data in a :class:`CoverageData` can be serialized and deserialized with
     :meth:`dumps` and :meth:`loads`.
 
+    The methods used during the coverage.py collection phase
+    (:meth:`add_lines`, :meth:`add_arcs`, :meth:`set_context`, and
+    :meth:`add_file_tracers`) are thread-safe.  Other methods may not be.
+
     """
 
     def __init__(self, basename=None, suffix=None, no_disk=False, warn=None, debug=None):
@@ -207,6 +212,8 @@
         # Maps thread ids to SqliteDb objects.
         self._dbs = {}
         self._pid = os.getpid()
+        # Synchronize the operations used during collection.
+        self._lock = threading.Lock()
 
         # Are we in sync with the data file?
         self._have_used = False
@@ -218,6 +225,15 @@
         self._current_context_id = None
         self._query_context_ids = None
 
+    def _locked(method):            # pylint: disable=no-self-argument
+        """A decorator for methods that should hold self._lock."""
+        @functools.wraps(method)
+        def _wrapped(self, *args, **kwargs):
+            with self._lock:
+                # pylint: disable=not-callable
+                return method(self, *args, **kwargs)
+        return _wrapped
+
     def _choose_filename(self):
         """Set self._filename based on inited attributes."""
         if self._no_disk:
@@ -243,31 +259,31 @@
 
         Initializes the schema and certain metadata.
         """
-        if self._debug.should('dataio'):
-            self._debug.write("Creating data file {!r}".format(self._filename))
-        self._dbs[get_thread_id()] = db = SqliteDb(self._filename, self._debug)
+        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')),
+                    ("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'):
-            self._debug.write("Opening data file {!r}".format(self._filename))
-        self._dbs[get_thread_id()] = SqliteDb(self._filename, self._debug)
+        if self._debug.should("dataio"):
+            self._debug.write(f"Opening data file {self._filename!r}")
+        self._dbs[threading.get_ident()] = SqliteDb(self._filename, self._debug)
         self._read_db()
 
     def _read_db(self):
         """Read the metadata from a database so that we are ready to use it."""
-        with self._dbs[get_thread_id()] as db:
+        with self._dbs[threading.get_ident()] as db:
             try:
                 schema_version, = db.execute_one("select version from coverage_schema")
             except Exception as exc:
@@ -275,7 +291,7 @@
                     "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(
@@ -293,15 +309,15 @@
 
     def _connect(self):
         """Get the SqliteDb object to use."""
-        if get_thread_id() not in self._dbs:
+        if threading.get_ident() not in self._dbs:
             if os.path.exists(self._filename):
                 self._open_db()
             else:
                 self._create_db()
-        return self._dbs[get_thread_id()]
+        return self._dbs[threading.get_ident()]
 
     def __nonzero__(self):
-        if (get_thread_id() not in self._dbs and not os.path.exists(self._filename)):
+        if (threading.get_ident() not in self._dbs and not os.path.exists(self._filename)):
             return False
         try:
             with self._connect() as con:
@@ -312,7 +328,7 @@
 
     __bool__ = __nonzero__
 
-    @contract(returns='bytes')
+    @contract(returns="bytes")
     def dumps(self):
         """Serialize the current data to a byte string.
 
@@ -320,38 +336,45 @@
         suitable for use with :meth:`loads` in the same version of
         coverage.py.
 
+        Note that this serialization is not what gets stored in coverage data
+        files.  This method is meant to produce bytes that can be transmitted
+        elsewhere and then deserialized with :meth:`loads`.
+
         Returns:
             A byte string of serialized data.
 
         .. versionadded:: 5.0
 
         """
-        if self._debug.should('dataio'):
-            self._debug.write("Dumping data from data file {!r}".format(self._filename))
+        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(to_bytes(con.dump()))
+            return b"z" + zlib.compress(con.dump().encode("utf-8"))
 
-    @contract(data='bytes')
+    @contract(data="bytes")
     def loads(self, data):
-        """Deserialize data from :meth:`dumps`
+        """Deserialize data from :meth:`dumps`.
 
         Use with a newly-created empty :class:`CoverageData` object.  It's
         undefined what happens if the object already has data in it.
 
+        Note that this is not for reading data from a coverage data file.  It
+        is only for use on data you produced with :meth:`dumps`.
+
         Arguments:
             data: A byte string of serialized data produced by :meth:`dumps`.
 
         .. versionadded:: 5.0
 
         """
-        if self._debug.should('dataio'):
-            self._debug.write("Loading data into data file {!r}".format(self._filename))
-        if data[:1] != b'z':
+        if self._debug.should("dataio"):
+            self._debug.write(f"Loading data into data file {self._filename!r}")
+        if data[:1] != b"z":
             raise CoverageException(
-                "Unrecognized serialization: {!r} (head of {} bytes)".format(data[:40], len(data))
+                f"Unrecognized serialization: {data[:40]!r} (head of {len(data)} bytes)"
                 )
-        script = to_string(zlib.decompress(data[1:]))
-        self._dbs[get_thread_id()] = db = SqliteDb(self._filename, self._debug)
+        script = zlib.decompress(data[1:]).decode("utf-8")
+        self._dbs[threading.get_ident()] = db = SqliteDb(self._filename, self._debug)
         with db:
             db.executescript(script)
         self._read_db()
@@ -381,6 +404,7 @@
             else:
                 return None
 
+    @_locked
     def set_context(self, context):
         """Set the current context for future :meth:`add_lines` etc.
 
@@ -390,8 +414,8 @@
         .. versionadded:: 5.0
 
         """
-        if self._debug.should('dataop'):
-            self._debug.write("Setting context: %r" % (context,))
+        if self._debug.should("dataop"):
+            self._debug.write(f"Setting context: {context!r}")
         self._current_context = context
         self._current_context_id = None
 
@@ -422,15 +446,16 @@
         """
         return self._filename
 
+    @_locked
     def add_lines(self, line_data):
         """Add measured line data.
 
-        `line_data` is a dictionary mapping file names to dictionaries::
+        `line_data` is a dictionary mapping file names to iterables of ints::
 
-            { filename: { lineno: None, ... }, ...}
+            { filename: { line1, line2, ... }, ...}
 
         """
-        if self._debug.should('dataop'):
+        if self._debug.should("dataop"):
             self._debug.write("Adding lines: %d files, %d lines total" % (
                 len(line_data), sum(len(lines) for lines in line_data.values())
             ))
@@ -440,7 +465,7 @@
             return
         with self._connect() as con:
             self._set_context_id()
-            for filename, linenos in iitems(line_data):
+            for filename, linenos in line_data.items():
                 linemap = nums_to_numbits(linenos)
                 file_id = self._file_id(filename, add=True)
                 query = "select numbits from line_bits where file_id = ? and context_id = ?"
@@ -449,20 +474,22 @@
                     linemap = numbits_union(linemap, existing[0][0])
 
                 con.execute(
-                    "insert or replace into line_bits "
+                    "insert or replace into line_bits " +
                     " (file_id, context_id, numbits) values (?, ?, ?)",
                     (file_id, self._current_context_id, linemap),
                 )
 
+    @_locked
     def add_arcs(self, arc_data):
         """Add measured arc data.
 
-        `arc_data` is a dictionary mapping file names to dictionaries::
+        `arc_data` is a dictionary mapping file names to iterables of pairs of
+        ints::
 
-            { filename: { (l1,l2): None, ... }, ...}
+            { filename: { (l1,l2), (l1,l2), ... }, ...}
 
         """
-        if self._debug.should('dataop'):
+        if self._debug.should("dataop"):
             self._debug.write("Adding arcs: %d files, %d arcs total" % (
                 len(arc_data), sum(len(arcs) for arcs in arc_data.values())
             ))
@@ -472,11 +499,11 @@
             return
         with self._connect() as con:
             self._set_context_id()
-            for filename, arcs in iitems(arc_data):
+            for filename, arcs in arc_data.items():
                 file_id = self._file_id(filename, add=True)
                 data = [(file_id, self._current_context_id, fromno, tono) for fromno, tono in arcs]
                 con.executemany(
-                    "insert or ignore into arc "
+                    "insert or ignore into arc " +
                     "(file_id, context_id, fromno, tono) values (?, ?, ?, ?)",
                     data,
                 )
@@ -495,33 +522,34 @@
             with self._connect() as con:
                 con.execute(
                     "insert into meta (key, value) values (?, ?)",
-                    ('has_arcs', str(int(arcs)))
+                    ("has_arcs", str(int(arcs)))
                 )
 
+    @_locked
     def add_file_tracers(self, file_tracers):
         """Add per-file plugin information.
 
         `file_tracers` is { filename: plugin_name, ... }
 
         """
-        if self._debug.should('dataop'):
+        if self._debug.should("dataop"):
             self._debug.write("Adding file tracers: %d files" % (len(file_tracers),))
         if not file_tracers:
             return
         self._start_using()
         with self._connect() as con:
-            for filename, plugin_name in iitems(file_tracers):
+            for filename, plugin_name in file_tracers.items():
                 file_id = self._file_id(filename)
                 if file_id is None:
                     raise CoverageException(
-                        "Can't add file tracer data for unmeasured file '%s'" % (filename,)
+                        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(
-                            "Conflicting file tracer name for '%s': %r vs %r" % (
+                            "Conflicting file tracer name for '{}': {!r} vs {!r}".format(
                                 filename, existing_plugin, plugin_name,
                             )
                         )
@@ -545,8 +573,8 @@
         `plugin_name` is the name of the plugin responsible for these files. It is used
         to associate the right filereporter, etc.
         """
-        if self._debug.should('dataop'):
-            self._debug.write("Touching %r" % (filenames,))
+        if self._debug.should("dataop"):
+            self._debug.write(f"Touching {filenames!r}")
         self._start_using()
         with self._connect(): # Use this to get one transaction.
             if not self._has_arcs and not self._has_lines:
@@ -564,9 +592,9 @@
         If `aliases` is provided, it's a `PathAliases` object that is used to
         re-map paths to match the local machine's.
         """
-        if self._debug.should('dataop'):
-            self._debug.write("Updating with data from %r" % (
-                getattr(other_data, '_filename', '???'),
+        if self._debug.should("dataop"):
+            self._debug.write("Updating with data from {!r}".format(
+                getattr(other_data, "_filename", "???"),
             ))
         if self._has_lines and other_data._has_arcs:
             raise CoverageException("Can't combine arc data with line data")
@@ -583,79 +611,76 @@
         other_data.read()
         with other_data._connect() as conn:
             # Get files data.
-            cur = conn.execute('select path from file')
+            cur = conn.execute("select path from file")
             files = {path: aliases.map(path) for (path,) in cur}
             cur.close()
 
             # Get contexts data.
-            cur = conn.execute('select context from context')
+            cur = conn.execute("select context from context")
             contexts = [context for (context,) in cur]
             cur.close()
 
             # Get arc data.
             cur = conn.execute(
-                'select file.path, context.context, arc.fromno, arc.tono '
-                'from arc '
-                'inner join file on file.id = arc.file_id '
-                'inner join context on context.id = arc.context_id'
+                "select file.path, context.context, arc.fromno, arc.tono " +
+                "from arc " +
+                "inner join file on file.id = arc.file_id " +
+                "inner join context on context.id = arc.context_id"
             )
             arcs = [(files[path], context, fromno, tono) for (path, context, fromno, tono) in cur]
             cur.close()
 
             # Get line data.
             cur = conn.execute(
-                'select file.path, context.context, line_bits.numbits '
-                'from line_bits '
-                'inner join file on file.id = line_bits.file_id '
-                'inner join context on context.id = line_bits.context_id'
+                "select file.path, context.context, line_bits.numbits " +
+                "from line_bits " +
+                "inner join file on file.id = line_bits.file_id " +
+                "inner join context on context.id = line_bits.context_id"
                 )
-            lines = {
-                (files[path], context): numbits
-                for (path, context, numbits) in cur
-                }
+            lines = {(files[path], context): numbits for (path, context, numbits) in cur}
             cur.close()
 
             # Get tracer data.
             cur = conn.execute(
-                'select file.path, tracer '
-                'from tracer '
-                'inner join file on file.id = tracer.file_id'
+                "select file.path, tracer " +
+                "from tracer " +
+                "inner join file on file.id = tracer.file_id"
             )
             tracers = {files[path]: tracer for (path, tracer) in cur}
             cur.close()
 
         with self._connect() as conn:
-            conn.con.isolation_level = 'IMMEDIATE'
+            conn.con.isolation_level = "IMMEDIATE"
 
             # Get all tracers in the DB. Files not in the tracers are assumed
             # to have an empty string tracer. Since Sqlite does not support
             # full outer joins, we have to make two queries to fill the
             # dictionary.
-            this_tracers = {path: '' for path, in conn.execute('select path from file')}
+            this_tracers = {path: "" for path, in conn.execute("select path from file")}
             this_tracers.update({
                 aliases.map(path): tracer
                 for path, tracer in conn.execute(
-                    'select file.path, tracer from tracer '
-                    'inner join file on file.id = tracer.file_id'
+                    "select file.path, tracer from tracer " +
+                    "inner join file on file.id = tracer.file_id"
                 )
             })
 
             # Create all file and context rows in the DB.
             conn.executemany(
-                'insert or ignore into file (path) values (?)',
+                "insert or ignore into file (path) values (?)",
                 ((file,) for file in files.values())
             )
             file_ids = {
                 path: id
-                for id, path in conn.execute('select id, path from file')
+                for id, path in conn.execute("select id, path from file")
             }
             conn.executemany(
-                'insert or ignore into context (context) values (?)',
+                "insert or ignore into context (context) values (?)",
                 ((context,) for context in contexts)
             )
             context_ids = {
                 context: id
-                for id, context in conn.execute('select id, context from context')
+                for id, context in conn.execute("select id, context from context")
             }
 
             # Prepare tracers and fail, if a conflict is found.
@@ -664,11 +689,11 @@
             tracer_map = {}
             for path in files.values():
                 this_tracer = this_tracers.get(path)
-                other_tracer = tracers.get(path, '')
+                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(
-                        "Conflicting file tracer name for '%s': %r vs %r" % (
+                        "Conflicting file tracer name for '{}': {!r} vs {!r}".format(
                             path, this_tracer, other_tracer
                         )
                     )
@@ -684,10 +709,10 @@
 
             # Get line data.
             cur = conn.execute(
-                'select file.path, context.context, line_bits.numbits '
-                'from line_bits '
-                'inner join file on file.id = line_bits.file_id '
-                'inner join context on context.id = line_bits.context_id'
+                "select file.path, context.context, line_bits.numbits " +
+                "from line_bits " +
+                "inner join file on file.id = line_bits.file_id " +
+                "inner join context on context.id = line_bits.context_id"
                 )
             for path, context, numbits in cur:
                 key = (aliases.map(path), context)
@@ -701,8 +726,8 @@
 
                 # Write the combined data.
                 conn.executemany(
-                    'insert or ignore into arc '
-                    '(file_id, context_id, fromno, tono) values (?, ?, ?, ?)',
+                    "insert or ignore into arc " +
+                    "(file_id, context_id, fromno, tono) values (?, ?, ?, ?)",
                     arc_rows
                 )
 
@@ -710,7 +735,7 @@
                 self._choose_lines_or_arcs(lines=True)
                 conn.execute("delete from line_bits")
                 conn.executemany(
-                    "insert into line_bits "
+                    "insert into line_bits " +
                     "(file_id, context_id, numbits) values (?, ?, ?)",
                     [
                         (file_ids[file], context_ids[context], numbits)
@@ -718,7 +743,7 @@
                     ]
                 )
             conn.executemany(
-                'insert or ignore into tracer (file_id, tracer) values (?, ?)',
+                "insert or ignore into tracer (file_id, tracer) values (?, ?)",
                 ((file_ids[filename], tracer) for filename, tracer in tracer_map.items())
             )
 
@@ -736,16 +761,16 @@
         self._reset()
         if self._no_disk:
             return
-        if self._debug.should('dataio'):
-            self._debug.write("Erasing data file {!r}".format(self._filename))
+        if self._debug.should("dataio"):
+            self._debug.write(f"Erasing data file {self._filename!r}")
         file_be_gone(self._filename)
         if parallel:
             data_dir, local = os.path.split(self._filename)
-            localdot = local + '.*'
+            localdot = local + ".*"
             pattern = os.path.join(os.path.abspath(data_dir), localdot)
             for filename in glob.glob(pattern):
-                if self._debug.should('dataio'):
-                    self._debug.write("Erasing parallel data file {!r}".format(filename))
+                if self._debug.should("dataio"):
+                    self._debug.write(f"Erasing parallel data file {filename!r}")
                 file_be_gone(filename)
 
     def read(self):
@@ -836,14 +861,14 @@
         self._start_using()
         if contexts:
             with self._connect() as con:
-                context_clause = ' or '.join(['context regexp ?'] * len(contexts))
+                context_clause = " or ".join(["context regexp ?"] * len(contexts))
                 cur = con.execute("select id from context where " + context_clause, contexts)
                 self._query_context_ids = [row[0] for row in cur.fetchall()]
         else:
             self._query_context_ids = None
 
     def lines(self, filename):
-        """Get the list of lines executed for a file.
+        """Get the list of lines executed for a source file.
 
         If the file was not measured, returns None.  A file might be measured,
         and have no lines executed, in which case an empty list is returned.
@@ -867,7 +892,7 @@
                 query = "select numbits from line_bits where file_id = ?"
                 data = [file_id]
                 if self._query_context_ids is not None:
-                    ids_array = ', '.join('?' * len(self._query_context_ids))
+                    ids_array = ", ".join("?" * len(self._query_context_ids))
                     query += " and context_id in (" + ids_array + ")"
                     data += self._query_context_ids
                 bitmaps = list(con.execute(query, data))
@@ -902,7 +927,7 @@
                 query = "select distinct fromno, tono from arc where file_id = ?"
                 data = [file_id]
                 if self._query_context_ids is not None:
-                    ids_array = ', '.join('?' * len(self._query_context_ids))
+                    ids_array = ", ".join("?" * len(self._query_context_ids))
                     query += " and context_id in (" + ids_array + ")"
                     data += self._query_context_ids
                 arcs = con.execute(query, data)
@@ -917,43 +942,45 @@
         .. versionadded:: 5.0
 
         """
-        lineno_contexts_map = collections.defaultdict(list)
         self._start_using()
         with self._connect() as con:
             file_id = self._file_id(filename)
             if file_id is None:
-                return lineno_contexts_map
+                return {}
+
+            lineno_contexts_map = collections.defaultdict(set)
             if self.has_arcs():
                 query = (
-                    "select arc.fromno, arc.tono, context.context "
-                    "from arc, context "
+                    "select arc.fromno, arc.tono, context.context " +
+                    "from arc, context " +
                     "where arc.file_id = ? and arc.context_id = context.id"
                 )
                 data = [file_id]
                 if self._query_context_ids is not None:
-                    ids_array = ', '.join('?' * len(self._query_context_ids))
+                    ids_array = ", ".join("?" * len(self._query_context_ids))
                     query += " and arc.context_id in (" + ids_array + ")"
                     data += self._query_context_ids
                 for fromno, tono, context in con.execute(query, data):
-                    if context not in lineno_contexts_map[fromno]:
-                        lineno_contexts_map[fromno].append(context)
-                    if context not in lineno_contexts_map[tono]:
-                        lineno_contexts_map[tono].append(context)
+                    if fromno > 0:
+                        lineno_contexts_map[fromno].add(context)
+                    if tono > 0:
+                        lineno_contexts_map[tono].add(context)
             else:
                 query = (
-                    "select l.numbits, c.context from line_bits l, context c "
-                    "where l.context_id = c.id "
+                    "select l.numbits, c.context from line_bits l, context c " +
+                    "where l.context_id = c.id " +
                     "and file_id = ?"
                     )
                 data = [file_id]
                 if self._query_context_ids is not None:
-                    ids_array = ', '.join('?' * len(self._query_context_ids))
+                    ids_array = ", ".join("?" * len(self._query_context_ids))
                     query += " and l.context_id in (" + ids_array + ")"
                     data += self._query_context_ids
                 for numbits, context in con.execute(query, data):
                     for lineno in numbits_to_nums(numbits):
-                        lineno_contexts_map[lineno].append(context)
-        return lineno_contexts_map
+                        lineno_contexts_map[lineno].add(context)
+
+        return {lineno: list(contexts) for lineno, contexts in lineno_contexts_map.items()}
 
     @classmethod
     def sys_info(cls):
@@ -964,13 +991,16 @@
         """
         with SqliteDb(":memory:", debug=NoDebugging()) as db:
             temp_store = [row[0] for row in db.execute("pragma temp_store")]
-            compile_options = [row[0] for row in db.execute("pragma compile_options")]
+            copts = [row[0] for row in db.execute("pragma compile_options")]
+            # Yes, this is overkill. I don't like the long list of options
+            # at the end of "debug sys", but I don't want to omit information.
+            copts = ["; ".join(copts[i:i + 3]) for i in range(0, len(copts), 3)]
 
         return [
-            ('sqlite3_version', sqlite3.version),
-            ('sqlite3_sqlite_version', sqlite3.sqlite_version),
-            ('sqlite3_temp_store', temp_store),
-            ('sqlite3_compile_options', compile_options),
+            ("sqlite3_version", sqlite3.version),
+            ("sqlite3_sqlite_version", sqlite3.sqlite_version),
+            ("sqlite3_temp_store", temp_store),
+            ("sqlite3_compile_options", copts),
         ]
 
 
@@ -985,7 +1015,7 @@
 
     """
     def __init__(self, filename, debug):
-        self.debug = debug if debug.should('sql') else None
+        self.debug = debug if debug.should("sql") else None
         self.filename = filename
         self.nest = 0
         self.con = None
@@ -995,29 +1025,19 @@
         if self.con is not None:
             return
 
-        # SQLite on Windows on py2 won't open a file if the filename argument
-        # has non-ascii characters in it.  Opening a relative file name avoids
-        # a problem if the current directory has non-ascii.
-        filename = self.filename
-        if env.WINDOWS and env.PY2:
-            try:
-                filename = os.path.relpath(self.filename)
-            except ValueError:
-                # ValueError can be raised under Windows when os.getcwd() returns a
-                # folder from a different drive than the drive of self.filename in
-                # which case we keep the original value of self.filename unchanged,
-                # hoping that we won't face the non-ascii directory problem.
-                pass
-
         # It can happen that Python switches threads while the tracer writes
         # data. The second thread will also try to write to the data,
         # effectively causing a nested context. However, given the idempotent
         # nature of the tracer operations, sharing a connection among threads
         # is not a problem.
         if self.debug:
-            self.debug.write("Connecting to {!r}".format(self.filename))
-        self.con = sqlite3.connect(filename, check_same_thread=False)
-        self.con.create_function('REGEXP', 2, _regexp)
+            self.debug.write(f"Connecting to {self.filename!r}")
+        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
+
+        self.con.create_function("REGEXP", 2, _regexp)
 
         # This pragma makes writing faster. It disables rollbacks, but we never need them.
         # PyPy needs the .close() calls here, or sqlite gets twisted up:
@@ -1047,14 +1067,14 @@
                 self.close()
             except Exception as exc:
                 if self.debug:
-                    self.debug.write("EXCEPTION from __exit__: {}".format(exc))
-                raise
+                    self.debug.write(f"EXCEPTION from __exit__: {exc}")
+                raise CoverageException(f"Couldn't end data file {self.filename!r}: {exc}") from exc
 
     def execute(self, sql, parameters=()):
         """Same as :meth:`python:sqlite3.Connection.execute`."""
         if self.debug:
-            tail = " with {!r}".format(parameters) if parameters else ""
-            self.debug.write("Executing {!r}{}".format(sql, tail))
+            tail = f" with {parameters!r}" if parameters else ""
+            self.debug.write(f"Executing {sql!r}{tail}")
         try:
             try:
                 return self.con.execute(sql, parameters)
@@ -1072,14 +1092,14 @@
                     cov4_sig = b"!coverage.py: This is a private format"
                     if bad_file.read(len(cov4_sig)) == cov4_sig:
                         msg = (
-                            "Looks like a coverage 4.x data file. "
+                            "Looks like a coverage 4.x data file. " +
                             "Are you mixing versions of coverage?"
                         )
-            except Exception:
+            except Exception:   # pragma: cant happen
                 pass
             if self.debug:
-                self.debug.write("EXCEPTION from execute: {}".format(msg))
-            raise CoverageException("Couldn't use data file {!r}: {}".format(self.filename, msg))
+                self.debug.write(f"EXCEPTION from execute: {msg}")
+            raise CoverageException(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.
@@ -1096,14 +1116,20 @@
         elif len(rows) == 1:
             return rows[0]
         else:
-            raise CoverageException("Sql {!r} shouldn't return {} rows".format(sql, len(rows)))
+            raise AssertionError(f"SQL {sql!r} shouldn't return {len(rows)} rows")
 
     def executemany(self, sql, data):
         """Same as :meth:`python:sqlite3.Connection.executemany`."""
         if self.debug:
             data = list(data)
-            self.debug.write("Executing many {!r} with {} rows".format(sql, len(data)))
-        return self.con.executemany(sql, data)
+            self.debug.write(f"Executing many {sql!r} with {len(data)} rows")
+        try:
+            return self.con.executemany(sql, data)
+        except Exception:   # pragma: cant happen
+            # In some cases, an error might happen that isn't really an
+            # error.  Try again immediately.
+            # https://github.com/nedbat/coveragepy/issues/1010
+            return self.con.executemany(sql, data)
 
     def executescript(self, script):
         """Same as :meth:`python:sqlite3.Connection.executescript`."""
--- a/eric7/DebugClients/Python/coverage/summary.py	Fri Nov 19 19:28:47 2021 +0100
+++ b/eric7/DebugClients/Python/coverage/summary.py	Sat Nov 20 16:47:38 2021 +0100
@@ -5,13 +5,13 @@
 
 import sys
 
-from coverage import env
+from coverage.exceptions import CoverageException
+from coverage.misc import human_sorted_items
 from coverage.report import get_analysis_to_report
 from coverage.results import Numbers
-from coverage.misc import CoverageException, output_encoding
 
 
-class SummaryReporter(object):
+class SummaryReporter:
     """A reporter for writing the summary report."""
 
     def __init__(self, coverage):
@@ -22,13 +22,11 @@
         self.fr_analysis = []
         self.skipped_count = 0
         self.empty_count = 0
-        self.total = Numbers()
-        self.fmt_err = u"%s   %s: %s"
+        self.total = Numbers(precision=self.config.precision)
+        self.fmt_err = "%s   %s: %s"
 
     def writeout(self, line):
         """Write a line to the output, adding a newline."""
-        if env.PY2:
-            line = line.encode(output_encoding())
         self.outfile.write(line.rstrip())
         self.outfile.write("\n")
 
@@ -47,22 +45,22 @@
 
         # Prepare the formatting strings, header, and column sorting.
         max_name = max([len(fr.relative_filename()) for (fr, analysis) in self.fr_analysis] + [5])
-        fmt_name = u"%%- %ds  " % max_name
-        fmt_skip_covered = u"\n%s file%s skipped due to complete coverage."
-        fmt_skip_empty = u"\n%s empty file%s skipped."
+        fmt_name = "%%- %ds  " % max_name
+        fmt_skip_covered = "\n%s file%s skipped due to complete coverage."
+        fmt_skip_empty = "\n%s empty file%s skipped."
 
-        header = (fmt_name % "Name") + u" Stmts   Miss"
-        fmt_coverage = fmt_name + u"%6d %6d"
+        header = (fmt_name % "Name") + " Stmts   Miss"
+        fmt_coverage = fmt_name + "%6d %6d"
         if self.branches:
-            header += u" Branch BrPart"
-            fmt_coverage += u" %6d %6d"
-        width100 = Numbers.pc_str_width()
-        header += u"%*s" % (width100+4, "Cover")
-        fmt_coverage += u"%%%ds%%%%" % (width100+3,)
+            header += " Branch BrPart"
+            fmt_coverage += " %6d %6d"
+        width100 = Numbers(precision=self.config.precision).pc_str_width()
+        header += "%*s" % (width100+4, "Cover")
+        fmt_coverage += "%%%ds%%%%" % (width100+3,)
         if self.config.show_missing:
-            header += u"   Missing"
-            fmt_coverage += u"   %s"
-        rule = u"-" * len(header)
+            header += "   Missing"
+            fmt_coverage += "   %s"
+        rule = "-" * len(header)
 
         column_order = dict(name=0, stmts=1, miss=2, cover=-1)
         if self.branches:
@@ -92,18 +90,20 @@
             lines.append((text, args))
 
         # Sort the lines and write them out.
-        if getattr(self.config, 'sort', None):
-            sort_option = self.config.sort.lower()
-            reverse = False
-            if sort_option[0] == '-':
-                reverse = True
-                sort_option = sort_option[1:]
-            elif sort_option[0] == '+':
-                sort_option = sort_option[1:]
+        sort_option = (self.config.sort or "name").lower()
+        reverse = False
+        if sort_option[0] == '-':
+            reverse = True
+            sort_option = sort_option[1:]
+        elif sort_option[0] == '+':
+            sort_option = sort_option[1:]
 
+        if sort_option == "name":
+            lines = human_sorted_items(lines, reverse=reverse)
+        else:
             position = column_order.get(sort_option)
             if position is None:
-                raise CoverageException("Invalid sorting option: {!r}".format(self.config.sort))
+                raise CoverageException(f"Invalid sorting option: {self.config.sort!r}")
             lines.sort(key=lambda l: (l[1][position], l[0]), reverse=reverse)
 
         for line in lines:
--- a/eric7/DebugClients/Python/coverage/templite.py	Fri Nov 19 19:28:47 2021 +0100
+++ b/eric7/DebugClients/Python/coverage/templite.py	Sat Nov 20 16:47:38 2021 +0100
@@ -12,8 +12,6 @@
 
 import re
 
-from coverage import env
-
 
 class TempliteSyntaxError(ValueError):
     """Raised when a template has a syntax error."""
@@ -25,7 +23,7 @@
     pass
 
 
-class CodeBuilder(object):
+class CodeBuilder:
     """Build source code conveniently."""
 
     def __init__(self, indent=0):
@@ -71,7 +69,7 @@
         return global_namespace
 
 
-class Templite(object):
+class Templite:
     """A simple template renderer, for a nano-subset of Django syntax.
 
     Supported constructs are extended variable access::
@@ -137,10 +135,7 @@
         code.add_line("result = []")
         code.add_line("append_result = result.append")
         code.add_line("extend_result = result.extend")
-        if env.PY2:
-            code.add_line("to_str = unicode")
-        else:
-            code.add_line("to_str = str")
+        code.add_line("to_str = str")
 
         buffered = []
 
@@ -193,7 +188,7 @@
                         ops_stack.append('for')
                         self._variable(words[1], self.loop_vars)
                         code.add_line(
-                            "for c_%s in %s:" % (
+                            "for c_{} in {}:".format(
                                 words[1],
                                 self._expr_code(words[3])
                             )
@@ -233,7 +228,7 @@
         flush_output()
 
         for var_name in self.all_vars - self.loop_vars:
-            vars_code.add_line("c_%s = context[%r]" % (var_name, var_name))
+            vars_code.add_line(f"c_{var_name} = context[{var_name!r}]")
 
         code.add_line('return "".join(result)')
         code.dedent()
@@ -246,12 +241,12 @@
             code = self._expr_code(pipes[0])
             for func in pipes[1:]:
                 self._variable(func, self.all_vars)
-                code = "c_%s(%s)" % (func, code)
+                code = f"c_{func}({code})"
         elif "." in expr:
             dots = expr.split(".")
             code = self._expr_code(dots[0])
             args = ", ".join(repr(d) for d in dots[1:])
-            code = "do_dots(%s, %s)" % (code, args)
+            code = f"do_dots({code}, {args})"
         else:
             self._variable(expr, self.all_vars)
             code = "c_%s" % expr
@@ -259,7 +254,7 @@
 
     def _syntax_error(self, msg, thing):
         """Raise a syntax error using `msg`, and showing `thing`."""
-        raise TempliteSyntaxError("%s: %r" % (msg, thing))
+        raise TempliteSyntaxError(f"{msg}: {thing!r}")
 
     def _variable(self, name, vars_set):
         """Track that `name` is used as a variable.
@@ -293,10 +288,10 @@
             except AttributeError:
                 try:
                     value = value[dot]
-                except (TypeError, KeyError):
+                except (TypeError, KeyError) as exc:
                     raise TempliteValueError(
-                        "Couldn't evaluate %r.%s" % (value, dot)
-                    )
+                        f"Couldn't evaluate {value!r}.{dot}"
+                    ) from exc
             if callable(value):
                 value = value()
         return value
--- a/eric7/DebugClients/Python/coverage/tomlconfig.py	Fri Nov 19 19:28:47 2021 +0100
+++ b/eric7/DebugClients/Python/coverage/tomlconfig.py	Sat Nov 20 16:47:38 2021 +0100
@@ -3,19 +3,19 @@
 
 """TOML configuration support for coverage.py"""
 
-import io
+import configparser
 import os
 import re
 
-from coverage import env
-from coverage.backward import configparser, path_types
-from coverage.misc import CoverageException, substitute_variables
+from coverage.exceptions import CoverageException
+from coverage.misc import import_third_party, substitute_variables
 
-# TOML support is an install-time extra option.
-try:
-    import toml
-except ImportError:         # pragma: not covered
-    toml = None
+# TOML support is an install-time extra option. (Import typing is here because
+# import_third_party will unload any module that wasn't already imported.
+# tomli imports typing, and if we unload it, later it's imported again, and on
+# Python 3.6, this causes infinite recursion.)
+import typing   # pylint: disable=unused-import, wrong-import-order
+tomli = import_third_party("tomli")
 
 
 class TomlDecodeError(Exception):
@@ -37,22 +37,20 @@
     def read(self, filenames):
         # RawConfigParser takes a filename or list of filenames, but we only
         # ever call this with a single filename.
-        assert isinstance(filenames, path_types)
-        filename = filenames
-        if env.PYVERSION >= (3, 6):
-            filename = os.fspath(filename)
+        assert isinstance(filenames, (bytes, str, os.PathLike))
+        filename = os.fspath(filenames)
 
         try:
-            with io.open(filename, encoding='utf-8') as fp:
+            with open(filename, encoding='utf-8') as fp:
                 toml_text = fp.read()
-        except IOError:
+        except OSError:
             return []
-        if toml:
+        if tomli is not None:
             toml_text = substitute_variables(toml_text, os.environ)
             try:
-                self.data = toml.loads(toml_text)
-            except toml.TomlDecodeError as err:
-                raise TomlDecodeError(*err.args)
+                self.data = tomli.loads(toml_text)
+            except tomli.TOMLDecodeError as err:
+                raise TomlDecodeError(str(err)) from err
             return [filename]
         else:
             has_toml = re.search(r"^\[tool\.coverage\.", toml_text, flags=re.MULTILINE)
@@ -98,8 +96,8 @@
             raise configparser.NoSectionError(section)
         try:
             return name, data[option]
-        except KeyError:
-            raise configparser.NoOptionError(option, name)
+        except KeyError as exc:
+            raise configparser.NoOptionError(option, name) from exc
 
     def has_option(self, section, option):
         _, data = self._get_section(section)
@@ -150,9 +148,7 @@
             try:
                 re.compile(value)
             except re.error as e:
-                raise CoverageException(
-                    "Invalid [%s].%s value %r: %s" % (name, option, value, e)
-                )
+                raise CoverageException(f"Invalid [{name}].{option} value {value!r}: {e}") from e
         return values
 
     def getint(self, section, option):
--- a/eric7/DebugClients/Python/coverage/version.py	Fri Nov 19 19:28:47 2021 +0100
+++ b/eric7/DebugClients/Python/coverage/version.py	Sat Nov 20 16:47:38 2021 +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 = (5, 5, 0, "final", 0)
+version_info = (6, 1, 2, "final", 0)
 
 
 def _make_version(major, minor, micro, releaselevel, serial):
@@ -25,7 +25,7 @@
     url = "https://coverage.readthedocs.io"
     if releaselevel != 'final':
         # For pre-releases, use a version-specific URL.
-        url += "/en/coverage-" + _make_version(major, minor, micro, releaselevel, serial)
+        url += "/en/" + _make_version(major, minor, micro, releaselevel, serial)
     return url
 
 
--- a/eric7/DebugClients/Python/coverage/xmlreport.py	Fri Nov 19 19:28:47 2021 +0100
+++ b/eric7/DebugClients/Python/coverage/xmlreport.py	Sat Nov 20 16:47:38 2021 +0100
@@ -1,4 +1,3 @@
-# coding: utf-8
 # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
 # For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt
 
@@ -10,10 +9,8 @@
 import time
 import xml.dom.minidom
 
-from coverage import env
 from coverage import __url__, __version__, files
-from coverage.backward import iitems
-from coverage.misc import isolate_module
+from coverage.misc import isolate_module, human_sorted, human_sorted_items
 from coverage.report import get_analysis_to_report
 
 os = isolate_module(os)
@@ -30,9 +27,11 @@
         return "%.4g" % (float(hit) / num)
 
 
-class XmlReporter(object):
+class XmlReporter:
     """A reporter for writing Cobertura-style XML coverage results."""
 
+    report_type = "XML report"
+
     def __init__(self, coverage):
         self.coverage = coverage
         self.config = self.coverage.config
@@ -80,7 +79,7 @@
         xcoverage.appendChild(xsources)
 
         # Populate the XML DOM with the source info.
-        for path in sorted(self.source_paths):
+        for path in human_sorted(self.source_paths):
             xsource = self.xml_out.createElement("source")
             xsources.appendChild(xsource)
             txt = self.xml_out.createTextNode(path)
@@ -93,13 +92,13 @@
         xcoverage.appendChild(xpackages)
 
         # Populate the XML DOM with the package info.
-        for pkg_name, pkg_data in sorted(iitems(self.packages)):
+        for pkg_name, pkg_data in human_sorted_items(self.packages.items()):
             class_elts, lhits, lnum, bhits, bnum = pkg_data
             xpackage = self.xml_out.createElement("package")
             xpackages.appendChild(xpackage)
             xclasses = self.xml_out.createElement("classes")
             xpackage.appendChild(xclasses)
-            for _, class_elt in sorted(iitems(class_elts)):
+            for _, class_elt in human_sorted_items(class_elts.items()):
                 xclasses.appendChild(class_elt)
             xpackage.setAttribute("name", pkg_name.replace(os.sep, '.'))
             xpackage.setAttribute("line-rate", rate(lhits, lnum))
@@ -158,7 +157,7 @@
             rel_name = fr.relative_filename()
             self.source_paths.add(fr.filename[:-len(rel_name)].rstrip(r"\/"))
 
-        dirname = os.path.dirname(rel_name) or u"."
+        dirname = os.path.dirname(rel_name) or "."
         dirname = "/".join(dirname.split("/")[:self.config.xml_package_depth])
         package_name = dirname.replace("/", ".")
 
@@ -228,7 +227,4 @@
 
 def serialize_xml(dom):
     """Serialize a minidom node to XML."""
-    out = dom.toprettyxml()
-    if env.PY2:
-        out = out.encode("utf8")
-    return out
+    return dom.toprettyxml()

eric ide

mercurial