- coverage.py v7.5.1a0.dev1, - created at 2024-04-29 17:40 -0300 + coverage.py v7.6.13a0.dev1, + created at 2025-02-15 17:25 -0500
From 207bab0abe5ead99fa5549b4fa10498478dc92ad Mon Sep 17 00:00:00 2001
From: Ned Batchelder
- coverage.py v7.6.10,
- created at 2024-12-26 11:29 -0500
+ coverage.py v7.6.11,
+ created at 2025-02-08 09:03 -0500
Classes
- coverage.py v7.6.11, - created at 2025-02-08 09:03 -0500 + coverage.py v7.6.12, + created at 2025-02-11 08:59 -0500
@@ -537,8 +537,8 @@- coverage.py v7.5.1a0.dev1, - created at 2024-04-29 17:40 -0300 + coverage.py v7.6.13a0.dev1, + created at 2025-02-15 17:25 -0500
- coverage.py v7.5.1a0.dev1, - created at 2024-04-29 17:40 -0300 + coverage.py v7.6.13a0.dev1, + created at 2025-02-15 17:25 -0500
- coverage.py v7.5.1a0.dev1, - created at 2024-04-29 17:40 -0300 + coverage.py v7.6.13a0.dev1, + created at 2025-02-15 17:25 -0500
- coverage.py v7.5.1a0.dev1, - created at 2024-04-29 17:40 -0300 + coverage.py v7.6.13a0.dev1, + created at 2025-02-15 17:25 -0500
- coverage.py v7.5.1a0.dev1, - created at 2024-04-29 17:40 -0300 + coverage.py v7.6.13a0.dev1, + created at 2025-02-15 17:25 -0500
- coverage.py v7.5.1a0.dev1, - created at 2024-04-29 17:40 -0300 + coverage.py v7.6.13a0.dev1, + created at 2025-02-15 17:25 -0500
- coverage.py v7.6.0a0.dev1, - created at 2024-07-10 12:20 -0400 + coverage.py v7.6.13a0.dev1, + created at 2025-02-15 12:46 -0500
- coverage.py v7.6.0a0.dev1, - created at 2024-07-10 12:20 -0400 + coverage.py v7.6.13a0.dev1, + created at 2025-02-15 12:46 -0500
- coverage.py v7.6.0a0.dev1, - created at 2024-07-10 12:20 -0400 + coverage.py v7.6.13a0.dev1, + created at 2025-02-15 12:46 -0500
- coverage.py v7.6.0a0.dev1, - created at 2024-07-10 12:20 -0400 + coverage.py v7.6.13a0.dev1, + created at 2025-02-15 12:46 -0500
- coverage.py v7.6.0a0.dev1, - created at 2024-07-10 12:20 -0400 + coverage.py v7.6.13a0.dev1, + created at 2025-02-15 12:46 -0500
- coverage.py v7.6.0a0.dev1, - created at 2024-07-10 12:20 -0400 + coverage.py v7.6.13a0.dev1, + created at 2025-02-15 12:46 -0500
« prev ^ index » next - coverage.py v7.6.0a0.dev1, - created at 2024-07-10 12:20 -0400 + coverage.py v7.6.13a0.dev1, + created at 2025-02-15 12:46 -0500
10if 0:
11 never_happen()
-13if 13: 13 ↛ 16line 13 didn't jump to line 16 because the condition on line 13 was always true
+13if 13:
14 a = 14
16if a == 16:
@@ -107,8 +107,8 @@' as Python source: '.*' at line \d+"
+ msg = r"Couldn't parse '' as Python source: ['\"].*['\"] at line \d+"
with pytest.raises(NotPython, match=msg):
_ = self.parse_text(text)
diff --git a/tox.ini b/tox.ini
index ccc9e702f..8cf006b7c 100644
--- a/tox.ini
+++ b/tox.ini
@@ -26,7 +26,7 @@ install_command = python -m pip install -U {opts} {packages}
passenv = *
setenv =
- pypy3{,9,10}: COVERAGE_TEST_CORES=pytrace
+ pypy3{,9,10,11}: COVERAGE_TEST_CORES=pytrace
# For some tests, we need .pyc files written in the current directory,
# so override any local setting.
PYTHONPYCACHEPREFIX=
From 1aecfa7ae2a81aa9c278cbb43718ae7e4bdcf5b5 Mon Sep 17 00:00:00 2001
From: Ned Batchelder
Date: Thu, 20 Feb 2025 18:17:03 -0500
Subject: [PATCH 52/90] docs: mention PyPy 3.11 support
---
.github/workflows/kit.yml | 2 +-
.github/workflows/testsuite.yml | 17 ++++++++---------
CHANGES.rst | 2 ++
README.rst | 2 +-
doc/index.rst | 2 +-
tests/test_concurrency.py | 5 ++++-
6 files changed, 17 insertions(+), 13 deletions(-)
diff --git a/.github/workflows/kit.yml b/.github/workflows/kit.yml
index 9462e80ff..8c3dd1c7c 100644
--- a/.github/workflows/kit.yml
+++ b/.github/workflows/kit.yml
@@ -255,7 +255,7 @@ jobs:
run: |
# One wheel works for all PyPy versions. PYVERSIONS
# yes, this is weird syntax: https://github.com/pypa/build/issues/202
- echo -e "[bdist_wheel]\npython_tag=pp39.pp310" > $DIST_EXTRA_CONFIG
+ echo -e "[bdist_wheel]\npython_tag=pp39.pp310.pp311" > $DIST_EXTRA_CONFIG
pypy3 -m build -w
- name: "List wheels"
diff --git a/.github/workflows/testsuite.yml b/.github/workflows/testsuite.yml
index 84f1b6be0..0f831e81d 100644
--- a/.github/workflows/testsuite.yml
+++ b/.github/workflows/testsuite.yml
@@ -54,15 +54,14 @@ jobs:
- "3.14"
- "pypy-3.9"
- "pypy-3.10"
- exclude:
- # Windows pypy 3.9 and 3.10 get stuck with PyPy 7.3.15. I hope to
- # unstick them, but I don't want that to block all other progress, so
- # skip them for now. These excludes can be removed once GitHub uses
- # PyPy 7.3.16 on Windows. https://github.com/pypy/pypy/issues/4876
- - os: windows
- python-version: "pypy-3.9"
- - os: windows
- python-version: "pypy-3.10"
+ - "pypy-3.11"
+ #
+ # If we need to exclude any combinations, do it like this:
+ # exclude:
+ # # Windows pypy 3.9 and 3.10 get stuck with PyPy 7.3.15.
+ # - os: windows
+ # python-version: "pypy-3.10"
+ #
# If we need to tweak the os version we can do it with an include like
# this:
# include:
diff --git a/CHANGES.rst b/CHANGES.rst
index 41c029726..ecb50a234 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -32,6 +32,8 @@ Unreleased
for passing in plugin objects directly, thanks to `Alex Gaynor `_.
+- Confirmed support for PyPy 3.11. Thanks Michał Górny.
+
.. _pull 1919: https://github.com/nedbat/coveragepy/pull/1919
diff --git a/README.rst b/README.rst
index 9877294a3..ebc8fb405 100644
--- a/README.rst
+++ b/README.rst
@@ -26,7 +26,7 @@ Coverage.py runs on these versions of Python:
.. PYVERSIONS
* Python 3.9 through 3.14 alpha 4, including free-threading.
-* PyPy3 versions 3.9 and 3.10.
+* PyPy3 versions 3.9, 3.10, and 3.11.
Documentation is on `Read the Docs`_. Code repository and issue tracker are on
`GitHub`_.
diff --git a/doc/index.rst b/doc/index.rst
index 0dd80371b..c8d74c375 100644
--- a/doc/index.rst
+++ b/doc/index.rst
@@ -19,7 +19,7 @@ supported on:
.. PYVERSIONS
* Python 3.9 through 3.14 alpha 4, including free-threading.
-* PyPy3 versions 3.9 and 3.10.
+* PyPy3 versions 3.9, 3.10, and 3.11.
.. ifconfig:: prerelease
diff --git a/tests/test_concurrency.py b/tests/test_concurrency.py
index 6635629f6..ce163aba7 100644
--- a/tests/test_concurrency.py
+++ b/tests/test_concurrency.py
@@ -788,7 +788,10 @@ def on_sigterm(signum, frame):
signal.signal(signal.SIGTERM, on_sigterm)
x.value = 0
- time.sleep(.1)
+ try:
+ time.sleep(.1)
+ except OSError: # This happens on PyPy3.11 on Mac
+ pass
print("END", flush=True)
if __name__ == "__main__":
From 6c5291af8b2fe38500a92d06dc5a350f271f4c7b Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 3 Mar 2025 08:06:06 -0500
Subject: [PATCH 53/90] chore: bump the action-dependencies group with 4
updates (#1931)
Bumps the action-dependencies group with 4 updates: [github/codeql-action](https://github.com/github/codeql-action), [actions/upload-artifact](https://github.com/actions/upload-artifact), [actions/download-artifact](https://github.com/actions/download-artifact) and [actions/attest-build-provenance](https://github.com/actions/attest-build-provenance).
Updates `github/codeql-action` from 3.28.9 to 3.28.10
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/9e8d0789d4a0fa9ceb6b1738f7e269594bdd67f0...b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d)
Updates `actions/upload-artifact` from 4.6.0 to 4.6.1
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08...4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1)
Updates `actions/download-artifact` from 4.1.8 to 4.1.9
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/fa0a91b85d4f404e444e00e005971372dc801d16...cc203385981b70ca67e1cc392babf9cc229d5806)
Updates `actions/attest-build-provenance` from 2.2.0 to 2.2.2
- [Release notes](https://github.com/actions/attest-build-provenance/releases)
- [Changelog](https://github.com/actions/attest-build-provenance/blob/main/RELEASE.md)
- [Commits](https://github.com/actions/attest-build-provenance/compare/520d128f165991a6c774bcb264f323e3d70747f4...bd77c077858b8d561b7a36cbe48ef4cc642ca39d)
---
updated-dependencies:
- dependency-name: github/codeql-action
dependency-type: direct:production
update-type: version-update:semver-patch
dependency-group: action-dependencies
- dependency-name: actions/upload-artifact
dependency-type: direct:production
update-type: version-update:semver-patch
dependency-group: action-dependencies
- dependency-name: actions/download-artifact
dependency-type: direct:production
update-type: version-update:semver-patch
dependency-group: action-dependencies
- dependency-name: actions/attest-build-provenance
dependency-type: direct:production
update-type: version-update:semver-patch
dependency-group: action-dependencies
...
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
.github/workflows/codeql-analysis.yml | 6 +++---
.github/workflows/coverage.yml | 8 ++++----
.github/workflows/kit.yml | 10 +++++-----
.github/workflows/publish.yml | 8 ++++----
4 files changed, 16 insertions(+), 16 deletions(-)
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index ce8f631e5..06471c1f4 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -51,7 +51,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
- uses: github/codeql-action/init@9e8d0789d4a0fa9ceb6b1738f7e269594bdd67f0 # v3
+ uses: github/codeql-action/init@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v3
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -62,7 +62,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
- uses: github/codeql-action/autobuild@9e8d0789d4a0fa9ceb6b1738f7e269594bdd67f0 # v3
+ uses: github/codeql-action/autobuild@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v3
# ℹ️ Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
@@ -76,4 +76,4 @@ jobs:
# make release
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@9e8d0789d4a0fa9ceb6b1738f7e269594bdd67f0 # v3
+ uses: github/codeql-action/analyze@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v3
diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml
index 07c2250fe..d94227f04 100644
--- a/.github/workflows/coverage.yml
+++ b/.github/workflows/coverage.yml
@@ -125,7 +125,7 @@ jobs:
mv .metacov .metacov.$MATRIX_ID
- name: "Upload coverage data"
- uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
+ uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
with:
name: metacov-${{ env.MATRIX_ID }}
path: .metacov.*
@@ -170,7 +170,7 @@ jobs:
python igor.py zip_mods
- name: "Download coverage data"
- uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
+ uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4.1.9
with:
pattern: metacov-*
merge-multiple: true
@@ -184,7 +184,7 @@ jobs:
python igor.py combine_html
- name: "Upload HTML report"
- uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
+ uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
with:
name: html_report
path: htmlcov
@@ -239,7 +239,7 @@ jobs:
- name: "Download coverage HTML report"
if: ${{ github.ref == 'refs/heads/master' }}
- uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
+ uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4.1.9
with:
name: html_report
path: reports_repo/${{ env.report_dir }}
diff --git a/.github/workflows/kit.yml b/.github/workflows/kit.yml
index 8c3dd1c7c..421ea5af0 100644
--- a/.github/workflows/kit.yml
+++ b/.github/workflows/kit.yml
@@ -182,7 +182,7 @@ jobs:
python -m twine check wheelhouse/*
- name: "Upload binary wheels"
- uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
+ uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
with:
name: dist-${{ env.MATRIX_ID }}
path: wheelhouse/*.whl
@@ -223,7 +223,7 @@ jobs:
python -m twine check dist/*
- name: "Upload non-binary artifacts"
- uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
+ uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
with:
name: dist-non-binary
path: dist/*
@@ -267,7 +267,7 @@ jobs:
python -m twine check dist/*
- name: "Upload wheels"
- uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
+ uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
with:
name: dist-pypy
path: dist/*.whl
@@ -286,7 +286,7 @@ jobs:
id-token: write
steps:
- name: "Download artifacts"
- uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
+ uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4.1.9
with:
pattern: dist-*
merge-multiple: true
@@ -308,7 +308,7 @@ jobs:
ls -alR
- name: "Upload signatures"
- uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
+ uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
with:
name: signatures
path: "*.sigstore.json"
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index b920ae65d..cac0ce7a9 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -64,7 +64,7 @@ jobs:
steps:
- name: "Download dists"
- uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
+ uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4.1.9
with:
repository: "nedbat/coveragepy"
run-id: ${{ needs.find-run.outputs.run-id }}
@@ -81,7 +81,7 @@ jobs:
files=$(ls dist 2>/dev/null | wc -l) && [ "$files" -eq $EXPECTED ] || exit 1
- name: "Generate attestations"
- uses: actions/attest-build-provenance@520d128f165991a6c774bcb264f323e3d70747f4 # v2.2.0
+ uses: actions/attest-build-provenance@bd77c077858b8d561b7a36cbe48ef4cc642ca39d # v2.2.2
with:
subject-path: "dist/*"
@@ -104,7 +104,7 @@ jobs:
steps:
- name: "Download dists"
- uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
+ uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4.1.9
with:
repository: "nedbat/coveragepy"
run-id: ${{ needs.find-run.outputs.run-id }}
@@ -121,7 +121,7 @@ jobs:
files=$(ls dist 2>/dev/null | wc -l) && [ "$files" -eq $EXPECTED ] || exit 1
- name: "Generate attestations"
- uses: actions/attest-build-provenance@520d128f165991a6c774bcb264f323e3d70747f4 # v2.2.0
+ uses: actions/attest-build-provenance@bd77c077858b8d561b7a36cbe48ef4cc642ca39d # v2.2.2
with:
subject-path: "dist/*"
From 97ce68c488405b672ee0c15e619b628d6ccb6ff1 Mon Sep 17 00:00:00 2001
From: Ned Batchelder
Date: Mon, 3 Mar 2025 08:10:15 -0500
Subject: [PATCH 54/90] chore: make upgrade doc_upgrade
---
doc/requirements.pip | 20 +++++++++++++-------
requirements/dev.pip | 12 ++++++------
requirements/kit.pip | 8 ++++----
requirements/light-threads.pip | 2 +-
requirements/mypy.pip | 6 +++---
requirements/pip-tools.pip | 2 +-
requirements/pip.pip | 2 +-
requirements/pytest.pip | 4 ++--
requirements/tox.pip | 2 +-
9 files changed, 32 insertions(+), 26 deletions(-)
diff --git a/doc/requirements.pip b/doc/requirements.pip
index 9d48239a6..d8b2ac6f9 100644
--- a/doc/requirements.pip
+++ b/doc/requirements.pip
@@ -10,9 +10,9 @@ anyio==4.8.0
# via
# starlette
# watchfiles
-babel==2.16.0
+babel==2.17.0
# via sphinx
-certifi==2024.12.14
+certifi==2025.1.31
# via requests
charset-normalizer==3.4.1
# via requests
@@ -44,7 +44,7 @@ markupsafe==3.0.2
# via jinja2
packaging==24.2
# via sphinx
-pbr==6.1.0
+pbr==6.1.1
# via stevedore
polib==1.2.0
# via sphinx-lint
@@ -64,11 +64,13 @@ requests==2.32.3
# sphinxcontrib-spelling
restructuredtext-lint==1.4.0
# via doc8
+roman-numerals-py==3.1.0
+ # via sphinx
sniffio==1.3.1
# via anyio
snowballstemmer==2.2.0
# via sphinx
-sphinx==8.1.3
+sphinx==8.2.3
# via
# -r doc/requirements.in
# sphinx-autobuild
@@ -103,9 +105,9 @@ sphinxcontrib-serializinghtml==2.0.0
# via sphinx
sphinxcontrib-spelling==8.0.1
# via -r doc/requirements.in
-starlette==0.45.3
+starlette==0.46.0
# via sphinx-autobuild
-stevedore==5.4.0
+stevedore==5.4.1
# via doc8
typing-extensions==4.12.2
# via anyio
@@ -115,5 +117,9 @@ uvicorn==0.34.0
# via sphinx-autobuild
watchfiles==1.0.4
# via sphinx-autobuild
-websockets==14.2
+websockets==15.0
# via sphinx-autobuild
+
+# The following packages are considered to be unsafe in a requirements file:
+setuptools==75.8.2
+ # via pbr
diff --git a/requirements/dev.pip b/requirements/dev.pip
index 5336a389e..cbcc7123f 100644
--- a/requirements/dev.pip
+++ b/requirements/dev.pip
@@ -12,7 +12,7 @@ backports-tarfile==1.2.0
# via jaraco-context
build==1.2.2.post1
# via check-manifest
-cachetools==5.5.1
+cachetools==5.5.2
# via tox
certifi==2025.1.31
# via requests
@@ -49,7 +49,7 @@ flaky==3.8.1
# via -r /Users/ned/coverage/trunk/requirements/pytest.in
greenlet==3.1.1
# via -r requirements/dev.in
-hypothesis==6.125.3
+hypothesis==6.127.5
# via -r /Users/ned/coverage/trunk/requirements/pytest.in
id==1.5.0
# via twine
@@ -62,7 +62,7 @@ importlib-metadata==8.6.1
# twine
iniconfig==2.0.0
# via pytest
-isort==6.0.0
+isort==6.0.1
# via pylint
jaraco-classes==3.4.0
# via keyring
@@ -86,7 +86,7 @@ more-itertools==10.6.0
# via
# jaraco-classes
# jaraco-functools
-nh3==0.2.20
+nh3==0.2.21
# via readme-renderer
packaging==24.2
# via
@@ -121,7 +121,7 @@ pyproject-api==1.9.0
# via tox
pyproject-hooks==1.2.0
# via build
-pytest==8.3.4
+pytest==8.3.5
# via
# -r /Users/ned/coverage/trunk/requirements/pytest.in
# pytest-xdist
@@ -195,7 +195,7 @@ zipp==3.21.0
# The following packages are considered to be unsafe in a requirements file:
pip==25.0.1
# via -r /Users/ned/coverage/trunk/requirements/pip.in
-setuptools==75.8.0
+setuptools==75.8.2
# via
# -r /Users/ned/coverage/trunk/requirements/pip.in
# check-manifest
diff --git a/requirements/kit.pip b/requirements/kit.pip
index 33e6e8828..27ebb5711 100644
--- a/requirements/kit.pip
+++ b/requirements/kit.pip
@@ -20,7 +20,7 @@ certifi==2025.1.31
# requests
charset-normalizer==3.4.1
# via requests
-cibuildwheel==2.22.0
+cibuildwheel==2.23.0
# via -r requirements/kit.in
colorama==0.4.6
# via -r requirements/kit.in
@@ -55,7 +55,7 @@ more-itertools==10.6.0
# via
# jaraco-classes
# jaraco-functools
-nh3==0.2.20
+nh3==0.2.21
# via readme-renderer
packaging==24.2
# via
@@ -66,7 +66,7 @@ packaging==24.2
# twine
platformdirs==4.3.6
# via cibuildwheel
-pyelftools==0.31
+pyelftools==0.32
# via auditwheel
pygments==2.19.1
# via
@@ -108,5 +108,5 @@ zipp==3.21.0
# via importlib-metadata
# The following packages are considered to be unsafe in a requirements file:
-setuptools==75.8.0
+setuptools==75.8.2
# via -r requirements/kit.in
diff --git a/requirements/light-threads.pip b/requirements/light-threads.pip
index dfb02c03c..5bca6b6e6 100644
--- a/requirements/light-threads.pip
+++ b/requirements/light-threads.pip
@@ -25,7 +25,7 @@ zope-interface==7.2
# via gevent
# The following packages are considered to be unsafe in a requirements file:
-setuptools==75.8.0
+setuptools==75.8.2
# via
# zope-event
# zope-interface
diff --git a/requirements/mypy.pip b/requirements/mypy.pip
index b90264afc..8ab8cf251 100644
--- a/requirements/mypy.pip
+++ b/requirements/mypy.pip
@@ -16,7 +16,7 @@ execnet==2.1.1
# via pytest-xdist
flaky==3.8.1
# via -r /Users/ned/coverage/trunk/requirements/pytest.in
-hypothesis==6.125.3
+hypothesis==6.127.5
# via -r /Users/ned/coverage/trunk/requirements/pytest.in
iniconfig==2.0.0
# via pytest
@@ -30,7 +30,7 @@ pluggy==1.5.0
# via pytest
pygments==2.19.1
# via -r /Users/ned/coverage/trunk/requirements/pytest.in
-pytest==8.3.4
+pytest==8.3.5
# via
# -r /Users/ned/coverage/trunk/requirements/pytest.in
# pytest-xdist
@@ -42,7 +42,7 @@ tomli==2.2.1
# via
# mypy
# pytest
-types-requests==2.32.0.20241016
+types-requests==2.32.0.20250301
# via -r requirements/mypy.in
types-tabulate==0.9.0.20241207
# via -r requirements/mypy.in
diff --git a/requirements/pip-tools.pip b/requirements/pip-tools.pip
index a4d538c8d..88eb7105a 100644
--- a/requirements/pip-tools.pip
+++ b/requirements/pip-tools.pip
@@ -30,5 +30,5 @@ zipp==3.21.0
# The following packages are considered to be unsafe in a requirements file:
pip==25.0.1
# via pip-tools
-setuptools==75.8.0
+setuptools==75.8.2
# via pip-tools
diff --git a/requirements/pip.pip b/requirements/pip.pip
index a29b21579..765b5d8e2 100644
--- a/requirements/pip.pip
+++ b/requirements/pip.pip
@@ -18,5 +18,5 @@ virtualenv==20.28.1
# The following packages are considered to be unsafe in a requirements file:
pip==25.0.1
# via -r requirements/pip.in
-setuptools==75.8.0
+setuptools==75.8.2
# via -r requirements/pip.in
diff --git a/requirements/pytest.pip b/requirements/pytest.pip
index e905838d9..f96757b8b 100644
--- a/requirements/pytest.pip
+++ b/requirements/pytest.pip
@@ -16,7 +16,7 @@ execnet==2.1.1
# via pytest-xdist
flaky==3.8.1
# via -r requirements/pytest.in
-hypothesis==6.125.3
+hypothesis==6.127.5
# via -r requirements/pytest.in
iniconfig==2.0.0
# via pytest
@@ -26,7 +26,7 @@ pluggy==1.5.0
# via pytest
pygments==2.19.1
# via -r requirements/pytest.in
-pytest==8.3.4
+pytest==8.3.5
# via
# -r requirements/pytest.in
# pytest-xdist
diff --git a/requirements/tox.pip b/requirements/tox.pip
index fa4634d70..a2387c529 100644
--- a/requirements/tox.pip
+++ b/requirements/tox.pip
@@ -4,7 +4,7 @@
#
# make upgrade
#
-cachetools==5.5.1
+cachetools==5.5.2
# via tox
chardet==5.2.0
# via tox
From 8c5a5ff49b853bee970ffb9beebd85685b90e788 Mon Sep 17 00:00:00 2001
From: Robin <167366979+allrob23@users.noreply.github.com>
Date: Tue, 4 Mar 2025 18:59:53 -0300
Subject: [PATCH 55/90] refactor: improve warning suppression performance by
using sets (#1932)
* feat(performance): improve warning suppression lookup by using set
* fix: initialize _no_warn_slugs as a empty set
---
coverage/control.py | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/coverage/control.py b/coverage/control.py
index bce5f0a17..e051de828 100644
--- a/coverage/control.py
+++ b/coverage/control.py
@@ -258,7 +258,7 @@ def __init__( # pylint: disable=too-many-arguments
self._warn_no_data = True
self._warn_unimported_source = True
self._warn_preimported_source = check_preimported
- self._no_warn_slugs: list[str] = []
+ self._no_warn_slugs: set[str] = set()
self._messages = messages
# A record of all the warnings that have been issued.
@@ -438,7 +438,7 @@ def _warn(self, msg: str, slug: str | None = None, once: bool = False) -> None:
"""
if not self._no_warn_slugs:
- self._no_warn_slugs = list(self.config.disable_warnings)
+ self._no_warn_slugs = set(self.config.disable_warnings)
if slug in self._no_warn_slugs:
# Don't issue the warning
@@ -453,7 +453,7 @@ def _warn(self, msg: str, slug: str | None = None, once: bool = False) -> None:
if once:
assert slug is not None
- self._no_warn_slugs.append(slug)
+ self._no_warn_slugs.add(slug)
def _message(self, msg: str) -> None:
"""Write a message to the user, if configured to do so."""
From 3418eb93b0fefc4e82ff86e21fc6365d9c76414c Mon Sep 17 00:00:00 2001
From: Ned Batchelder
Date: Wed, 5 Mar 2025 19:06:13 -0500
Subject: [PATCH 56/90] clean up the warnings, some we don't need anymore
---
pyproject.toml | 6 +++++-
tests/conftest.py | 7 +++----
2 files changed, 8 insertions(+), 5 deletions(-)
diff --git a/pyproject.toml b/pyproject.toml
index b08f23242..2f08ab55c 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -120,7 +120,11 @@ markers = [
# How come these warnings are suppressed successfully here, but not in conftest.py??
filterwarnings = [
# Sample 'ignore':
- #"ignore:the imp module is deprecated in favour of importlib:DeprecationWarning",
+ # "ignore:the imp module is deprecated in favour of importlib:DeprecationWarning",
+
+ # Note: when writing the regex for the message, it's matched with re.match,
+ # so it has to match the beginning of the message. Add ".*" to make it
+ # match something in the middle of the message.
## Pytest warns if it can't collect things that seem to be tests. This should be an error.
"error::pytest.PytestCollectionWarning",
diff --git a/tests/conftest.py b/tests/conftest.py
index eff1d27d6..b06ffe2ff 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -19,7 +19,6 @@
import pytest
-from coverage import env
from coverage.files import set_relative_directory
# Pytest will rewrite assertions in test modules, but not elsewhere.
@@ -45,9 +44,9 @@ def set_warnings() -> None:
# Warnings to suppress:
# How come these warnings are successfully suppressed here, but not in pyproject.toml??
- if env.PYPY:
- # pypy3 warns about unclosed files a lot.
- warnings.filterwarnings("ignore", r".*unclosed file", category=ResourceWarning)
+ # Note: when writing the regex for the message, it's matched with re.match,
+ # so it has to match the beginning of the message. Add ".*" to make it
+ # match something in the middle of the message.
# Don't warn about unclosed SQLite connections.
# We don't close ":memory:" databases because we don't have a way to connect
From 2e88d428bcaf250209f837b268edea87f8586f69 Mon Sep 17 00:00:00 2001
From: Ned Batchelder
Date: Thu, 6 Feb 2025 18:29:25 -0500
Subject: [PATCH 57/90] feat: sys.monitoring, switch from BRANCH_TAKEN to
BRANCH_RIGHT
Now branch measurement should be fast on 3.14.0a5+. It is still not a
default core, but we might be getting close.
---
CHANGES.rst | 5 +
coverage/control.py | 3 +-
coverage/core.py | 52 ++++--
coverage/env.py | 5 +
coverage/sysmon.py | 378 ++++++++++++++++++++++++++------------
doc/cmd.rst | 18 +-
igor.py | 5 +-
lab/run_sysmon.py | 51 ++---
metacov.ini | 1 +
pyproject.toml | 2 +
tests/conftest.py | 3 +
tests/coveragetest.py | 2 +-
tests/test_arcs.py | 2 +-
tests/test_concurrency.py | 8 +-
tests/test_oddball.py | 2 +-
tests/test_process.py | 10 +
tox.ini | 2 +-
17 files changed, 375 insertions(+), 174 deletions(-)
diff --git a/CHANGES.rst b/CHANGES.rst
index ecb50a234..eaa4de343 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -28,6 +28,11 @@ Unreleased
with one path not taken. Now it is understood as always true and no coverage
is missing.
+- The experimental sys.monitoring support should now work for branch coverage
+ if you are using Python newer than 3.14.0 alpha 5. This should greatly
+ reduce the overhead coverage.py imposes on your test suite. Set the
+ environment variable ``COVERAGE_CORE=sysmon`` to try it out.
+
- The :class:`Coverage constructor<.Coverage>` now has a ``plugins`` parameter
for passing in plugin objects directly, thanks to `Alex Gaynor `_.
diff --git a/coverage/control.py b/coverage/control.py
index e051de828..70ecb833d 100644
--- a/coverage/control.py
+++ b/coverage/control.py
@@ -548,7 +548,8 @@ def _init_for_start(self) -> None:
self._core = Core(
warn=self._warn,
- timid=self.config.timid,
+ config=self.config,
+ dynamic_contexts=(should_start_context is not None),
metacov=self._metacov,
)
self._collector = Collector(
diff --git a/coverage/core.py b/coverage/core.py
index b19ecd532..38c27578b 100644
--- a/coverage/core.py
+++ b/coverage/core.py
@@ -10,6 +10,7 @@
from typing import Any
from coverage import env
+from coverage.config import CoverageConfig
from coverage.disposition import FileDisposition
from coverage.exceptions import ConfigError
from coverage.misc import isolate_module
@@ -17,8 +18,8 @@
from coverage.sysmon import SysMonitor
from coverage.types import (
TFileDisposition,
- Tracer,
TWarnFn,
+ Tracer,
)
@@ -52,36 +53,47 @@ class Core:
packed_arcs: bool
systrace: bool
- def __init__(self,
+ def __init__(
+ self,
warn: TWarnFn,
- timid: bool,
+ config: CoverageConfig,
+ dynamic_contexts: bool,
metacov: bool,
) -> None:
- # Defaults
- self.tracer_kwargs = {}
+ # Check the conditions that preclude us from using sys.monitoring.
+ reason_no_sysmon = ""
+ if not env.PYBEHAVIOR.pep669:
+ reason_no_sysmon = "isn't available in this version"
+ elif config.branch and not env.PYBEHAVIOR.branch_right_left:
+ reason_no_sysmon = "can't measure branches in this version"
+ elif dynamic_contexts:
+ reason_no_sysmon = "doesn't yet support dynamic contexts"
- core_name: str | None
- if timid:
+ core_name: str | None = None
+ if config.timid:
core_name = "pytrace"
- else:
+
+ if core_name is None:
core_name = os.getenv("COVERAGE_CORE")
- if core_name == "sysmon" and not env.PYBEHAVIOR.pep669:
- warn("sys.monitoring isn't available, using default core", slug="no-sysmon")
- core_name = None
+ if core_name == "sysmon" and reason_no_sysmon:
+ warn(f"sys.monitoring {reason_no_sysmon}, using default core", slug="no-sysmon")
+ core_name = None
+
+ if core_name is None:
+ # Someday we will default to sysmon, but it's still experimental:
+ # if not reason_no_sysmon:
+ # core_name = "sysmon"
+ if HAS_CTRACER:
+ core_name = "ctrace"
+ else:
+ core_name = "pytrace"
- if not core_name:
- # Once we're comfortable with sysmon as a default:
- # if env.PYBEHAVIOR.pep669 and self.should_start_context is None:
- # core_name = "sysmon"
- if HAS_CTRACER:
- core_name = "ctrace"
- else:
- core_name = "pytrace"
+ self.tracer_kwargs = {}
if core_name == "sysmon":
self.tracer_class = SysMonitor
- self.tracer_kwargs = {"tool_id": 3 if metacov else 1}
+ self.tracer_kwargs["tool_id"] = 3 if metacov else 1
self.file_disposition_class = FileDisposition
self.supports_plugins = False
self.packed_arcs = False
diff --git a/coverage/env.py b/coverage/env.py
index 6f0a9aa8b..a88161b38 100644
--- a/coverage/env.py
+++ b/coverage/env.py
@@ -156,6 +156,11 @@ class PYBEHAVIOR:
# PEP649 and PEP749: Deferred annotations
deferred_annotations = (PYVERSION >= (3, 14))
+ # Does sys.monitoring support BRANCH_RIGHT and BRANCH_LEFT? The names
+ # were added in early 3.14 alphas, but didn't work entirely correctly until
+ # after 3.14.0a5.
+ branch_right_left = (pep669 and (PYVERSION > (3, 14, 0, "alpha", 5, 0)))
+
# Coverage.py specifics, about testing scenarios. See tests/testenv.py also.
diff --git a/coverage/sysmon.py b/coverage/sysmon.py
index 2809aa087..ca31513d6 100644
--- a/coverage/sysmon.py
+++ b/coverage/sysmon.py
@@ -3,10 +3,9 @@
"""Callback functions and support for sys.monitoring data collection."""
-# TODO: https://github.com/python/cpython/issues/111963#issuecomment-2386584080
-
from __future__ import annotations
+import dis
import functools
import inspect
import os
@@ -16,14 +15,17 @@
import traceback
from dataclasses import dataclass
-from types import CodeType, FrameType
+from types import CodeType
from typing import (
Any,
Callable,
- TYPE_CHECKING,
+ Iterable,
+ NewType,
+ Optional,
cast,
)
+from coverage import env
from coverage.debug import short_filename, short_stack
from coverage.misc import isolate_module
from coverage.types import (
@@ -43,17 +45,28 @@
# pylint: disable=unused-argument
-LOG = False
+# $set_env.py: COVERAGE_LOG_SYSMON - Log sys.monitoring activity
+LOG = bool(int(os.getenv("COVERAGE_LOG_SYSMON", 0)))
# This module will be imported in all versions of Python, but only used in 3.12+
# It will be type-checked for 3.12, but not for earlier versions.
sys_monitoring = getattr(sys, "monitoring", None)
-if TYPE_CHECKING:
- assert sys_monitoring is not None
- # I want to say this but it's not allowed:
- # MonitorReturn = Literal[sys.monitoring.DISABLE] | None
- MonitorReturn = Any
+DISABLE_TYPE = NewType("DISABLE_TYPE", object)
+MonitorReturn = Optional[DISABLE_TYPE]
+DISABLE = cast(MonitorReturn, getattr(sys_monitoring, "DISABLE", None))
+TOffset = int
+
+ALWAYS_JUMPS: set[int] = set()
+RETURNS: set[int] = set()
+
+if env.PYBEHAVIOR.branch_right_left:
+ ALWAYS_JUMPS.update(
+ dis.opmap[name]
+ for name in ["JUMP_FORWARD", "JUMP_BACKWARD", "JUMP_BACKWARD_NO_INTERRUPT"]
+ )
+
+ RETURNS.update(dis.opmap[name] for name in ["RETURN_VALUE", "RETURN_GENERATOR"])
if LOG: # pragma: debugging
@@ -76,7 +89,10 @@ def _wrapped(*args: Any, **kwargs: Any) -> Any:
assert sys_monitoring is not None
short_stack = functools.partial(
- short_stack, full=True, short_filenames=True, frame_ids=True,
+ short_stack,
+ full=True,
+ short_filenames=True,
+ frame_ids=True,
)
seen_threads: set[int] = set()
@@ -99,7 +115,10 @@ def log(msg: str) -> None:
# f"{root}-{pid}-{tslug}.out",
]:
with open(filename, "a") as f:
- print(f"{pid}:{tslug}: {msg}", file=f, flush=True)
+ try:
+ print(f"{pid}:{tslug}: {msg}", file=f, flush=True)
+ except UnicodeError:
+ print(f"{pid}:{tslug}: {ascii(msg)}", file=f, flush=True)
def arg_repr(arg: Any) -> str:
"""Make a customized repr for logged values."""
@@ -130,7 +149,9 @@ def _wrapped(self: Any, *args: Any) -> Any:
return ret
except Exception as exc:
log(f"!!{exc.__class__.__name__}: {exc}")
- log("".join(traceback.format_exception(exc))) # pylint: disable=[no-value-for-parameter]
+ if 1:
+ # pylint: disable=no-value-for-parameter
+ log("".join(traceback.format_exception(exc)))
try:
assert sys_monitoring is not None
sys_monitoring.set_events(sys.monitoring.COVERAGE_ID, 0)
@@ -157,17 +178,152 @@ def _decorator(meth: AnyCallable) -> AnyCallable:
return _decorator
+class InstructionWalker:
+ """Utility to step through trails of instructions.
+
+ We have two reasons to need sequences of instructions from a code object:
+ First, in strict sequence to visit all the instructions in the object.
+ This is `walk(follow_jumps=False)`. Second, we want to follow jumps to
+ understand how execution will flow: `walk(follow_jumps=True)`.
+
+ """
+
+ def __init__(self, code: CodeType) -> None:
+ self.code = code
+ self.insts: dict[TOffset, dis.Instruction] = {}
+
+ inst = None
+ for inst in dis.get_instructions(code):
+ self.insts[inst.offset] = inst
+
+ assert inst is not None
+ self.max_offset = inst.offset
+
+ def walk(
+ self, *, start_at: TOffset = 0, follow_jumps: bool = True
+ ) -> Iterable[dis.Instruction]:
+ """
+ Yield instructions starting from `start_at`. Follow unconditional
+ jumps if `follow_jumps` is true.
+ """
+ seen = set()
+ offset = start_at
+ while offset < self.max_offset + 1:
+ if offset in seen:
+ break
+ seen.add(offset)
+ if inst := self.insts.get(offset):
+ yield inst
+ if follow_jumps and inst.opcode in ALWAYS_JUMPS:
+ offset = inst.jump_target
+ continue
+ offset += 2
+
+
+def populate_branch_trails(code: CodeType, code_info: CodeInfo) -> None:
+ """
+ Populate the `branch_trails` attribute on `code_info`.
+
+ Instructions can have a jump_target, where they might jump to next. Some
+ instructions with a jump_target are unconditional jumps (ALWAYS_JUMPS), so
+ they aren't interesting to us, since they aren't the start of a branch
+ possibility.
+
+ Instructions that might or might not jump somewhere else are branch
+ possibilities. For each of those, we track a trail of instructions. These
+ are lists of instruction offsets, the next instructions that can execute.
+ We follow the trail until we get to a new source line. That gives us the
+ arc from the original instruction's line to the new source line.
+
+ """
+ log(f"populate_branch_trails: {code}")
+ iwalker = InstructionWalker(code)
+ for inst in iwalker.walk(follow_jumps=False):
+ log(f"considering {inst=}")
+ if not inst.jump_target:
+ # We only care about instructions with jump targets.
+ log("no jump_target")
+ continue
+ if inst.opcode in ALWAYS_JUMPS:
+ # We don't care about unconditional jumps.
+ log("always jumps")
+ continue
+
+ from_line = inst.line_number
+ if from_line is None:
+ continue
+
+ def walk_one_branch(
+ start_at: TOffset, branch_kind: str
+ ) -> tuple[list[TOffset], TArc | None]:
+ # pylint: disable=cell-var-from-loop
+ inst_offsets: list[TOffset] = []
+ to_line = None
+ for inst2 in iwalker.walk(start_at=start_at):
+ inst_offsets.append(inst2.offset)
+ if inst2.line_number and inst2.line_number != from_line:
+ to_line = inst2.line_number
+ break
+ elif inst2.jump_target and (inst2.opcode not in ALWAYS_JUMPS):
+ log(
+ f"stop: {inst2.jump_target=}, "
+ + f"{inst2.opcode=} ({dis.opname[inst2.opcode]}), "
+ + f"{ALWAYS_JUMPS=}"
+ )
+ break
+ elif inst2.opcode in RETURNS:
+ to_line = -code.co_firstlineno
+ break
+ if to_line is not None:
+ log(
+ f"possible branch from @{start_at}: "
+ + f"{inst_offsets}, {(from_line, to_line)} {code}"
+ )
+ return inst_offsets, (from_line, to_line)
+ else:
+ log(f"no possible branch from @{start_at}: {inst_offsets}")
+ return [], None
+
+ # Calculate two trails: one from the next instruction, and one from the
+ # jump_target instruction.
+ trails = [
+ walk_one_branch(start_at=inst.offset + 2, branch_kind="not-taken"),
+ walk_one_branch(start_at=inst.jump_target, branch_kind="taken"),
+ ]
+ code_info.branch_trails[inst.offset] = trails
+
+ # Sometimes we get BRANCH_RIGHT or BRANCH_LEFT events from instructions
+ # other than the original jump possibility instruction. Register each
+ # trail under all of their offsets so we can pick up in the middle of a
+ # trail if need be.
+ for trail in trails:
+ for offset in trail[0]:
+ if offset not in code_info.branch_trails:
+ code_info.branch_trails[offset] = []
+ code_info.branch_trails[offset].append(trail)
+
@dataclass
class CodeInfo:
"""The information we want about each code object."""
tracing: bool
file_data: TTraceFileData | None
- # TODO: what is byte_to_line for?
- byte_to_line: dict[int, int] | None
-
-
-def bytes_to_lines(code: CodeType) -> dict[int, int]:
+ byte_to_line: dict[TOffset, TLineNo] | None
+
+ # Keys are start instruction offsets for branches.
+ # Values are lists:
+ # [
+ # ([offset, offset, ...], (from_line, to_line)),
+ # ([offset, offset, ...], (from_line, to_line)),
+ # ]
+ # Two possible trails from the branch point, left and right.
+ branch_trails: dict[
+ TOffset,
+ list[tuple[list[TOffset], TArc | None]],
+ ]
+
+
+def bytes_to_lines(code: CodeType) -> dict[TOffset, TLineNo]:
"""Make a dict mapping byte code offsets to line numbers."""
b2l = {}
for bstart, bend, lineno in code.co_lines():
@@ -204,9 +360,6 @@ def __init__(self, tool_id: int) -> None:
# A list of code_objects, just to keep them alive so that id's are
# useful as identity.
self.code_objects: list[CodeType] = []
- self.last_lines: dict[FrameType, int] = {}
- # Map id(code_object) -> code_object
- self.local_event_codes: dict[int, CodeType] = {}
self.sysmon_on = False
self.lock = threading.Lock()
@@ -230,20 +383,23 @@ def start(self) -> None:
assert sys_monitoring is not None
sys_monitoring.use_tool_id(self.myid, "coverage.py")
register = functools.partial(sys_monitoring.register_callback, self.myid)
- events = sys_monitoring.events
+ events = sys.monitoring.events
+
+ sys_monitoring.set_events(self.myid, events.PY_START)
+ register(events.PY_START, self.sysmon_py_start)
if self.trace_arcs:
- sys_monitoring.set_events(
- self.myid,
- events.PY_START | events.PY_UNWIND,
- )
- register(events.PY_START, self.sysmon_py_start)
- register(events.PY_RESUME, self.sysmon_py_resume_arcs)
- register(events.PY_RETURN, self.sysmon_py_return_arcs)
- register(events.PY_UNWIND, self.sysmon_py_unwind_arcs)
+ register(events.PY_RETURN, self.sysmon_py_return)
register(events.LINE, self.sysmon_line_arcs)
+ if env.PYBEHAVIOR.branch_right_left:
+ register(
+ events.BRANCH_RIGHT, # type:ignore[attr-defined]
+ self.sysmon_branch_either,
+ )
+ register(
+ events.BRANCH_LEFT, # type:ignore[attr-defined]
+ self.sysmon_branch_either,
+ )
else:
- sys_monitoring.set_events(self.myid, events.PY_START)
- register(events.PY_START, self.sysmon_py_start)
register(events.LINE, self.sysmon_line_lines)
sys_monitoring.restart_events()
self.sysmon_on = True
@@ -257,11 +413,7 @@ def stop(self) -> None:
return
assert sys_monitoring is not None
sys_monitoring.set_events(self.myid, 0)
- with self.lock:
- self.sysmon_on = False
- for code in self.local_event_codes.values():
- sys_monitoring.set_local_events(self.myid, code, 0)
- self.local_event_codes = {}
+ self.sysmon_on = False
sys_monitoring.free_tool_id(self.myid)
@panopticon()
@@ -281,23 +433,10 @@ def get_stats(self) -> dict[str, int] | None:
"""Return a dictionary of statistics, or None."""
return None
- # The number of frames in callers_frame takes @panopticon into account.
- if LOG:
-
- def callers_frame(self) -> FrameType:
- """Get the frame of the Python code we're monitoring."""
- return (
- inspect.currentframe().f_back.f_back.f_back # type: ignore[union-attr,return-value]
- )
-
- else:
-
- def callers_frame(self) -> FrameType:
- """Get the frame of the Python code we're monitoring."""
- return inspect.currentframe().f_back.f_back # type: ignore[union-attr,return-value]
-
@panopticon("code", "@")
- def sysmon_py_start(self, code: CodeType, instruction_offset: int) -> MonitorReturn:
+ def sysmon_py_start( # pylint: disable=useless-return
+ self, code: CodeType, instruction_offset: TOffset
+ ) -> MonitorReturn:
"""Handle sys.monitoring.events.PY_START events."""
# Entering a new frame. Decide if we should trace in this file.
self._activity = True
@@ -337,11 +476,16 @@ def sysmon_py_start(self, code: CodeType, instruction_offset: int) -> MonitorRet
file_data = None
b2l = None
- self.code_infos[id(code)] = CodeInfo(
+ code_info = CodeInfo(
tracing=tracing_code,
file_data=file_data,
byte_to_line=b2l,
+ branch_trails={},
)
+ self.code_infos[id(code)] = code_info
+ if self.trace_arcs:
+ populate_branch_trails(code, code_info)
+ log(f"branch_trails for {code}:\n {code_info.branch_trails}")
self.code_objects.append(code)
if tracing_code:
@@ -349,90 +493,82 @@ def sysmon_py_start(self, code: CodeType, instruction_offset: int) -> MonitorRet
with self.lock:
if self.sysmon_on:
assert sys_monitoring is not None
- sys_monitoring.set_local_events(
- self.myid,
- code,
- events.PY_RETURN
- #
- | events.PY_RESUME
- # | events.PY_YIELD
- | events.LINE,
- # | events.BRANCH
- # | events.JUMP
- )
- self.local_event_codes[id(code)] = code
-
- if tracing_code and self.trace_arcs:
- frame = self.callers_frame()
- self.last_lines[frame] = -code.co_firstlineno
- return None
- else:
- return sys.monitoring.DISABLE
+ local_events = events.PY_RETURN | events.PY_RESUME | events.LINE
+ if self.trace_arcs:
+ assert env.PYBEHAVIOR.branch_right_left
+ local_events |= (
+ events.BRANCH_RIGHT # type:ignore[attr-defined]
+ | events.BRANCH_LEFT # type:ignore[attr-defined]
+ )
+ sys_monitoring.set_local_events(self.myid, code, local_events)
- @panopticon("code", "@")
- def sysmon_py_resume_arcs(
- self, code: CodeType, instruction_offset: int,
- ) -> MonitorReturn:
- """Handle sys.monitoring.events.PY_RESUME events for branch coverage."""
- frame = self.callers_frame()
- self.last_lines[frame] = frame.f_lineno
+ return None
@panopticon("code", "@", None)
- def sysmon_py_return_arcs(
- self, code: CodeType, instruction_offset: int, retval: object,
+ def sysmon_py_return( # pylint: disable=useless-return
+ self,
+ code: CodeType,
+ instruction_offset: TOffset,
+ retval: object,
) -> MonitorReturn:
"""Handle sys.monitoring.events.PY_RETURN events for branch coverage."""
- frame = self.callers_frame()
- code_info = self.code_infos.get(id(code))
- if code_info is not None and code_info.file_data is not None:
- last_line = self.last_lines.get(frame)
- if last_line is not None:
- arc = (last_line, -code.co_firstlineno)
- # log(f"adding {arc=}")
- cast(set[TArc], code_info.file_data).add(arc)
-
- # Leaving this function, no need for the frame any more.
- self.last_lines.pop(frame, None)
-
- @panopticon("code", "@", "exc")
- def sysmon_py_unwind_arcs(
- self, code: CodeType, instruction_offset: int, exception: BaseException,
- ) -> MonitorReturn:
- """Handle sys.monitoring.events.PY_UNWIND events for branch coverage."""
- frame = self.callers_frame()
- # Leaving this function.
- last_line = self.last_lines.pop(frame, None)
- if isinstance(exception, GeneratorExit):
- # We don't want to count generator exits as arcs.
- return
code_info = self.code_infos.get(id(code))
if code_info is not None and code_info.file_data is not None:
+ assert code_info.byte_to_line is not None
+ last_line = code_info.byte_to_line[instruction_offset]
if last_line is not None:
arc = (last_line, -code.co_firstlineno)
- # log(f"adding {arc=}")
cast(set[TArc], code_info.file_data).add(arc)
-
+ log(f"adding {arc=}")
+ return None
@panopticon("code", "line")
- def sysmon_line_lines(self, code: CodeType, line_number: int) -> MonitorReturn:
+ def sysmon_line_lines(self, code: CodeType, line_number: TLineNo) -> MonitorReturn:
"""Handle sys.monitoring.events.LINE events for line coverage."""
- code_info = self.code_infos[id(code)]
- if code_info.file_data is not None:
+ code_info = self.code_infos.get(id(code))
+ if code_info is not None and code_info.file_data is not None:
cast(set[TLineNo], code_info.file_data).add(line_number)
- # log(f"adding {line_number=}")
- return sys.monitoring.DISABLE
+ log(f"adding {line_number=}")
+ return DISABLE
@panopticon("code", "line")
- def sysmon_line_arcs(self, code: CodeType, line_number: int) -> MonitorReturn:
+ def sysmon_line_arcs(self, code: CodeType, line_number: TLineNo) -> MonitorReturn:
"""Handle sys.monitoring.events.LINE events for branch coverage."""
code_info = self.code_infos[id(code)]
- ret = None
if code_info.file_data is not None:
- frame = self.callers_frame()
- last_line = self.last_lines.get(frame)
- if last_line is not None:
- arc = (last_line, line_number)
+ arc = (line_number, line_number)
+ cast(set[TArc], code_info.file_data).add(arc)
+ log(f"adding {arc=}")
+ return DISABLE
+
+ @panopticon("code", "@", "@")
+ def sysmon_branch_either(
+ self, code: CodeType, instruction_offset: TOffset, destination_offset: TOffset
+ ) -> MonitorReturn:
+ """Handle BRANCH_RIGHT and BRANCH_LEFT events."""
+ code_info = self.code_infos[id(code)]
+ added_arc = False
+ if code_info.file_data is not None:
+ dest_info = code_info.branch_trails.get(instruction_offset)
+ log(f"{dest_info = }")
+ if dest_info is not None:
+ for offsets, arc in dest_info:
+ if arc is None:
+ continue
+ if destination_offset in offsets:
+ cast(set[TArc], code_info.file_data).add(arc)
+ log(f"adding {arc=}")
+ added_arc = True
+ break
+
+ if not added_arc:
+ # This could be an exception jumping from line to line.
+ assert code_info.byte_to_line is not None
+ l1 = code_info.byte_to_line[instruction_offset]
+ l2 = code_info.byte_to_line[destination_offset]
+ if l1 != l2:
+ arc = (l1, l2)
cast(set[TArc], code_info.file_data).add(arc)
- # log(f"adding {arc=}")
- self.last_lines[frame] = line_number
- return ret
+ log(f"adding unforeseen {arc=}")
+
+ return DISABLE
diff --git a/doc/cmd.rst b/doc/cmd.rst
index fa6565678..3629322e2 100644
--- a/doc/cmd.rst
+++ b/doc/cmd.rst
@@ -273,14 +273,26 @@ Conflicting dynamic contexts (dynamic-conflict)
:meth:`.Coverage.switch_context` function to change the context. Only one of
these mechanisms should be in use at a time.
-sys.monitoring isn't available, using default core (no-sysmon)
+sys.monitoring isn't available in this version, using default core (no-sysmon)
You requested to use the sys.monitoring measurement core, but are running on
Python 3.11 or lower where it isn't available. A default core will be used
instead.
+sys.monitoring can't measure branches in this version, using default core (no-sysmon)
+ You requested the sys.monitoring measurement core and also branch coverage.
+ This isn't supported until the later alphas of Python 3.14. A default core
+ will be used instead.
+
+sys.monitoring doesn't yet support dynamic contexts, using default core (no-sysmon)
+ You requested the sys.monitoring measurement core and also dynamic contexts.
+ This isn't supported by coverage.py yet. A default core will be used
+ instead.
+
Individual warnings can be disabled with the :ref:`disable_warnings
-` configuration setting. To silence "No data was
-collected," add this to your configuration file:
+` configuration setting. It is a list of the
+short parenthetical nicknames in the warning messages. For example, to silence
+"No data was collected (no-data-collected)", add this to your configuration
+file:
.. [[[cog
show_configs(
diff --git a/igor.py b/igor.py
index 11b295cc4..aecd46299 100644
--- a/igor.py
+++ b/igor.py
@@ -133,7 +133,10 @@ def should_skip(core):
only_one = os.getenv("COVERAGE_ONE_CORE")
if only_one:
if CPYTHON:
- if core != "ctrace":
+ if sys.version_info >= (3, 12):
+ if core != "sysmon":
+ skipper = f"Only one core: not running {core}"
+ elif core != "ctrace":
skipper = f"Only one core: not running {core}"
else:
if core != "pytrace":
diff --git a/lab/run_sysmon.py b/lab/run_sysmon.py
index fbbd6a315..f88988bbe 100644
--- a/lab/run_sysmon.py
+++ b/lab/run_sysmon.py
@@ -27,6 +27,15 @@ def bytes_to_lines(code):
return b2l
+MY_EVENTS = (
+ events.PY_RETURN
+ | events.PY_RESUME
+ | events.LINE
+ | events.BRANCH_RIGHT
+ | events.BRANCH_LEFT
+ | events.JUMP
+)
+
def show_off(label, code, instruction_offset):
if code.co_filename == the_program:
b2l = bytes_to_lines(code)
@@ -49,12 +58,7 @@ def sysmon_py_start(code, instruction_offset):
sys.monitoring.set_local_events(
my_id,
code,
- events.PY_RETURN
- | events.PY_RESUME
- | events.LINE
- | events.BRANCH_TAKEN
- | events.BRANCH_NOT_TAKEN
- | events.JUMP,
+ MY_EVENTS,
)
@@ -78,13 +82,13 @@ def sysmon_branch(code, instruction_offset, destination_offset):
return sys.monitoring.DISABLE
-def sysmon_branch_taken(code, instruction_offset, destination_offset):
- show_off_off("BRANCH_TAKEN", code, instruction_offset, destination_offset)
+def sysmon_branch_right(code, instruction_offset, destination_offset):
+ show_off_off("BRANCH_RIGHT", code, instruction_offset, destination_offset)
return sys.monitoring.DISABLE
-def sysmon_branch_not_taken(code, instruction_offset, destination_offset):
- show_off_off("BRANCH_NOT_TAKEN", code, instruction_offset, destination_offset)
+def sysmon_branch_left(code, instruction_offset, destination_offset):
+ show_off_off("BRANCH_LEFT", code, instruction_offset, destination_offset)
return sys.monitoring.DISABLE
@@ -93,18 +97,19 @@ def sysmon_jump(code, instruction_offset, destination_offset):
return sys.monitoring.DISABLE
-sys.monitoring.set_events(
- my_id,
- events.PY_START | events.PY_UNWIND,
-)
-register(events.PY_START, sysmon_py_start)
-register(events.PY_RESUME, sysmon_py_resume)
-register(events.PY_RETURN, sysmon_py_return)
-# register(events.PY_UNWIND, sysmon_py_unwind_arcs)
-register(events.LINE, sysmon_line)
-#register(events.BRANCH, sysmon_branch)
-register(events.BRANCH_TAKEN, sysmon_branch_taken)
-register(events.BRANCH_NOT_TAKEN, sysmon_branch_not_taken)
-register(events.JUMP, sysmon_jump)
+if 1:
+ sys.monitoring.set_events(
+ my_id,
+ events.PY_START | events.PY_UNWIND,
+ )
+ register(events.PY_START, sysmon_py_start)
+ register(events.PY_RESUME, sysmon_py_resume)
+ register(events.PY_RETURN, sysmon_py_return)
+ # register(events.PY_UNWIND, sysmon_py_unwind_arcs)
+ register(events.LINE, sysmon_line)
+ register(events.BRANCH, sysmon_branch)
+ register(events.BRANCH_RIGHT, sysmon_branch_right)
+ register(events.BRANCH_LEFT, sysmon_branch_left)
+ register(events.JUMP, sysmon_jump)
exec(code)
diff --git a/metacov.ini b/metacov.ini
index 8e00747ba..1aa05926b 100644
--- a/metacov.ini
+++ b/metacov.ini
@@ -9,6 +9,7 @@
[run]
branch = true
data_file = ${COVERAGE_METAFILE-.metacov}
+disable_warnings = no-sysmon
parallel = true
relative_files = true
source =
diff --git a/pyproject.toml b/pyproject.toml
index 2f08ab55c..1d3199d12 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -128,6 +128,8 @@ filterwarnings = [
## Pytest warns if it can't collect things that seem to be tests. This should be an error.
"error::pytest.PytestCollectionWarning",
+
+ "ignore:.*no-sysmon"
]
# xfail tests that pass should fail the test suite
diff --git a/tests/conftest.py b/tests/conftest.py
index b06ffe2ff..876dc0827 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -58,6 +58,8 @@ def set_warnings() -> None:
# https://github.com/python/cpython/issues/105539
warnings.filterwarnings("ignore", r"unclosed database", category=ResourceWarning)
+ warnings.filterwarnings("ignore", r".*no-sysmon")
+
@pytest.fixture(autouse=True)
def reset_sys_path() -> Iterator[None]:
@@ -86,6 +88,7 @@ def reset_filesdotpy_globals() -> Iterator[None]:
def pytest_sessionstart() -> None:
"""Run once at the start of the test session."""
+ warnings.filterwarnings("ignore", r".*no-sysmon")
# Only in the main process...
if WORKER == "none":
# Create a .pth file for measuring subprocess coverage.
diff --git a/tests/coveragetest.py b/tests/coveragetest.py
index bdc16f99c..8179433f7 100644
--- a/tests/coveragetest.py
+++ b/tests/coveragetest.py
@@ -60,7 +60,7 @@ def arcs_to_branches(arcs: Iterable[TArc]) -> dict[TLineNo, list[TLineNo]]:
def branches_to_arcs(branches: dict[TLineNo, list[TLineNo]]) -> list[TArc]:
- """Convert a dict od branches into a list of arcs."""
+ """Convert a dict of branches into a list of arcs."""
return [(fromno, tono) for fromno, tonos in branches.items() for tono in tonos]
diff --git a/tests/test_arcs.py b/tests/test_arcs.py
index c22e3d1f4..7ec0e663a 100644
--- a/tests/test_arcs.py
+++ b/tests/test_arcs.py
@@ -1660,7 +1660,7 @@ def test_if_not_debug(self) -> None:
lines.add(5)
else:
lines.add(7)
- assert lines == set([7])
+ assert lines == {7}
""",
branchz=branchz,
)
diff --git a/tests/test_concurrency.py b/tests/test_concurrency.py
index ce163aba7..98f72f77a 100644
--- a/tests/test_concurrency.py
+++ b/tests/test_concurrency.py
@@ -554,13 +554,19 @@ def test_multiprocessing_with_branching(self, start_method: str) -> None:
code = (SQUARE_OR_CUBE_WORK + MULTI_CODE).format(NPROCS=nprocs, UPTO=upto)
total = sum(x*x if x%2 else x*x*x for x in range(upto))
expected_out = f"{nprocs} pids, total = {total}"
+ expect_warn = (
+ env.PYBEHAVIOR.pep669
+ and (not env.PYBEHAVIOR.branch_right_left)
+ and testenv.SYS_MON
+ )
self.make_file("multi.py", code)
self.make_file("multi.rc", """\
[run]
concurrency = multiprocessing
branch = True
omit = */site-packages/*
- """)
+ """ + ("disable_warnings = no-sysmon" if expect_warn else "")
+ )
out = self.run_command(f"coverage run --rcfile=multi.rc multi.py {start_method}")
assert out.rstrip() == expected_out
diff --git a/tests/test_oddball.py b/tests/test_oddball.py
index 690b3d5a1..21d1fc421 100644
--- a/tests/test_oddball.py
+++ b/tests/test_oddball.py
@@ -502,7 +502,7 @@ def swap_it():
def test_setting_new_trace_function(self) -> None:
# https://github.com/nedbat/coveragepy/issues/436
- if testenv.SETTRACE_CORE:
+ if testenv.SETTRACE_CORE or not env.PYBEHAVIOR.branch_right_left:
missing = "5-7, 13-14"
else:
missing = "5-7"
diff --git a/tests/test_process.py b/tests/test_process.py
index 61a9d5ecf..082fa917f 100644
--- a/tests/test_process.py
+++ b/tests/test_process.py
@@ -1101,6 +1101,8 @@ def test_core_default(self) -> None:
out = self.run_command("coverage run --debug=sys numbers.py")
assert out.endswith("123 456\n")
core = re_line(r" core:", out).strip()
+ # if env.PYBEHAVIOR.pep669:
+ # assert core == "core: SysMonitor"
if self.has_ctracer:
assert core == "core: CTracer"
else:
@@ -1140,6 +1142,14 @@ def test_core_request_sysmon(self) -> None:
assert core in ("core: CTracer", "core: PyTracer")
assert warns
+ def test_core_request_nosuchcore(self) -> None:
+ self.del_environ("COVERAGE_TEST_CORES")
+ self.set_environ("COVERAGE_CORE", "nosuchcore")
+ self.make_file("numbers.py", "print(123, 456)")
+ out = self.run_command("coverage run numbers.py")
+ assert "Unknown core value: 'nosuchcore'\n" in out
+ assert "123 456" not in out
+
class FailUnderNoFilesTest(CoverageTest):
"""Test that nothing to report results in an error exit status."""
diff --git a/tox.ini b/tox.ini
index 8cf006b7c..9314b3322 100644
--- a/tox.ini
+++ b/tox.ini
@@ -4,7 +4,7 @@
[tox]
# When changing this list, be sure to check the [gh] list below.
# PYVERSIONS
-envlist = py3{9,10,11,12,13,14}, pypy3, doc, lint, mypy
+envlist = py3{9,10,11,12,13,14}, pypy3, anypy, doc, lint, mypy
skip_missing_interpreters = {env:COVERAGE_SKIP_MISSING_INTERPRETERS:True}
toxworkdir = {env:TOXWORKDIR:.tox}
From e3504b035f182ad4bfe4f5a75e17d0ec5a7b7ee6 Mon Sep 17 00:00:00 2001
From: Ned Batchelder
Date: Sun, 9 Mar 2025 16:02:37 -0400
Subject: [PATCH 58/90] test: benchmark tweaks
Though it didn't result in a runnable benchmark :(
---
benchmark/README.rst | 9 +++++++++
benchmark/benchmark.py | 2 ++
benchmark/run.py | 15 ++++++++-------
3 files changed, 19 insertions(+), 7 deletions(-)
diff --git a/benchmark/README.rst b/benchmark/README.rst
index 3e1588547..2372a7c6c 100644
--- a/benchmark/README.rst
+++ b/benchmark/README.rst
@@ -50,6 +50,15 @@ for the table. There will be a row for each combination of the two dimensions.
The `column` argument is the remaining dimension that is used to add columns to
the table, one for each item in that dimension.
+To run a benchmark, create a Python file with a run_experiment call in it.
+Many are in run.py, guarded by ``if 0:`` and ``if 1:`` clauses. In the
+benchmark directory, run your Python file. If you haven't provided the
+``num_runs`` argument to run_experiment, put the number of runs on the command
+line::
+
+ % cd benchmark
+ % python3 run.py 3
+
For example::
run_experiment(
diff --git a/benchmark/benchmark.py b/benchmark/benchmark.py
index 0a17218db..6739f39cd 100644
--- a/benchmark/benchmark.py
+++ b/benchmark/benchmark.py
@@ -823,7 +823,9 @@ def __init__(
tweaks: TweaksType = None,
env_vars: Env_VarsType = None,
):
+ # Check that it really is a coverage source directory.
directory = file_must_exist(directory_name, "coverage directory")
+ file_must_exist(str(directory / "igor.py"))
super().__init__(
slug=slug,
pip_args=str(directory),
diff --git a/benchmark/run.py b/benchmark/run.py
index e606cade9..527468e5e 100644
--- a/benchmark/run.py
+++ b/benchmark/run.py
@@ -72,7 +72,7 @@
],
)
-if 1:
+if 0:
# Compare N Python versions
vers = [10, 11, 12, 13]
run_experiment(
@@ -93,12 +93,13 @@
],
)
-if 0:
+if 1:
# Compare sysmon on many projects
run_experiment(
py_versions=[
- Python(3, 12),
+ # Python(3, 12),
+ AdHocPython("/usr/local/cpython", "main"),
],
cov_versions=[
NoCoverage("nocov"),
@@ -107,14 +108,14 @@
],
projects=[
# ProjectSphinx(), # Works, slow
- ProjectPygments(), # Works
+ # ProjectPygments(), # Doesn't work on 3.14
# ProjectRich(), # Doesn't work
# ProjectTornado(), # Works, tests fail
# ProjectDulwich(), # Works
# ProjectBlack(), # Works, slow
- # ProjectMpmath(), # Works, slow
- ProjectMypy(), # Works, slow
- # ProjectHtml5lib(), # Works
+ ProjectMpmath(), # Works, slow
+ # ProjectMypy(), # Works, slow
+ # ProjectHtml5lib(), # Doesn't work on 3.14
# ProjectUrllib3(), # Works
],
rows=["pyver", "proj"],
From 79c7fae6e5c39116c258d75057b6f60a80c91e43 Mon Sep 17 00:00:00 2001
From: Ned Batchelder
Date: Sun, 9 Mar 2025 16:25:36 -0400
Subject: [PATCH 59/90] chore: make upgrade doc_upgrade
---
doc/requirements.pip | 8 ++++----
requirements/dev.pip | 8 ++++----
requirements/kit.pip | 2 +-
requirements/light-threads.pip | 4 ++--
requirements/mypy.pip | 4 ++--
requirements/pip-tools.pip | 2 +-
requirements/pip.pip | 2 +-
requirements/pytest.pip | 2 +-
8 files changed, 16 insertions(+), 16 deletions(-)
diff --git a/doc/requirements.pip b/doc/requirements.pip
index d8b2ac6f9..ae5de5420 100644
--- a/doc/requirements.pip
+++ b/doc/requirements.pip
@@ -38,7 +38,7 @@ idna==3.10
# requests
imagesize==1.4.1
# via sphinx
-jinja2==3.1.5
+jinja2==3.1.6
# via sphinx
markupsafe==3.0.2
# via jinja2
@@ -105,7 +105,7 @@ sphinxcontrib-serializinghtml==2.0.0
# via sphinx
sphinxcontrib-spelling==8.0.1
# via -r doc/requirements.in
-starlette==0.46.0
+starlette==0.46.1
# via sphinx-autobuild
stevedore==5.4.1
# via doc8
@@ -117,9 +117,9 @@ uvicorn==0.34.0
# via sphinx-autobuild
watchfiles==1.0.4
# via sphinx-autobuild
-websockets==15.0
+websockets==15.0.1
# via sphinx-autobuild
# The following packages are considered to be unsafe in a requirements file:
-setuptools==75.8.2
+setuptools==76.0.0
# via pbr
diff --git a/requirements/dev.pip b/requirements/dev.pip
index cbcc7123f..13e6b0d81 100644
--- a/requirements/dev.pip
+++ b/requirements/dev.pip
@@ -4,7 +4,7 @@
#
# make upgrade
#
-astroid==3.3.8
+astroid==3.3.9
# via pylint
attrs==25.1.0
# via hypothesis
@@ -49,7 +49,7 @@ flaky==3.8.1
# via -r /Users/ned/coverage/trunk/requirements/pytest.in
greenlet==3.1.1
# via -r requirements/dev.in
-hypothesis==6.127.5
+hypothesis==6.128.1
# via -r /Users/ned/coverage/trunk/requirements/pytest.in
id==1.5.0
# via twine
@@ -115,7 +115,7 @@ pygments==2.19.1
# pudb
# readme-renderer
# rich
-pylint==3.3.4
+pylint==3.3.5
# via -r requirements/dev.in
pyproject-api==1.9.0
# via tox
@@ -195,7 +195,7 @@ zipp==3.21.0
# The following packages are considered to be unsafe in a requirements file:
pip==25.0.1
# via -r /Users/ned/coverage/trunk/requirements/pip.in
-setuptools==75.8.2
+setuptools==76.0.0
# via
# -r /Users/ned/coverage/trunk/requirements/pip.in
# check-manifest
diff --git a/requirements/kit.pip b/requirements/kit.pip
index 27ebb5711..7b7c8a6a3 100644
--- a/requirements/kit.pip
+++ b/requirements/kit.pip
@@ -108,5 +108,5 @@ zipp==3.21.0
# via importlib-metadata
# The following packages are considered to be unsafe in a requirements file:
-setuptools==75.8.2
+setuptools==76.0.0
# via -r requirements/kit.in
diff --git a/requirements/light-threads.pip b/requirements/light-threads.pip
index 5bca6b6e6..aff5c3b49 100644
--- a/requirements/light-threads.pip
+++ b/requirements/light-threads.pip
@@ -8,7 +8,7 @@ cffi==1.17.1
# via -r requirements/light-threads.in
dnspython==2.7.0
# via eventlet
-eventlet==0.39.0
+eventlet==0.39.1
# via -r requirements/light-threads.in
gevent==24.11.1
# via -r requirements/light-threads.in
@@ -25,7 +25,7 @@ zope-interface==7.2
# via gevent
# The following packages are considered to be unsafe in a requirements file:
-setuptools==75.8.2
+setuptools==76.0.0
# via
# zope-event
# zope-interface
diff --git a/requirements/mypy.pip b/requirements/mypy.pip
index 8ab8cf251..6d63badc7 100644
--- a/requirements/mypy.pip
+++ b/requirements/mypy.pip
@@ -16,7 +16,7 @@ execnet==2.1.1
# via pytest-xdist
flaky==3.8.1
# via -r /Users/ned/coverage/trunk/requirements/pytest.in
-hypothesis==6.127.5
+hypothesis==6.128.1
# via -r /Users/ned/coverage/trunk/requirements/pytest.in
iniconfig==2.0.0
# via pytest
@@ -42,7 +42,7 @@ tomli==2.2.1
# via
# mypy
# pytest
-types-requests==2.32.0.20250301
+types-requests==2.32.0.20250306
# via -r requirements/mypy.in
types-tabulate==0.9.0.20241207
# via -r requirements/mypy.in
diff --git a/requirements/pip-tools.pip b/requirements/pip-tools.pip
index 88eb7105a..72ccd349b 100644
--- a/requirements/pip-tools.pip
+++ b/requirements/pip-tools.pip
@@ -30,5 +30,5 @@ zipp==3.21.0
# The following packages are considered to be unsafe in a requirements file:
pip==25.0.1
# via pip-tools
-setuptools==75.8.2
+setuptools==76.0.0
# via pip-tools
diff --git a/requirements/pip.pip b/requirements/pip.pip
index 765b5d8e2..4827fc3c1 100644
--- a/requirements/pip.pip
+++ b/requirements/pip.pip
@@ -18,5 +18,5 @@ virtualenv==20.28.1
# The following packages are considered to be unsafe in a requirements file:
pip==25.0.1
# via -r requirements/pip.in
-setuptools==75.8.2
+setuptools==76.0.0
# via -r requirements/pip.in
diff --git a/requirements/pytest.pip b/requirements/pytest.pip
index f96757b8b..4d12a374a 100644
--- a/requirements/pytest.pip
+++ b/requirements/pytest.pip
@@ -16,7 +16,7 @@ execnet==2.1.1
# via pytest-xdist
flaky==3.8.1
# via -r requirements/pytest.in
-hypothesis==6.127.5
+hypothesis==6.128.1
# via -r requirements/pytest.in
iniconfig==2.0.0
# via pytest
From 824b3ba3efbbe07f188ce13e9a8143604747fece Mon Sep 17 00:00:00 2001
From: Ned Batchelder
Date: Sun, 9 Mar 2025 16:26:05 -0400
Subject: [PATCH 60/90] style: fix one change from updated pylint
---
coverage/cmdline.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/coverage/cmdline.py b/coverage/cmdline.py
index 7c01ccbf0..783345e01 100644
--- a/coverage/cmdline.py
+++ b/coverage/cmdline.py
@@ -6,7 +6,7 @@
from __future__ import annotations
import glob
-import optparse # pylint: disable=deprecated-module
+import optparse
import os
import os.path
import shlex
From 0973f44cc4a73bab859af74a3dd42078b0b2cc85 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 10 Mar 2025 09:18:02 -0400
Subject: [PATCH 61/90] chore: bump the action-dependencies group with 2
updates (#1935)
Bumps the action-dependencies group with 2 updates: [github/codeql-action](https://github.com/github/codeql-action) and [actions/attest-build-provenance](https://github.com/actions/attest-build-provenance).
Updates `github/codeql-action` from 3.28.10 to 3.28.11
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d...6bb031afdd8eb862ea3fc1848194185e076637e5)
Updates `actions/attest-build-provenance` from 2.2.2 to 2.2.3
- [Release notes](https://github.com/actions/attest-build-provenance/releases)
- [Changelog](https://github.com/actions/attest-build-provenance/blob/main/RELEASE.md)
- [Commits](https://github.com/actions/attest-build-provenance/compare/bd77c077858b8d561b7a36cbe48ef4cc642ca39d...c074443f1aee8d4aeeae555aebba3282517141b2)
---
updated-dependencies:
- dependency-name: github/codeql-action
dependency-type: direct:production
update-type: version-update:semver-patch
dependency-group: action-dependencies
- dependency-name: actions/attest-build-provenance
dependency-type: direct:production
update-type: version-update:semver-patch
dependency-group: action-dependencies
...
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
.github/workflows/codeql-analysis.yml | 6 +++---
.github/workflows/publish.yml | 4 ++--
2 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index 06471c1f4..5eddd8137 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -51,7 +51,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
- uses: github/codeql-action/init@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v3
+ uses: github/codeql-action/init@6bb031afdd8eb862ea3fc1848194185e076637e5 # v3
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -62,7 +62,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
- uses: github/codeql-action/autobuild@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v3
+ uses: github/codeql-action/autobuild@6bb031afdd8eb862ea3fc1848194185e076637e5 # v3
# ℹ️ Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
@@ -76,4 +76,4 @@ jobs:
# make release
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v3
+ uses: github/codeql-action/analyze@6bb031afdd8eb862ea3fc1848194185e076637e5 # v3
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index cac0ce7a9..e6098e027 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -81,7 +81,7 @@ jobs:
files=$(ls dist 2>/dev/null | wc -l) && [ "$files" -eq $EXPECTED ] || exit 1
- name: "Generate attestations"
- uses: actions/attest-build-provenance@bd77c077858b8d561b7a36cbe48ef4cc642ca39d # v2.2.2
+ uses: actions/attest-build-provenance@c074443f1aee8d4aeeae555aebba3282517141b2 # v2.2.3
with:
subject-path: "dist/*"
@@ -121,7 +121,7 @@ jobs:
files=$(ls dist 2>/dev/null | wc -l) && [ "$files" -eq $EXPECTED ] || exit 1
- name: "Generate attestations"
- uses: actions/attest-build-provenance@bd77c077858b8d561b7a36cbe48ef4cc642ca39d # v2.2.2
+ uses: actions/attest-build-provenance@c074443f1aee8d4aeeae555aebba3282517141b2 # v2.2.3
with:
subject-path: "dist/*"
From c9908d731187f515093105f76b5b4cc39dbff9ba Mon Sep 17 00:00:00 2001
From: Ned Batchelder
Date: Tue, 11 Mar 2025 15:52:14 -0400
Subject: [PATCH 62/90] test: benchmark improvements
---
benchmark/benchmark.py | 43 +++++++++++++++++++++++++++++++++++++++---
benchmark/run.py | 4 +++-
2 files changed, 43 insertions(+), 4 deletions(-)
diff --git a/benchmark/benchmark.py b/benchmark/benchmark.py
index 6739f39cd..35cf938c7 100644
--- a/benchmark/benchmark.py
+++ b/benchmark/benchmark.py
@@ -191,11 +191,15 @@ class ProjectToTest:
# Where can we clone the project from?
git_url: str = ""
+ local_git: str = ""
slug: str = ""
env_vars: Env_VarsType = {}
def __init__(self) -> None:
- url_must_exist(self.git_url)
+ if self.git_url:
+ url_must_exist(self.git_url)
+ if self.local_git:
+ file_must_exist(self.local_git)
if not self.slug:
if self.git_url:
self.slug = self.git_url.split("/")[-1]
@@ -211,12 +215,13 @@ def make_dir(self) -> None:
def get_source(self, shell: ShellSession, retries: int = 5) -> None:
"""Get the source of the project."""
+ git_source = self.local_git or self.git_url
for retry in range(retries):
try:
- shell.run_command(f"git clone {self.git_url} {self.dir}")
+ shell.run_command(f"git clone {git_source} {self.dir}")
return
except Exception as e:
- print(f"Retrying to clone {self.git_url} due to error:\n{e}")
+ print(f"Retrying to clone {git_source} due to error:\n{e}")
if retry == retries - 1:
raise e
@@ -423,6 +428,38 @@ def __init__(self, more_pytest_args: str = ""):
self.slug = "mashbranch"
+class ProjectPillow(ProjectToTest):
+ git_url = "https://github.com/python-pillow/Pillow"
+ local_git = "/src/Pillow"
+
+ def __init__(self, more_pytest_args: str = ""):
+ super().__init__()
+ self.more_pytest_args = more_pytest_args
+
+ def prep_environment(self, env: Env) -> None:
+ env.shell.run_command(f"{env.python} -m pip install '.[tests]'")
+
+ def run_no_coverage(self, env: Env) -> float:
+ env.shell.run_command(f"{env.python} -m pytest {self.more_pytest_args}")
+ return env.shell.last_duration
+
+ def run_with_coverage(self, env: Env, cov_ver: Coverage) -> float:
+ env.shell.run_command(f"{env.python} -m pip install {cov_ver.pip_args}")
+ env.shell.run_command(
+ f"{env.python} -m pytest --cov=PIL --cov=Tests {self.more_pytest_args}"
+ )
+ duration = env.shell.last_duration
+ report = env.shell.run_command(f"{env.python} -m coverage report --precision=6")
+ print("Results:", report.splitlines()[-1])
+ return duration
+
+
+class ProjectPillowBranch(ProjectPillow):
+ def __init__(self, more_pytest_args: str = ""):
+ super().__init__(more_pytest_args="--cov-branch " + more_pytest_args)
+ self.slug = "Pilbranch"
+
+
class ProjectOperator(ProjectToTest):
git_url = "https://github.com/nedbat/operator"
diff --git a/benchmark/run.py b/benchmark/run.py
index 527468e5e..075196a6d 100644
--- a/benchmark/run.py
+++ b/benchmark/run.py
@@ -107,13 +107,15 @@
CoverageSource(slug="sysmon", env_vars={"COVERAGE_CORE": "sysmon"}),
],
projects=[
+ ProjectPillow(), #"-k test_pickle"),
+ ProjectPillowBranch(), #"-k test_pickle"),
# ProjectSphinx(), # Works, slow
# ProjectPygments(), # Doesn't work on 3.14
# ProjectRich(), # Doesn't work
# ProjectTornado(), # Works, tests fail
# ProjectDulwich(), # Works
# ProjectBlack(), # Works, slow
- ProjectMpmath(), # Works, slow
+ # ProjectMpmath(), # Works, slow
# ProjectMypy(), # Works, slow
# ProjectHtml5lib(), # Doesn't work on 3.14
# ProjectUrllib3(), # Works
From 1e99d2881b1b60af1b889ea364e1ede018401740 Mon Sep 17 00:00:00 2001
From: Ned Batchelder
Date: Tue, 11 Mar 2025 16:03:16 -0400
Subject: [PATCH 63/90] perf(sysmon): improve speed
Don't compute trails until we need them. No logging. Tweak the
environment variables.
---
coverage/sysmon.py | 85 +++++++++++++++++++++++++---------------------
1 file changed, 46 insertions(+), 39 deletions(-)
diff --git a/coverage/sysmon.py b/coverage/sysmon.py
index ca31513d6..84d37107c 100644
--- a/coverage/sysmon.py
+++ b/coverage/sysmon.py
@@ -45,8 +45,11 @@
# pylint: disable=unused-argument
-# $set_env.py: COVERAGE_LOG_SYSMON - Log sys.monitoring activity
-LOG = bool(int(os.getenv("COVERAGE_LOG_SYSMON", 0)))
+# $set_env.py: COVERAGE_SYSMON_LOG - Log sys.monitoring activity
+LOG = bool(int(os.getenv("COVERAGE_SYSMON_LOG", 0)))
+
+# $set_env.py: COVERAGE_SYSMON_STATS - Collect sys.monitoring stats
+COLLECT_STATS = bool(int(os.getenv("COVERAGE_SYSMON_STATS", 0)))
# This module will be imported in all versions of Python, but only used in 3.12+
# It will be type-checked for 3.12, but not for earlier versions.
@@ -236,17 +239,17 @@ def populate_branch_trails(code: CodeType, code_info: CodeInfo) -> None:
arc from the original instruction's line to the new source line.
"""
- log(f"populate_branch_trails: {code}")
+ # log(f"populate_branch_trails: {code}")
iwalker = InstructionWalker(code)
for inst in iwalker.walk(follow_jumps=False):
- log(f"considering {inst=}")
+ # log(f"considering {inst=}")
if not inst.jump_target:
# We only care about instructions with jump targets.
- log("no jump_target")
+ # log("no jump_target")
continue
if inst.opcode in ALWAYS_JUMPS:
# We don't care about unconditional jumps.
- log("always jumps")
+ # log("always jumps")
continue
from_line = inst.line_number
@@ -265,23 +268,23 @@ def walk_one_branch(
to_line = inst2.line_number
break
elif inst2.jump_target and (inst2.opcode not in ALWAYS_JUMPS):
- log(
- f"stop: {inst2.jump_target=}, "
- + f"{inst2.opcode=} ({dis.opname[inst2.opcode]}), "
- + f"{ALWAYS_JUMPS=}"
- )
+ # log(
+ # f"stop: {inst2.jump_target=}, "
+ # + f"{inst2.opcode=} ({dis.opname[inst2.opcode]}), "
+ # + f"{ALWAYS_JUMPS=}"
+ # )
break
elif inst2.opcode in RETURNS:
to_line = -code.co_firstlineno
break
if to_line is not None:
- log(
- f"possible branch from @{start_at}: "
- + f"{inst_offsets}, {(from_line, to_line)} {code}"
- )
+ # log(
+ # f"possible branch from @{start_at}: "
+ # + f"{inst_offsets}, {(from_line, to_line)} {code}"
+ # )
return inst_offsets, (from_line, to_line)
else:
- log(f"no possible branch from @{start_at}: {inst_offsets}")
+ # log(f"no possible branch from @{start_at}: {inst_offsets}")
return [], None
# Calculate two trails: one from the next instruction, and one from the
@@ -302,6 +305,7 @@ def walk_one_branch(
code_info.branch_trails[offset] = []
code_info.branch_trails[offset].append(trail)
+
@dataclass
class CodeInfo:
"""The information we want about each code object."""
@@ -363,9 +367,11 @@ def __init__(self, tool_id: int) -> None:
self.sysmon_on = False
self.lock = threading.Lock()
- self.stats = {
- "starts": 0,
- }
+ self.stats: dict[str, int] | None = None
+ if COLLECT_STATS:
+ self.stats = {
+ "starts": 0,
+ }
self.stopped = False
self._activity = False
@@ -431,7 +437,7 @@ def reset_activity(self) -> None:
def get_stats(self) -> dict[str, int] | None:
"""Return a dictionary of statistics, or None."""
- return None
+ return self.stats
@panopticon("code", "@")
def sysmon_py_start( # pylint: disable=useless-return
@@ -440,7 +446,8 @@ def sysmon_py_start( # pylint: disable=useless-return
"""Handle sys.monitoring.events.PY_START events."""
# Entering a new frame. Decide if we should trace in this file.
self._activity = True
- self.stats["starts"] += 1
+ if self.stats is not None:
+ self.stats["starts"] += 1
code_info = self.code_infos.get(id(code))
tracing_code: bool | None = None
@@ -483,9 +490,6 @@ def sysmon_py_start( # pylint: disable=useless-return
branch_trails={},
)
self.code_infos[id(code)] = code_info
- if self.trace_arcs:
- populate_branch_trails(code, code_info)
- log(f"branch_trails for {code}:\n {code_info.branch_trails}")
self.code_objects.append(code)
if tracing_code:
@@ -519,7 +523,7 @@ def sysmon_py_return( # pylint: disable=useless-return
if last_line is not None:
arc = (last_line, -code.co_firstlineno)
cast(set[TArc], code_info.file_data).add(arc)
- log(f"adding {arc=}")
+ # log(f"adding {arc=}")
return None
@panopticon("code", "line")
@@ -528,7 +532,7 @@ def sysmon_line_lines(self, code: CodeType, line_number: TLineNo) -> MonitorRetu
code_info = self.code_infos.get(id(code))
if code_info is not None and code_info.file_data is not None:
cast(set[TLineNo], code_info.file_data).add(line_number)
- log(f"adding {line_number=}")
+ # log(f"adding {line_number=}")
return DISABLE
@panopticon("code", "line")
@@ -538,7 +542,7 @@ def sysmon_line_arcs(self, code: CodeType, line_number: TLineNo) -> MonitorRetur
if code_info.file_data is not None:
arc = (line_number, line_number)
cast(set[TArc], code_info.file_data).add(arc)
- log(f"adding {arc=}")
+ # log(f"adding {arc=}")
return DISABLE
@panopticon("code", "@", "@")
@@ -547,28 +551,31 @@ def sysmon_branch_either(
) -> MonitorReturn:
"""Handle BRANCH_RIGHT and BRANCH_LEFT events."""
code_info = self.code_infos[id(code)]
- added_arc = False
if code_info.file_data is not None:
+ if not code_info.branch_trails:
+ populate_branch_trails(code, code_info)
+ # log(f"branch_trails for {code}:\n {code_info.branch_trails}")
+ added_arc = False
dest_info = code_info.branch_trails.get(instruction_offset)
- log(f"{dest_info = }")
+ # log(f"{dest_info = }")
if dest_info is not None:
for offsets, arc in dest_info:
if arc is None:
continue
if destination_offset in offsets:
cast(set[TArc], code_info.file_data).add(arc)
- log(f"adding {arc=}")
+ # log(f"adding {arc=}")
added_arc = True
break
- if not added_arc:
- # This could be an exception jumping from line to line.
- assert code_info.byte_to_line is not None
- l1 = code_info.byte_to_line[instruction_offset]
- l2 = code_info.byte_to_line[destination_offset]
- if l1 != l2:
- arc = (l1, l2)
- cast(set[TArc], code_info.file_data).add(arc)
- log(f"adding unforeseen {arc=}")
+ if not added_arc:
+ # This could be an exception jumping from line to line.
+ assert code_info.byte_to_line is not None
+ l1 = code_info.byte_to_line[instruction_offset]
+ l2 = code_info.byte_to_line[destination_offset]
+ if l1 != l2:
+ arc = (l1, l2)
+ cast(set[TArc], code_info.file_data).add(arc)
+ # log(f"adding unforeseen {arc=}")
return DISABLE
From 7e9f2f20c81a6ab49a19c42b1cd3c5ba4282d223 Mon Sep 17 00:00:00 2001
From: Ned Batchelder
Date: Fri, 14 Mar 2025 12:46:45 -0400
Subject: [PATCH 64/90] perf(sysmon): silly mistake
---
coverage/sysmon.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/coverage/sysmon.py b/coverage/sysmon.py
index 84d37107c..0fd8c1336 100644
--- a/coverage/sysmon.py
+++ b/coverage/sysmon.py
@@ -506,7 +506,7 @@ def sysmon_py_start( # pylint: disable=useless-return
)
sys_monitoring.set_local_events(self.myid, code, local_events)
- return None
+ return DISABLE
@panopticon("code", "@", None)
def sysmon_py_return( # pylint: disable=useless-return
@@ -524,7 +524,7 @@ def sysmon_py_return( # pylint: disable=useless-return
arc = (last_line, -code.co_firstlineno)
cast(set[TArc], code_info.file_data).add(arc)
# log(f"adding {arc=}")
- return None
+ return DISABLE
@panopticon("code", "line")
def sysmon_line_lines(self, code: CodeType, line_number: TLineNo) -> MonitorReturn:
From c4919cbf84efef68c1e9572d8438204e64b32d4c Mon Sep 17 00:00:00 2001
From: Ned Batchelder
Date: Sat, 15 Mar 2025 07:18:27 -0400
Subject: [PATCH 65/90] lint: somehow these snuck through
---
coverage/sysmon.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/coverage/sysmon.py b/coverage/sysmon.py
index 0fd8c1336..805b5f9a3 100644
--- a/coverage/sysmon.py
+++ b/coverage/sysmon.py
@@ -440,7 +440,7 @@ def get_stats(self) -> dict[str, int] | None:
return self.stats
@panopticon("code", "@")
- def sysmon_py_start( # pylint: disable=useless-return
+ def sysmon_py_start(
self, code: CodeType, instruction_offset: TOffset
) -> MonitorReturn:
"""Handle sys.monitoring.events.PY_START events."""
@@ -509,7 +509,7 @@ def sysmon_py_start( # pylint: disable=useless-return
return DISABLE
@panopticon("code", "@", None)
- def sysmon_py_return( # pylint: disable=useless-return
+ def sysmon_py_return(
self,
code: CodeType,
instruction_offset: TOffset,
From 33f12dfa365f3faa349d844e603b0432ba3476d3 Mon Sep 17 00:00:00 2001
From: Ned Batchelder
Date: Sun, 16 Mar 2025 09:19:57 -0400
Subject: [PATCH 66/90] feat: Coverage.branch_stats() #1888
---
CHANGES.rst | 4 ++++
coverage/control.py | 15 +++++++++++++++
coverage/results.py | 1 +
tests/test_api.py | 25 +++++++++++++++++++------
4 files changed, 39 insertions(+), 6 deletions(-)
diff --git a/CHANGES.rst b/CHANGES.rst
index eaa4de343..e19e872e2 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -23,6 +23,9 @@ upgrading your version of coverage.py.
Unreleased
----------
+- The Coverage object has a new method, :meth:`.Coverage.branch_stats` for
+ getting simple branch information for a module. Closes `issue 1888`_.
+
- Many constant tests in if statements are now recognized as being optimized
away. For example, previously ``if 13:`` would have been considered a branch
with one path not taken. Now it is understood as always true and no coverage
@@ -39,6 +42,7 @@ Unreleased
- Confirmed support for PyPy 3.11. Thanks Michał Górny.
+.. _issue 1888: https://github.com/nedbat/coveragepy/issues/1888
.. _pull 1919: https://github.com/nedbat/coveragepy/pull/1919
diff --git a/coverage/control.py b/coverage/control.py
index 70ecb833d..d79c97ace 100644
--- a/coverage/control.py
+++ b/coverage/control.py
@@ -950,6 +950,7 @@ def analysis2(
analysis.missing_formatted(),
)
+ @functools.lru_cache(maxsize=1)
def _analyze(self, morf: TMorf) -> Analysis:
"""Analyze a module or file. Private for now."""
self._init()
@@ -960,6 +961,20 @@ def _analyze(self, morf: TMorf) -> Analysis:
filename = self._file_mapper(file_reporter.filename)
return analysis_from_file_reporter(data, self.config.precision, file_reporter, filename)
+ def branch_stats(self, morf: TMorf) -> dict[TLineNo, tuple[int, int]]:
+ """Get branch statistics about a module.
+
+ `morf` is a module or a file name.
+
+ Returns a dict mapping line numbers to a tuple:
+ (total_exits, taken_exits).
+
+ .. versionadded:: 7.7
+
+ """
+ analysis = self._analyze(morf)
+ return analysis.branch_stats()
+
@functools.lru_cache(maxsize=1)
def _get_file_reporter(self, morf: TMorf) -> FileReporter:
"""Get a FileReporter for a module or file name."""
diff --git a/coverage/results.py b/coverage/results.py
index 74de21dbe..6d28e73f7 100644
--- a/coverage/results.py
+++ b/coverage/results.py
@@ -231,6 +231,7 @@ def branch_stats(self) -> dict[TLineNo, tuple[int, int]]:
Returns a dict mapping line numbers to a tuple:
(total_exits, taken_exits).
+
"""
missing_arcs = self.missing_branch_arcs()
diff --git a/tests/test_api.py b/tests/test_api.py
index ab738e3c3..d85b89764 100644
--- a/tests/test_api.py
+++ b/tests/test_api.py
@@ -1008,7 +1008,10 @@ def fun1(x):
print("done") # pragma: nocover
def fun2(x):
- print("x")
+ if x:
+ print("x")
+ else:
+ print("not x")
fun2(3)
""")
@@ -1018,12 +1021,22 @@ def fun2(x):
nums = cov._analyze("missing.py").numbers
assert nums.n_files == 1
- assert nums.n_statements == 7
+ assert nums.n_statements == 9
assert nums.n_excluded == 1
- assert nums.n_missing == 3
- assert nums.n_branches == 2
- assert nums.n_partial_branches == 0
- assert nums.n_missing_branches == 2
+ assert nums.n_missing == 4
+ assert nums.n_branches == 4
+ assert nums.n_partial_branches == 1
+ assert nums.n_missing_branches == 3
+
+ filename, statements, excluded, missing, missing_formatted = cov.analysis2("missing.py")
+ assert os.path.relpath(filename) == "missing.py"
+ assert statements == [1, 2, 3, 5, 8, 9, 10, 12, 14]
+ assert excluded == [6]
+ assert missing == [2, 3, 5, 12]
+ assert missing_formatted == "2-5, 12"
+
+ branch_stats = cov.branch_stats("missing.py")
+ assert branch_stats == {2: (2, 0), 9: (2, 1)}
class TestRunnerPluginTest(CoverageTest):
From 8f6e00641e607307d77ae16cbe3cd5afb555c58c Mon Sep 17 00:00:00 2001
From: Ned Batchelder
Date: Sun, 16 Mar 2025 12:59:55 -0400
Subject: [PATCH 67/90] docs: edit the changelog
---
CHANGES.rst | 16 ++++++++--------
1 file changed, 8 insertions(+), 8 deletions(-)
diff --git a/CHANGES.rst b/CHANGES.rst
index e19e872e2..11bdf8584 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -26,19 +26,19 @@ Unreleased
- The Coverage object has a new method, :meth:`.Coverage.branch_stats` for
getting simple branch information for a module. Closes `issue 1888`_.
+- The :class:`Coverage constructor<.Coverage>` now has a ``plugins`` parameter
+ for passing in plugin objects directly, thanks to `Alex Gaynor `_.
+
- Many constant tests in if statements are now recognized as being optimized
away. For example, previously ``if 13:`` would have been considered a branch
with one path not taken. Now it is understood as always true and no coverage
is missing.
-- The experimental sys.monitoring support should now work for branch coverage
- if you are using Python newer than 3.14.0 alpha 5. This should greatly
- reduce the overhead coverage.py imposes on your test suite. Set the
- environment variable ``COVERAGE_CORE=sysmon`` to try it out.
-
-- The :class:`Coverage constructor<.Coverage>` now has a ``plugins`` parameter
- for passing in plugin objects directly, thanks to `Alex Gaynor `_.
+- The experimental sys.monitoring support now works for branch coverage if you
+ are using Python 3.14.0 alpha 6 or newer. This should reduce the overhead
+ coverage.py imposes on your test suite. Set the environment variable
+ ``COVERAGE_CORE=sysmon`` to try it out.
- Confirmed support for PyPy 3.11. Thanks Michał Górny.
From 35a31c5c2d642280e4f247469779a76ee5d03709 Mon Sep 17 00:00:00 2001
From: Ned Batchelder
Date: Sun, 16 Mar 2025 13:28:25 -0400
Subject: [PATCH 68/90] docs: prep for 7.7.0
---
CHANGES.rst | 10 ++++++----
README.rst | 3 ++-
coverage/version.py | 4 ++--
doc/conf.py | 6 +++---
doc/index.rst | 2 +-
5 files changed, 14 insertions(+), 11 deletions(-)
diff --git a/CHANGES.rst b/CHANGES.rst
index 11bdf8584..27b90e8ca 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -20,8 +20,12 @@ upgrading your version of coverage.py.
.. Version 9.8.1 — 2027-07-27
.. --------------------------
-Unreleased
-----------
+.. start-releases
+
+.. _changes_7-7-0:
+
+Version 7.7.0 — 2025-03-16
+--------------------------
- The Coverage object has a new method, :meth:`.Coverage.branch_stats` for
getting simple branch information for a module. Closes `issue 1888`_.
@@ -46,8 +50,6 @@ Unreleased
.. _pull 1919: https://github.com/nedbat/coveragepy/pull/1919
-.. start-releases
-
.. _changes_7-6-12:
Version 7.6.12 — 2025-02-11
diff --git a/README.rst b/README.rst
index ebc8fb405..cb5f41b2d 100644
--- a/README.rst
+++ b/README.rst
@@ -25,7 +25,7 @@ Coverage.py runs on these versions of Python:
.. PYVERSIONS
-* Python 3.9 through 3.14 alpha 4, including free-threading.
+* Python 3.9 through 3.14 alpha 6, including free-threading.
* PyPy3 versions 3.9, 3.10, and 3.11.
Documentation is on `Read the Docs`_. Code repository and issue tracker are on
@@ -35,6 +35,7 @@ Documentation is on `Read the Docs`_. Code repository and issue tracker are on
.. _GitHub: https://github.com/nedbat/coveragepy
**New in 7.x:**
+``Coverage.branch_stats()``;
multi-line exclusion patterns;
function/class reporting;
experimental support for sys.monitoring;
diff --git a/coverage/version.py b/coverage/version.py
index b4fc9d123..d9c814811 100644
--- a/coverage/version.py
+++ b/coverage/version.py
@@ -8,8 +8,8 @@
# version_info: same semantics as sys.version_info.
# _dev: the .devN suffix if any.
-version_info = (7, 7, 0, "alpha", 0)
-_dev = 1
+version_info = (7, 7, 0, "final", 0)
+_dev = 0
def _make_version(
diff --git a/doc/conf.py b/doc/conf.py
index 79c68d962..f94d30f37 100644
--- a/doc/conf.py
+++ b/doc/conf.py
@@ -67,11 +67,11 @@
# @@@ editable
copyright = "2009–2025, Ned Batchelder" # pylint: disable=redefined-builtin
# The short X.Y.Z version.
-version = "7.6.12"
+version = "7.7.0"
# The full version, including alpha/beta/rc tags.
-release = "7.6.12"
+release = "7.7.0"
# The date of release, in "monthname day, year" format.
-release_date = "February 11, 2025"
+release_date = "March 16, 2025"
# @@@ end
rst_epilog = f"""
diff --git a/doc/index.rst b/doc/index.rst
index c8d74c375..ea3486e12 100644
--- a/doc/index.rst
+++ b/doc/index.rst
@@ -18,7 +18,7 @@ supported on:
.. PYVERSIONS
-* Python 3.9 through 3.14 alpha 4, including free-threading.
+* Python 3.9 through 3.14 alpha 6, including free-threading.
* PyPy3 versions 3.9, 3.10, and 3.11.
.. ifconfig:: prerelease
From 61dcf7188c18db699ebf542f362f00a5d0481281 Mon Sep 17 00:00:00 2001
From: Ned Batchelder
Date: Sun, 16 Mar 2025 13:28:51 -0400
Subject: [PATCH 69/90] docs: sample HTML for 7.7.0
---
doc/sample_html/class_index.html | 8 ++++----
doc/sample_html/function_index.html | 8 ++++----
doc/sample_html/index.html | 8 ++++----
doc/sample_html/status.json | 2 +-
doc/sample_html/z_7b071bdc2a35fa80___init___py.html | 8 ++++----
doc/sample_html/z_7b071bdc2a35fa80___main___py.html | 8 ++++----
doc/sample_html/z_7b071bdc2a35fa80_cogapp_py.html | 8 ++++----
doc/sample_html/z_7b071bdc2a35fa80_makefiles_py.html | 8 ++++----
doc/sample_html/z_7b071bdc2a35fa80_test_cogapp_py.html | 8 ++++----
doc/sample_html/z_7b071bdc2a35fa80_test_makefiles_py.html | 8 ++++----
.../z_7b071bdc2a35fa80_test_whiteutils_py.html | 8 ++++----
doc/sample_html/z_7b071bdc2a35fa80_utils_py.html | 8 ++++----
doc/sample_html/z_7b071bdc2a35fa80_whiteutils_py.html | 8 ++++----
13 files changed, 49 insertions(+), 49 deletions(-)
diff --git a/doc/sample_html/class_index.html b/doc/sample_html/class_index.html
index b0648001c..d41b6c268 100644
--- a/doc/sample_html/class_index.html
+++ b/doc/sample_html/class_index.html
@@ -56,8 +56,8 @@
Classes
- coverage.py v7.6.12,
- created at 2025-02-11 08:59 -0500
+ coverage.py v7.7.0,
+ created at 2025-03-16 13:28 -0400
@@ -537,8 +537,8 @@
- coverage.py v7.6.12,
- created at 2025-02-11 08:59 -0500
+ coverage.py v7.7.0,
+ created at 2025-03-16 13:28 -0400
diff --git a/doc/sample_html/function_index.html b/doc/sample_html/function_index.html
index 0e9a68f7f..f0a897a21 100644
--- a/doc/sample_html/function_index.html
+++ b/doc/sample_html/function_index.html
@@ -56,8 +56,8 @@
Classes
- coverage.py v7.6.12,
- created at 2025-02-11 08:59 -0500
+ coverage.py v7.7.0,
+ created at 2025-03-16 13:28 -0400
@@ -2377,8 +2377,8 @@
- coverage.py v7.6.12,
- created at 2025-02-11 08:59 -0500
+ coverage.py v7.7.0,
+ created at 2025-03-16 13:28 -0400
diff --git a/doc/sample_html/index.html b/doc/sample_html/index.html
index c3f973134..5b48a3b20 100644
--- a/doc/sample_html/index.html
+++ b/doc/sample_html/index.html
@@ -55,8 +55,8 @@
Classes
- coverage.py v7.6.12,
- created at 2025-02-11 08:59 -0500
+ coverage.py v7.7.0,
+ created at 2025-03-16 13:28 -0400
@@ -175,8 +175,8 @@
- coverage.py v7.6.12,
- created at 2025-02-11 08:59 -0500
+ coverage.py v7.7.0,
+ created at 2025-03-16 13:28 -0400
diff --git a/doc/sample_html/status.json b/doc/sample_html/status.json
index 0f55b78fe..226561088 100644
--- a/doc/sample_html/status.json
+++ b/doc/sample_html/status.json
@@ -1 +1 @@
-{"note":"This file is an internal implementation detail to speed up HTML report generation. Its format can change at any time. You might be looking for the JSON report: https://coverage.rtfd.io/cmd.html#cmd-json","format":5,"version":"7.6.12","globals":"9448a2fca87bc23614b5712a1310ed1d","files":{"z_7b071bdc2a35fa80___init___py":{"hash":"70a508cdcdeb999b005ef6bbb19ef352","index":{"url":"z_7b071bdc2a35fa80___init___py.html","file":"cogapp/__init__.py","description":"","nums":{"precision":2,"n_files":1,"n_statements":1,"n_excluded":0,"n_missing":0,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_7b071bdc2a35fa80___main___py":{"hash":"6d9d0d551879aa3e73791f40c5739845","index":{"url":"z_7b071bdc2a35fa80___main___py.html","file":"cogapp/__main__.py","description":"","nums":{"precision":2,"n_files":1,"n_statements":3,"n_excluded":0,"n_missing":3,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_7b071bdc2a35fa80_cogapp_py":{"hash":"5ba0c64e49e07207b0c428615ecf9962","index":{"url":"z_7b071bdc2a35fa80_cogapp_py.html","file":"cogapp/cogapp.py","description":"","nums":{"precision":2,"n_files":1,"n_statements":483,"n_excluded":1,"n_missing":228,"n_branches":190,"n_partial_branches":27,"n_missing_branches":131}}},"z_7b071bdc2a35fa80_makefiles_py":{"hash":"eaf4689c0c47697806b20a0a782f9e2a","index":{"url":"z_7b071bdc2a35fa80_makefiles_py.html","file":"cogapp/makefiles.py","description":"","nums":{"precision":2,"n_files":1,"n_statements":22,"n_excluded":0,"n_missing":18,"n_branches":14,"n_partial_branches":0,"n_missing_branches":14}}},"z_7b071bdc2a35fa80_test_cogapp_py":{"hash":"172bea166b8565483126315dbf382f3d","index":{"url":"z_7b071bdc2a35fa80_test_cogapp_py.html","file":"cogapp/test_cogapp.py","description":"","nums":{"precision":2,"n_files":1,"n_statements":854,"n_excluded":2,"n_missing":598,"n_branches":20,"n_partial_branches":1,"n_missing_branches":17}}},"z_7b071bdc2a35fa80_test_makefiles_py":{"hash":"a4a125d4209ab0e413c7c49768fd322f","index":{"url":"z_7b071bdc2a35fa80_test_makefiles_py.html","file":"cogapp/test_makefiles.py","description":"","nums":{"precision":2,"n_files":1,"n_statements":68,"n_excluded":0,"n_missing":51,"n_branches":6,"n_partial_branches":0,"n_missing_branches":6}}},"z_7b071bdc2a35fa80_test_whiteutils_py":{"hash":"59819ec39ae83287b478821e619c36df","index":{"url":"z_7b071bdc2a35fa80_test_whiteutils_py.html","file":"cogapp/test_whiteutils.py","description":"","nums":{"precision":2,"n_files":1,"n_statements":68,"n_excluded":0,"n_missing":50,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_7b071bdc2a35fa80_utils_py":{"hash":"1d33832a970f998ddfb7b6f9400abd57","index":{"url":"z_7b071bdc2a35fa80_utils_py.html","file":"cogapp/utils.py","description":"","nums":{"precision":2,"n_files":1,"n_statements":37,"n_excluded":0,"n_missing":8,"n_branches":6,"n_partial_branches":2,"n_missing_branches":2}}},"z_7b071bdc2a35fa80_whiteutils_py":{"hash":"828c0e3a8398ba557c1f936ae3093939","index":{"url":"z_7b071bdc2a35fa80_whiteutils_py.html","file":"cogapp/whiteutils.py","description":"","nums":{"precision":2,"n_files":1,"n_statements":44,"n_excluded":0,"n_missing":5,"n_branches":32,"n_partial_branches":4,"n_missing_branches":4}}}}}
\ No newline at end of file
+{"note":"This file is an internal implementation detail to speed up HTML report generation. Its format can change at any time. You might be looking for the JSON report: https://coverage.rtfd.io/cmd.html#cmd-json","format":5,"version":"7.7.0","globals":"9448a2fca87bc23614b5712a1310ed1d","files":{"z_7b071bdc2a35fa80___init___py":{"hash":"70a508cdcdeb999b005ef6bbb19ef352","index":{"url":"z_7b071bdc2a35fa80___init___py.html","file":"cogapp/__init__.py","description":"","nums":{"precision":2,"n_files":1,"n_statements":1,"n_excluded":0,"n_missing":0,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_7b071bdc2a35fa80___main___py":{"hash":"6d9d0d551879aa3e73791f40c5739845","index":{"url":"z_7b071bdc2a35fa80___main___py.html","file":"cogapp/__main__.py","description":"","nums":{"precision":2,"n_files":1,"n_statements":3,"n_excluded":0,"n_missing":3,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_7b071bdc2a35fa80_cogapp_py":{"hash":"5ba0c64e49e07207b0c428615ecf9962","index":{"url":"z_7b071bdc2a35fa80_cogapp_py.html","file":"cogapp/cogapp.py","description":"","nums":{"precision":2,"n_files":1,"n_statements":483,"n_excluded":1,"n_missing":228,"n_branches":190,"n_partial_branches":27,"n_missing_branches":131}}},"z_7b071bdc2a35fa80_makefiles_py":{"hash":"eaf4689c0c47697806b20a0a782f9e2a","index":{"url":"z_7b071bdc2a35fa80_makefiles_py.html","file":"cogapp/makefiles.py","description":"","nums":{"precision":2,"n_files":1,"n_statements":22,"n_excluded":0,"n_missing":18,"n_branches":14,"n_partial_branches":0,"n_missing_branches":14}}},"z_7b071bdc2a35fa80_test_cogapp_py":{"hash":"172bea166b8565483126315dbf382f3d","index":{"url":"z_7b071bdc2a35fa80_test_cogapp_py.html","file":"cogapp/test_cogapp.py","description":"","nums":{"precision":2,"n_files":1,"n_statements":854,"n_excluded":2,"n_missing":598,"n_branches":20,"n_partial_branches":1,"n_missing_branches":17}}},"z_7b071bdc2a35fa80_test_makefiles_py":{"hash":"a4a125d4209ab0e413c7c49768fd322f","index":{"url":"z_7b071bdc2a35fa80_test_makefiles_py.html","file":"cogapp/test_makefiles.py","description":"","nums":{"precision":2,"n_files":1,"n_statements":68,"n_excluded":0,"n_missing":51,"n_branches":6,"n_partial_branches":0,"n_missing_branches":6}}},"z_7b071bdc2a35fa80_test_whiteutils_py":{"hash":"59819ec39ae83287b478821e619c36df","index":{"url":"z_7b071bdc2a35fa80_test_whiteutils_py.html","file":"cogapp/test_whiteutils.py","description":"","nums":{"precision":2,"n_files":1,"n_statements":68,"n_excluded":0,"n_missing":50,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_7b071bdc2a35fa80_utils_py":{"hash":"1d33832a970f998ddfb7b6f9400abd57","index":{"url":"z_7b071bdc2a35fa80_utils_py.html","file":"cogapp/utils.py","description":"","nums":{"precision":2,"n_files":1,"n_statements":37,"n_excluded":0,"n_missing":8,"n_branches":6,"n_partial_branches":2,"n_missing_branches":2}}},"z_7b071bdc2a35fa80_whiteutils_py":{"hash":"828c0e3a8398ba557c1f936ae3093939","index":{"url":"z_7b071bdc2a35fa80_whiteutils_py.html","file":"cogapp/whiteutils.py","description":"","nums":{"precision":2,"n_files":1,"n_statements":44,"n_excluded":0,"n_missing":5,"n_branches":32,"n_partial_branches":4,"n_missing_branches":4}}}}}
\ No newline at end of file
diff --git a/doc/sample_html/z_7b071bdc2a35fa80___init___py.html b/doc/sample_html/z_7b071bdc2a35fa80___init___py.html
index 48429a01c..9752c999d 100644
--- a/doc/sample_html/z_7b071bdc2a35fa80___init___py.html
+++ b/doc/sample_html/z_7b071bdc2a35fa80___init___py.html
@@ -66,8 +66,8 @@
^ index
» next
- coverage.py v7.6.12,
- created at 2025-02-11 08:59 -0500
+ coverage.py v7.7.0,
+ created at 2025-03-16 13:28 -0400
@@ -97,8 +97,8 @@
^ index
» next
- coverage.py v7.6.12,
- created at 2025-02-11 08:59 -0500
+ coverage.py v7.7.0,
+ created at 2025-03-16 13:28 -0400
diff --git a/doc/sample_html/z_7b071bdc2a35fa80___main___py.html b/doc/sample_html/z_7b071bdc2a35fa80___main___py.html
index 86c241e01..1bbd252bd 100644
--- a/doc/sample_html/z_7b071bdc2a35fa80___main___py.html
+++ b/doc/sample_html/z_7b071bdc2a35fa80___main___py.html
@@ -66,8 +66,8 @@
^ index
» next
- coverage.py v7.6.12,
- created at 2025-02-11 08:59 -0500
+ coverage.py v7.7.0,
+ created at 2025-03-16 13:28 -0400
@@ -97,8 +97,8 @@
^ index
» next
- coverage.py v7.6.12,
- created at 2025-02-11 08:59 -0500
+ coverage.py v7.7.0,
+ created at 2025-03-16 13:28 -0400
diff --git a/doc/sample_html/z_7b071bdc2a35fa80_cogapp_py.html b/doc/sample_html/z_7b071bdc2a35fa80_cogapp_py.html
index 870380e0e..5bb5d7fa6 100644
--- a/doc/sample_html/z_7b071bdc2a35fa80_cogapp_py.html
+++ b/doc/sample_html/z_7b071bdc2a35fa80_cogapp_py.html
@@ -66,8 +66,8 @@
^ index
» next
- coverage.py v7.6.12,
- created at 2025-02-11 08:59 -0500
+ coverage.py v7.7.0,
+ created at 2025-03-16 13:28 -0400
@@ -928,8 +928,8 @@
^ index
» next
- coverage.py v7.6.12,
- created at 2025-02-11 08:59 -0500
+ coverage.py v7.7.0,
+ created at 2025-03-16 13:28 -0400
diff --git a/doc/sample_html/z_7b071bdc2a35fa80_makefiles_py.html b/doc/sample_html/z_7b071bdc2a35fa80_makefiles_py.html
index 754c3e398..d89a20036 100644
--- a/doc/sample_html/z_7b071bdc2a35fa80_makefiles_py.html
+++ b/doc/sample_html/z_7b071bdc2a35fa80_makefiles_py.html
@@ -66,8 +66,8 @@
^ index
» next
- coverage.py v7.6.12,
- created at 2025-02-11 08:59 -0500
+ coverage.py v7.7.0,
+ created at 2025-03-16 13:28 -0400
@@ -127,8 +127,8 @@
^ index
» next
- coverage.py v7.6.12,
- created at 2025-02-11 08:59 -0500
+ coverage.py v7.7.0,
+ created at 2025-03-16 13:28 -0400
- coverage.py v7.7.0, - created at 2025-03-16 13:28 -0400 + coverage.py v7.7.1, + created at 2025-03-21 12:53 -0400
@@ -537,8 +537,8 @@- coverage.py v7.7.0, - created at 2025-03-16 13:28 -0400 + coverage.py v7.7.1, + created at 2025-03-21 12:53 -0400
- coverage.py v7.7.0, - created at 2025-03-16 13:28 -0400 + coverage.py v7.7.1, + created at 2025-03-21 12:53 -0400
@@ -2377,8 +2377,8 @@- coverage.py v7.7.0, - created at 2025-03-16 13:28 -0400 + coverage.py v7.7.1, + created at 2025-03-21 12:53 -0400
- coverage.py v7.7.0, - created at 2025-03-16 13:28 -0400 + coverage.py v7.7.1, + created at 2025-03-21 12:53 -0400
@@ -175,8 +175,8 @@- coverage.py v7.7.0, - created at 2025-03-16 13:28 -0400 + coverage.py v7.7.1, + created at 2025-03-21 12:53 -0400
- coverage.py v7.7.1, - created at 2025-03-21 12:53 -0400 + coverage.py v7.8.0, + created at 2025-03-30 15:44 -0400
@@ -537,8 +537,8 @@- coverage.py v7.7.1, - created at 2025-03-21 12:53 -0400 + coverage.py v7.8.0, + created at 2025-03-30 15:44 -0400
- coverage.py v7.7.1, - created at 2025-03-21 12:53 -0400 + coverage.py v7.8.0, + created at 2025-03-30 15:44 -0400
@@ -2377,8 +2377,8 @@- coverage.py v7.7.1, - created at 2025-03-21 12:53 -0400 + coverage.py v7.8.0, + created at 2025-03-30 15:44 -0400
- coverage.py v7.7.1, - created at 2025-03-21 12:53 -0400 + coverage.py v7.8.0, + created at 2025-03-30 15:44 -0400
@@ -175,8 +175,8 @@- coverage.py v7.7.1, - created at 2025-03-21 12:53 -0400 + coverage.py v7.8.0, + created at 2025-03-30 15:44 -0400