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 <dmuel...@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: n...@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">&nbsp;</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: n...@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

Reply via email to