Sat, 20 Nov 2021 16:47:38 +0100
Upgraded coverage to 6.1.2.
--- 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 ' ' 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", ".join( - u"{} ↛ {}".format(ldata.number, d) + ldata.annotate = ", ".join( + f"{ldata.number} ↛ {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(); + } +});
--- /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> 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>
--- /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 %} + toggle line displays + </p> + <p class="keyhelp"> + <kbd>j</kbd> + <kbd>k</kbd> next/prev highlighted chunk + </p> + <p class="keyhelp"> + <kbd>0</kbd> (zero) top of page + </p> + <p class="keyhelp"> + <kbd>1</kbd> (one) first highlighted chunk + </p> + </div> + </div> + </div> + + <h2> + <span class="text">{{nums.n_statements}} statements </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}} </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">« index</a> <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()