diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 764f927e2..f256e499a 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -10,6 +10,9 @@ updates:
# Check for updates to GitHub Actions once a week
interval: "weekly"
day: "sunday"
+ cooldown:
+ # https://blog.yossarian.net/2025/11/21/We-should-all-be-using-dependency-cooldowns
+ default-days: 10
groups:
action-dependencies:
patterns:
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index 9fcae10fc..cca43efe8 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -55,7 +55,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
- uses: github/codeql-action/init@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2
+ uses: github/codeql-action/init@014f16e7ab1402f30e7c3329d33797e7948572db # v4.31.3
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -66,7 +66,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@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2
+ uses: github/codeql-action/autobuild@014f16e7ab1402f30e7c3329d33797e7948572db # v4.31.3
# ℹ️ Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
@@ -80,4 +80,4 @@ jobs:
# make release
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2
+ uses: github/codeql-action/analyze@014f16e7ab1402f30e7c3329d33797e7948572db # v4.31.3
diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml
index 720f86324..b2fc8e5c7 100644
--- a/.github/workflows/coverage.yml
+++ b/.github/workflows/coverage.yml
@@ -63,7 +63,7 @@ jobs:
coverage:
name: "${{ matrix.python-version }} on ${{ matrix.os }}"
runs-on: "${{ matrix.os }}-${{ matrix.os-version || 'latest' }}"
- timeout-minutes: 30
+ timeout-minutes: 10
# Only run coverage if Python files or this workflow changed.
needs: changed
@@ -94,10 +94,13 @@ jobs:
- "3.15"
- "3.15t"
- "pypy-3.10"
+ - "pypy-3.11"
exclude:
# Mac PyPy always takes the longest, and doesn't add anything.
- os: macos
python-version: "pypy-3.10"
+ - os: macos
+ python-version: "pypy-3.11"
# Windows pypy 3.10 gets 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.
@@ -120,7 +123,7 @@ jobs:
persist-credentials: false
- name: "Set up Python"
- uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
+ uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with:
python-version: "${{ matrix.python-version }}"
allow-prereleases: true
@@ -181,7 +184,7 @@ jobs:
persist-credentials: false
- name: "Set up Python"
- uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
+ uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with:
python-version: "3.10" # Minimum of PYVERSIONS
# At a certain point, installing dependencies failed on pypy 3.9 and
@@ -259,7 +262,7 @@ jobs:
echo "sha10=$SHA10" >> $GITHUB_ENV
echo "slug=$SLUG" >> $GITHUB_ENV
echo "report_dir=reports/$SLUG/htmlcov" >> $GITHUB_ENV
- echo "url=https://htmlpreview.github.io/?https://github.com/coveragepy/metacov-reports/blob/main/reports/$SLUG/htmlcov/index.html" >> $GITHUB_ENV
+ echo "url=https://coveragepy.github.io/metacov-reports/reports/$SLUG/htmlcov" >> $GITHUB_ENV
echo "branch=${REF#refs/heads/}" >> $GITHUB_ENV
- name: "Checkout reports repo"
diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml
index 6bf969a74..8d6f8bcee 100644
--- a/.github/workflows/dependency-review.yml
+++ b/.github/workflows/dependency-review.yml
@@ -33,7 +33,7 @@ jobs:
persist-credentials: false
- name: 'Dependency Review'
- uses: actions/dependency-review-action@40c09b7dc99638e5ddb0bfd91c1673effc064d8a # v4.8.1
+ uses: actions/dependency-review-action@3c4e3dcb1aa7874d2c16be7d79418e9b7efd6261 # v4.8.2
with:
base-ref: ${{ github.event.pull_request.base.sha || 'main' }}
head-ref: ${{ github.event.pull_request.head.sha || github.ref }}
diff --git a/.github/workflows/kit.yml b/.github/workflows/kit.yml
index 03075d282..8a6ff5a17 100644
--- a/.github/workflows/kit.yml
+++ b/.github/workflows/kit.yml
@@ -171,7 +171,7 @@ jobs:
persist-credentials: false
- name: "Install Python"
- uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
+ uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with:
python-version: "${{ matrix.minpy || '3.11' }}" # PYVERSIONS needed by cibuildwheel
cache: pip
@@ -232,7 +232,7 @@ jobs:
persist-credentials: false
- name: "Install Python"
- uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
+ uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with:
python-version: "3.11" # PYVERSIONS: the kit-building version
cache: pip
diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml
index 4b0231eff..4ee344be5 100644
--- a/.github/workflows/quality.yml
+++ b/.github/workflows/quality.yml
@@ -76,7 +76,7 @@ jobs:
persist-credentials: false
- name: "Install Python"
- uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
+ uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with:
python-version: "3.10" # Minimum of PYVERSIONS
cache: pip
@@ -104,7 +104,7 @@ jobs:
persist-credentials: false
- name: "Install Python"
- uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
+ uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with:
python-version: "3.10" # Minimum of PYVERSIONS
cache: pip
@@ -137,7 +137,7 @@ jobs:
git fetch origin main --depth=1
- name: "Install Python"
- uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
+ uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with:
python-version: "3.11" # Doc version from PYVERSIONS
cache: pip
@@ -175,7 +175,7 @@ jobs:
persist-credentials: false
- name: Install the latest version of uv
- uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 #v7.1.2
+ uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 #v7.1.4
with:
enable-cache: false
diff --git a/.github/workflows/testsuite.yml b/.github/workflows/testsuite.yml
index 9132058b7..7f9d63483 100644
--- a/.github/workflows/testsuite.yml
+++ b/.github/workflows/testsuite.yml
@@ -60,7 +60,7 @@ jobs:
tests:
name: "${{ matrix.python-version }} on ${{ matrix.os }}"
runs-on: "${{ matrix.os }}-${{ matrix.os-version || 'latest' }}"
- timeout-minutes: 30
+ timeout-minutes: 10
# Don't run tests if the branch name includes "-notests".
# Only run tests if files that affect tests have changed.
@@ -116,7 +116,7 @@ jobs:
persist-credentials: false
- name: "Set up Python"
- uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
+ uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with:
python-version: "${{ matrix.python-version }}"
allow-prereleases: true
diff --git a/.gitignore b/.gitignore
index e38a5b0a4..4afbc6efc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -49,6 +49,7 @@ doc/sample_html_beta
# Build intermediaries.
tmp
+a1_coverage.pth
# OS junk
.DS_Store
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 1a5843aab..0be9cb9f7 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -16,7 +16,7 @@ repos:
exclude: "stress_phystoken|\\.py,cover$"
- repo: https://github.com/astral-sh/ruff-pre-commit
- rev: v0.14.4
+ rev: v0.14.5
hooks:
- id: ruff-format
diff --git a/CHANGES.rst b/CHANGES.rst
index d4ed013f7..8ddc09e81 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -22,6 +22,80 @@ upgrading your version of coverage.py.
.. start-releases
+.. _changes_7-13-0:
+
+Version 7.13.0 — 2025-12-08
+---------------------------
+
+- Feature: coverage.py now supports :file:`.coveragerc.toml` configuration
+ files. These files use TOML syntax and take priority over
+ :file:`pyproject.toml` but lower priority than :file:`.coveragerc` files.
+ Closes `issue 1643`_ thanks to `Olena Yefymenko `_.
+
+- Fix: we now include a permanent .pth file which is installed with the code,
+ fixing `issue 2084`_. In 7.12.1b1 this was done incorrectly: it didn't work
+ when using the source wheel (``py3-none-any``). This is now fixed. Thanks,
+ `Henry Schreiner `_.
+
+- Deprecated: when coverage.py is installed, it creates three command entry
+ points: ``coverage``, ``coverage3``, and ``coverage-3.10`` (if installed for
+ Python 3.10). The second and third of these are not needed and will
+ eventually be removed. They still work for now, but print a message about
+ their deprecation.
+
+.. _issue 1643: https://github.com/coveragepy/coveragepy/issues/1643
+.. _pull 1952: https://github.com/coveragepy/coveragepy/pull/1952
+.. _pull 2100: https://github.com/coveragepy/coveragepy/pull/2100
+
+
+.. _changes_7-12-1b1:
+
+Version 7.12.1b1 — 2025-11-30
+-----------------------------
+
+- Fix: coverage.py now includes a permanent .pth file in the distribution which
+ is installed with the code. This fixes `issue 2084`_: failure to patch for
+ subprocess measurement when site-packages is not writable.
+
+.. _issue 2084: https://github.com/coveragepy/coveragepy/issues/2084
+
+
+.. _changes_7-12-0:
+
+Version 7.12.0 — 2025-11-18
+---------------------------
+
+- The HTML report now shows separate coverage totals for statements and
+ branches, as well as the usual combined coverage percentage. Thanks to Ryuta
+ Otsuka for the `discussion `_ and the `implementation
+ `_.
+
+- The JSON report now includes separate coverage totals for statements and
+ branches, thanks to `Ryuta Otsuka `_.
+
+- Fix: ``except*`` clauses were not handled properly under the "sysmon"
+ measurement core, causing KeyError exceptions as described in `issue 2086`_.
+ This is now fixed.
+
+- Fix: we now defend against aggressive mocking of ``open()`` that could cause
+ errors inside coverage.py. An example of a failure is in `issue 2083`_.
+
+- Fix: in unusual cases where a test suite intentionally exhausts the system's
+ file descriptors to test handling errors in ``open()``, coverage.py would
+ fail when trying to open source files, as described in `issue 2091`_. This
+ is now fixed.
+
+- A small tweak to the HTML report: file paths now use thin spaces around
+ slashes to make them easier to read.
+
+.. _issue 2081: https://github.com/coveragepy/coveragepy/issues/2081
+.. _issue 2083: https://github.com/coveragepy/coveragepy/issues/2083
+.. _pull 2085: https://github.com/coveragepy/coveragepy/pull/2085
+.. _issue 2086: https://github.com/coveragepy/coveragepy/issues/2086
+.. _pull 2090: https://github.com/coveragepy/coveragepy/pull/2090
+.. _issue 2091: https://github.com/coveragepy/coveragepy/issues/2091
+
+
.. _changes_7-11-3:
Version 7.11.3 — 2025-11-09
diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt
index 2587df5f5..5506aea2c 100644
--- a/CONTRIBUTORS.txt
+++ b/CONTRIBUTORS.txt
@@ -188,6 +188,7 @@ Nils Kattenbeck
Noel O'Boyle
Oleg Höfling
Oleh Krehel
+Olena Yefymenko
Olivier Grisel
Ori Avtalion
Pablo Carballo
@@ -208,6 +209,7 @@ Roland Illig
Ross Lawley
Roy Williams
Russell Keith-Magee
+Ryuta Otsuka
S. Y. Lee
Salvatore Zagaria
Sandra Martocchia
diff --git a/Makefile b/Makefile
index ff049db60..6c3327973 100644
--- a/Makefile
+++ b/Makefile
@@ -37,11 +37,12 @@ clean: debug_clean _clean_platform ## Remove artifacts of test execution, instal
@rm -f MANIFEST
@rm -f .coverage .coverage.* .metacov*
@rm -f coverage.xml coverage.json
- @rm -f .tox/*/lib/*/site-packages/zzz_metacov.pth
+ @rm -f .tox/*/lib/*/site-packages/a0_metacov.pth
@rm -f */.coverage */*/.coverage */*/*/.coverage */*/*/*/.coverage */*/*/*/*/.coverage */*/*/*/*/*/.coverage
@rm -f tests/covmain.zip tests/zipmods.zip tests/zip1.zip
@rm -rf doc/_build doc/_spell doc/sample_html_beta
@rm -rf tmp
+ @rm -rf a1_coverage.pth
@rm -rf .*cache */.*cache */*/.*cache */*/*/.*cache .hypothesis
@rm -rf tests/actual
@-make -C tests/gold/html clean
@@ -114,7 +115,9 @@ KITBIN = .tox/$(KITVER)/bin
$(KITBIN):
tox -q -e $(KITVER) --notest
-PIP_COMPILE = uv pip compile -q --universal ${COMPILE_OPTS}
+# Limit to packages that were released more than 10 days ago.
+# https://blog.yossarian.net/2025/11/21/We-should-all-be-using-dependency-cooldowns
+PIP_COMPILE = uv pip compile -q --universal --exclude-newer=$$(date -v-10d +%Y-%m-%d) ${COMPILE_OPTS}
upgrade: ## Update the *.pip files with the latest packages satisfying *.in files.
$(MAKE) _upgrade COMPILE_OPTS="--upgrade"
@@ -189,6 +192,7 @@ sample_html_beta: _sample_cog_html ## Generate sample HTML report for a beta rel
.PHONY: release_version edit_for_release cheats relbranch relcommit1 relcommit2
.PHONY: kit pypi_upload test_upload kit_local build_kits update_rtd
+.PHONY: _check_github_auth download_kits
.PHONY: tag bump_version
REPO_OWNER = coveragepy/coveragepy
@@ -213,8 +217,11 @@ relcommit2: #: Commit the latest sample HTML report (see howto.txt).
git add doc/sample_html
git commit -am "docs: sample HTML for $$(python setup.py --version)"
-kit: ## Make the source distribution.
+kit: ## Make a source distribution and some wheels.
+ @# Makes sdist and binary wheel for current Python version and platform:
python -m build
+ @# Makes py3-none-any wheel:
+ COVERAGE_DISABLE_EXTENSION=1 python -m build --wheel
pypi_upload: ## Upload the built distributions to PyPI.
python ci/trigger_action.py $(REPO_OWNER) publish-pypi
@@ -233,14 +240,25 @@ kit_local:
# don't go crazy trying to figure out why our new code isn't installing.
find ~/Library/Caches/pip/wheels -name 'coverage-*' -delete
-build_kits: ## Trigger GitHub to build kits.
+_check_github_auth: #: Check that we have GITHUB_TOKEN for other commands that need it.
+ @if [[ -z "$$GITHUB_TOKEN" ]]; then \
+ echo 'Missing GITHUB_TOKEN: opvars github'; \
+ exit 1; \
+ fi
+
+build_kits: _check_github_auth ## Trigger GitHub to build all the distributions.
python ci/trigger_action.py $(REPO_OWNER) build-kits
+download_kits: _check_github_auth ## Download the kits built on GitHub.
+ @# This is only if we need to examine them for debugging.
+ mkdir -p dist
+ gh run download --dir=dist $$(gh run list --workflow=Kits --json=databaseId --jq='.[0].databaseId')
+
tag: #: Make a git tag with the version number (see howto.txt).
git tag -s -m "Version $$(python setup.py --version)" $$(python setup.py --version)
git push --follow-tags
-update_rtd: #: Update ReadTheDocs with the versions to show
+update_rtd: #: Update ReadTheDocs with the versions to show.
python ci/update_rtfd.py $(RTD_PROJECT)
bump_version: #: Edit sources to bump the version after a release (see howto.txt).
diff --git a/README.rst b/README.rst
index a3ebe1046..03a3207c0 100644
--- a/README.rst
+++ b/README.rst
@@ -35,27 +35,6 @@ Documentation is on `Read the Docs`_. Code repository and issue tracker are on
.. _Read the Docs: https://coverage.readthedocs.io/
.. _GitHub: https://github.com/coveragepy/coveragepy
-**New in 7.x:**
-``[run] patch`` setting;
-``--save-signal`` option;
-``[run] core`` setting;
-``[run] source_dirs`` setting;
-``Coverage.branch_stats()``;
-multi-line exclusion patterns;
-function/class reporting;
-experimental support for sys.monitoring;
-dropped support for Python up to 3.9;
-added ``Coverage.collect()`` context manager;
-improved data combining;
-``[run] exclude_also`` setting;
-``report --format=``;
-type annotations.
-
-**New in 6.x:**
-dropped support for Python 2.7, 3.5, and 3.6;
-write data on SIGTERM;
-added support for 3.10 match/case statements.
-
For Enterprise
--------------
diff --git a/coverage/__init__.py b/coverage/__init__.py
index 1f7086a33..5441133c3 100644
--- a/coverage/__init__.py
+++ b/coverage/__init__.py
@@ -11,8 +11,6 @@
from __future__ import annotations
-# isort: skip_file
-
# mypy's convention is that "import as" names are public from the module.
# We import names as themselves to indicate that. Pylint sees it as pointless,
# so disable its warning.
diff --git a/coverage/cmdline.py b/coverage/cmdline.py
index 5287327e7..c8c4e49cd 100644
--- a/coverage/cmdline.py
+++ b/coverage/cmdline.py
@@ -1161,6 +1161,20 @@ def main(argv: list[str] | None = None) -> int | None:
return status
+def main_deprecated(argv: list[str] | None = None) -> int | None:
+ """For entry points we'll be getting rid of."""
+ print(
+ textwrap.dedent("""\
+ **
+ ** This entry point is deprecated and will be removed.
+ ** Send me an email if you want to keep this command name working:
+ ** ned@nedbatchelder.com
+ **
+ """)
+ )
+ return main(argv)
+
+
# Profiling using ox_profile. Install it from GitHub:
# pip install git+https://github.com/emin63/ox_profile.git
#
diff --git a/coverage/config.py b/coverage/config.py
index 274d71d66..7787ab838 100644
--- a/coverage/config.py
+++ b/coverage/config.py
@@ -658,6 +658,7 @@ def config_files_to_try(config_file: bool | str) -> list[tuple[str, bool, bool]]
assert isinstance(config_file, str)
files_to_try = [
(config_file, True, specified_file),
+ (".coveragerc.toml", True, False),
("setup.cfg", False, False),
("tox.ini", False, False),
("pyproject.toml", False, False),
diff --git a/coverage/control.py b/coverage/control.py
index 81308d330..7aeccd9e5 100644
--- a/coverage/control.py
+++ b/coverage/control.py
@@ -12,7 +12,6 @@
import functools
import os
import os.path
-import platform
import signal
import sys
import threading
@@ -288,9 +287,6 @@ def __init__( # pylint: disable=too-many-arguments
self._no_warn_slugs: set[str] = set()
self._messages = messages
- # If we're invoked from a .pth file, we shouldn't try to make another one.
- self._make_pth_file = True
-
# A record of all the warnings that have been issued.
self._warnings: list[str] = []
@@ -354,6 +350,17 @@ def __init__( # pylint: disable=too-many-arguments
if not env.METACOV:
_prevent_sub_process_measurement()
+ def __repr__(self) -> str:
+ core_name = self._core.tracer_class.__name__ if self._core is not None else "-none-"
+ data_file = repr(self._data._filename) if self._data is not None else "-none-"
+ return (
+ ""
+ )
+
def _init(self) -> None:
"""Set all the initial state.
@@ -708,7 +715,7 @@ def start(self) -> None:
if self._auto_load:
self.load()
- apply_patches(self, self.config, self._debug, make_pth_file=self._make_pth_file)
+ apply_patches(self, self.config, self._debug)
self._collector.start()
self._started = True
@@ -1354,6 +1361,9 @@ def lcov_report(
def sys_info(self) -> Iterable[tuple[str, Any]]:
"""Return a list of (key, value) pairs showing internal information."""
+ import glob
+ import platform
+ import site
import coverage as covmod
self._init()
@@ -1369,6 +1379,10 @@ def plugin_info(plugins: list[Any]) -> list[str]:
entries.append(entry)
return entries
+ pth_files = []
+ for spdir in site.getsitepackages():
+ pth_files.extend(glob.glob(f"{spdir}/*cov*.pth"))
+
info = [
("coverage_version", covmod.__version__),
("coverage_module", covmod.__file__),
@@ -1391,6 +1405,7 @@ def plugin_info(plugins: list[Any]) -> list[str]:
("build", platform.python_build()),
("gil_enabled", getattr(sys, "_is_gil_enabled", lambda: True)()),
("executable", sys.executable),
+ ("pth_files", pth_files),
("def_encoding", sys.getdefaultencoding()),
("fs_encoding", sys.getfilesystemencoding()),
("pid", os.getpid()),
@@ -1418,12 +1433,21 @@ def plugin_info(plugins: list[Any]) -> list[str]:
)(Coverage)
-def process_startup(*, force: bool = False) -> Coverage | None:
+def process_startup(
+ *,
+ force: bool = False,
+ slug: str = "default", # pylint: disable=unused-argument
+) -> Coverage | None:
"""Call this at Python start-up to perhaps measure coverage.
- If the environment variable COVERAGE_PROCESS_START is defined, coverage
- measurement is started. The value of the variable is the config file
- to use.
+ Coverage is started if one of these environment variables is defined:
+
+ - COVERAGE_PROCESS_START: the config file to use.
+ - COVERAGE_PROCESS_CONFIG: the config data to use, a string produced by
+ CoverageConfig.serialize, prefixed by ":data:".
+
+ If one of these is defined, it's used to get the coverage configuration,
+ and coverage is started.
For details, see https://coverage.readthedocs.io/en/latest/subprocess.html.
@@ -1431,6 +1455,29 @@ def process_startup(*, force: bool = False) -> Coverage | None:
not started by this call.
"""
+ # This function can be called more than once in a process, for a few
+ # reasons.
+ #
+ # 1) We install a .pth file in multiple places reported by the site module,
+ # so this function can be called more than once even in simple
+ # situations.
+ #
+ # 2) In some virtualenv configurations the same directory is visible twice
+ # in sys.path. This means that the .pth file will be found twice and
+ # executed twice, executing this function twice.
+ # https://github.com/coveragepy/coveragepy/issues/340 has more details.
+ #
+ # We set a global flag (an attribute on this function) to indicate that
+ # coverage.py has already been started, so we can avoid starting it twice.
+
+ if not force and hasattr(process_startup, "coverage"):
+ # We've annotated this function before, so we must have already
+ # auto-started coverage.py in this process. Nothing to do.
+ return None
+
+ # Now check for the environment variables that request coverage. If they
+ # aren't set, do nothing.
+
config_data = os.getenv("COVERAGE_PROCESS_CONFIG")
cps = os.getenv("COVERAGE_PROCESS_START")
if config_data is not None:
@@ -1441,27 +1488,12 @@ def process_startup(*, force: bool = False) -> Coverage | None:
# No request for coverage, nothing to do.
return None
- # This function can be called more than once in a process. This happens
- # because some virtualenv configurations make the same directory visible
- # twice in sys.path. This means that the .pth file will be found twice,
- # and executed twice, executing this function twice. We set a global
- # flag (an attribute on this function) to indicate that coverage.py has
- # already been started, so we can avoid doing it twice.
- #
- # https://github.com/coveragepy/coveragepy/issues/340 has more details.
-
- if not force and hasattr(process_startup, "coverage"):
- # We've annotated this function before, so we must have already
- # auto-started coverage.py in this process. Nothing to do.
- return None
-
cov = Coverage(config_file=config_file)
process_startup.coverage = cov # type: ignore[attr-defined]
cov._warn_no_data = False
cov._warn_unimported_source = False
cov._warn_preimported_source = False
cov._auto_save = True
- cov._make_pth_file = False
cov.start()
return cov
@@ -1471,7 +1503,7 @@ def _after_fork_in_child() -> None:
"""Used by patch=fork in the child process to restart coverage."""
if cov := Coverage.current():
cov.stop()
- process_startup(force=True)
+ process_startup(force=True, slug="fork")
def _prevent_sub_process_measurement() -> None:
diff --git a/coverage/ctracer/tracer.c b/coverage/ctracer/tracer.c
index 1096bef7a..cee4b723c 100644
--- a/coverage/ctracer/tracer.c
+++ b/coverage/ctracer/tracer.c
@@ -137,23 +137,25 @@ static void
CTracer_showlog(CTracer * self, int lineno, PyObject * filename, const char * msg)
{
if (logging) {
+ FILE *flog = fopen("/tmp/debug_trace.txt", "a");
int depth = self->pdata_stack->depth;
- printf("%x: %s%3d ", (int)self, indent(depth), depth);
+ fprintf(flog, "%p: %s%3d ", self, indent(depth), depth);
if (lineno) {
- printf("%4d", lineno);
+ fprintf(flog, "%4d", lineno);
}
else {
- printf(" ");
+ fprintf(flog, " ");
}
if (filename) {
PyObject *ascii = PyUnicode_AsASCIIString(filename);
- printf(" %s", PyBytes_AS_STRING(ascii));
+ fprintf(flog, " %s", PyBytes_AS_STRING(ascii));
Py_DECREF(ascii);
}
if (msg) {
- printf(" %s", msg);
+ fprintf(flog, " %s", msg);
}
- printf("\n");
+ fprintf(flog, "\n");
+ fclose(flog);
}
}
diff --git a/coverage/html.py b/coverage/html.py
index 1fff59569..807661c4b 100644
--- a/coverage/html.py
+++ b/coverage/html.py
@@ -313,7 +313,7 @@ def __init__(self, cov: Coverage) -> None:
# Functions available in the templates.
"escape": escape,
"pair": pair,
- "len": len,
+ "pretty_file": pretty_file,
# Constants for this report.
"__url__": __url__,
"__version__": coverage.__version__,
@@ -321,7 +321,6 @@ def __init__(self, cov: Coverage) -> None:
"time_stamp": format_local_datetime(datetime.datetime.now()),
"extra_css": self.extra_css,
"has_arcs": self.has_arcs,
- "show_contexts": self.config.show_contexts,
"statics": {},
# Constants for all reports.
# These css classes determine which lines are highlighted by default.
@@ -854,3 +853,8 @@ def escape(t: str) -> str:
def pair(ratio: tuple[int, int]) -> str:
"""Format a pair of numbers so JavaScript can read them in an attribute."""
return "{} {}".format(*ratio)
+
+
+def pretty_file(filename: str) -> str:
+ """Return a prettier version of `filename` for display."""
+ return re.sub(r"[/\\]", "\N{THIN SPACE}\\g<0>\N{THIN SPACE}", filename)
diff --git a/coverage/htmlfiles/coverage_html.js b/coverage/htmlfiles/coverage_html.js
index 851d4b41f..6f871742c 100644
--- a/coverage/htmlfiles/coverage_html.js
+++ b/coverage/htmlfiles/coverage_html.js
@@ -140,12 +140,15 @@ coverage.wire_up_filter = function () {
const table_body_rows = table.querySelectorAll("tbody tr");
const no_rows = document.getElementById("no_rows");
+ const footer = table.tFoot.rows[0];
+ const ratio_columns = Array.from(footer.cells).map(cell => Boolean(cell.dataset.ratio));
+
// Observe filter keyevents.
const filter_handler = (event => {
// Keep running total of each metric, first index contains number of shown rows
- const totals = new Array(table.rows[0].cells.length).fill(0);
- // Accumulate the percentage as fraction
- totals[totals.length - 1] = { "numer": 0, "denom": 0 }; // nosemgrep: eslint.detect-object-injection
+ const totals = ratio_columns.map(
+ is_ratio => is_ratio ? {"numer": 0, "denom": 0} : 0
+ );
var text = document.getElementById("filter").value;
// Store filter value
@@ -191,11 +194,11 @@ coverage.wire_up_filter = function () {
for (let column = 0; column < totals.length; column++) {
// Accumulate dynamic totals
cell = row.cells[column] // nosemgrep: eslint.detect-object-injection
- if (cell.classList.contains("name")) {
+ if (cell.matches(".name, .spacer")) {
continue;
}
- if (column === totals.length - 1) {
- // Last column contains percentage
+ if (ratio_columns[column] && cell.dataset.ratio) {
+ // Column stores a ratio
const [numer, denom] = cell.dataset.ratio.split(" ");
totals[column]["numer"] += parseInt(numer, 10); // nosemgrep: eslint.detect-object-injection
totals[column]["denom"] += parseInt(denom, 10); // nosemgrep: eslint.detect-object-injection
@@ -218,17 +221,16 @@ coverage.wire_up_filter = function () {
no_rows.style.display = null;
table.style.display = null;
- const footer = table.tFoot.rows[0];
// Calculate new dynamic sum values based on visible rows.
for (let column = 0; column < totals.length; column++) {
// Get footer cell element.
const cell = footer.cells[column]; // nosemgrep: eslint.detect-object-injection
- if (cell.classList.contains("name")) {
+ if (cell.matches(".name, .spacer")) {
continue;
}
// Set value into dynamic footer cell element.
- if (column === totals.length - 1) {
+ if (ratio_columns[column]) {
// Percentage column uses the numerator and denominator,
// and adapts to the number of decimal places.
const match = /\.([0-9]+)/.exec(cell.textContent);
diff --git a/coverage/htmlfiles/index.html b/coverage/htmlfiles/index.html
index bb84b4403..7c5803978 100644
--- a/coverage/htmlfiles/index.html
+++ b/coverage/htmlfiles/index.html
@@ -81,53 +81,88 @@
{# The title="" attr doesn't work in Safari. #}
+ {% if has_arcs %}
+