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