Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-coverage for openSUSE:Factory checked in at 2024-06-07 15:02:08 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-coverage (Old) and /work/SRC/openSUSE:Factory/.python-coverage.new.24587 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Package is "python-coverage" Fri Jun 7 15:02:08 2024 rev:63 rq:1178912 version:7.5.3 Changes: -------- --- /work/SRC/openSUSE:Factory/python-coverage/python-coverage.changes 2024-05-16 17:12:31.694165865 +0200 +++ /work/SRC/openSUSE:Factory/.python-coverage.new.24587/python-coverage.changes 2024-06-07 15:02:15.501350003 +0200 @@ -1,0 +2,30 @@ +Thu Jun 6 07:29:28 UTC 2024 - Dirk Müller <dmueller@suse.com> + +- update to 7.5.3: + * Performance improvements for combining data files, especially + when measuring line coverage. A few different quadratic + behaviors were eliminated. In one extreme case of combining + 700+ data files, the time dropped from more than three hours + to seven minutes. Thanks for Kraken Tech for funding the + fix. + * Performance improvements for generating HTML reports, with a + side benefit of reducing memory use, closing issue 1791. + Thanks to Daniel Diniz for helping to diagnose the problem. + * Fix: nested matches of exclude patterns could exclude too + much code, as reported in issue 1779. This is now fixed. + * Changed: previously, coverage.py would consider a module + docstring to be an executable statement if it appeared after + line 1 in the file, but not executable if it was the first + line. Now module docstrings are never counted as executable + statements. This can change coverage.py's count of the + number of statements in a file, which can slightly change the + coverage percentage reported. + * In the HTML report, the filter term and "hide covered" + checkbox settings are remembered between viewings, thanks to + Daniel Diniz. + * Python 3.13.0b1 is supported. + * Fix: parsing error handling is improved to ensure bizarre + source files are handled gracefully, and to unblock oss-fuzz + fuzzing, thanks to Liam DeVoe. Closes issue 1787. + +------------------------------------------------------------------- Old: ---- coverage-7.5.1.tar.gz New: ---- coverage-7.5.3.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-coverage.spec ++++++ --- /var/tmp/diff_new_pack.XK7TeO/_old 2024-06-07 15:02:17.021405379 +0200 +++ /var/tmp/diff_new_pack.XK7TeO/_new 2024-06-07 15:02:17.021405379 +0200 @@ -18,7 +18,7 @@ %{?sle15_python_module_pythons} Name: python-coverage -Version: 7.5.1 +Version: 7.5.3 Release: 0 Summary: Code coverage measurement for Python License: Apache-2.0 ++++++ coverage-7.5.1.tar.gz -> coverage-7.5.3.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/coverage-7.5.1/.github/workflows/python-nightly.yml new/coverage-7.5.3/.github/workflows/python-nightly.yml --- old/coverage-7.5.1/.github/workflows/python-nightly.yml 2024-05-04 16:44:25.000000000 +0200 +++ new/coverage-7.5.3/.github/workflows/python-nightly.yml 2024-05-28 15:52:29.000000000 +0200 @@ -58,6 +58,7 @@ # https://launchpad.net/~deadsnakes/+archive/ubuntu/nightly/+packages - "3.12-dev" - "3.13-dev" + - "3.14-dev" # https://github.com/actions/setup-python#available-versions-of-pypy - "pypy-3.8-nightly" - "pypy-3.9-nightly" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/coverage-7.5.1/CHANGES.rst new/coverage-7.5.3/CHANGES.rst --- old/coverage-7.5.1/CHANGES.rst 2024-05-04 16:44:25.000000000 +0200 +++ new/coverage-7.5.3/CHANGES.rst 2024-05-28 15:52:29.000000000 +0200 @@ -22,6 +22,53 @@ .. scriv-start-here +.. _changes_7-5-3: + +Version 7.5.3 — 2024-05-28 +-------------------------- + +- Performance improvements for combining data files, especially when measuring + line coverage. A few different quadratic behaviors were eliminated. In one + extreme case of combining 700+ data files, the time dropped from more than + three hours to seven minutes. Thanks for Kraken Tech for funding the fix. + +- Performance improvements for generating HTML reports, with a side benefit of + reducing memory use, closing `issue 1791`_. Thanks to Daniel Diniz for + helping to diagnose the problem. + +.. _issue 1791: https://github.com/nedbat/coveragepy/issues/1791 + + +.. _changes_7-5-2: + +Version 7.5.2 — 2024-05-24 +-------------------------- + +- Fix: nested matches of exclude patterns could exclude too much code, as + reported in `issue 1779`_. This is now fixed. + +- Changed: previously, coverage.py would consider a module docstring to be an + executable statement if it appeared after line 1 in the file, but not + executable if it was the first line. Now module docstrings are never counted + as executable statements. This can change coverage.py's count of the number + of statements in a file, which can slightly change the coverage percentage + reported. + +- In the HTML report, the filter term and "hide covered" checkbox settings are + remembered between viewings, thanks to `Daniel Diniz <pull 1776_>`_. + +- Python 3.13.0b1 is supported. + +- Fix: parsing error handling is improved to ensure bizarre source files are + handled gracefully, and to unblock oss-fuzz fuzzing, thanks to `Liam DeVoe + <pull 1788_>`_. Closes `issue 1787`_. + +.. _pull 1776: https://github.com/nedbat/coveragepy/pull/1776 +.. _issue 1779: https://github.com/nedbat/coveragepy/issues/1779 +.. _issue 1787: https://github.com/nedbat/coveragepy/issues/1787 +.. _pull 1788: https://github.com/nedbat/coveragepy/pull/1788 + + .. _changes_7-5-1: Version 7.5.1 — 2024-05-04 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/coverage-7.5.1/CONTRIBUTORS.txt new/coverage-7.5.3/CONTRIBUTORS.txt --- old/coverage-7.5.1/CONTRIBUTORS.txt 2024-05-04 16:44:25.000000000 +0200 +++ new/coverage-7.5.3/CONTRIBUTORS.txt 2024-05-28 15:52:29.000000000 +0200 @@ -132,6 +132,7 @@ Leonardo Pistone Lewis Gaul Lex Berezhny +Liam DeVoe Loïc Dachary Lorenzo Micò Louis Heredero diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/coverage-7.5.1/PKG-INFO new/coverage-7.5.3/PKG-INFO --- old/coverage-7.5.1/PKG-INFO 2024-05-04 16:44:36.584669000 +0200 +++ new/coverage-7.5.3/PKG-INFO 2024-05-28 15:52:36.482906000 +0200 @@ -1,12 +1,12 @@ Metadata-Version: 2.1 Name: coverage -Version: 7.5.1 +Version: 7.5.3 Summary: Code coverage measurement for Python Home-page: https://github.com/nedbat/coveragepy -Author: Ned Batchelder and 226 others +Author: Ned Batchelder and 227 others Author-email: ned@nedbatchelder.com License: Apache-2.0 -Project-URL: Documentation, https://coverage.readthedocs.io/en/7.5.1 +Project-URL: Documentation, https://coverage.readthedocs.io/en/7.5.3 Project-URL: Funding, https://tidelift.com/subscription/pkg/pypi-coverage?utm_source=pypi-coverage&utm_medium=referral&utm_campaign=pypi Project-URL: Issues, https://github.com/nedbat/coveragepy/issues Project-URL: Mastodon, https://hachyderm.io/@coveragepy @@ -62,13 +62,13 @@ .. PYVERSIONS -* Python 3.8 through 3.12, and 3.13.0a6 and up. +* Python 3.8 through 3.12, and 3.13.0b1 and up. * PyPy3 versions 3.8 through 3.10. Documentation is on `Read the Docs`_. Code repository and issue tracker are on `GitHub`_. -.. _Read the Docs: https://coverage.readthedocs.io/en/7.5.1/ +.. _Read the Docs: https://coverage.readthedocs.io/en/7.5.3/ .. _GitHub: https://github.com/nedbat/coveragepy **New in 7.x:** @@ -112,7 +112,7 @@ Looking to run ``coverage`` on your test suite? See the `Quick Start section`_ of the docs. -.. _Quick Start section: https://coverage.readthedocs.io/en/7.5.1/#quick-start +.. _Quick Start section: https://coverage.readthedocs.io/en/7.5.3/#quick-start Change history @@ -120,7 +120,7 @@ The complete history of changes is on the `change history page`_. -.. _change history page: https://coverage.readthedocs.io/en/7.5.1/changes.html +.. _change history page: https://coverage.readthedocs.io/en/7.5.3/changes.html Code of Conduct @@ -139,7 +139,7 @@ Found a bug? Want to help improve the code or documentation? See the `Contributing section`_ of the docs. -.. _Contributing section: https://coverage.readthedocs.io/en/7.5.1/contributing.html +.. _Contributing section: https://coverage.readthedocs.io/en/7.5.3/contributing.html Security @@ -167,7 +167,7 @@ :target: https://github.com/nedbat/coveragepy/actions/workflows/quality.yml :alt: Quality check status .. |docs| image:: https://readthedocs.org/projects/coverage/badge/?version=latest&style=flat - :target: https://coverage.readthedocs.io/en/7.5.1/ + :target: https://coverage.readthedocs.io/en/7.5.3/ :alt: Documentation .. |kit| image:: https://img.shields.io/pypi/v/coverage :target: https://pypi.org/project/coverage/ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/coverage-7.5.1/README.rst new/coverage-7.5.3/README.rst --- old/coverage-7.5.1/README.rst 2024-05-04 16:44:25.000000000 +0200 +++ new/coverage-7.5.3/README.rst 2024-05-28 15:52:29.000000000 +0200 @@ -25,7 +25,7 @@ .. PYVERSIONS -* Python 3.8 through 3.12, and 3.13.0a6 and up. +* Python 3.8 through 3.12, and 3.13.0b1 and up. * PyPy3 versions 3.8 through 3.10. Documentation is on `Read the Docs`_. Code repository and issue tracker are on diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/coverage-7.5.1/coverage/control.py new/coverage-7.5.3/coverage/control.py --- old/coverage-7.5.1/coverage/control.py 2024-05-04 16:44:25.000000000 +0200 +++ new/coverage-7.5.3/coverage/control.py 2024-05-28 15:52:29.000000000 +0200 @@ -998,7 +998,7 @@ if self.config.paths: mapped_data = CoverageData(warn=self._warn, debug=self._debug, no_disk=True) if self._data is not None: - mapped_data.update(self._data, aliases=self._make_aliases()) + mapped_data.update(self._data, map_path=self._make_aliases().map) self._data = mapped_data def report( diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/coverage-7.5.1/coverage/data.py new/coverage-7.5.3/coverage/data.py --- old/coverage-7.5.1/coverage/data.py 2024-05-04 16:44:25.000000000 +0200 +++ new/coverage-7.5.3/coverage/data.py 2024-05-28 15:52:29.000000000 +0200 @@ -12,6 +12,7 @@ from __future__ import annotations +import functools import glob import hashlib import os.path @@ -134,6 +135,11 @@ if strict and not files_to_combine: raise NoDataError("No data to combine") + if aliases is None: + map_path = None + else: + map_path = functools.lru_cache(maxsize=None)(aliases.map) + file_hashes = set() combined_any = False @@ -176,7 +182,7 @@ message(f"Couldn't combine data file {rel_file_name}: {exc}") delete_this_one = False else: - data.update(new_data, aliases=aliases) + data.update(new_data, map_path=map_path) combined_any = True if message: message(f"Combined data file {rel_file_name}") diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/coverage-7.5.1/coverage/html.py new/coverage-7.5.3/coverage/html.py --- old/coverage-7.5.1/coverage/html.py 2024-05-04 16:44:25.000000000 +0200 +++ new/coverage-7.5.3/coverage/html.py 2024-05-28 15:52:29.000000000 +0200 @@ -597,7 +597,7 @@ "regions": index_page.summaries, "totals": index_page.totals, "noun": index_page.noun, - "column2": index_page.noun if index_page.noun != "file" else "", + "region_noun": index_page.noun if index_page.noun != "file" else "", "skip_covered": self.skip_covered, "skipped_covered_msg": skipped_covered_msg, "skipped_empty_msg": skipped_empty_msg, diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/coverage-7.5.1/coverage/htmlfiles/coverage_html.js new/coverage-7.5.3/coverage/htmlfiles/coverage_html.js --- old/coverage-7.5.1/coverage/htmlfiles/coverage_html.js 2024-05-04 16:44:25.000000000 +0200 +++ new/coverage-7.5.3/coverage/htmlfiles/coverage_html.js 2024-05-28 15:52:29.000000000 +0200 @@ -125,6 +125,16 @@ // Create the events for the filter box. coverage.wire_up_filter = function () { + // Populate the filter and hide100 inputs if there are saved values for them. + const saved_filter_value = localStorage.getItem(coverage.FILTER_STORAGE); + if (saved_filter_value) { + document.getElementById("filter").value = saved_filter_value; + } + const saved_hide100_value = localStorage.getItem(coverage.HIDE100_STORAGE); + if (saved_hide100_value) { + document.getElementById("hide100").checked = JSON.parse(saved_hide100_value); + } + // Cache elements. const table = document.querySelector("table.index"); const table_body_rows = table.querySelectorAll("tbody tr"); @@ -138,8 +148,12 @@ totals[totals.length - 1] = { "numer": 0, "denom": 0 }; // nosemgrep: eslint.detect-object-injection var text = document.getElementById("filter").value; + // Store filter value + localStorage.setItem(coverage.FILTER_STORAGE, text); const casefold = (text === text.toLowerCase()); const hide100 = document.getElementById("hide100").checked; + // Store hide value. + localStorage.setItem(coverage.HIDE100_STORAGE, JSON.stringify(hide100)); // Hide / show elements. table_body_rows.forEach(row => { @@ -240,6 +254,8 @@ document.getElementById("filter").dispatchEvent(new Event("input")); document.getElementById("hide100").dispatchEvent(new Event("input")); }; +coverage.FILTER_STORAGE = "COVERAGE_FILTER_VALUE"; +coverage.HIDE100_STORAGE = "COVERAGE_HIDE100_VALUE"; // Set up the click-to-sort columns. coverage.wire_up_sorting = function () { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/coverage-7.5.1/coverage/htmlfiles/index.html new/coverage-7.5.3/coverage/htmlfiles/index.html --- old/coverage-7.5.1/coverage/htmlfiles/index.html 2024-05-04 16:44:25.000000000 +0200 +++ new/coverage-7.5.3/coverage/htmlfiles/index.html 2024-05-28 15:52:29.000000000 +0200 @@ -31,7 +31,7 @@ <div class="keyhelp"> <p> <kbd>f</kbd> - {% if column2 %} + {% if region_noun %} <kbd>n</kbd> {% endif %} <kbd>s</kbd> @@ -83,8 +83,8 @@ {# The title="" attr doesn't work in Safari. #} <tr class="tablehead" title="Click to sort"> <th id="file" class="name left" aria-sort="none" data-shortcut="f">File<span class="arrows"></span></th> - {% if column2 %} - <th id="region" class="name left" aria-sort="none" data-default-sort-order="ascending" data-shortcut="n">{{ column2 }}<span class="arrows"></span></th> + {% if region_noun %} + <th id="region" class="name left" aria-sort="none" data-default-sort-order="ascending" data-shortcut="n">{{ region_noun }}<span class="arrows"></span></th> {% endif %} <th id="statements" aria-sort="none" data-default-sort-order="descending" data-shortcut="s">statements<span class="arrows"></span></th> <th id="missing" aria-sort="none" data-default-sort-order="descending" data-shortcut="m">missing<span class="arrows"></span></th> @@ -100,7 +100,7 @@ {% for region in regions %} <tr class="region"> <td class="name left"><a href="{{region.url}}">{{region.file}}</a></td> - {% if column2 %} + {% if region_noun %} <td class="name left"><a href="{{region.url}}">{{region.description}}</a></td> {% endif %} <td>{{region.nums.n_statements}}</td> @@ -117,7 +117,7 @@ <tfoot> <tr class="total"> <td class="name left">Total</td> - {% if column2 %} + {% if region_noun %} <td class="name left"> </td> {% endif %} <td>{{totals.n_statements}}</td> diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/coverage-7.5.1/coverage/misc.py new/coverage-7.5.3/coverage/misc.py --- old/coverage-7.5.1/coverage/misc.py 2024-05-04 16:44:25.000000000 +0200 +++ new/coverage-7.5.3/coverage/misc.py 2024-05-28 15:52:29.000000000 +0200 @@ -13,7 +13,6 @@ import importlib import importlib.util import inspect -import locale import os import os.path import re @@ -22,7 +21,7 @@ from types import ModuleType from typing import ( - Any, IO, Iterable, Iterator, Mapping, NoReturn, Sequence, TypeVar, + Any, Iterable, Iterator, Mapping, NoReturn, Sequence, TypeVar, ) from coverage.exceptions import CoverageException @@ -156,18 +155,6 @@ ensure_dir(os.path.dirname(path)) -def output_encoding(outfile: IO[str] | None = None) -> str: - """Determine the encoding to use for output written to `outfile` or stdout.""" - if outfile is None: - outfile = sys.stdout - encoding = ( - getattr(outfile, "encoding", None) or - getattr(sys.__stdout__, "encoding", None) or - locale.getpreferredencoding() - ) - return encoding - - class Hasher: """Hashes Python data for fingerprinting.""" def __init__(self) -> None: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/coverage-7.5.1/coverage/parser.py new/coverage-7.5.3/coverage/parser.py --- old/coverage-7.5.1/coverage/parser.py 2024-05-04 16:44:25.000000000 +0200 +++ new/coverage-7.5.3/coverage/parser.py 2024-05-28 15:52:29.000000000 +0200 @@ -25,7 +25,7 @@ from coverage.bytecode import code_objects from coverage.debug import short_stack from coverage.exceptions import NoSource, NotPython -from coverage.misc import join_regex, nice_pair +from coverage.misc import nice_pair from coverage.phystokens import generate_tokens from coverage.types import TArc, TLineNo @@ -62,8 +62,8 @@ self.exclude = exclude - # The text lines of the parsed code. - self.lines: list[str] = self.text.split("\n") + # The parsed AST of the text. + self._ast_root: ast.AST | None = None # The normalized line numbers of the statements in the code. Exclusions # are taken into account, and statements are adjusted to their first @@ -101,19 +101,16 @@ self._all_arcs: set[TArc] | None = None self._missing_arc_fragments: TArcFragments | None = None - @functools.lru_cache() - def lines_matching(self, *regexes: str) -> set[TLineNo]: - """Find the lines matching one of a list of regexes. + def lines_matching(self, regex: str) -> set[TLineNo]: + """Find the lines matching a regex. - Returns a set of line numbers, the lines that contain a match for one - of the regexes in `regexes`. The entire line needn't match, just a - part of it. + Returns a set of line numbers, the lines that contain a match for + `regex`. The entire line needn't match, just a part of it. """ - combined = join_regex(regexes) - regex_c = re.compile(combined) + regex_c = re.compile(regex) matches = set() - for i, ltext in enumerate(self.lines, start=1): + for i, ltext in enumerate(self.text.split("\n"), start=1): if regex_c.search(ltext): matches.add(self._multiline.get(i, i)) return matches @@ -127,26 +124,18 @@ # Find lines which match an exclusion pattern. if self.exclude: self.raw_excluded = self.lines_matching(self.exclude) + self.excluded = set(self.raw_excluded) - # Tokenize, to find excluded suites, to find docstrings, and to find - # multi-line statements. - - # The last token seen. Start with INDENT to get module docstrings - prev_toktype: int = token.INDENT # The current number of indents. indent: int = 0 # An exclusion comment will exclude an entire clause at this indent. exclude_indent: int = 0 # Are we currently excluding lines? excluding: bool = False - # Are we excluding decorators now? - excluding_decorators: bool = False # The line number of the first line in a multi-line statement. first_line: int = 0 # Is the file empty? empty: bool = True - # Is this the first token on a line? - first_on_line: bool = True # Parenthesis (and bracket) nesting level. nesting: int = 0 @@ -162,42 +151,22 @@ indent += 1 elif toktype == token.DEDENT: indent -= 1 - elif toktype == token.NAME: - if ttext == "class": - # Class definitions look like branches in the bytecode, so - # we need to exclude them. The simplest way is to note the - # lines with the "class" keyword. - self.raw_classdefs.add(slineno) elif toktype == token.OP: if ttext == ":" and nesting == 0: should_exclude = ( - self.raw_excluded.intersection(range(first_line, elineno + 1)) - or excluding_decorators + self.excluded.intersection(range(first_line, elineno + 1)) ) if not excluding and should_exclude: # Start excluding a suite. We trigger off of the colon # token so that the #pragma comment will be recognized on # the same line as the colon. - self.raw_excluded.add(elineno) + self.excluded.add(elineno) exclude_indent = indent excluding = True - excluding_decorators = False - elif ttext == "@" and first_on_line: - # A decorator. - if elineno in self.raw_excluded: - excluding_decorators = True - if excluding_decorators: - self.raw_excluded.add(elineno) elif ttext in "([{": nesting += 1 elif ttext in ")]}": nesting -= 1 - elif toktype == token.STRING: - if prev_toktype == token.INDENT: - # Strings that are first on an indented line are docstrings. - # (a trick from trace.py in the stdlib.) This works for - # 99.9999% of cases. - self.raw_docstrings.update(range(slineno, elineno+1)) elif toktype == token.NEWLINE: if first_line and elineno != first_line: # We're at the end of a line, and we've ended on a @@ -206,7 +175,6 @@ for l in range(first_line, elineno+1): self._multiline[l] = first_line first_line = 0 - first_on_line = True if ttext.strip() and toktype != tokenize.COMMENT: # A non-white-space token. @@ -218,10 +186,7 @@ if excluding and indent <= exclude_indent: excluding = False if excluding: - self.raw_excluded.add(elineno) - first_on_line = False - - prev_toktype = toktype + self.excluded.add(elineno) # Find the starts of the executable statements. if not empty: @@ -234,6 +199,34 @@ if env.PYBEHAVIOR.module_firstline_1 and self._multiline: self._multiline[1] = min(self.raw_statements) + self.excluded = self.first_lines(self.excluded) + + # AST lets us find classes, docstrings, and decorator-affected + # functions and classes. + assert self._ast_root is not None + for node in ast.walk(self._ast_root): + # Find class definitions. + if isinstance(node, ast.ClassDef): + self.raw_classdefs.add(node.lineno) + # Find docstrings. + if isinstance(node, (ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef, ast.Module)): + if node.body: + first = node.body[0] + if ( + isinstance(first, ast.Expr) + and isinstance(first.value, ast.Constant) + and isinstance(first.value.value, str) + ): + self.raw_docstrings.update( + range(first.lineno, cast(int, first.end_lineno) + 1) + ) + # Exclusions carry from decorators and signatures to the bodies of + # functions and classes. + if isinstance(node, (ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)): + first_line = min((d.lineno for d in node.decorator_list), default=node.lineno) + if self.excluded.intersection(range(first_line, node.lineno + 1)): + self.excluded.update(range(first_line, cast(int, node.end_lineno) + 1)) + @functools.lru_cache(maxsize=1000) def first_line(self, lineno: TLineNo) -> TLineNo: """Return the first line number of the statement including `lineno`.""" @@ -268,6 +261,7 @@ """ try: + self._ast_root = ast.parse(self.text) self._raw_parse() except (tokenize.TokenError, IndentationError, SyntaxError) as err: if hasattr(err, "lineno"): @@ -279,8 +273,6 @@ f"{err.args[0]!r} at line {lineno}", ) from err - self.excluded = self.first_lines(self.raw_excluded) - ignore = self.excluded | self.raw_docstrings starts = self.raw_statements - ignore self.statements = self.first_lines(starts) - ignore @@ -303,7 +295,8 @@ `_all_arcs` is the set of arcs in the code. """ - aaa = AstArcAnalyzer(self.text, self.raw_statements, self._multiline) + assert self._ast_root is not None + aaa = AstArcAnalyzer(self._ast_root, self.raw_statements, self._multiline) aaa.analyze() self._all_arcs = set() @@ -403,14 +396,9 @@ self.code = code else: assert filename is not None - try: - self.code = compile(text, filename, "exec", dont_inherit=True) - except SyntaxError as synerr: - raise NotPython( - "Couldn't parse '%s' as Python source: '%s' at line %d" % ( - filename, synerr.msg, synerr.lineno or 0, - ), - ) from synerr + # We only get here if earlier ast parsing succeeded, so no need to + # catch errors. + self.code = compile(text, filename, "exec", dont_inherit=True) def child_parsers(self) -> Iterable[ByteParser]: """Iterate over all the code objects nested within this one. @@ -685,11 +673,11 @@ def __init__( self, - text: str, + root_node: ast.AST, statements: set[TLineNo], multiline: dict[TLineNo, TLineNo], ) -> None: - self.root_node = ast.parse(text) + self.root_node = root_node # TODO: I think this is happening in too many places. self.statements = {multiline.get(l, l) for l in statements} self.multiline = multiline diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/coverage-7.5.1/coverage/phystokens.py new/coverage-7.5.3/coverage/phystokens.py --- old/coverage-7.5.1/coverage/phystokens.py 2024-05-04 16:44:25.000000000 +0200 +++ new/coverage-7.5.3/coverage/phystokens.py 2024-05-28 15:52:29.000000000 +0200 @@ -6,7 +6,6 @@ from __future__ import annotations import ast -import functools import io import keyword import re @@ -163,20 +162,15 @@ yield line -@functools.lru_cache(maxsize=100) def generate_tokens(text: str) -> TokenInfos: - """A cached version of `tokenize.generate_tokens`. + """A helper around `tokenize.generate_tokens`. + + Originally this was used to cache the results, but it didn't seem to make + reporting go faster, and caused issues with using too much memory. - When reporting, coverage.py tokenizes files twice, once to find the - structure of the file, and once to syntax-color it. Tokenizing is - expensive, and easily cached. - - Unfortunately, the HTML report code tokenizes all the files the first time - before then tokenizing them a second time, so we cache many. Ideally we'd - rearrange the code to tokenize each file twice before moving onto the next. """ readline = io.StringIO(text).readline - return list(tokenize.generate_tokens(readline)) + return tokenize.generate_tokens(readline) def source_encoding(source: bytes) -> str: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/coverage-7.5.1/coverage/python.py new/coverage-7.5.3/coverage/python.py --- old/coverage-7.5.1/coverage/python.py 2024-05-04 16:44:25.000000000 +0200 +++ new/coverage-7.5.3/coverage/python.py 2024-05-28 15:52:29.000000000 +0200 @@ -206,8 +206,10 @@ def no_branch_lines(self) -> set[TLineNo]: assert self.coverage is not None 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_list + + self.coverage.config.partial_always_list + ) ) return no_branch diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/coverage-7.5.1/coverage/pytracer.py new/coverage-7.5.3/coverage/pytracer.py --- old/coverage-7.5.1/coverage/pytracer.py 2024-05-04 16:44:25.000000000 +0200 +++ new/coverage-7.5.3/coverage/pytracer.py 2024-05-28 15:52:29.000000000 +0200 @@ -166,12 +166,12 @@ if event == "call": # Should we start a new context? if self.should_start_context and self.context is None: - context_maybe = self.should_start_context(frame) + context_maybe = self.should_start_context(frame) # pylint: disable=not-callable if context_maybe is not None: self.context = context_maybe started_context = True assert self.switch_context is not None - self.switch_context(self.context) + self.switch_context(self.context) # pylint: disable=not-callable else: started_context = False else: @@ -280,7 +280,7 @@ if self.started_context: assert self.switch_context is not None self.context = None - self.switch_context(None) + self.switch_context(None) # pylint: disable=not-callable return self._cached_bound_method_trace def start(self) -> TTraceFn: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/coverage-7.5.1/coverage/sqldata.py new/coverage-7.5.3/coverage/sqldata.py --- old/coverage-7.5.1/coverage/sqldata.py 2024-05-04 16:44:25.000000000 +0200 +++ new/coverage-7.5.3/coverage/sqldata.py 2024-05-28 15:52:29.000000000 +0200 @@ -21,13 +21,12 @@ import zlib from typing import ( - cast, Any, Collection, Mapping, + cast, Any, Callable, Collection, Mapping, Sequence, ) from coverage.debug import NoDebugging, auto_repr from coverage.exceptions import CoverageException, DataError -from coverage.files import PathAliases from coverage.misc import file_be_gone, isolate_module from coverage.numbits import numbits_to_nums, numbits_union, nums_to_numbits from coverage.sqlitedb import SqliteDb @@ -647,12 +646,16 @@ continue con.execute_void(sql, (file_id,)) - def update(self, other_data: CoverageData, aliases: PathAliases | None = None) -> None: - """Update this data with data from several other :class:`CoverageData` instances. + def update( + self, + other_data: CoverageData, + map_path: Callable[[str], str] | None = None, + ) -> None: + """Update this data with data from another :class:`CoverageData`. - If `aliases` is provided, it's a `PathAliases` object that is used to - re-map paths to match the local machine's. Note: `aliases` is None - only when called directly from the test suite. + If `map_path` is provided, it's a function that re-map paths to match + the local machine's. Note: `map_path` is None only when called + directly from the test suite. """ if self._debug.should("dataop"): @@ -664,7 +667,7 @@ if self._has_arcs and other_data._has_lines: raise DataError("Can't combine line data with arc data") - aliases = aliases or PathAliases() + map_path = map_path or (lambda p: p) # Force the database we're writing to to exist before we start nesting contexts. self._start_using() @@ -674,7 +677,7 @@ with other_data._connect() as con: # Get files data. with con.execute("select path from file") as cur: - files = {path: aliases.map(path) for (path,) in cur} + files = {path: map_path(path) for (path,) in cur} # Get contexts data. with con.execute("select context from context") as cur: @@ -729,7 +732,7 @@ "inner join file on file.id = tracer.file_id", ) as cur: this_tracers.update({ - aliases.map(path): tracer + map_path(path): tracer for path, tracer in cur }) @@ -767,27 +770,15 @@ # Prepare arc and line rows to be inserted by converting the file # and context strings with integer ids. Then use the efficient # `executemany()` to insert all rows at once. - arc_rows = ( - (file_ids[file], context_ids[context], fromno, tono) - for file, context, fromno, tono in arcs - ) - - # Get line data. - with 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", - ) as cur: - for path, context, numbits in cur: - key = (aliases.map(path), context) - if key in lines: - numbits = numbits_union(lines[key], numbits) - lines[key] = numbits if arcs: self._choose_lines_or_arcs(arcs=True) + arc_rows = ( + (file_ids[file], context_ids[context], fromno, tono) + for file, context, fromno, tono in arcs + ) + # Write the combined data. con.executemany_void( "insert or ignore into arc " + @@ -797,15 +788,25 @@ if lines: self._choose_lines_or_arcs(lines=True) - con.execute_void("delete from line_bits") + + for (file, context), numbits in lines.items(): + with con.execute( + "select numbits from line_bits where file_id = ? and context_id = ?", + (file_ids[file], context_ids[context]), + ) as cur: + existing = list(cur) + if existing: + lines[(file, context)] = numbits_union(numbits, existing[0][0]) + con.executemany_void( - "insert into line_bits " + + "insert or replace into line_bits " + "(file_id, context_id, numbits) values (?, ?, ?)", [ (file_ids[file], context_ids[context], numbits) for (file, context), numbits in lines.items() ], ) + con.executemany_void( "insert or ignore into tracer (file_id, tracer) values (?, ?)", ((file_ids[filename], tracer) for filename, tracer in tracer_map.items()), diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/coverage-7.5.1/coverage/version.py new/coverage-7.5.3/coverage/version.py --- old/coverage-7.5.1/coverage/version.py 2024-05-04 16:44:25.000000000 +0200 +++ new/coverage-7.5.3/coverage/version.py 2024-05-28 15:52:29.000000000 +0200 @@ -8,7 +8,7 @@ # version_info: same semantics as sys.version_info. # _dev: the .devN suffix if any. -version_info = (7, 5, 1, "final", 0) +version_info = (7, 5, 3, "final", 0) _dev = 0 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/coverage-7.5.1/coverage.egg-info/PKG-INFO new/coverage-7.5.3/coverage.egg-info/PKG-INFO --- old/coverage-7.5.1/coverage.egg-info/PKG-INFO 2024-05-04 16:44:36.000000000 +0200 +++ new/coverage-7.5.3/coverage.egg-info/PKG-INFO 2024-05-28 15:52:36.000000000 +0200 @@ -1,12 +1,12 @@ Metadata-Version: 2.1 Name: coverage -Version: 7.5.1 +Version: 7.5.3 Summary: Code coverage measurement for Python Home-page: https://github.com/nedbat/coveragepy -Author: Ned Batchelder and 226 others +Author: Ned Batchelder and 227 others Author-email: ned@nedbatchelder.com License: Apache-2.0 -Project-URL: Documentation, https://coverage.readthedocs.io/en/7.5.1 +Project-URL: Documentation, https://coverage.readthedocs.io/en/7.5.3 Project-URL: Funding, https://tidelift.com/subscription/pkg/pypi-coverage?utm_source=pypi-coverage&utm_medium=referral&utm_campaign=pypi Project-URL: Issues, https://github.com/nedbat/coveragepy/issues Project-URL: Mastodon, https://hachyderm.io/@coveragepy @@ -62,13 +62,13 @@ .. PYVERSIONS -* Python 3.8 through 3.12, and 3.13.0a6 and up. +* Python 3.8 through 3.12, and 3.13.0b1 and up. * PyPy3 versions 3.8 through 3.10. Documentation is on `Read the Docs`_. Code repository and issue tracker are on `GitHub`_. -.. _Read the Docs: https://coverage.readthedocs.io/en/7.5.1/ +.. _Read the Docs: https://coverage.readthedocs.io/en/7.5.3/ .. _GitHub: https://github.com/nedbat/coveragepy **New in 7.x:** @@ -112,7 +112,7 @@ Looking to run ``coverage`` on your test suite? See the `Quick Start section`_ of the docs. -.. _Quick Start section: https://coverage.readthedocs.io/en/7.5.1/#quick-start +.. _Quick Start section: https://coverage.readthedocs.io/en/7.5.3/#quick-start Change history @@ -120,7 +120,7 @@ The complete history of changes is on the `change history page`_. -.. _change history page: https://coverage.readthedocs.io/en/7.5.1/changes.html +.. _change history page: https://coverage.readthedocs.io/en/7.5.3/changes.html Code of Conduct @@ -139,7 +139,7 @@ Found a bug? Want to help improve the code or documentation? See the `Contributing section`_ of the docs. -.. _Contributing section: https://coverage.readthedocs.io/en/7.5.1/contributing.html +.. _Contributing section: https://coverage.readthedocs.io/en/7.5.3/contributing.html Security @@ -167,7 +167,7 @@ :target: https://github.com/nedbat/coveragepy/actions/workflows/quality.yml :alt: Quality check status .. |docs| image:: https://readthedocs.org/projects/coverage/badge/?version=latest&style=flat - :target: https://coverage.readthedocs.io/en/7.5.1/ + :target: https://coverage.readthedocs.io/en/7.5.3/ :alt: Documentation .. |kit| image:: https://img.shields.io/pypi/v/coverage :target: https://pypi.org/project/coverage/ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/coverage-7.5.1/doc/branch.rst new/coverage-7.5.3/doc/branch.rst --- old/coverage-7.5.1/doc/branch.rst 2024-05-04 16:44:25.000000000 +0200 +++ new/coverage-7.5.3/doc/branch.rst 2024-05-28 15:52:29.000000000 +0200 @@ -116,3 +116,16 @@ at some point. Coverage.py can't work that out on its own, but the "no branch" pragma indicates that the branch is known to be partial, and the line is not flagged. + +Generator expressions +===================== + +Generator expressions may also report partial branch coverage. Consider the +following example:: + + value = next(i in range(1)) + +While we might expect this line of code to be reported as covered, the +generator did not iterate until ``StopIteration`` is raised, the indication +that the loop is complete. This is another case +where adding ``# pragma: no branch`` may be desirable. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/coverage-7.5.1/doc/changes.rst new/coverage-7.5.3/doc/changes.rst --- old/coverage-7.5.1/doc/changes.rst 2024-05-04 16:44:25.000000000 +0200 +++ new/coverage-7.5.3/doc/changes.rst 2024-05-28 15:52:29.000000000 +0200 @@ -845,10 +845,10 @@ would cause a "No data to report" error, as reported in `issue 549`_. This is now fixed; thanks, Loïc Dachary. -- If-statements can be optimized away during compilation, for example, `if 0:` - or `if __debug__:`. Coverage.py had problems properly understanding these - statements which existed in the source, but not in the compiled bytecode. - This problem, reported in `issue 522`_, is now fixed. +- If-statements can be optimized away during compilation, for example, + ``if 0:`` or ``if __debug__:``. Coverage.py had problems properly + understanding these statements which existed in the source, but not in the + compiled bytecode. This problem, reported in `issue 522`_, is now fixed. - If you specified ``--source`` as a directory, then coverage.py would look for importable Python files in that directory, and could identify ones that had diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/coverage-7.5.1/doc/conf.py new/coverage-7.5.3/doc/conf.py --- old/coverage-7.5.1/doc/conf.py 2024-05-04 16:44:25.000000000 +0200 +++ new/coverage-7.5.3/doc/conf.py 2024-05-28 15:52:29.000000000 +0200 @@ -67,11 +67,11 @@ # @@@ editable copyright = "2009–2024, Ned Batchelder" # pylint: disable=redefined-builtin # The short X.Y.Z version. -version = "7.5.1" +version = "7.5.3" # The full version, including alpha/beta/rc tags. -release = "7.5.1" +release = "7.5.3" # The date of release, in "monthname day, year" format. -release_date = "May 4, 2024" +release_date = "May 28, 2024" # @@@ end rst_epilog = f""" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/coverage-7.5.1/doc/index.rst new/coverage-7.5.3/doc/index.rst --- old/coverage-7.5.1/doc/index.rst 2024-05-04 16:44:25.000000000 +0200 +++ new/coverage-7.5.3/doc/index.rst 2024-05-28 15:52:29.000000000 +0200 @@ -18,7 +18,7 @@ .. PYVERSIONS -* Python 3.8 through 3.12, and 3.13.0a6 and up. +* Python 3.8 through 3.12, and 3.13.0b1 and up. * PyPy3 versions 3.8 through 3.10. .. ifconfig:: prerelease diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/coverage-7.5.1/doc/requirements.in new/coverage-7.5.3/doc/requirements.in --- old/coverage-7.5.1/doc/requirements.in 2024-05-04 16:44:25.000000000 +0200 +++ new/coverage-7.5.3/doc/requirements.in 2024-05-28 15:52:29.000000000 +0200 @@ -14,5 +14,6 @@ sphinx-autobuild sphinx_rtd_theme sphinx-code-tabs +sphinx-lint sphinxcontrib-restbuilder sphinxcontrib-spelling diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/coverage-7.5.1/doc/requirements.pip new/coverage-7.5.3/doc/requirements.pip --- old/coverage-7.5.1/doc/requirements.pip 2024-05-04 16:44:25.000000000 +0200 +++ new/coverage-7.5.3/doc/requirements.pip 2024-05-28 15:52:29.000000000 +0200 @@ -12,7 +12,7 @@ # watchfiles attrs==23.2.0 # via scriv -babel==2.14.0 +babel==2.15.0 # via sphinx certifi==2024.2.2 # via requests @@ -45,7 +45,7 @@ # requests imagesize==1.4.1 # via sphinx -jinja2==3.1.3 +jinja2==3.1.4 # via # scriv # sphinx @@ -59,14 +59,18 @@ # via sphinx pbr==6.0.0 # via stevedore +polib==1.2.0 + # via sphinx-lint pyenchant==3.2.2 # via # -r doc/requirements.in # sphinxcontrib-spelling -pygments==2.17.2 +pygments==2.18.0 # via # doc8 # sphinx +regex==2024.4.28 + # via sphinx-lint requests==2.31.0 # via # scriv @@ -92,6 +96,8 @@ # via -r doc/requirements.in sphinx-code-tabs==0.5.5 # via -r doc/requirements.in +sphinx-lint==0.9.1 + # via -r doc/requirements.in sphinx-rtd-theme==2.0.0 # via -r doc/requirements.in sphinxcontrib-applehelp==1.0.8 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/coverage-7.5.1/howto.txt new/coverage-7.5.3/howto.txt --- old/coverage-7.5.1/howto.txt 2024-05-04 16:44:25.000000000 +0200 +++ new/coverage-7.5.3/howto.txt 2024-05-28 15:52:29.000000000 +0200 @@ -34,10 +34,11 @@ - check in the new sample html $ make relcommit2 - Done with changes to source files - - check them in on the release prep branch - - wait for ci to finish - - merge to master - - git push + - g puo; gshipit + - check them in on the release prep branch + - wait for ci to finish + - merge to master + - git push - Start the kits: - opvars github - Trigger the kit GitHub Action @@ -77,10 +78,8 @@ - IF NOT PRE-RELEASE: - @ https://readthedocs.org/dashboard/coverage/advanced/ - change the "default version" to the new version - - @ https://readthedocs.org/projects/coverage/builds/ - - manually build "latest" - - wait for the new tag build to finish successfully. - Once CI passes, merge the bump-version branch to master and push it + - gshipit - things to automate: - readthedocs api to do the readthedocs changes diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/coverage-7.5.1/igor.py new/coverage-7.5.3/igor.py --- old/coverage-7.5.1/igor.py 2024-05-04 16:44:25.000000000 +0200 +++ new/coverage-7.5.3/igor.py 2024-05-28 15:52:29.000000000 +0200 @@ -248,6 +248,7 @@ os.getenv("COVERAGE_DYNCTX") or os.getenv("COVERAGE_CONTEXT"), ) cov.html_report(show_contexts=show_contexts) + cov.json_report(show_contexts=show_contexts, pretty_print=True) def do_test_with_core(core, *runner_args): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/coverage-7.5.1/lab/extract_code.py new/coverage-7.5.3/lab/extract_code.py --- old/coverage-7.5.1/lab/extract_code.py 2024-05-04 16:44:25.000000000 +0200 +++ new/coverage-7.5.3/lab/extract_code.py 2024-05-28 15:52:29.000000000 +0200 @@ -5,8 +5,8 @@ Use this to copy some indented code from the coverage.py test suite into a standalone file for deeper testing, or writing bug reports. -Give it a file name and a line number, and it will find the indentend -multiline string containing that line number, and output the dedented +Give it a file name and a line number, and it will find the indented +multi-line string containing that line number, and output the dedented contents of the string. If tests/test_arcs.py has this (partial) content:: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/coverage-7.5.1/lab/parser.py new/coverage-7.5.3/lab/parser.py --- old/coverage-7.5.1/lab/parser.py 2024-05-04 16:44:25.000000000 +0200 +++ new/coverage-7.5.3/lab/parser.py 2024-05-28 15:52:29.000000000 +0200 @@ -80,7 +80,7 @@ if options.dis: print("Main code:") - disassemble(pyparser) + disassemble(pyparser.text) arcs = pyparser.arcs() @@ -95,8 +95,8 @@ exit_counts = pyparser.exit_counts() - for lineno, ltext in enumerate(pyparser.lines, start=1): - marks = [' ', ' ', ' ', ' ', ' '] + for lineno, ltext in enumerate(pyparser.text.splitlines(), start=1): + marks = [' '] * 6 a = ' ' if lineno in pyparser.raw_statements: marks[0] = '-' @@ -110,7 +110,13 @@ if lineno in pyparser.raw_classdefs: marks[3] = 'C' if lineno in pyparser.raw_excluded: - marks[4] = 'x' + marks[4] = 'X' + elif lineno in pyparser.excluded: + marks[4] = '×' + if lineno in pyparser._multiline.values(): + marks[5] = 'o' + elif lineno in pyparser._multiline.keys(): + marks[5] = '.' if arc_chars: a = arc_chars[lineno].ljust(arc_width) @@ -173,13 +179,13 @@ yield code -def disassemble(pyparser): +def disassemble(text): """Disassemble code, for ad-hoc experimenting.""" - code = compile(pyparser.text, "", "exec", dont_inherit=True) + code = compile(text, "", "exec", dont_inherit=True) for code_obj in all_code_objects(code): - if pyparser.text: - srclines = pyparser.text.splitlines() + if text: + srclines = text.splitlines() else: srclines = None print("\n%s: " % code_obj) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/coverage-7.5.1/tests/coveragetest.py new/coverage-7.5.3/tests/coveragetest.py --- old/coverage-7.5.1/tests/coveragetest.py 2024-05-04 16:44:25.000000000 +0200 +++ new/coverage-7.5.3/tests/coveragetest.py 2024-05-28 15:52:29.000000000 +0200 @@ -151,7 +151,7 @@ self, text: str, lines: Sequence[TLineNo] | Sequence[list[TLineNo]] | None = None, - missing: str | Sequence[str] = "", + missing: str = "", report: str = "", excludes: Iterable[str] | None = None, partials: Iterable[str] = (), @@ -226,15 +226,8 @@ assert False, f"None of the lines choices matched {statements!r}" missing_formatted = analysis.missing_formatted() - if isinstance(missing, str): - msg = f"missing: {missing_formatted!r} != {missing!r}" - assert missing_formatted == missing, msg - else: - for missing_list in missing: - if missing_formatted == missing_list: - break - else: - assert False, f"None of the missing choices matched {missing_formatted!r}" + msg = f"missing: {missing_formatted!r} != {missing!r}" + assert missing_formatted == missing, msg if arcs is not None: # print("Possible arcs:") diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/coverage-7.5.1/tests/gold/html/support/coverage_html.js new/coverage-7.5.3/tests/gold/html/support/coverage_html.js --- old/coverage-7.5.1/tests/gold/html/support/coverage_html.js 2024-05-04 16:44:25.000000000 +0200 +++ new/coverage-7.5.3/tests/gold/html/support/coverage_html.js 2024-05-28 15:52:29.000000000 +0200 @@ -125,6 +125,16 @@ // Create the events for the filter box. coverage.wire_up_filter = function () { + // Populate the filter and hide100 inputs if there are saved values for them. + const saved_filter_value = localStorage.getItem(coverage.FILTER_STORAGE); + if (saved_filter_value) { + document.getElementById("filter").value = saved_filter_value; + } + const saved_hide100_value = localStorage.getItem(coverage.HIDE100_STORAGE); + if (saved_hide100_value) { + document.getElementById("hide100").checked = JSON.parse(saved_hide100_value); + } + // Cache elements. const table = document.querySelector("table.index"); const table_body_rows = table.querySelectorAll("tbody tr"); @@ -138,8 +148,12 @@ totals[totals.length - 1] = { "numer": 0, "denom": 0 }; // nosemgrep: eslint.detect-object-injection var text = document.getElementById("filter").value; + // Store filter value + localStorage.setItem(coverage.FILTER_STORAGE, text); const casefold = (text === text.toLowerCase()); const hide100 = document.getElementById("hide100").checked; + // Store hide value. + localStorage.setItem(coverage.HIDE100_STORAGE, JSON.stringify(hide100)); // Hide / show elements. table_body_rows.forEach(row => { @@ -240,6 +254,8 @@ document.getElementById("filter").dispatchEvent(new Event("input")); document.getElementById("hide100").dispatchEvent(new Event("input")); }; +coverage.FILTER_STORAGE = "COVERAGE_FILTER_VALUE"; +coverage.HIDE100_STORAGE = "COVERAGE_HIDE100_VALUE"; // Set up the click-to-sort columns. coverage.wire_up_sorting = function () { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/coverage-7.5.1/tests/helpers.py new/coverage-7.5.3/tests/helpers.py --- old/coverage-7.5.1/tests/helpers.py 2024-05-04 16:44:25.000000000 +0200 +++ new/coverage-7.5.3/tests/helpers.py 2024-05-28 15:52:29.000000000 +0200 @@ -9,6 +9,7 @@ import contextlib import dis import io +import locale import os import os.path import re @@ -28,7 +29,6 @@ from coverage import env from coverage.debug import DebugControl from coverage.exceptions import CoverageWarning -from coverage.misc import output_encoding from coverage.types import TArc, TLineNo @@ -44,11 +44,13 @@ with open("/tmp/processes.txt", "a") as proctxt: # type: ignore[unreachable] print(os.getenv("PYTEST_CURRENT_TEST", "unknown"), file=proctxt, flush=True) + encoding = os.device_encoding(1) or locale.getpreferredencoding() + # In some strange cases (PyPy3 in a virtualenv!?) the stdout encoding of # the subprocess is set incorrectly to ascii. Use an environment variable # to force the encoding to be the same as ours. sub_env = dict(os.environ) - sub_env['PYTHONIOENCODING'] = output_encoding() + sub_env['PYTHONIOENCODING'] = encoding proc = subprocess.Popen( cmd, @@ -62,7 +64,7 @@ status = proc.returncode # Get the output, and canonicalize it to strings with newlines. - output_str = output.decode(output_encoding()).replace("\r", "") + output_str = output.decode(encoding).replace("\r", "") return status, output_str @@ -114,8 +116,11 @@ print(f"# {os.path.abspath(filename)}", file=fdis) cur_test = os.getenv("PYTEST_CURRENT_TEST", "unknown") print(f"# PYTEST_CURRENT_TEST = {cur_test}", file=fdis) + kwargs = {} + if env.PYVERSION >= (3, 13): + kwargs["show_offsets"] = True try: - dis.dis(text, file=fdis) + dis.dis(text, file=fdis, **kwargs) except Exception as exc: # Some tests make .py files that aren't Python, so dis will # fail, which is expected. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/coverage-7.5.1/tests/test_coverage.py new/coverage-7.5.3/tests/test_coverage.py --- old/coverage-7.5.1/tests/test_coverage.py 2024-05-04 16:44:25.000000000 +0200 +++ new/coverage-7.5.3/tests/test_coverage.py 2024-05-28 15:52:29.000000000 +0200 @@ -41,15 +41,6 @@ [1,2,3], missing="3", ) - # You can specify a list of possible missing lines. - self.check_coverage("""\ - a = 1 - if a == 2: - a = 3 - """, - [1,2,3], - missing=("47-49", "3", "100,102"), - ) def test_failed_coverage(self) -> None: # If the lines are wrong, the message shows right and wrong. @@ -79,17 +70,6 @@ [1,2,3], missing="37", ) - # If the missing lines possibilities are wrong, the msg shows right. - msg = r"None of the missing choices matched '3'" - with pytest.raises(AssertionError, match=msg): - self.check_coverage("""\ - a = 1 - if a == 2: - a = 3 - """, - [1,2,3], - missing=("37", "4-10"), - ) def test_exceptions_really_fail(self) -> None: # An assert in the checked code will really raise up to us. @@ -502,6 +482,7 @@ ) def test_strange_unexecuted_continue(self) -> None: + # This used to be true, but no longer is: # Peephole optimization of jumps to jumps can mean that some statements # never hit the line tracer. The behavior is different in different # versions of Python, so be careful when running this test. @@ -529,7 +510,7 @@ assert a == 33 and b == 50 and c == 50 """, lines=[1,2,3,4,5,6,8,9,10, 12,13,14,15,16,17,19,20,21], - missing=["", "6"], + missing="", ) def test_import(self) -> None: @@ -682,14 +663,13 @@ """, [2, 3], ) - lines = [2, 3, 4] self.check_coverage("""\ - # Start with a comment, because it changes the behavior(!?) + # Start with a comment, even though it doesn't change the behavior. '''I am a module docstring.''' a = 3 b = 4 """, - lines, + [3, 4], ) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/coverage-7.5.1/tests/test_parser.py new/coverage-7.5.3/tests/test_parser.py --- old/coverage-7.5.1/tests/test_parser.py 2024-05-04 16:44:25.000000000 +0200 +++ new/coverage-7.5.3/tests/test_parser.py 2024-05-28 15:52:29.000000000 +0200 @@ -124,30 +124,23 @@ """) assert parser.exit_counts() == { 1:1, 2:1, 3:1, 6:1 } - def test_indentation_error(self) -> None: - msg = ( - "Couldn't parse '<code>' as Python source: " + - "'unindent does not match any outer indentation level.*' at line 3" - ) - with pytest.raises(NotPython, match=msg): - _ = self.parse_text("""\ - 0 spaces - 2 - 1 - """) - - def test_token_error(self) -> None: - submsgs = [ - r"EOF in multi-line string", # before 3.12.0b1 - r"unterminated triple-quoted string literal .detected at line 1.", # after 3.12.0b1 - ] - msg = ( - r"Couldn't parse '<code>' as Python source: '" - + r"(" + "|".join(submsgs) + ")" - + r"' at line 1" - ) + @pytest.mark.parametrize("text", [ + pytest.param("0 spaces\n 2\n 1", id="bad_indent"), + pytest.param("'''", id="string_eof"), + pytest.param("$hello", id="dollar"), + # on 3.10 this passes ast.parse but fails on tokenize.generate_tokens + pytest.param( + "\r'\\\n'''", + id="leading_newline_eof", + marks=[ + pytest.mark.skipif(env.PYVERSION >= (3, 12), reason="parses fine in 3.12"), + ] + ) + ]) + def test_not_python(self, text: str) -> None: + msg = r"Couldn't parse '<code>' as Python source: '.*' at line \d+" with pytest.raises(NotPython, match=msg): - _ = self.parse_text("'''") + _ = self.parse_text(text) def test_empty_decorated_function(self) -> None: parser = self.parse_text("""\ @@ -180,6 +173,20 @@ assert expected_arcs == parser.arcs() assert expected_exits == parser.exit_counts() + def test_module_docstrings(self) -> None: + parser = self.parse_text("""\ + '''The docstring on line 1''' + a = 2 + """) + assert {2} == parser.statements + + parser = self.parse_text("""\ + # Docstring is not line 1 + '''The docstring on line 2''' + a = 3 + """) + assert {3} == parser.statements + def test_fuzzed_double_parse(self) -> None: # https://bugs.chromium.org/p/oss-fuzz/issues/detail?id=50381 # The second parse used to raise `TypeError: 'NoneType' object is not iterable` @@ -740,6 +747,10 @@ assert parser.raw_statements == raw_statements assert parser.statements == set() + @pytest.mark.xfail( + env.PYPY and env.PYVERSION[:2] == (3, 8), + reason="AST doesn't mark end of classes correctly", + ) def test_class_decorator_pragmas(self) -> None: parser = self.parse_text("""\ class Foo(object): @@ -754,6 +765,22 @@ assert parser.raw_statements == {1, 2, 3, 5, 6, 7, 8} assert parser.statements == {1, 2, 3} + def test_over_exclusion_bug1779(self) -> None: + # https://github.com/nedbat/coveragepy/issues/1779 + parser = self.parse_text("""\ + import abc + + class MyProtocol: # nocover 3 + @abc.abstractmethod # nocover 4 + def my_method(self) -> int: + ... # 6 + + def function() -> int: + return 9 + """) + assert parser.raw_statements == {1, 3, 4, 5, 6, 8, 9} + assert parser.statements == {1, 8, 9} + class ParserMissingArcDescriptionTest(PythonParserTestBase): """Tests for PythonParser.missing_arc_description.""" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/coverage-7.5.1/tests/test_report.py new/coverage-7.5.3/tests/test_report.py --- old/coverage-7.5.1/tests/test_report.py 2024-05-04 16:44:25.000000000 +0200 +++ new/coverage-7.5.3/tests/test_report.py 2024-05-28 15:52:29.000000000 +0200 @@ -668,6 +668,34 @@ assert "not_covered.py 3 3 0.000000%" in report assert "TOTAL 3 3 0.000000%" in report + def test_report_module_docstrings(self) -> None: + self.make_file("main.py", """\ + # Line 1 + '''Line 2 docstring.''' + import other + a = 4 + """) + self.make_file("other.py", """\ + '''Line 1''' + a = 2 + """) + cov = coverage.Coverage() + self.start_import_stop(cov, "main") + report = self.get_report(cov) + + # Name Stmts Miss Cover + # ------------------------------ + # main.py 2 0 100% + # other.py 1 0 100% + # ------------------------------ + # TOTAL 3 0 100% + + assert self.line_count(report) == 6, report + squeezed = self.squeezed_lines(report) + assert squeezed[2] == "main.py 2 0 100%" + assert squeezed[3] == "other.py 1 0 100%" + assert squeezed[5] == "TOTAL 3 0 100%" + def test_dotpy_not_python(self) -> None: # We run a .py file, and when reporting, we can't parse it as Python. # We should get an error message in the report. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/coverage-7.5.1/tests/test_setup.py new/coverage-7.5.3/tests/test_setup.py --- old/coverage-7.5.1/tests/test_setup.py 2024-05-04 16:44:25.000000000 +0200 +++ new/coverage-7.5.3/tests/test_setup.py 2024-05-28 15:52:29.000000000 +0200 @@ -9,7 +9,10 @@ from typing import List, cast +import pytest + import coverage +from coverage import env from tests.coveragetest import CoverageTest @@ -35,6 +38,10 @@ assert "github.com/nedbat/coveragepy" in out[2] assert "Ned Batchelder" in out[3] + @pytest.mark.skipif( + env.PYVERSION[3:5] == ("alpha", 0), + reason="don't expect classifiers until labelled builds", + ) def test_more_metadata(self) -> None: # Let's be sure we pick up our own setup.py # CoverageTest restores the original sys.path for us. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/coverage-7.5.1/tox.ini new/coverage-7.5.3/tox.ini --- old/coverage-7.5.1/tox.ini 2024-05-04 16:44:25.000000000 +0200 +++ new/coverage-7.5.3/tox.ini 2024-05-28 15:52:29.000000000 +0200 @@ -46,7 +46,7 @@ python -m pip install {env:COVERAGE_PIP_ARGS} -q -e . python igor.py test_with_core ctrace {posargs} - py3{12,13},anypy: python igor.py test_with_core sysmon {posargs} + py3{12,13,14},anypy: python igor.py test_with_core sysmon {posargs} # Remove the C extension so that we can test the PyTracer python igor.py remove_extension @@ -76,6 +76,7 @@ # If this command fails, see the comment at the top of doc/cmd.rst python -m cogapp -cP --check --verbosity=1 doc/*.rst doc8 -q --ignore-path 'doc/_*' doc CHANGES.rst README.rst + sphinx-lint doc CHANGES.rst README.rst sphinx-build -b html -aEnqW doc doc/_build/html rst2html.py --strict README.rst doc/_build/trash - sphinx-build -b html -b linkcheck -aEnq doc doc/_build/html @@ -96,7 +97,7 @@ # If this command fails, see the comment at the top of doc/cmd.rst python -m cogapp -cP --check --verbosity=1 doc/*.rst python -m cogapp -cP --check --verbosity=1 .github/workflows/*.yml - python -m pylint --notes= --ignore-paths 'doc/_build/.*' {env:LINTABLE} + python -m pylint -j 0 --notes= --ignore-paths 'doc/_build/.*' {env:LINTABLE} check-manifest --ignore 'doc/sample_html/*,.treerc' # If 'build -q' becomes a thing (https://github.com/pypa/build/issues/188), # this can be simplified: @@ -128,4 +129,5 @@ 3.11 = py311 3.12 = py312 3.13 = py313 + 3.14 = py314 pypy-3 = pypy3