Tue, 24 May 2022 11:00:52 +0200
Third Party packages
- upgraded coverage to 6.4.0
--- a/docs/changelog Tue May 24 10:22:46 2022 +0200 +++ b/docs/changelog Tue May 24 11:00:52 2022 +0200 @@ -21,6 +21,7 @@ function - Third Party packages -- upgraded pip-licenses to version 3.5.4 + -- upgraded coverage to 6.4.0 Version 22.5: - bug fixes
--- a/eric7/DebugClients/Python/coverage/cmdline.py Tue May 24 10:22:46 2022 +0200 +++ b/eric7/DebugClients/Python/coverage/cmdline.py Tue May 24 11:00:52 2022 +0200 @@ -232,9 +232,7 @@ """ def __init__(self, *args, **kwargs): - super().__init__( - add_help_option=False, *args, **kwargs - ) + super().__init__(add_help_option=False, *args, **kwargs) self.set_defaults( # Keep these arguments alphabetized by their names. action=None, @@ -267,7 +265,7 @@ timid=None, title=None, version=None, - ) + ) self.disable_interspersed_args() @@ -352,7 +350,7 @@ Opts.debug, Opts.help, Opts.rcfile, - ] +] COMMANDS = { 'annotate': CmdOptionParser( @@ -473,7 +471,7 @@ Opts.output_lcov, Opts.omit, Opts.quiet, - ] + GLOBAL_ARGS, + ] + GLOBAL_ARGS, usage="[options] [modules]", description="Generate an LCOV report of coverage results.", ), @@ -648,7 +646,7 @@ check_preimported=True, context=options.context, messages=not options.quiet, - ) + ) if options.action == "debug": return self.do_debug(args) @@ -675,7 +673,7 @@ omit=omit, include=include, contexts=contexts, - ) + ) # We need to be able to import from the current directory, because # plugins may try to, for example, to read Django settings. @@ -692,7 +690,7 @@ skip_empty=options.skip_empty, sort=options.sort, **report_args - ) + ) elif options.action == "annotate": self.coverage.annotate(directory=options.directory, **report_args) elif options.action == "html": @@ -704,25 +702,25 @@ show_contexts=options.show_contexts, title=options.title, **report_args - ) + ) elif options.action == "xml": total = self.coverage.xml_report( outfile=options.outfile, skip_empty=options.skip_empty, **report_args - ) + ) elif options.action == "json": total = self.coverage.json_report( 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
--- a/eric7/DebugClients/Python/coverage/collector.py Tue May 24 10:22:46 2022 +0200 +++ b/eric7/DebugClients/Python/coverage/collector.py Tue May 24 11:00:52 2022 +0200 @@ -145,6 +145,7 @@ raise ConfigError(f"Conflicting concurrency settings: {show}") do_threading = False + tried = "nothing" # to satisfy pylint try: if "greenlet" in concurrencies: tried = "greenlet" @@ -327,8 +328,7 @@ self._collectors.append(self) # Replay all the events from fullcoverage into the new trace function. - for args in traces0: - (frame, event, arg), lineno = args + for (frame, event, arg), lineno in traces0: try: fn(frame, event, arg, lineno=lineno) except TypeError as ex:
--- a/eric7/DebugClients/Python/coverage/config.py Tue May 24 10:22:46 2022 +0200 +++ b/eric7/DebugClients/Python/coverage/config.py Tue May 24 11:00:52 2022 +0200 @@ -190,6 +190,7 @@ self.relative_files = False self.run_include = None self.run_omit = None + self.sigterm = False self.source = None self.source_pkgs = [] self.timid = False @@ -364,6 +365,7 @@ ('relative_files', 'run:relative_files', 'boolean'), ('run_include', 'run:include', 'list'), ('run_omit', 'run:omit', 'list'), + ('sigterm', 'run:sigterm', 'boolean'), ('source', 'run:source', 'list'), ('source_pkgs', 'run:source_pkgs', 'list'), ('timid', 'run:timid', 'boolean'), @@ -499,7 +501,7 @@ """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):
--- a/eric7/DebugClients/Python/coverage/control.py Tue May 24 10:22:46 2022 +0200 +++ b/eric7/DebugClients/Python/coverage/control.py Tue May 24 11:00:52 2022 +0200 @@ -250,7 +250,7 @@ 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 @@ -489,7 +489,7 @@ branch=self.config.branch, warn=self._warn, concurrency=concurrency, - ) + ) suffix = self._data_suffix_specified if suffix: @@ -515,10 +515,10 @@ ", ".join( plugin._coverage_plugin_name for plugin in self._plugins.file_tracers - ), + ), self._collector.tracer_name(), - ) ) + ) for plugin in self._plugins.file_tracers: plugin._coverage_enabled = False @@ -536,12 +536,13 @@ # 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) + if self.config.sigterm: + 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.""" @@ -835,7 +836,7 @@ sorted(analysis.excluded), sorted(analysis.missing), analysis.missing_formatted(), - ) + ) def _analyze(self, it): """Analyze a single morf or code unit. @@ -1146,7 +1147,7 @@ ) )), ('command_line', " ".join(getattr(sys, 'argv', ['-none-']))), - ] + ] if self._inorout: info.extend(self._inorout.sys_info())
--- a/eric7/DebugClients/Python/coverage/debug.py Tue May 24 10:22:46 2022 +0200 +++ b/eric7/DebugClients/Python/coverage/debug.py Tue May 24 11:00:52 2022 +0200 @@ -211,7 +211,7 @@ klass=self.__class__.__name__, id=id(self), attrs=" ".join(f"{k}={v!r}" for k, v in show_attrs), - ) + ) def simplify(v): # pragma: debugging
--- a/eric7/DebugClients/Python/coverage/doc/CHANGES.rst Tue May 24 10:22:46 2022 +0200 +++ b/eric7/DebugClients/Python/coverage/doc/CHANGES.rst Tue May 24 11:00:52 2022 +0200 @@ -17,6 +17,59 @@ .. Version 9.8.1 — 2027-07-27 .. -------------------------- +.. _changes_64: + +Version 6.4 — 2022-05-22 +------------------------ + +- A new setting, :ref:`config_run_sigterm`, controls whether a SIGTERM signal + handler is used. In 6.3, the signal handler was always installed, to capture + data at unusual process ends. Unfortunately, this introduced other problems + (see `issue 1310`_). Now the signal handler is only used if you opt-in by + setting ``[run] sigterm = true``. + +- Small changes to the HTML report: + + - Added links to next and previous file, and more keyboard shortcuts: ``[`` + and ``]`` for next file and previous file; ``u`` for up to the index; and + ``?`` to open/close the help panel. Thanks, `J. M. F. Tsang + <pull 1364_>`_. + + - The timestamp and version are displayed at the top of the report. Thanks, + `Ammar Askar <pull 1354_>`_. Closes `issue 1351`_. + +- A new debug option ``debug=sqldata`` adds more detail to ``debug=sql``, + logging all the data being written to the database. + +- Previously, running ``coverage report`` (or any of the reporting commands) in + an empty directory would create a .coverage data file. Now they do not, + fixing `issue 1328`_. + +- On Python 3.11, the ``[toml]`` extra no longer installs tomli, instead using + tomllib from the standard library. Thanks `Shantanu <pull 1359_>`_. + +- In-memory CoverageData objects now properly update(), closing `issue 1323`_. + +.. _issue 1310: https://github.com/nedbat/coveragepy/issues/1310 +.. _issue 1323: https://github.com/nedbat/coveragepy/issues/1323 +.. _issue 1328: https://github.com/nedbat/coveragepy/issues/1328 +.. _issue 1351: https://github.com/nedbat/coveragepy/issues/1351 +.. _pull 1354: https://github.com/nedbat/coveragepy/pull/1354 +.. _pull 1359: https://github.com/nedbat/coveragepy/pull/1359 +.. _pull 1364: https://github.com/nedbat/coveragepy/pull/1364 + + +.. _changes_633: + +Version 6.3.3 — 2022-05-12 +-------------------------- + +- Fix: Coverage.py now builds successfully on CPython 3.11 (3.11.0b1) again. + Closes `issue 1367`_. Some results for generators may have changed. + +.. _issue 1367: https://github.com/nedbat/coveragepy/issues/1367 + + .. _changes_632: Version 6.3.2 — 2022-02-20
--- a/eric7/DebugClients/Python/coverage/doc/CONTRIBUTORS.txt Tue May 24 10:22:46 2022 +0200 +++ b/eric7/DebugClients/Python/coverage/doc/CONTRIBUTORS.txt Tue May 24 11:00:52 2022 +0200 @@ -14,6 +14,7 @@ Alex Sandro Alexander Todorov Alexander Walters +Ammar Askar Andrew Hoos Anthony Sottile Arcadiy Ivanov @@ -75,6 +76,7 @@ Ilia Meerovich Imri Goldberg Ionel Cristian Mărieș +J. M. F. Tsang JT Olds Jerin Peter George Jessamyn Smith @@ -137,6 +139,7 @@ Stefan Behnel Stephan Richter Stephen Finucane +Steve Dower Steve Leonard Steve Peak S. Y. Lee
--- a/eric7/DebugClients/Python/coverage/doc/README.rst Tue May 24 10:22:46 2022 +0200 +++ b/eric7/DebugClients/Python/coverage/doc/README.rst Tue May 24 11:00:52 2022 +0200 @@ -7,11 +7,17 @@ Code coverage testing for Python. +.. image:: https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/banner2-direct.svg + :target: https://vshymanskyy.github.io/StandWithUkraine + :alt: Stand with Ukraine + +------------- + | |license| |versions| |status| | |test-status| |quality-status| |docs| |metacov| | |kit| |downloads| |format| |repos| | |stars| |forks| |contributors| -| |tidelift| |twitter-coveragepy| |twitter-nedbat| +| |tidelift| |sponsor| |twitter-coveragepy| |twitter-nedbat| Coverage.py measures code coverage, typically during test execution. It uses the code analysis tools and tracing hooks provided in the Python standard @@ -21,7 +27,7 @@ .. PYVERSIONS -* CPython 3.7 through 3.11.0a4. +* CPython 3.7 through 3.11.0b1. * PyPy3 7.3.8. Documentation is on `Read the Docs`_. Code repository and issue tracker are on @@ -148,3 +154,6 @@ .. |twitter-nedbat| image:: https://img.shields.io/twitter/follow/nedbat.svg?label=nedbat&style=flat&logo=twitter&logoColor=4FADFF :target: https://twitter.com/nedbat :alt: nedbat on Twitter +.. |sponsor| image:: https://img.shields.io/badge/%E2%9D%A4-Sponsor%20me-brightgreen?style=flat&logo=GitHub + :target: https://github.com/sponsors/nedbat + :alt: Sponsor me on GitHub
--- a/eric7/DebugClients/Python/coverage/html.py Tue May 24 10:22:46 2022 +0200 +++ b/eric7/DebugClients/Python/coverage/html.py Tue May 24 11:00:52 2022 +0200 @@ -122,6 +122,15 @@ return file_data +class FileToReport: + """A file we're considering reporting.""" + def __init__(self, fr, analysis): + self.fr = fr + self.analysis = analysis + self.rootname = flat_rootname(fr.relative_filename()) + self.html_filename = self.rootname + ".html" + + class HtmlReporter: """HTML reporting.""" @@ -165,6 +174,8 @@ self.datagen = HtmlDataGeneration(self.coverage) self.totals = Numbers(precision=self.config.precision) self.directory_was_empty = False + self.first_fr = None + self.final_fr = None self.template_globals = { # Functions available in the templates. @@ -188,7 +199,7 @@ 'mis': 'mis show_mis', 'par': 'par run show_par', 'run': 'run', - } + }, } self.pyfile_html_source = read_data("pyfile.html") self.source_tmpl = Templite(self.pyfile_html_source, self.template_globals) @@ -204,9 +215,28 @@ self.incr.read() self.incr.check_global_data(self.config, self.pyfile_html_source) - # Process all the files. + # Process all the files. For each page we need to supply a link + # to the next and previous page. + files_to_report = [] + for fr, analysis in get_analysis_to_report(self.coverage, morfs): - self.html_file(fr, analysis) + ftr = FileToReport(fr, analysis) + should = self.should_report_file(ftr) + if should: + files_to_report.append(ftr) + else: + file_be_gone(os.path.join(self.directory, ftr.html_filename)) + + for i, ftr in enumerate(files_to_report): + if i == 0: + prev_html = "index.html" + else: + prev_html = files_to_report[i - 1].html_filename + if i == len(files_to_report) - 1: + next_html = "index.html" + else: + next_html = files_to_report[i + 1].html_filename + self.write_html_file(ftr, prev_html, next_html) if not self.all_files_nums: raise NoDataError("No data to report.") @@ -214,11 +244,22 @@ self.totals = sum(self.all_files_nums) # Write the index file. - self.index_file() + if files_to_report: + first_html = files_to_report[0].html_filename + final_html = files_to_report[-1].html_filename + else: + first_html = final_html = "index.html" + self.index_file(first_html, final_html) self.make_local_static_report_files() return self.totals.n_statements and self.totals.pc_covered + def make_directory(self): + """Make sure our htmlcov directory exists.""" + ensure_dir(self.directory) + if not os.listdir(self.directory): + self.directory_was_empty = True + def make_local_static_report_files(self): """Make local instances of static files for HTML report.""" # The files we provide must always be copied. @@ -236,17 +277,10 @@ if 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.""" - 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) - + def should_report_file(self, ftr): + """Determine if we'll report this file.""" # Get the numbers for this file. - nums = analysis.numbers + nums = ftr.analysis.numbers self.all_files_nums.append(nums) if self.skip_covered: @@ -255,24 +289,28 @@ no_missing_branches = (nums.n_partial_branches == 0) 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 + return False 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 + return False + + return True + + def write_html_file(self, ftr, prev_html, next_html): + """Generate an HTML file for one source file.""" + self.make_directory() # Find out if the file on disk is already correct. - if self.incr.can_skip_file(self.data, fr, rootname): - self.file_summaries.append(self.incr.index_info(rootname)) + if self.incr.can_skip_file(self.data, ftr.fr, ftr.rootname): + self.file_summaries.append(self.incr.index_info(ftr.rootname)) return # Write the HTML page for this file. - file_data = self.datagen.data_for_file(fr, analysis) + file_data = self.datagen.data_for_file(ftr.fr, ftr.analysis) for ldata in file_data.lines: # Build the HTML for the line. html = [] @@ -292,7 +330,7 @@ ldata.annotate = ", ".join( f"{ldata.number} ↛ {d}" for d in ldata.short_annotations - ) + ) else: ldata.annotate = None @@ -306,7 +344,7 @@ ", ".join( f"{num:d}) {ann_long}" for num, ann_long in enumerate(longs, start=1) - ), + ), ) else: ldata.annotate_long = None @@ -316,20 +354,26 @@ css_classes.append(self.template_globals['category'][ldata.category]) ldata.css_class = ' '.join(css_classes) or "pln" - html = self.source_tmpl.render(file_data.__dict__) + html_path = os.path.join(self.directory, ftr.html_filename) + html = self.source_tmpl.render({ + **file_data.__dict__, + 'prev_html': prev_html, + 'next_html': next_html, + }) write_html(html_path, html) # Save this file's information for the index file. index_info = { - 'nums': nums, - 'html_filename': html_filename, - 'relative_filename': fr.relative_filename(), + 'nums': ftr.analysis.numbers, + 'html_filename': ftr.html_filename, + 'relative_filename': ftr.fr.relative_filename(), } self.file_summaries.append(index_info) - self.incr.set_index_info(rootname, index_info) + self.incr.set_index_info(ftr.rootname, index_info) - def index_file(self): + def index_file(self, first_html, final_html): """Write the index.html file for this report.""" + self.make_directory() index_tmpl = Templite(read_data("index.html"), self.template_globals) skipped_covered_msg = skipped_empty_msg = "" @@ -345,6 +389,8 @@ 'totals': self.totals, 'skipped_covered_msg': skipped_covered_msg, 'skipped_empty_msg': skipped_empty_msg, + 'first_html': first_html, + 'final_html': final_html, }) index_file = os.path.join(self.directory, "index.html")
--- a/eric7/DebugClients/Python/coverage/htmlfiles/coverage_html.js Tue May 24 10:22:46 2022 +0200 +++ b/eric7/DebugClients/Python/coverage/htmlfiles/coverage_html.js Tue May 24 11:00:52 2022 +0200 @@ -25,6 +25,13 @@ return !(rect.bottom < viewTop || rect.top >= viewBottom); } +function on_click(sel, fn) { + const elt = document.querySelector(sel); + if (elt) { + elt.addEventListener("click", fn); + } +} + // Helpers for table sorting function getCellValue(row, column = 0) { const cell = row.cells[column] @@ -193,6 +200,11 @@ direction: th.getAttribute("aria-sort"), })); }); + + on_click(".button_prev_file", coverage.to_prev_file); + on_click(".button_next_file", coverage.to_next_file); + + on_click(".button_show_hide_help", coverage.show_hide_help); }; // -- pyfile stuff -- @@ -209,12 +221,6 @@ 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); @@ -225,6 +231,12 @@ on_click(".button_top_of_page", coverage.to_top); on_click(".button_first_chunk", coverage.to_first_chunk); + on_click(".button_prev_file", coverage.to_prev_file); + on_click(".button_next_file", coverage.to_next_file); + on_click(".button_to_index", coverage.to_index); + + on_click(".button_show_hide_help", coverage.show_hide_help); + coverage.filters = undefined; try { coverage.filters = localStorage.getItem(coverage.LINE_FILTERS_STORAGE); @@ -299,6 +311,23 @@ coverage.to_next_chunk(); }; +coverage.to_prev_file = function () { + window.location = document.getElementById("prevFileLink").href; +} + +coverage.to_next_file = function () { + window.location = document.getElementById("nextFileLink").href; +} + +coverage.to_index = function () { + location.href = document.getElementById("indexLink").href; +} + +coverage.show_hide_help = function () { + const helpCheck = document.getElementById("help_panel_state") + helpCheck.checked = !helpCheck.checked; +} + // Return a string indicating what kind of chunk this line belongs to, // or null if not a chunk. coverage.chunk_indicator = function (line_elt) {
--- a/eric7/DebugClients/Python/coverage/htmlfiles/index.html Tue May 24 10:22:46 2022 +0200 +++ b/eric7/DebugClients/Python/coverage/htmlfiles/index.html Tue May 24 11:00:52 2022 +0200 @@ -21,15 +21,15 @@ <span class="pc_cov">{{totals.pc_covered_str}}%</span> </h1> - <div id="help_panel_wrapper"> + <aside 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"> + <div class="keyhelp"> + <p> <kbd>n</kbd> <kbd>s</kbd> <kbd>m</kbd> @@ -38,15 +38,29 @@ <kbd>b</kbd> <kbd>p</kbd> {% endif %} - <kbd>c</kbd> change column sorting + <kbd>c</kbd> + change column sorting + </p> + <p> + <kbd>[</kbd> + <kbd>]</kbd> + prev/next file + </p> + <p> + <kbd>?</kbd> show/hide this help </p> </div> </div> - </div> + </aside> <form id="filter_container"> <input id="filter" type="text" value="" placeholder="filter..." /> </form> + + <p class="text"> + <a class="nav" href="{{__url__}}">coverage.py v{{__version__}}</a>, + created at {{ time_stamp }} + </p> </div> </header> @@ -115,6 +129,13 @@ created at {{ time_stamp }} </p> </div> + <aside class="hidden"> + <a id="prevFileLink" class="nav" href="{{ final_html }}"/> + <a id="nextFileLink" class="nav" href="{{ first_html }}"/> + <button type="button" class="button_prev_file" data-shortcut="["/> + <button type="button" class="button_next_file" data-shortcut="]"/> + <button type="button" class="button_show_hide_help" data-shortcut="?"/> + </aside> </footer> </body>
--- a/eric7/DebugClients/Python/coverage/htmlfiles/pyfile.html Tue May 24 10:22:46 2022 +0200 +++ b/eric7/DebugClients/Python/coverage/htmlfiles/pyfile.html Tue May 24 11:00:52 2022 +0200 @@ -5,9 +5,6 @@ <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"> @@ -25,15 +22,15 @@ <span class="pc_cov">{{nums.pc_covered_str}}%</span> </h1> - <div id="help_panel_wrapper"> + <aside 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"> + <div class="keyhelp"> + <p> <kbd>r</kbd> <kbd>m</kbd> <kbd>x</kbd> @@ -42,19 +39,31 @@ {% endif %} toggle line displays </p> - <p class="keyhelp"> + <p> <kbd>j</kbd> - <kbd>k</kbd> next/prev highlighted chunk + <kbd>k</kbd> + next/prev highlighted chunk </p> - <p class="keyhelp"> + <p> <kbd>0</kbd> (zero) top of page </p> - <p class="keyhelp"> + <p> <kbd>1</kbd> (one) first highlighted chunk </p> + <p> + <kbd>[</kbd> + <kbd>]</kbd> + prev/next file + </p> + <p> + <kbd>u</kbd> up to the index + </p> + <p> + <kbd>?</kbd> show/hide this help + </p> </div> </div> - </div> + </aside> <h2> <span class="text">{{nums.n_statements}} statements </span> @@ -66,12 +75,25 @@ {% 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> + <p class="text"> + <a id="prevFileLink" class="nav" href="{{ prev_html }}">« prev</a> + <a id="indexLink" class="nav" href="index.html">^ index</a> + <a id="nextFileLink" class="nav" href="{{ next_html }}">» next</a> + + <a class="nav" href="{{__url__}}">coverage.py v{{__version__}}</a>, + created at {{ time_stamp }} + </p> + + <aside class="hidden"> + <button type="button" class="button_next_chunk" data-shortcut="j"/> + <button type="button" class="button_prev_chunk" data-shortcut="k"/> + <button type="button" class="button_top_of_page" data-shortcut="0"/> + <button type="button" class="button_first_chunk" data-shortcut="1"/> + <button type="button" class="button_prev_file" data-shortcut="["/> + <button type="button" class="button_next_file" data-shortcut="]"/> + <button type="button" class="button_to_index" data-shortcut="u"/> + <button type="button" class="button_show_hide_help" data-shortcut="?"/> + </aside> </div> </header> @@ -110,7 +132,11 @@ <footer> <div class="content"> <p> - <a class="nav" href="index.html">« index</a> <a class="nav" href="{{__url__}}">coverage.py v{{__version__}}</a>, + <a id="prevFileLink" class="nav" href="{{ prev_html }}">« prev</a> + <a id="indexLink" class="nav" href="index.html">^ index</a> + <a id="nextFileLink" class="nav" href="{{ next_html }}">» next</a> + + <a class="nav" href="{{__url__}}">coverage.py v{{__version__}}</a>, created at {{ time_stamp }} </p> </div>
--- a/eric7/DebugClients/Python/coverage/htmlfiles/style.css Tue May 24 10:22:46 2022 +0200 +++ b/eric7/DebugClients/Python/coverage/htmlfiles/style.css Tue May 24 11:00:52 2022 +0200 @@ -28,6 +28,8 @@ a.nav:hover { text-decoration: underline; color: inherit; } +.hidden { display: none; } + header { background: #f8f8f8; width: 100%; z-index: 2; border-bottom: 1px solid #ccc; } @media (prefers-color-scheme: dark) { header { background: black; } } @@ -38,6 +40,10 @@ header h2 { margin-top: .5em; font-size: 1em; } +header p.text { margin: .5em 0 -.5em; color: #666; font-style: italic; } + +@media (prefers-color-scheme: dark) { header p.text { color: #aaa; } } + header.sticky { position: fixed; left: 0; right: 0; height: 2.5em; } header.sticky .text { display: none; } @@ -52,9 +58,7 @@ main { position: relative; z-index: 1; } -.indexfile footer { margin: 1rem 3.5rem; } - -.pyfile footer { margin: 1rem 1rem; } +footer { margin: 1rem 3.5rem; } footer .content { padding: 0; color: #666; font-style: italic; } @@ -124,7 +128,9 @@ #help_panel_state { display: none; } -#help_panel { top: 25px; right: 0; padding: .75em; border: 1px solid #883; } +#help_panel { top: 25px; right: 0; padding: .75em; border: 1px solid #883; color: #333; } + +#help_panel .keyhelp p { margin-top: .75em; } #help_panel .legend { font-style: italic; margin-bottom: 1em; } @@ -134,8 +140,6 @@ #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; }
--- a/eric7/DebugClients/Python/coverage/htmlfiles/style.scss Tue May 24 10:22:46 2022 +0200 +++ b/eric7/DebugClients/Python/coverage/htmlfiles/style.scss Tue May 24 11:00:52 2022 +0200 @@ -156,6 +156,10 @@ } } +.hidden { + display: none; +} + // Page structure header { background: $light-gray1; @@ -174,6 +178,13 @@ font-size: 1em; } + p.text { + margin: .5em 0 -.5em; + color: $light-gray5; + @include color-dark($dark-gray5); + font-style: italic; + } + &.sticky { position: fixed; left: 0; @@ -208,19 +219,15 @@ z-index: 1; } -.indexfile footer { +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; + .content { + padding: 0; + color: $light-gray5; + @include color-dark($dark-gray5); + font-style: italic; + } } #index { @@ -351,6 +358,12 @@ padding: .75em; border: 1px solid #883; + color: #333; + + .keyhelp p { + margin-top: .75em; + } + .legend { font-style: italic; margin-bottom: 1em; @@ -358,12 +371,10 @@ .indexfile & { width: 25em; - //min-height: 4em; } .pyfile & { width: 18em; - //min-height: 8em; } #help_panel_state:checked ~ & { @@ -371,10 +382,6 @@ } } -.keyhelp { - margin-top: .75em; -} - kbd { border: 1px solid black; border-color: #888 #333 #333 #888;
--- a/eric7/DebugClients/Python/coverage/inorout.py Tue May 24 10:22:46 2022 +0200 +++ b/eric7/DebugClients/Python/coverage/inorout.py Tue May 24 11:00:52 2022 +0200 @@ -591,7 +591,7 @@ 'source_match', 'source_pkgs_match', 'include_match', 'omit_match', 'cover_match', 'pylib_match', 'third_match', - ] + ] for matcher_name in matcher_names: matcher = getattr(self, matcher_name)
--- a/eric7/DebugClients/Python/coverage/jsonreport.py Tue May 24 10:22:46 2022 +0200 +++ b/eric7/DebugClients/Python/coverage/jsonreport.py Tue May 24 11:00:52 2022 +0200 @@ -70,7 +70,7 @@ json.dump( self.report_data, outfile, - indent=4 if self.config.json_pretty_print else None + indent=(4 if self.config.json_pretty_print else None), ) return self.total.n_statements and self.total.pc_covered
--- a/eric7/DebugClients/Python/coverage/misc.py Tue May 24 10:22:46 2022 +0200 +++ b/eric7/DebugClients/Python/coverage/misc.py Tue May 24 11:00:52 2022 +0200 @@ -272,7 +272,7 @@ raise NotImplementedError( f"{thing} {name!r} needs to implement {func_name}()" - ) + ) class DefaultValue:
--- a/eric7/DebugClients/Python/coverage/parser.py Tue May 24 10:22:46 2022 +0200 +++ b/eric7/DebugClients/Python/coverage/parser.py Tue May 24 11:00:52 2022 +0200 @@ -1231,15 +1231,15 @@ 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 @@ -1287,6 +1287,7 @@ self.add_arc(start, -start, None, f"didn't finish the {noun} on line {start}") return _code_object__expression_callable + # pylint: disable=too-many-function-args _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")
--- a/eric7/DebugClients/Python/coverage/phystokens.py Tue May 24 10:22:46 2022 +0200 +++ b/eric7/DebugClients/Python/coverage/phystokens.py Tue May 24 11:00:52 2022 +0200 @@ -60,7 +60,7 @@ 99999, "\\\n", (slineno, ccol), (slineno, ccol+2), last_line - ) + ) last_line = ltext if ttype not in (tokenize.NEWLINE, tokenize.NL): last_ttext = ttext
--- a/eric7/DebugClients/Python/coverage/python.py Tue May 24 10:22:46 2022 +0200 +++ b/eric7/DebugClients/Python/coverage/python.py Tue May 24 11:00:52 2022 +0200 @@ -202,8 +202,8 @@ def no_branch_lines(self): no_branch = self.parser.lines_matching( join_regex(self.coverage.config.partial_list), - join_regex(self.coverage.config.partial_always_list) - ) + join_regex(self.coverage.config.partial_always_list), + ) return no_branch @expensive
--- a/eric7/DebugClients/Python/coverage/results.py Tue May 24 10:22:46 2022 +0200 +++ b/eric7/DebugClients/Python/coverage/results.py Tue May 24 11:00:52 2022 +0200 @@ -271,10 +271,10 @@ nums.n_branches = self.n_branches + other.n_branches nums.n_partial_branches = ( self.n_partial_branches + other.n_partial_branches - ) + ) nums.n_missing_branches = ( self.n_missing_branches + other.n_missing_branches - ) + ) return nums def __radd__(self, other):
--- a/eric7/DebugClients/Python/coverage/sqldata.py Tue May 24 10:22:46 2022 +0200 +++ b/eric7/DebugClients/Python/coverage/sqldata.py Tue May 24 11:00:52 2022 +0200 @@ -1,10 +1,7 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 # For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt -"""Sqlite coverage data.""" - -# TODO: factor out dataop debugging to a wrapper class? -# TODO: make sure all dataop debugging is in place somehow +"""SQLite coverage data.""" import collections import datetime @@ -17,6 +14,7 @@ import socket import sqlite3 import sys +import textwrap import threading import zlib @@ -30,7 +28,7 @@ os = isolate_module(os) # If you change the schema, increment the SCHEMA_VERSION, and update the -# docs in docs/dbschema.rst also. +# docs in docs/dbschema.rst by running "make cogdoc". SCHEMA_VERSION = 7 @@ -252,10 +250,10 @@ def _reset(self): """Reset our attributes.""" - if self._dbs: + if not self._no_disk: for db in self._dbs.values(): db.close() - self._dbs = {} + self._dbs = {} self._file_map = {} self._have_used = False self._current_context_id = None @@ -293,7 +291,7 @@ self._has_arcs = bool(int(row[0])) self._has_lines = not self._has_arcs - for path, file_id in db.execute("select path, id from file"): + for file_id, path in db.execute("select id, path from file"): self._file_map[path] = file_id def _init_db(self, db): @@ -389,8 +387,10 @@ if filename not in self._file_map: if add: with self._connect() as con: - cur = con.execute("insert or replace into file (path) values (?)", (filename,)) - self._file_map[filename] = cur.lastrowid + self._file_map[filename] = con.execute_for_rowid( + "insert or replace into file (path) values (?)", + (filename,) + ) return self._file_map.get(filename) def _context_id(self, context): @@ -427,8 +427,10 @@ self._current_context_id = context_id else: with self._connect() as con: - cur = con.execute("insert into context (context) values (?)", (context,)) - self._current_context_id = cur.lastrowid + self._current_context_id = con.execute_for_rowid( + "insert into context (context) values (?)", + (context,) + ) def base_filename(self): """The base filename for storing data. @@ -501,9 +503,6 @@ 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 " + @@ -616,19 +615,19 @@ # Collector for all arcs, lines and tracers other_data.read() - with other_data._connect() as conn: + with other_data._connect() as con: # Get files data. - cur = conn.execute("select path from file") + cur = con.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 = con.execute("select context from context") contexts = [context for (context,) in cur] cur.close() # Get arc data. - cur = conn.execute( + cur = con.execute( "select file.path, context.context, arc.fromno, arc.tono " + "from arc " + "inner join file on file.id = arc.file_id " + @@ -638,17 +637,17 @@ cur.close() # Get line data. - cur = conn.execute( + cur = con.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" - ) + ) lines = {(files[path], context): numbits for (path, context, numbits) in cur} cur.close() # Get tracer data. - cur = conn.execute( + cur = con.execute( "select file.path, tracer " + "from tracer " + "inner join file on file.id = tracer.file_id" @@ -656,38 +655,39 @@ tracers = {files[path]: tracer for (path, tracer) in cur} cur.close() - with self._connect() as conn: - conn.con.isolation_level = "IMMEDIATE" + with self._connect() as con: + con.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 con.execute("select path from file")} this_tracers.update({ aliases.map(path): tracer - for path, tracer in conn.execute( + for path, tracer in con.execute( "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( + con.executemany( "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 con.execute("select id, path from file") } - conn.executemany( + self._file_map.update(file_ids) + con.executemany( "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 con.execute("select id, context from context") } # Prepare tracers and fail, if a conflict is found. @@ -715,12 +715,12 @@ ) # Get line data. - cur = conn.execute( + cur = con.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" - ) + ) for path, context, numbits in cur: key = (aliases.map(path), context) if key in lines: @@ -732,7 +732,7 @@ self._choose_lines_or_arcs(arcs=True) # Write the combined data. - conn.executemany( + con.executemany( "insert or ignore into arc " + "(file_id, context_id, fromno, tono) values (?, ?, ?, ?)", arc_rows @@ -740,8 +740,8 @@ if lines: self._choose_lines_or_arcs(lines=True) - conn.execute("delete from line_bits") - conn.executemany( + con.execute("delete from line_bits") + con.executemany( "insert into line_bits " + "(file_id, context_id, numbits) values (?, ?, ?)", [ @@ -749,14 +749,15 @@ for (file, context), numbits in lines.items() ] ) - conn.executemany( + con.executemany( "insert or ignore into tracer (file_id, tracer) values (?, ?)", ((file_ids[filename], tracer) for filename, tracer in tracer_map.items()) ) - # Update all internal cache data. - self._reset() - self.read() + if not self._no_disk: + # Update all internal cache data. + self._reset() + self.read() def erase(self, parallel=False): """Erase the data in this object. @@ -782,8 +783,9 @@ def read(self): """Start using an existing data file.""" - with self._connect(): # TODO: doesn't look right - self._have_used = True + if os.path.exists(self._filename): + with self._connect(): + self._have_used = True def write(self): """Ensure the data is written to the data file.""" @@ -977,7 +979,7 @@ "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)) @@ -999,9 +1001,7 @@ with SqliteDb(":memory:", debug=NoDebugging()) as db: temp_store = [row[0] for row in db.execute("pragma temp_store")] 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)] + copts = textwrap.wrap(", ".join(copts), width=75) return [ ("sqlite3_version", sqlite3.version), @@ -1042,7 +1042,7 @@ """ def __init__(self, filename, debug): - self.debug = debug if debug.should("sql") else None + self.debug = debug self.filename = filename self.nest = 0 self.con = None @@ -1057,7 +1057,7 @@ # 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: + if self.debug.should("sql"): self.debug.write(f"Connecting to {self.filename!r}") try: self.con = sqlite3.connect(self.filename, check_same_thread=False) @@ -1093,13 +1093,13 @@ self.con.__exit__(exc_type, exc_value, traceback) self.close() except Exception as exc: - if self.debug: + if self.debug.should("sql"): self.debug.write(f"EXCEPTION from __exit__: {exc}") raise DataError(f"Couldn't end data file {self.filename!r}: {exc}") from exc def execute(self, sql, parameters=()): """Same as :meth:`python:sqlite3.Connection.execute`.""" - if self.debug: + if self.debug.should("sql"): tail = f" with {parameters!r}" if parameters else "" self.debug.write(f"Executing {sql!r}{tail}") try: @@ -1124,10 +1124,18 @@ ) except Exception: # pragma: cant happen pass - if self.debug: + if self.debug.should("sql"): self.debug.write(f"EXCEPTION from execute: {msg}") raise DataError(f"Couldn't use data file {self.filename!r}: {msg}") from exc + def execute_for_rowid(self, sql, parameters=()): + """Like execute, but returns the lastrowid.""" + con = self.execute(sql, parameters) + rowid = con.lastrowid + if self.debug.should("sqldata"): + self.debug.write(f"Row id result: {rowid!r}") + return rowid + def execute_one(self, sql, parameters=()): """Execute a statement and return the one row that results. @@ -1147,9 +1155,13 @@ def executemany(self, sql, data): """Same as :meth:`python:sqlite3.Connection.executemany`.""" - if self.debug: + if self.debug.should("sql"): data = list(data) - self.debug.write(f"Executing many {sql!r} with {len(data)} rows") + final = ":" if self.debug.should("sqldata") else "" + self.debug.write(f"Executing many {sql!r} with {len(data)} rows{final}") + if self.debug.should("sqldata"): + for i, row in enumerate(data): + self.debug.write(f"{i:4d}: {row!r}") try: return self.con.executemany(sql, data) except Exception: # pragma: cant happen @@ -1160,7 +1172,7 @@ def executescript(self, script): """Same as :meth:`python:sqlite3.Connection.executescript`.""" - if self.debug: + if self.debug.should("sql"): self.debug.write("Executing script with {} chars: {}".format( len(script), clipped_repr(script, 100), ))
--- a/eric7/DebugClients/Python/coverage/summary.py Tue May 24 10:22:46 2022 +0200 +++ b/eric7/DebugClients/Python/coverage/summary.py Tue May 24 11:00:52 2022 +0200 @@ -127,11 +127,11 @@ if self.config.skip_covered and self.skipped_count: self.writeout( fmt_skip_covered % (self.skipped_count, 's' if self.skipped_count > 1 else '') - ) + ) if self.config.skip_empty and self.empty_count: self.writeout( fmt_skip_empty % (self.empty_count, 's' if self.empty_count > 1 else '') - ) + ) return self.total.n_statements and self.total.pc_covered
--- a/eric7/DebugClients/Python/coverage/tomlconfig.py Tue May 24 10:22:46 2022 +0200 +++ b/eric7/DebugClients/Python/coverage/tomlconfig.py Tue May 24 11:00:52 2022 +0200 @@ -7,15 +7,21 @@ import os import re +from coverage import env from coverage.exceptions import ConfigError from coverage.misc import import_third_party, substitute_variables -# TOML support is an install-time extra option. (Import typing is here because -# 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") + +if env.PYVERSION >= (3, 11): + import tomllib # pylint: disable=import-error +else: + # TOML support on Python 3.10 and below 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 + tomllib = import_third_party("tomli") class TomlDecodeError(Exception): @@ -45,11 +51,11 @@ toml_text = fp.read() except OSError: return [] - if tomli is not None: + if tomllib is not None: toml_text = substitute_variables(toml_text, os.environ) try: - self.data = tomli.loads(toml_text) - except tomli.TOMLDecodeError as err: + self.data = tomllib.loads(toml_text) + except tomllib.TOMLDecodeError as err: raise TomlDecodeError(str(err)) from err return [filename] else:
--- a/eric7/DebugClients/Python/coverage/version.py Tue May 24 10:22:46 2022 +0200 +++ b/eric7/DebugClients/Python/coverage/version.py Tue May 24 11:00:52 2022 +0200 @@ -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, 3, 2, "final", 0) +version_info = (6, 4, 0, "final", 0) def _make_version(major, minor, micro, releaselevel, serial):
--- a/eric7/DebugClients/Python/coverage/xmlreport.py Tue May 24 10:22:46 2022 +0200 +++ b/eric7/DebugClients/Python/coverage/xmlreport.py Tue May 24 11:00:52 2022 +0200 @@ -68,7 +68,7 @@ xcoverage.setAttribute("timestamp", str(int(time.time()*1000))) xcoverage.appendChild(self.xml_out.createComment( f" Generated by coverage.py: {__url__} " - )) + )) xcoverage.appendChild(self.xml_out.createComment(f" Based on {DTD_URL} ")) # Call xml_file for each file in the data. @@ -193,7 +193,7 @@ xline.setAttribute( "condition-coverage", "%d%% (%d/%d)" % (100*taken//total, taken, total) - ) + ) if line in missing_branch_arcs: annlines = ["exit" if b < 0 else str(b) for b in missing_branch_arcs[line]] xline.setAttribute("missing-branches", ",".join(annlines))