From 3eede50ee60578e47b0fedec4e565815e5d0f289 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Fri, 23 May 2025 07:42:56 -0400 Subject: [PATCH 01/26] build: bump version to 7.8.3 --- CHANGES.rst | 6 ++++++ coverage/version.py | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index e2264ab36..f814e5bbd 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -20,6 +20,12 @@ upgrading your version of coverage.py. .. Version 9.8.1 — 2027-07-27 .. -------------------------- +Unreleased +---------- + +Nothing yet. + + .. start-releases .. _changes_7-8-2: diff --git a/coverage/version.py b/coverage/version.py index d6a2eb793..2f1686f3e 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, 8, 2, "final", 0) -_dev = 0 +version_info = (7, 8, 3, "alpha", 0) +_dev = 1 def _make_version( From 94c89f7d9b14c6cf507c9125d03a504133c4a54e Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Fri, 23 May 2025 16:58:27 -0400 Subject: [PATCH 02/26] test: run 3.15 nightly tests --- .github/workflows/python-nightly.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/python-nightly.yml b/.github/workflows/python-nightly.yml index 99d407291..c6dc6a7f3 100644 --- a/.github/workflows/python-nightly.yml +++ b/.github/workflows/python-nightly.yml @@ -62,6 +62,7 @@ jobs: - "3.12" - "3.13" - "3.14" + - "3.15" # https://github.com/actions/setup-python#available-versions-of-pypy - "pypy-3.11" nogil: From f677a7448b25eae3f9c53f31afd1ed02ef0145ba Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 17 May 2025 11:11:33 -0400 Subject: [PATCH 03/26] feat: core can be specified in the config file. #1746 --- CHANGES.rst | 6 +++++- coverage/config.py | 9 +++++++++ coverage/core.py | 4 +++- coverage/version.py | 2 +- doc/cmd.rst | 5 +++-- doc/config.rst | 16 ++++++++++++++++ tests/test_config.py | 25 +++++++++++++++++++++++++ 7 files changed, 62 insertions(+), 5 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index f814e5bbd..b33a47277 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -23,7 +23,11 @@ upgrading your version of coverage.py. Unreleased ---------- -Nothing yet. +- Added a ``[run] core`` configuration setting to specify the measurement core, + which was previously only available through the COVERAGE_CORE environment + variable. Finishes `issue 1746`_. + +.. _issue 1746: https://github.com/nedbat/coveragepy/issues/1746 .. start-releases diff --git a/coverage/config.py b/coverage/config.py index 94831e070..bb8e95dfc 100644 --- a/coverage/config.py +++ b/coverage/config.py @@ -197,6 +197,7 @@ def __init__(self) -> None: self.command_line: str | None = None self.concurrency: list[str] = [] self.context: str | None = None + self.core: str | None = None self.cover_pylib = False self.data_file = ".coverage" self.debug: list[str] = [] @@ -379,6 +380,7 @@ def copy(self) -> CoverageConfig: ("command_line", "run:command_line"), ("concurrency", "run:concurrency", "list"), ("context", "run:context"), + ("core", "run:core"), ("cover_pylib", "run:cover_pylib", "boolean"), ("data_file", "run:data_file"), ("debug", "run:debug", "list"), @@ -617,11 +619,18 @@ def read_coverage_config( env_data_file = os.getenv("COVERAGE_FILE") if env_data_file: config.data_file = env_data_file + # $set_env.py: COVERAGE_DEBUG - Debug options: https://coverage.rtfd.io/cmd.html#debug debugs = os.getenv("COVERAGE_DEBUG") if debugs: config.debug.extend(d.strip() for d in debugs.split(",")) + # Read the COVERAGE_CORE environment variable for backward compatibility, + # and because we use it in the test suite to pick a specific core. + env_core = os.getenv("COVERAGE_CORE") + if env_core: + config.core = env_core + # 4) from constructor arguments: config.from_args(**kwargs) diff --git a/coverage/core.py b/coverage/core.py index c46fd241e..b7c509fb9 100644 --- a/coverage/core.py +++ b/coverage/core.py @@ -31,6 +31,8 @@ CTRACER_FILE: str | None = coverage.tracer.__file__ except ImportError: # Couldn't import the C extension, maybe it isn't built. + # We still need to check the environment variable directly here, + # as this code runs before configuration is loaded. if os.getenv("COVERAGE_CORE") == "ctrace": # pragma: part covered # During testing, we use the COVERAGE_CORE environment variable # to indicate that we've fiddled with the environment to test this @@ -74,7 +76,7 @@ def __init__( core_name = "pytrace" if core_name is None: - core_name = os.getenv("COVERAGE_CORE") + core_name = config.core if core_name == "sysmon" and reason_no_sysmon: warn(f"sys.monitoring {reason_no_sysmon}, using default core", slug="no-sysmon") diff --git a/coverage/version.py b/coverage/version.py index 2f1686f3e..fb4a43fc6 100644 --- a/coverage/version.py +++ b/coverage/version.py @@ -8,7 +8,7 @@ # version_info: same semantics as sys.version_info. # _dev: the .devN suffix if any. -version_info = (7, 8, 3, "alpha", 0) +version_info = (7, 9, 0, "alpha", 0) _dev = 1 diff --git a/doc/cmd.rst b/doc/cmd.rst index 74113549b..28f21f115 100644 --- a/doc/cmd.rst +++ b/doc/cmd.rst @@ -204,8 +204,9 @@ simpler but slower trace method, and might be needed in rare cases. In Python 3.12 and above, you can try an experimental core based on the new :mod:`sys.monitoring ` module by defining a -``COVERAGE_CORE=sysmon`` environment variable. This should be faster, though -plugins and dynamic contexts are not yet supported with it. +``COVERAGE_CORE=sysmon`` environment variable or by setting ``core = sysmon`` +in the configuration file. This should be faster, though plugins and +dynamic contexts are not yet supported with it. Coverage.py sets an environment variable, ``COVERAGE_RUN`` to indicate that your code is running under coverage measurement. The value is not relevant, diff --git a/doc/config.rst b/doc/config.rst index 7a02d6a04..fc5a190eb 100644 --- a/doc/config.rst +++ b/doc/config.rst @@ -328,6 +328,22 @@ Before version 4.2, this option only accepted a single string. .. versionadded:: 5.0 +.. _config_run_core: + +[run] core +.......... + +(string) Specify which trace function implementation to use. Valid values are: +"pytrace" for the pure Python implementation, "ctrace" for the C implementation +(default), or "sysmon" for the :mod:`sys.monitoring ` +implementation (Python 3.12+ only). + +This was previously only available as the COVERAGE_CORE environment variable. +Note that the "sysmon" core does not yet support plugins or dynamic contexts. + +.. versionadded:: 7.9 + + .. _config_run_cover_pylib: [run] cover_pylib diff --git a/tests/test_config.py b/tests/test_config.py index 190a27b1c..f1631a7fe 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -5,6 +5,8 @@ from __future__ import annotations +import os + from unittest import mock import pytest @@ -491,6 +493,29 @@ def test_exclude_also(self) -> None: expected = coverage.config.DEFAULT_EXCLUDE + ["foobar", "raise .*Error"] assert cov.config.exclude_list == expected + def test_core_option(self) -> None: + # Test that the core option can be set in the configuration file. + self.del_environ("COVERAGE_CORE") + cov = coverage.Coverage() + default_core = cov.config.core + core_to_set = "ctrace" if default_core == "pytrace" else "pytrace" + + self.make_file(".coveragerc", f"""\ + [run] + core = {core_to_set} + """) + cov = coverage.Coverage() + assert cov.config.core == core_to_set + os.remove(".coveragerc") + + self.make_file("pyproject.toml", f"""\ + [tool.coverage.run] + core = "{core_to_set}" + """) + + cov = coverage.Coverage() + assert cov.config.core == core_to_set + class ConfigFileTest(UsingModulesMixin, CoverageTest): """Tests of the config file settings in particular.""" From 7856862e25276ab7d4a8506dd76239b2c22792e9 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 24 May 2025 10:14:53 -0400 Subject: [PATCH 04/26] build: i haven't used the 'smoke' targets in a long time --- Makefile | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/Makefile b/Makefile index 426e43b46..c0e03f33c 100644 --- a/Makefile +++ b/Makefile @@ -64,21 +64,16 @@ install: ## Install the developer tools ##@ Tests and quality checks -.PHONY: lint smoke +.PHONY: lint lint: ## Run linters and checkers. tox -q -e lint -PYTEST_SMOKE_ARGS = -n auto -m "not expensive" --maxfail=3 $(ARGS) - -smoke: ## Run tests quickly with the C tracer in the lowest supported Python versions. - COVERAGE_TEST_CORES=ctrace tox -q -e py38 -- $(PYTEST_SMOKE_ARGS) - ##@ Metacov: coverage measurement of coverage.py itself # See metacov.ini for details. -.PHONY: metacov metahtml metasmoke +.PHONY: metacov metahtml metacov: ## Run meta-coverage, measuring ourself. COVERAGE_COVERAGE=yes tox -q $(ARGS) @@ -86,9 +81,6 @@ metacov: ## Run meta-coverage, measuring ourself. metahtml: ## Produce meta-coverage HTML reports. tox exec -q $(ARGS) -- python3 igor.py combine_html -metasmoke: - COVERAGE_TEST_CORES=ctrace ARGS="-e py39" make --keep-going metacov metahtml - ##@ Requirements management From 25bc5aa0c80c41b32c8d0a054a5e04da6b30a6cf Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 24 May 2025 10:15:25 -0400 Subject: [PATCH 05/26] test: COVERAGE_TEST_CORES only affects igor.py, no need to fiddle with it in the test suite --- tests/test_process.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tests/test_process.py b/tests/test_process.py index d2c95bbda..faa004c66 100644 --- a/tests/test_process.py +++ b/tests/test_process.py @@ -1129,7 +1129,6 @@ class CoverageCoreTest(CoverageTest): has_ctracer = False def test_core_default(self) -> None: - self.del_environ("COVERAGE_TEST_CORES") self.del_environ("COVERAGE_CORE") self.make_file("numbers.py", "print(123, 456)") out = self.run_command("coverage run --debug=sys numbers.py") @@ -1144,7 +1143,6 @@ def test_core_default(self) -> None: @pytest.mark.skipif(not has_ctracer, reason="No CTracer to request") def test_core_request_ctrace(self) -> None: - self.del_environ("COVERAGE_TEST_CORES") self.set_environ("COVERAGE_CORE", "ctrace") self.make_file("numbers.py", "print(123, 456)") out = self.run_command("coverage run --debug=sys numbers.py") @@ -1153,7 +1151,6 @@ def test_core_request_ctrace(self) -> None: assert core == "core: CTracer" def test_core_request_pytrace(self) -> None: - self.del_environ("COVERAGE_TEST_CORES") self.set_environ("COVERAGE_CORE", "pytrace") self.make_file("numbers.py", "print(123, 456)") out = self.run_command("coverage run --debug=sys numbers.py") @@ -1162,7 +1159,6 @@ def test_core_request_pytrace(self) -> None: assert core == "core: PyTracer" def test_core_request_sysmon(self) -> None: - self.del_environ("COVERAGE_TEST_CORES") self.set_environ("COVERAGE_CORE", "sysmon") self.make_file("numbers.py", "print(123, 456)") out = self.run_command("coverage run --debug=sys numbers.py") @@ -1177,7 +1173,6 @@ def test_core_request_sysmon(self) -> None: 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") From 061ce2abe49b7a58ec55d6497834a2173836cee1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 25 May 2025 08:20:53 -0400 Subject: [PATCH 06/26] chore: bump the action-dependencies group with 3 updates (#1973) Bumps the action-dependencies group with 3 updates: [github/codeql-action](https://github.com/github/codeql-action), [actions/dependency-review-action](https://github.com/actions/dependency-review-action) and [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv). Updates `github/codeql-action` from 3.28.17 to 3.28.18 - [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/60168efe1c415ce0f5521ea06d5c2062adbeed1b...ff0a06e83cb2de871e5a09832bc6a81e7276941f) Updates `actions/dependency-review-action` from 4.7.0 to 4.7.1 - [Release notes](https://github.com/actions/dependency-review-action/releases) - [Commits](https://github.com/actions/dependency-review-action/compare/38ecb5b593bf0eb19e335c03f97670f792489a8b...da24556b548a50705dd671f47852072ea4c105d9) Updates `astral-sh/setup-uv` from 6.0.1 to 6.1.0 - [Release notes](https://github.com/astral-sh/setup-uv/releases) - [Commits](https://github.com/astral-sh/setup-uv/compare/6b9c6063abd6010835644d4c2e1bef4cf5cd0fca...f0ec1fc3b38f5e7cd731bb6ce540c5af426746bb) --- updated-dependencies: - dependency-name: github/codeql-action dependency-version: 3.28.18 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: action-dependencies - dependency-name: actions/dependency-review-action dependency-version: 4.7.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: action-dependencies - dependency-name: astral-sh/setup-uv dependency-version: 6.1.0 dependency-type: direct:production update-type: version-update:semver-minor 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/dependency-review.yml | 2 +- .github/workflows/quality.yml | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index a47292b10..48f9e10b0 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@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3 + uses: github/codeql-action/init@ff0a06e83cb2de871e5a09832bc6a81e7276941f # 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@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3 + uses: github/codeql-action/autobuild@ff0a06e83cb2de871e5a09832bc6a81e7276941f # 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@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3 + uses: github/codeql-action/analyze@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3 diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index 84a28187a..4ba98033d 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -28,7 +28,7 @@ jobs: persist-credentials: false - name: 'Dependency Review' - uses: actions/dependency-review-action@38ecb5b593bf0eb19e335c03f97670f792489a8b # v4.7.0 + uses: actions/dependency-review-action@da24556b548a50705dd671f47852072ea4c105d9 # v4.7.1 with: base-ref: ${{ github.event.pull_request.base.sha || 'master' }} head-ref: ${{ github.event.pull_request.head.sha || github.ref }} diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index 3eb783830..0e1650fed 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -173,7 +173,7 @@ jobs: persist-credentials: false - name: Install the latest version of uv - uses: astral-sh/setup-uv@6b9c6063abd6010835644d4c2e1bef4cf5cd0fca #v6.0.1 + uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd731bb6ce540c5af426746bb #v6.1.0 with: enable-cache: false From 80e977cafcdfe47b842665e3a971fca7df931694 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Wed, 28 May 2025 06:39:38 -0400 Subject: [PATCH 07/26] refactor: pep 489 module initialization #1977 Code by Claude. Changelog by me. --- CHANGES.rst | 4 +++ coverage/ctracer/module.c | 73 ++++++++++++++++++++------------------- 2 files changed, 42 insertions(+), 35 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index b33a47277..22ea810d5 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -27,7 +27,11 @@ Unreleased which was previously only available through the COVERAGE_CORE environment variable. Finishes `issue 1746`_. +- The C extension module now conforms to `PEP 489`_, closing `issue 1977`_. + .. _issue 1746: https://github.com/nedbat/coveragepy/issues/1746 +.. _issue 1977: https://github.com/nedbat/coveragepy/issues/1977 +.. _PEP 489: https://peps.python.org/pep-0489 .. start-releases diff --git a/coverage/ctracer/module.c b/coverage/ctracer/module.c index 33b50e64f..25acf6104 100644 --- a/coverage/ctracer/module.c +++ b/coverage/ctracer/module.c @@ -9,65 +9,68 @@ #define MODULE_DOC PyDoc_STR("Fast coverage tracer.") -static PyModuleDef -moduledef = { - PyModuleDef_HEAD_INIT, - "coverage.tracer", - MODULE_DOC, - -1, - NULL, /* methods */ - NULL, - NULL, /* traverse */ - NULL, /* clear */ - NULL -}; - - -PyObject * -PyInit_tracer(void) +/* Module execution function for PEP 489 multi-phase initialization */ +static int +tracer_exec(PyObject *mod) { - PyObject * mod = PyModule_Create(&moduledef); - if (mod == NULL) { - return NULL; - } - -#ifdef Py_GIL_DISABLED - PyUnstable_Module_SetGIL(mod, Py_MOD_GIL_NOT_USED); -#endif - if (CTracer_intern_strings() < 0) { - return NULL; + return -1; } /* Initialize CTracer */ CTracerType.tp_new = PyType_GenericNew; if (PyType_Ready(&CTracerType) < 0) { - Py_DECREF(mod); - return NULL; + return -1; } Py_INCREF(&CTracerType); if (PyModule_AddObject(mod, "CTracer", (PyObject *)&CTracerType) < 0) { - Py_DECREF(mod); Py_DECREF(&CTracerType); - return NULL; + return -1; } /* Initialize CFileDisposition */ CFileDispositionType.tp_new = PyType_GenericNew; if (PyType_Ready(&CFileDispositionType) < 0) { - Py_DECREF(mod); Py_DECREF(&CTracerType); - return NULL; + return -1; } Py_INCREF(&CFileDispositionType); if (PyModule_AddObject(mod, "CFileDisposition", (PyObject *)&CFileDispositionType) < 0) { - Py_DECREF(mod); Py_DECREF(&CTracerType); Py_DECREF(&CFileDispositionType); - return NULL; + return -1; } - return mod; + return 0; +} + +/* Slots for PEP 489 multi-phase initialization */ +static PyModuleDef_Slot tracer_slots[] = { + {Py_mod_exec, tracer_exec}, +#ifdef Py_GIL_DISABLED + {Py_mod_gil, Py_MOD_GIL_NOT_USED}, +#endif + {0, NULL} +}; + +static PyModuleDef +moduledef = { + PyModuleDef_HEAD_INIT, + "coverage.tracer", + MODULE_DOC, + 0, /* m_size */ + NULL, /* methods */ + tracer_slots, /* slots */ + NULL, /* traverse */ + NULL, /* clear */ + NULL +}; + + +PyObject * +PyInit_tracer(void) +{ + return PyModuleDef_Init(&moduledef); } From a6a575964baa100bdc74408df6846184334475d4 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Wed, 28 May 2025 07:23:19 -0400 Subject: [PATCH 08/26] build: tests have to run if C code has changed --- .github/workflows/coverage.yml | 2 ++ .github/workflows/testsuite.yml | 2 ++ 2 files changed, 4 insertions(+) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 4863574a6..182ca894d 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -49,6 +49,8 @@ jobs: filters: | run_coverage: - "**.py" + - "**.h" + - "**.c" - ".github/workflows/coverage.yml" - "tox.ini" - "requirements/*.pip" diff --git a/.github/workflows/testsuite.yml b/.github/workflows/testsuite.yml index a27e41f99..38c88183c 100644 --- a/.github/workflows/testsuite.yml +++ b/.github/workflows/testsuite.yml @@ -48,6 +48,8 @@ jobs: filters: | run_tests: - "**.py" + - "**.h" + - "**.c" - ".github/workflows/testsuite.yml" - "tox.ini" - "requirements/*.pip" From 490ff2d841dee02b0f238ebbed174cee1bd81e17 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+AA-Turner@users.noreply.github.com> Date: Wed, 28 May 2025 14:37:14 +0100 Subject: [PATCH 09/26] refactor: revent module reinitialisation with multi-phase init (#1978) --- coverage/ctracer/module.c | 38 ++++++++++++++++++++++---------------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/coverage/ctracer/module.c b/coverage/ctracer/module.c index 25acf6104..468c21507 100644 --- a/coverage/ctracer/module.c +++ b/coverage/ctracer/module.c @@ -9,10 +9,19 @@ #define MODULE_DOC PyDoc_STR("Fast coverage tracer.") -/* Module execution function for PEP 489 multi-phase initialization */ +static int module_loaded = 0; + static int tracer_exec(PyObject *mod) { + // https://docs.python.org/3/howto/isolating-extensions.html#opt-out-limiting-to-one-module-object-per-process + if (module_loaded) { + PyErr_SetString(PyExc_ImportError, + "cannot load module more than once per process"); + return -1; + } + module_loaded = 1; + if (CTracer_intern_strings() < 0) { return -1; } @@ -46,30 +55,27 @@ tracer_exec(PyObject *mod) return 0; } -/* Slots for PEP 489 multi-phase initialization */ static PyModuleDef_Slot tracer_slots[] = { {Py_mod_exec, tracer_exec}, -#ifdef Py_GIL_DISABLED +#if PY_VERSION_HEX >= 0x030c00f0 // Python 3.12+ + {Py_mod_multiple_interpreters, Py_MOD_MULTIPLE_INTERPRETERS_NOT_SUPPORTED}, +#endif +#if PY_VERSION_HEX >= 0x030d00f0 // Python 3.13+ + // signal that this module supports running without an active GIL {Py_mod_gil, Py_MOD_GIL_NOT_USED}, #endif {0, NULL} }; -static PyModuleDef -moduledef = { - PyModuleDef_HEAD_INIT, - "coverage.tracer", - MODULE_DOC, - 0, /* m_size */ - NULL, /* methods */ - tracer_slots, /* slots */ - NULL, /* traverse */ - NULL, /* clear */ - NULL +static PyModuleDef moduledef = { + .m_base = PyModuleDef_HEAD_INIT, + .m_name = "coverage.tracer", + .m_doc = MODULE_DOC, + .m_size = 0, + .m_slots = tracer_slots, }; - -PyObject * +PyMODINIT_FUNC PyInit_tracer(void) { return PyModuleDef_Init(&moduledef); From b2655331e879030e8a96c570a65789d548477430 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Wed, 28 May 2025 09:38:26 -0400 Subject: [PATCH 10/26] docs: thanks, Adam Turner --- CHANGES.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 22ea810d5..8de0d1b8b 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -28,9 +28,11 @@ Unreleased variable. Finishes `issue 1746`_. - The C extension module now conforms to `PEP 489`_, closing `issue 1977`_. + Thanks, `Adam Turner `_. .. _issue 1746: https://github.com/nedbat/coveragepy/issues/1746 .. _issue 1977: https://github.com/nedbat/coveragepy/issues/1977 +.. _pull 1978: https://github.com/nedbat/coveragepy/pull/1978 .. _PEP 489: https://peps.python.org/pep-0489 From 6c5fe5189903105cee28e9fea0cba4bea0f7e186 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 31 May 2025 06:51:51 -0400 Subject: [PATCH 11/26] build: 3.15 wasn't in the tox-gh list So 3.15 nightly builds ran more tox environments than they should. --- tox.ini | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tox.ini b/tox.ini index eb6a0a20e..706cd50be 100644 --- a/tox.ini +++ b/tox.ini @@ -143,5 +143,7 @@ python = 3.13t = py313t 3.14 = py314 3.14t = py314t + 3.15 = py315 + 3.15t = py315t pypy-3 = pypy3 pypy-3.11 = pypy3 From 544267f07c4e0a32559ad2fc795015ecab6cdc00 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 31 May 2025 07:16:38 -0400 Subject: [PATCH 12/26] debug: show the platform build info always --- coverage/control.py | 1 + igor.py | 4 +--- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/coverage/control.py b/coverage/control.py index 281c3aec9..baa66c806 100644 --- a/coverage/control.py +++ b/coverage/control.py @@ -1350,6 +1350,7 @@ def plugin_info(plugins: list[Any]) -> list[str]: ("python", sys.version.replace("\n", "")), ("platform", platform.platform()), ("implementation", platform.python_implementation()), + ("build", platform.python_build()), ("gil_enabled", getattr(sys, '_is_gil_enabled', lambda: True)()), ("executable", sys.executable), ("def_encoding", sys.getdefaultencoding()), diff --git a/igor.py b/igor.py index 8bc0c7086..3acbf3594 100644 --- a/igor.py +++ b/igor.py @@ -311,9 +311,7 @@ def print_banner(label): if PYPY: version += " (pypy %s)" % ".".join(str(v) for v in sys.pypy_version_info) - rev = platform.python_revision() - if rev: - version += f" (rev {rev})" + version += f" ({' '.join(platform.python_build())})" gil = "gil" if getattr(sys, '_is_gil_enabled', lambda: True)() else "nogil" version += f" ({gil})" From 958255f8e6d98eaf8a6562aaa50c8c14151b6aaa Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 31 May 2025 07:32:20 -0400 Subject: [PATCH 13/26] build: close two new zizmor warnings --- .github/workflows/coverage.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 182ca894d..86e90435f 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -250,8 +250,9 @@ jobs: id: info env: REF: ${{ github.ref }} + SHA: ${{ github.sha }} run: | - export SHA10=$(echo ${{ github.sha }} | cut -c 1-10) + export SHA10=$(echo $SHA | cut -c 1-10) export SLUG=$(date +'%Y%m%d')_$SHA10 echo "sha10=$SHA10" >> $GITHUB_ENV echo "slug=$SLUG" >> $GITHUB_ENV @@ -267,9 +268,11 @@ jobs: - name: "Checkout reports repo" if: ${{ github.ref == 'refs/heads/master' }} + env: + TOKEN: ${{ secrets.COVERAGE_REPORTS_TOKEN }} run: | set -xe - git clone --depth=1 --no-checkout https://${{ secrets.COVERAGE_REPORTS_TOKEN }}@github.com/nedbat/coverage-reports reports_repo + git clone --depth=1 --no-checkout https://${TOKEN}@github.com/nedbat/coverage-reports reports_repo cd reports_repo git sparse-checkout init --cone git sparse-checkout set --skip-checks '/*' '!/reports' From 6beea4b52bcf58e1c6027a5787581543dca91441 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 31 May 2025 08:10:45 -0400 Subject: [PATCH 14/26] build: make coverage.yml more similar to testsuite.yml --- .github/workflows/coverage.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 86e90435f..27df82ec6 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -18,6 +18,7 @@ defaults: env: PIP_DISABLE_PIP_VERSION_CHECK: 1 + COVERAGE_IGOR_VERBOSE: 1 FORCE_COLOR: 1 # Get colored pytest output permissions: From 8381ea787ef56b67ca158d6f43c546e719ef0d32 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 31 May 2025 13:44:02 -0400 Subject: [PATCH 15/26] fix: allow module to be imported twice in the same process --- coverage/ctracer/module.c | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/coverage/ctracer/module.c b/coverage/ctracer/module.c index 468c21507..ee8f2599e 100644 --- a/coverage/ctracer/module.c +++ b/coverage/ctracer/module.c @@ -9,18 +9,14 @@ #define MODULE_DOC PyDoc_STR("Fast coverage tracer.") -static int module_loaded = 0; +static BOOL module_inited = FALSE; static int tracer_exec(PyObject *mod) { - // https://docs.python.org/3/howto/isolating-extensions.html#opt-out-limiting-to-one-module-object-per-process - if (module_loaded) { - PyErr_SetString(PyExc_ImportError, - "cannot load module more than once per process"); - return -1; + if (module_inited) { + return 0; } - module_loaded = 1; if (CTracer_intern_strings() < 0) { return -1; @@ -52,6 +48,7 @@ tracer_exec(PyObject *mod) return -1; } + module_inited = TRUE; return 0; } From 80083bb8a3fded17f4293c5fd0f55183e39344ed Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Mon, 2 Jun 2025 19:14:35 -0400 Subject: [PATCH 16/26] chore: make upgrade --- doc/requirements.pip | 8 ++++---- requirements/dev.pip | 15 ++++++++------- requirements/kit.pip | 8 ++++---- requirements/light-threads.pip | 2 +- requirements/mypy.pip | 18 +++++++++++------- requirements/pip.pip | 2 +- requirements/pytest.pip | 12 +++++++----- requirements/tox.pip | 4 ++-- 8 files changed, 38 insertions(+), 31 deletions(-) diff --git a/doc/requirements.pip b/doc/requirements.pip index 535525f49..8697f01ec 100644 --- a/doc/requirements.pip +++ b/doc/requirements.pip @@ -78,7 +78,7 @@ roman-numerals-py==3.1.0 # via sphinx scriv==1.7.0 # via -r doc/requirements.in -setuptools==80.8.0 +setuptools==80.9.0 # via pbr sniffio==1.3.1 # via anyio @@ -119,15 +119,15 @@ sphinxcontrib-serializinghtml==2.0.0 # via sphinx sphinxcontrib-spelling==8.0.1 # via -r doc/requirements.in -starlette==0.46.2 +starlette==0.47.0 # via sphinx-autobuild stevedore==5.4.1 # via doc8 -typing-extensions==4.13.2 +typing-extensions==4.14.0 # via anyio urllib3==2.4.0 # via requests -uvicorn==0.34.2 +uvicorn==0.34.3 # via sphinx-autobuild watchfiles==1.0.5 # via sphinx-autobuild diff --git a/requirements/dev.pip b/requirements/dev.pip index afac0d62a..6aa1fcbe7 100644 --- a/requirements/dev.pip +++ b/requirements/dev.pip @@ -10,7 +10,7 @@ backports-tarfile==1.2.0 # via jaraco-context build==1.2.2.post1 # via check-manifest -cachetools==5.5.2 +cachetools==6.0.0 # via tox certifi==2025.4.26 # via requests @@ -53,7 +53,7 @@ flaky==3.8.1 # via -r requirements/pytest.in greenlet==3.2.2 # via -r requirements/dev.in -hypothesis==6.131.21 +hypothesis==6.133.0 # via -r requirements/pytest.in id==1.5.0 # via twine @@ -125,6 +125,7 @@ pygments==2.19.1 # via # -r requirements/pytest.in # pudb + # pytest # readme-renderer # rich pylint==3.3.7 @@ -133,11 +134,11 @@ pyproject-api==1.9.1 # via tox pyproject-hooks==1.2.0 # via build -pytest==8.3.5 +pytest==8.4.0 # via # -r requirements/pytest.in # pytest-xdist -pytest-xdist==3.6.1 +pytest-xdist==3.7.0 # via -r requirements/pytest.in readme-renderer==44.0 # via @@ -158,7 +159,7 @@ rich==14.0.0 # via twine scriv==1.7.0 # via -r requirements/dev.in -setuptools==80.8.0 +setuptools==80.9.0 # via # -r requirements/pip.in # check-manifest @@ -184,7 +185,7 @@ tox-gh==1.5.0 # via -r requirements/tox.in twine==6.1.0 # via -r requirements/dev.in -typing-extensions==4.13.2 +typing-extensions==4.14.0 # via # astroid # exceptiongroup @@ -207,5 +208,5 @@ virtualenv==20.31.2 # tox wcwidth==0.2.13 # via urwid -zipp==3.21.0 +zipp==3.22.0 # via importlib-metadata diff --git a/requirements/kit.pip b/requirements/kit.pip index df39ec8bc..92f4bae42 100644 --- a/requirements/kit.pip +++ b/requirements/kit.pip @@ -1,6 +1,6 @@ # This file was autogenerated by uv via the following command: # make upgrade -auditwheel==6.3.0 +auditwheel==6.4.0 # via -r requirements/kit.in backports-tarfile==1.2.0 # via jaraco-context @@ -83,7 +83,7 @@ rfc3986==2.0.0 # via twine rich==14.0.0 # via twine -setuptools==80.8.0 +setuptools==80.9.0 # via -r requirements/kit.in tomli==2.2.1 # via @@ -92,7 +92,7 @@ tomli==2.2.1 # dependency-groups twine==6.1.0 # via -r requirements/kit.in -typing-extensions==4.13.2 +typing-extensions==4.14.0 # via # cibuildwheel # rich @@ -102,5 +102,5 @@ urllib3==2.4.0 # twine wheel==0.45.1 # via -r requirements/kit.in -zipp==3.21.0 +zipp==3.22.0 # via importlib-metadata diff --git a/requirements/light-threads.pip b/requirements/light-threads.pip index 393dbe6b2..469383788 100644 --- a/requirements/light-threads.pip +++ b/requirements/light-threads.pip @@ -15,7 +15,7 @@ greenlet==3.2.2 # gevent pycparser==2.22 # via cffi -setuptools==80.8.0 +setuptools==80.9.0 # via # zope-event # zope-interface diff --git a/requirements/mypy.pip b/requirements/mypy.pip index f9175ec13..d966b3747 100644 --- a/requirements/mypy.pip +++ b/requirements/mypy.pip @@ -12,25 +12,29 @@ execnet==2.1.1 # via pytest-xdist flaky==3.8.1 # via -r requirements/pytest.in -hypothesis==6.131.21 +hypothesis==6.133.0 # via -r requirements/pytest.in iniconfig==2.1.0 # via pytest -mypy==1.15.0 +mypy==1.16.0 # via -r requirements/mypy.in mypy-extensions==1.1.0 # via mypy packaging==25.0 # via pytest +pathspec==0.12.1 + # via mypy pluggy==1.6.0 # via pytest pygments==2.19.1 - # via -r requirements/pytest.in -pytest==8.3.5 + # via + # -r requirements/pytest.in + # pytest +pytest==8.4.0 # via # -r requirements/pytest.in # pytest-xdist -pytest-xdist==3.6.1 +pytest-xdist==3.7.0 # via -r requirements/pytest.in sortedcontainers==2.4.0 # via hypothesis @@ -38,11 +42,11 @@ tomli==2.2.1 # via # mypy # pytest -types-requests==2.32.0.20250515 +types-requests==2.32.0.20250602 # via -r requirements/mypy.in types-tabulate==0.9.0.20241207 # via -r requirements/mypy.in -typing-extensions==4.13.2 +typing-extensions==4.14.0 # via # exceptiongroup # mypy diff --git a/requirements/pip.pip b/requirements/pip.pip index 5a3b2c451..a09489a99 100644 --- a/requirements/pip.pip +++ b/requirements/pip.pip @@ -8,7 +8,7 @@ pip==25.1.1 # via -r requirements/pip.in platformdirs==4.3.8 # via virtualenv -setuptools==80.8.0 +setuptools==80.9.0 # via -r requirements/pip.in virtualenv==20.31.2 # via -r requirements/pip.in diff --git a/requirements/pytest.pip b/requirements/pytest.pip index ae1d0ac18..d7b4ec94c 100644 --- a/requirements/pytest.pip +++ b/requirements/pytest.pip @@ -12,7 +12,7 @@ execnet==2.1.1 # via pytest-xdist flaky==3.8.1 # via -r requirements/pytest.in -hypothesis==6.131.21 +hypothesis==6.133.0 # via -r requirements/pytest.in iniconfig==2.1.0 # via pytest @@ -21,16 +21,18 @@ packaging==25.0 pluggy==1.6.0 # via pytest pygments==2.19.1 - # via -r requirements/pytest.in -pytest==8.3.5 + # via + # -r requirements/pytest.in + # pytest +pytest==8.4.0 # via # -r requirements/pytest.in # pytest-xdist -pytest-xdist==3.6.1 +pytest-xdist==3.7.0 # via -r requirements/pytest.in sortedcontainers==2.4.0 # via hypothesis tomli==2.2.1 # via pytest -typing-extensions==4.13.2 +typing-extensions==4.14.0 # via exceptiongroup diff --git a/requirements/tox.pip b/requirements/tox.pip index 3017d6bb3..36c512561 100644 --- a/requirements/tox.pip +++ b/requirements/tox.pip @@ -1,6 +1,6 @@ # This file was autogenerated by uv via the following command: # make upgrade -cachetools==5.5.2 +cachetools==6.0.0 # via tox chardet==5.2.0 # via tox @@ -36,7 +36,7 @@ tox==4.26.0 # tox-gh tox-gh==1.5.0 # via -r requirements/tox.in -typing-extensions==4.13.2 +typing-extensions==4.14.0 # via tox virtualenv==20.31.2 # via tox From 52407da7b9ed99c8bd1d2536df82ba8749664f50 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Mon, 2 Jun 2025 19:37:45 -0400 Subject: [PATCH 17/26] refactor: mypy==1.16.0 had some complaints --- coverage/config.py | 6 +++--- coverage/inorout.py | 6 +++--- coverage/xmlreport.py | 1 + tests/test_xml.py | 8 ++++---- tox.ini | 1 - 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/coverage/config.py b/coverage/config.py index bb8e95dfc..435ddd5f9 100644 --- a/coverage/config.py +++ b/coverage/config.py @@ -61,16 +61,16 @@ def real_section(self, section: str) -> str | None: return real_section return None - def has_option(self, section: str, option: str) -> bool: + def has_option(self, section: str, option: str) -> bool: # type: ignore[override] real_section = self.real_section(section) if real_section is not None: return super().has_option(real_section, option) return False - def has_section(self, section: str) -> bool: + def has_section(self, section: str) -> bool: # type: ignore[override] return bool(self.real_section(section)) - def options(self, section: str) -> list[str]: + def options(self, section: str) -> list[str]: # type: ignore[override] real_section = self.real_section(section) if real_section is not None: return super().options(real_section) diff --git a/coverage/inorout.py b/coverage/inorout.py index 8a5a1e27d..9c5c2e0fd 100644 --- a/coverage/inorout.py +++ b/coverage/inorout.py @@ -66,7 +66,7 @@ def canonical_path(morf: TMorf, directory: bool = False) -> str: return morf_path -def name_for_module(filename: str, frame: FrameType | None) -> str: +def name_for_module(filename: str, frame: FrameType | None) -> str | None: """Get the name of the module for a filename and frame. For configurability's sake, we allow __main__ modules to be matched by @@ -79,7 +79,7 @@ def name_for_module(filename: str, frame: FrameType | None) -> str: """ module_globals = frame.f_globals if frame is not None else {} - dunder_name: str = module_globals.get("__name__", None) + dunder_name: str | None = module_globals.get("__name__", None) if isinstance(dunder_name, str) and dunder_name != "__main__": # This is the usual case: an imported module. @@ -411,7 +411,7 @@ def check_include_omit_etc(self, filename: str, frame: FrameType | None) -> str extra = "" ok = False if self.source_pkgs_match: - if self.source_pkgs_match.match(modulename): + if isinstance(modulename, str) and self.source_pkgs_match.match(modulename): ok = True if modulename in self.source_pkgs_unmatched: self.source_pkgs_unmatched.remove(modulename) diff --git a/coverage/xmlreport.py b/coverage/xmlreport.py index 487c2659c..c72ee08a8 100644 --- a/coverage/xmlreport.py +++ b/coverage/xmlreport.py @@ -95,6 +95,7 @@ def report(self, morfs: Iterable[TMorf] | None, outfile: IO[str] | None = None) # Write header stuff. xcoverage = self.xml_out.documentElement + assert xcoverage is not None xcoverage.setAttribute("version", __version__) xcoverage.setAttribute("timestamp", str(int(time.time()*1000))) xcoverage.appendChild(self.xml_out.createComment( diff --git a/tests/test_xml.py b/tests/test_xml.py index 15f01b35c..f63fa7aa8 100644 --- a/tests/test_xml.py +++ b/tests/test_xml.py @@ -226,7 +226,7 @@ def test_curdir_source(self) -> None: cov = self.run_doit() cov.xml_report() dom = ElementTree.parse("coverage.xml") - self.assert_source(dom, ".") + self.assert_source(dom, ".") # type: ignore[arg-type] sources = dom.findall(".//source") assert len(sources) == 1 @@ -250,8 +250,8 @@ def test_deep_source(self) -> None: ) dom = ElementTree.parse("coverage.xml") - self.assert_source(dom, "src/main") - self.assert_source(dom, "also/over/there") + self.assert_source(dom, "src/main") # type: ignore[arg-type] + self.assert_source(dom, "also/over/there") # type: ignore[arg-type] sources = dom.findall(".//source") assert len(sources) == 2 @@ -488,7 +488,7 @@ def test_source_prefix(self) -> None: ('class', {'filename': "mod.py", 'name': "mod.py"}), ]) dom = ElementTree.parse("coverage.xml") - self.assert_source(dom, "src") + self.assert_source(dom, "src") # type: ignore[arg-type] @pytest.mark.parametrize("trail", ["", "/", "\\"]) def test_relative_source(self, trail: str) -> None: diff --git a/tox.ini b/tox.ini index 706cd50be..cc7edbaa1 100644 --- a/tox.ini +++ b/tox.ini @@ -128,7 +128,6 @@ setenv = commands = # PYVERSIONS - mypy --python-version=3.9 --strict --exclude=sysmon --exclude=bytecode {env:TYPEABLE} mypy --python-version=3.13 --strict {env:TYPEABLE} [gh] From 3ae5add1ab94ff68eb0935b153c162d228cb0c98 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Thu, 5 Jun 2025 15:35:17 -0400 Subject: [PATCH 18/26] chore: make upgrade (cog 3.5.0) --- doc/requirements.pip | 2 +- requirements/dev.pip | 8 ++++---- requirements/light-threads.pip | 2 +- requirements/mypy.pip | 2 +- requirements/pytest.pip | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/doc/requirements.pip b/doc/requirements.pip index 8697f01ec..8188c89fb 100644 --- a/doc/requirements.pip +++ b/doc/requirements.pip @@ -21,7 +21,7 @@ click==8.2.1 # uvicorn click-log==0.4.0 # via scriv -cogapp==3.4.1 +cogapp==3.5.0 # via -r doc/requirements.in colorama==0.4.6 # via sphinx-autobuild diff --git a/requirements/dev.pip b/requirements/dev.pip index 6aa1fcbe7..fa63b10a1 100644 --- a/requirements/dev.pip +++ b/requirements/dev.pip @@ -26,7 +26,7 @@ click==8.1.8 # scriv click-log==0.4.0 # via scriv -cogapp==3.4.1 +cogapp==3.5.0 # via -r requirements/dev.in colorama==0.4.6 # via @@ -51,9 +51,9 @@ filelock==3.18.0 # virtualenv flaky==3.8.1 # via -r requirements/pytest.in -greenlet==3.2.2 +greenlet==3.2.3 # via -r requirements/dev.in -hypothesis==6.133.0 +hypothesis==6.135.0 # via -r requirements/pytest.in id==1.5.0 # via twine @@ -175,7 +175,7 @@ tomli==2.2.1 # pyproject-api # pytest # tox -tomlkit==0.13.2 +tomlkit==0.13.3 # via pylint tox==4.26.0 # via diff --git a/requirements/light-threads.pip b/requirements/light-threads.pip index 469383788..ee49ba59b 100644 --- a/requirements/light-threads.pip +++ b/requirements/light-threads.pip @@ -8,7 +8,7 @@ eventlet==0.40.0 # via -r requirements/light-threads.in gevent==25.5.1 # via -r requirements/light-threads.in -greenlet==3.2.2 +greenlet==3.2.3 # via # -r requirements/light-threads.in # eventlet diff --git a/requirements/mypy.pip b/requirements/mypy.pip index d966b3747..36444bce9 100644 --- a/requirements/mypy.pip +++ b/requirements/mypy.pip @@ -12,7 +12,7 @@ execnet==2.1.1 # via pytest-xdist flaky==3.8.1 # via -r requirements/pytest.in -hypothesis==6.133.0 +hypothesis==6.135.0 # via -r requirements/pytest.in iniconfig==2.1.0 # via pytest diff --git a/requirements/pytest.pip b/requirements/pytest.pip index d7b4ec94c..439c7f3bd 100644 --- a/requirements/pytest.pip +++ b/requirements/pytest.pip @@ -12,7 +12,7 @@ execnet==2.1.1 # via pytest-xdist flaky==3.8.1 # via -r requirements/pytest.in -hypothesis==6.133.0 +hypothesis==6.135.0 # via -r requirements/pytest.in iniconfig==2.1.0 # via pytest From 8891312e5d95b6054a95c0ea162f0bf9454686f0 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Thu, 5 Jun 2025 15:36:35 -0400 Subject: [PATCH 19/26] docs: updated cog checksums --- doc/cmd.rst | 24 ++++++++++++------------ doc/config.rst | 6 +++--- doc/contexts.rst | 4 ++-- doc/dbschema.rst | 4 ++-- doc/excluding.rst | 8 ++++---- doc/plugins.rst | 6 +++--- doc/source.rst | 4 ++-- 7 files changed, 28 insertions(+), 28 deletions(-) diff --git a/doc/cmd.rst b/doc/cmd.rst index 28f21f115..f20c8e18c 100644 --- a/doc/cmd.rst +++ b/doc/cmd.rst @@ -8,7 +8,7 @@ .. [[[cog from cog_helpers import show_configs, show_help .. ]]] -.. [[[end]]] (checksum: d41d8cd98f00b204e9800998ecf8427e) +.. [[[end]]] (sum: 1B2M2Y8Asg) .. _cmd: @@ -143,7 +143,7 @@ There are many options: --rcfile=RCFILE Specify configuration file. By default '.coveragerc', 'setup.cfg', 'tox.ini', and 'pyproject.toml' are tried. [env: COVERAGE_RCFILE] -.. [[[end]]] (checksum: b1a0fffe2768fc142f1d97ae556b621d) +.. [[[end]]] (sum: saD//ido/B) If you want :ref:`branch coverage ` measurement, use the ``--branch`` flag. Otherwise only statement coverage is measured. @@ -328,7 +328,7 @@ file: [coverage:run] disable_warnings = no-data-collected -.. [[[end]]] (checksum: 489285bcfa173b69a286f03fe13e4554) +.. [[[end]]] (sum: SJKFvPoXO2) .. _cmd_datafile: @@ -428,7 +428,7 @@ want to keep those files, use the ``--keep`` command-line option. --rcfile=RCFILE Specify configuration file. By default '.coveragerc', 'setup.cfg', 'tox.ini', and 'pyproject.toml' are tried. [env: COVERAGE_RCFILE] -.. [[[end]]] (checksum: 0bdd83f647ee76363c955bedd9ddf749) +.. [[[end]]] (sum: C92D9kfudj) .. _cmd_combine_remapping: @@ -477,7 +477,7 @@ To erase the collected data, use the **erase** command: --rcfile=RCFILE Specify configuration file. By default '.coveragerc', 'setup.cfg', 'tox.ini', and 'pyproject.toml' are tried. [env: COVERAGE_RCFILE] -.. [[[end]]] (checksum: cfeaef66ce8d5154dc6914831030b46b) +.. [[[end]]] (sum: z+rvZs6NUV) If your configuration file indicates parallel data collection, **erase** will remove all of the data files. @@ -575,7 +575,7 @@ as a percentage. --rcfile=RCFILE Specify configuration file. By default '.coveragerc', 'setup.cfg', 'tox.ini', and 'pyproject.toml' are tried. [env: COVERAGE_RCFILE] -.. [[[end]]] (checksum: 167272a29d9e7eb017a592a0e0747a06) +.. [[[end]]] (sum: FnJyop2efr) The ``-m`` flag also shows the line numbers of missing statements:: @@ -706,7 +706,7 @@ Click the keyboard icon in the upper right to see the complete list. --rcfile=RCFILE Specify configuration file. By default '.coveragerc', 'setup.cfg', 'tox.ini', and 'pyproject.toml' are tried. [env: COVERAGE_RCFILE] -.. [[[end]]] (checksum: e3a1a6e24ad9b303ba06d42880ed0219) +.. [[[end]]] (sum: 46Gm4krZsw) The title of the report can be set with the ``title`` setting in the ``[html]`` section of the configuration file, or the ``--title`` switch on @@ -783,7 +783,7 @@ compatible with `Cobertura`_. --rcfile=RCFILE Specify configuration file. By default '.coveragerc', 'setup.cfg', 'tox.ini', and 'pyproject.toml' are tried. [env: COVERAGE_RCFILE] -.. [[[end]]] (checksum: 8b239d89534be0b2c69489e10b1352a9) +.. [[[end]]] (sum: iyOdiVNL4L) You can specify the name of the output file with the ``-o`` switch. @@ -872,7 +872,7 @@ The **json** command writes coverage data to a "coverage.json" file. --rcfile=RCFILE Specify configuration file. By default '.coveragerc', 'setup.cfg', 'tox.ini', and 'pyproject.toml' are tried. [env: COVERAGE_RCFILE] -.. [[[end]]] (checksum: e53e60cb65d971c35d1db1c08324b72e) +.. [[[end]]] (sum: 5T5gy2XZcc) You can specify the name of the output file with the ``-o`` switch. The JSON can be nicely formatted by specifying the ``--pretty-print`` switch. @@ -918,7 +918,7 @@ The **lcov** command writes coverage data to a "coverage.lcov" file. --rcfile=RCFILE Specify configuration file. By default '.coveragerc', 'setup.cfg', 'tox.ini', and 'pyproject.toml' are tried. [env: COVERAGE_RCFILE] -.. [[[end]]] (checksum: 16acfbae8011d2e3b620695c5fe13746) +.. [[[end]]] (sum: Fqz7roAR0u) Common reporting options are described above in :ref:`cmd_reporting`. Also see :ref:`Configuration: [lcov] `. @@ -987,7 +987,7 @@ For example:: --rcfile=RCFILE Specify configuration file. By default '.coveragerc', 'setup.cfg', 'tox.ini', and 'pyproject.toml' are tried. [env: COVERAGE_RCFILE] -.. [[[end]]] (checksum: fd7d8fbd2dd6e24d37f868b389c2ad6d) +.. [[[end]]] (sum: /X2PvS3W4k) Other common reporting options are described above in :ref:`cmd_reporting`. @@ -1029,7 +1029,7 @@ A few types of information are available: --rcfile=RCFILE Specify configuration file. By default '.coveragerc', 'setup.cfg', 'tox.ini', and 'pyproject.toml' are tried. [env: COVERAGE_RCFILE] -.. [[[end]]] (checksum: c9b8dfb644da3448830b1c99bffa6880) +.. [[[end]]] (sum: ybjftkTaNE) .. _cmd_run_debug: diff --git a/doc/config.rst b/doc/config.rst index fc5a190eb..5d2eab092 100644 --- a/doc/config.rst +++ b/doc/config.rst @@ -8,7 +8,7 @@ .. [[[cog from cog_helpers import show_configs .. ]]] -.. [[[end]]] (checksum: d41d8cd98f00b204e9800998ecf8427e) +.. [[[end]]] (sum: 1B2M2Y8Asg) .. _config: @@ -257,7 +257,7 @@ Here's a sample configuration file, in each syntax: [coverage:html] directory = coverage_html_report -.. [[[end]]] (checksum: 1d4d59eb69af44aacb77c9ebad869b65) +.. [[[end]]] (sum: HU1Z62mvRK) The specific configuration settings are described below. Many sections and @@ -572,7 +572,7 @@ equivalent when combining data from different machines: /jenkins/build/*/src c:\myproj\src -.. [[[end]]] (checksum: a074a5f121a23135dcb6733bca3e20bd) +.. [[[end]]] (sum: oHSl8SGiMT) The names of the entries ("source" in this example) are ignored, you may choose diff --git a/doc/contexts.rst b/doc/contexts.rst index 75080f0cb..55cc87951 100644 --- a/doc/contexts.rst +++ b/doc/contexts.rst @@ -8,7 +8,7 @@ .. [[[cog from cog_helpers import show_configs .. ]]] -.. [[[end]]] (checksum: d41d8cd98f00b204e9800998ecf8427e) +.. [[[end]]] (sum: 1B2M2Y8Asg) .. _contexts: @@ -108,7 +108,7 @@ The ``[run] dynamic_context`` setting has only one option now. Set it to [coverage:run] dynamic_context = test_function -.. [[[end]]] (checksum: 7594c36231f0ef52b554aad8c835ccf4) +.. [[[end]]] (sum: dZTDYjHw71) Each test function you run will be considered a separate dynamic context, and coverage data will be segregated for each. A test function is any function diff --git a/doc/dbschema.rst b/doc/dbschema.rst index b576acaaf..7a3c4cbae 100644 --- a/doc/dbschema.rst +++ b/doc/dbschema.rst @@ -34,7 +34,7 @@ to: SCHEMA_VERSION = 7 -.. [[[end]]] (checksum: 95a75340df33237e7e9c93b02dd1814c) +.. [[[end]]] (sum: ladTQN8zI3) You can use SQLite tools such as the :mod:`sqlite3 ` module in the Python standard library to access the data. Some data is stored in a @@ -116,7 +116,7 @@ This is the database schema: foreign key (file_id) references file (id) ); -.. [[[end]]] (checksum: 6a04d14b07f08f86cccf43056328dcb7) +.. [[[end]]] (sum: agTRSwfwj4) .. _numbits: diff --git a/doc/excluding.rst b/doc/excluding.rst index 034627b55..ddd6652d2 100644 --- a/doc/excluding.rst +++ b/doc/excluding.rst @@ -8,7 +8,7 @@ .. [[[cog from cog_helpers import show_configs .. ]]] -.. [[[end]]] (checksum: d41d8cd98f00b204e9800998ecf8427e) +.. [[[end]]] (sum: 1B2M2Y8Asg) .. _excluding: @@ -133,7 +133,7 @@ all of them by adding a regex to the exclusion list: exclude_also = def __repr__ -.. [[[end]]] (checksum: f3e70ebf128fbef4087efe75dcfadcb8) +.. [[[end]]] (sum: 8+cOvxKPvv) For example, here's a list of exclusions I've used: @@ -222,7 +222,7 @@ For example, here's a list of exclusions I've used: class .*\bProtocol\): @(abc\.)?abstractmethod -.. [[[end]]] (checksum: 650b209edd27112381b5f0a8d2ee0c45) +.. [[[end]]] (sum: ZQsgnt0nES) The :ref:`config_report_exclude_also` option adds regexes to the built-in default list so that you can add your own exclusions. The older @@ -319,7 +319,7 @@ Here are some examples: ; 3. A pragma comment that excludes an entire file: \A(?s:.*# pragma: exclude file.*)\Z -.. [[[end]]] (checksum: c46e819ad9a1d3a8e37037a89d28cfde) +.. [[[end]]] (sum: xG6Bmtmh06) The first regex matches a specific except line followed by a specific function call. Both lines must be present for the exclusion to take effect. Note that diff --git a/doc/plugins.rst b/doc/plugins.rst index 147fb1db4..c463ec6c9 100644 --- a/doc/plugins.rst +++ b/doc/plugins.rst @@ -8,7 +8,7 @@ .. [[[cog from cog_helpers import show_configs .. ]]] -.. [[[end]]] (checksum: d41d8cd98f00b204e9800998ecf8427e) +.. [[[end]]] (sum: 1B2M2Y8Asg) .. _plugins: @@ -84,7 +84,7 @@ a coverage.py plug-in called ``something.plugin``. plugins = something.plugin - .. [[[end]]] (checksum: 6e866323d4bc319d42e3199b08615111) + .. [[[end]]] (sum: boZjI9S8MZ) #. If the plug-in needs its own configuration, you can add those settings in the .coveragerc file in a section named for the plug-in: @@ -127,7 +127,7 @@ a coverage.py plug-in called ``something.plugin``. option1 = True option2 = abc.foo - .. [[[end]]] (checksum: b690115dbe7f6c7806567e009b5715c4) + .. [[[end]]] (sum: tpARXb5/bH) Check the documentation for the plug-in for details on the options it takes. diff --git a/doc/source.rst b/doc/source.rst index bcf7a8af9..36399e00e 100644 --- a/doc/source.rst +++ b/doc/source.rst @@ -8,7 +8,7 @@ .. [[[cog from cog_helpers import show_configs .. ]]] -.. [[[end]]] (checksum: d41d8cd98f00b204e9800998ecf8427e) +.. [[[end]]] (sum: 1B2M2Y8Asg) .. _source: @@ -139,7 +139,7 @@ current directory: # omit this single file utils/tirefire.py -.. [[[end]]] (checksum: 84ad2743cc0c7a077770e50fcedab29d) +.. [[[end]]] (sum: hK0nQ8wMeg) The ``source``, ``include``, and ``omit`` values all work together to determine the source that will be measured. From 1958c3f60acd3f485c6597820f2c9f44711a4025 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Fri, 6 Jun 2025 11:37:48 -0400 Subject: [PATCH 20/26] refactor: no need for a version check since we run mypy on late python now --- coverage/phystokens.py | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/coverage/phystokens.py b/coverage/phystokens.py index 8c29402ad..f8a8a3795 100644 --- a/coverage/phystokens.py +++ b/coverage/phystokens.py @@ -152,19 +152,16 @@ def source_token_lines(source: str) -> TSourceTokenLines: if keyword.iskeyword(ttext): # Hard keywords are always keywords. tok_class = "key" - elif sys.version_info >= (3, 10): # PYVERSIONS - # Need the version_info check to keep mypy from borking - # on issoftkeyword here. - if env.PYBEHAVIOR.soft_keywords and keyword.issoftkeyword(ttext): - # Soft keywords appear at the start of their line. - if len(line) == 0: - is_start_of_line = True - elif (len(line) == 1) and line[0][0] == "ws": - is_start_of_line = True - else: - is_start_of_line = False - if is_start_of_line and sline in soft_key_lines: - tok_class = "key" + elif env.PYBEHAVIOR.soft_keywords and keyword.issoftkeyword(ttext): + # Soft keywords appear at the start of their line. + if len(line) == 0: + is_start_of_line = True + elif (len(line) == 1) and line[0][0] == "ws": + is_start_of_line = True + else: + is_start_of_line = False + if is_start_of_line and sline in soft_key_lines: + tok_class = "key" line.append((tok_class, part)) mark_end = True scol = 0 From 9f94c87677fd86c7ced9ea2f3cddfcc0b4d5bc2d Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 8 Jun 2025 19:33:01 -0400 Subject: [PATCH 21/26] fix: properly render double braces in f-strings. #1980 --- CHANGES.rst | 4 ++++ coverage/env.py | 3 +++ coverage/htmlfiles/style.scss | 2 +- coverage/phystokens.py | 5 ++++- tests/test_html.py | 15 +++++++++++++++ tests/test_phystokens.py | 24 ++++++++++++++++++++++-- 6 files changed, 49 insertions(+), 4 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 8de0d1b8b..671b6de33 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -27,12 +27,16 @@ Unreleased which was previously only available through the COVERAGE_CORE environment variable. Finishes `issue 1746`_. +- Fixed incorrect rendering of f-strings with doubled braces, closing `issue + 1980`_. + - The C extension module now conforms to `PEP 489`_, closing `issue 1977`_. Thanks, `Adam Turner `_. .. _issue 1746: https://github.com/nedbat/coveragepy/issues/1746 .. _issue 1977: https://github.com/nedbat/coveragepy/issues/1977 .. _pull 1978: https://github.com/nedbat/coveragepy/pull/1978 +.. _issue 1980: https://github.com/nedbat/coveragepy/issues/1980 .. _PEP 489: https://peps.python.org/pep-0489 diff --git a/coverage/env.py b/coverage/env.py index 365da7f93..ded665b16 100644 --- a/coverage/env.py +++ b/coverage/env.py @@ -147,6 +147,9 @@ class PYBEHAVIOR: # Some words are keywords in some places, identifiers in other places. soft_keywords = (PYVERSION >= (3, 10)) + # f-strings are parsed as code, pep 701 + fstring_syntax = (PYVERSION >= (3, 12)) + # PEP669 Low Impact Monitoring: https://peps.python.org/pep-0669/ pep669: Final[bool] = bool(getattr(sys, "monitoring", None)) diff --git a/coverage/htmlfiles/style.scss b/coverage/htmlfiles/style.scss index 9bf1a7cc6..24dd97d1c 100644 --- a/coverage/htmlfiles/style.scss +++ b/coverage/htmlfiles/style.scss @@ -501,7 +501,7 @@ $border-indicator-width: .2em; font-weight: bold; line-height: 1px; } - .str { + .str, .fst { color: $light-token-str; @include color-dark($dark-token-str); } diff --git a/coverage/phystokens.py b/coverage/phystokens.py index f8a8a3795..57e9a541e 100644 --- a/coverage/phystokens.py +++ b/coverage/phystokens.py @@ -70,7 +70,7 @@ def _phys_tokens(toks: TokenInfos) -> TokenInfos: # It's a multi-line string and the first line ends with # a backslash, so we don't need to inject another. inject_backslash = False - elif sys.version_info >= (3, 12) and ttype == token.FSTRING_MIDDLE: + elif env.PYBEHAVIOR.fstring_syntax and ttype == token.FSTRING_MIDDLE: inject_backslash = False if inject_backslash: # Figure out what column the backslash is in. @@ -144,6 +144,9 @@ def source_token_lines(source: str) -> TSourceTokenLines: elif ttype in ws_tokens: mark_end = False else: + if env.PYBEHAVIOR.fstring_syntax and ttype == token.FSTRING_MIDDLE: + part = part.replace("{", "{{").replace("}", "}}") + ecol = scol + len(part) if mark_start and scol > col: line.append(("ws", " " * (scol - col))) mark_start = False diff --git a/tests/test_html.py b/tests/test_html.py index 627690bd7..2b9828761 100644 --- a/tests/test_html.py +++ b/tests/test_html.py @@ -1224,6 +1224,21 @@ def test_bug_1836(self, leader: str) -> None: "7" + "'''", ] + def test_bug_1980(self) -> None: + self.make_file("fstring_middle.py", """\ + x = 1 + f'Look: {x} {{x}}!' + """) + + cov = coverage.Coverage() + the_mod = self.start_import_stop(cov, "fstring_middle") + cov.html_report(the_mod) + + assert self.get_html_report_text_lines("fstring_middle.py") == [ + "1" + "x = 1", + "2" + "f'Look: {x} {{x}}!'", + ] + def test_unicode(self) -> None: surrogate = "\U000e0100" diff --git a/tests/test_phystokens.py b/tests/test_phystokens.py index 49d8d25a7..2140bd712 100644 --- a/tests/test_phystokens.py +++ b/tests/test_phystokens.py @@ -131,6 +131,27 @@ def test_stress(self, fname: str) -> None: with open(stress, encoding="utf-8") as fstress: assert re.search(r"(?m) $", fstress.read()), f"{stress} needs a trailing space." + def test_fstring_middle(self) -> None: + tokens = list(source_token_lines(textwrap.dedent("""\ + f'Look: {x} {{x}}!' + """))) + if env.PYBEHAVIOR.fstring_syntax: + assert tokens == [ + [ + ("fst", "f'"), + ("fst", "Look: "), + ("op", "{"), + ("nam", "x"), + ("op", "}"), + ("fst", " {{"), + ("fst", "x}}"), + ("fst", "!"), + ("fst", "'"), + ], + ] + else: + assert tokens == [[("str", "f'Look: {x} {{x}}!'")]] + @pytest.mark.skipif(not env.PYBEHAVIOR.soft_keywords, reason="Soft keywords are new in Python 3.10") class SoftKeywordTest(CoverageTest): @@ -154,7 +175,6 @@ def match(): global case """) tokens = list(source_token_lines(source)) - print(tokens) assert tokens[0][0] == ("key", "match") assert tokens[0][4] == ("nam", "match") assert tokens[1][1] == ("key", "case") @@ -168,7 +188,7 @@ def match(): assert tokens[10][2] == ("nam", "match") assert tokens[11][3] == ("nam", "case") - @pytest.mark.skipif(sys.version_info < (3, 12), reason="type is a soft keyword in 3.12") + @pytest.mark.skipif(sys.version_info < (3, 12), reason="type isn't a soft keyword until 3.12") def test_soft_keyword_type(self) -> None: source = textwrap.dedent("""\ type Point = tuple[float, float] From d81a5f8341816f548d21cb6a9a9a0e5cab1e2bcc Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 10 Jun 2025 06:34:14 -0400 Subject: [PATCH 22/26] fix: prevent ValueError for bizarre empty modules https://oss-fuzz.com/testcase-detail/4972497630199808 This fuzzed example from oss-fuzz caused this error: ``` # The first line of modules can lie and say 1 always, even if the first # line of code is later. If so, map 1 to the actual first line of the # module. if env.PYBEHAVIOR.module_firstline_1 and self._multiline: > self._multiline[1] = min(self.raw_statements) ^^^^^^^^^^^^^^^^^^^^^^^^ E ValueError: min() arg is an empty sequence coverage/parser.py:206: ValueError ``` --- CHANGES.rst | 4 ++++ coverage/parser.py | 2 +- tests/test_parser.py | 4 ++++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 671b6de33..71aa699f8 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -33,11 +33,15 @@ Unreleased - The C extension module now conforms to `PEP 489`_, closing `issue 1977`_. Thanks, `Adam Turner `_. +- Fixed a "ValueError: min() arg is an empty sequence" error caused by strange + empty modules, found by `oss-fuzz`_. + .. _issue 1746: https://github.com/nedbat/coveragepy/issues/1746 .. _issue 1977: https://github.com/nedbat/coveragepy/issues/1977 .. _pull 1978: https://github.com/nedbat/coveragepy/pull/1978 .. _issue 1980: https://github.com/nedbat/coveragepy/issues/1980 .. _PEP 489: https://peps.python.org/pep-0489 +.. _oss-fuzz: https://google.github.io/oss-fuzz/ .. start-releases diff --git a/coverage/parser.py b/coverage/parser.py index 306123b47..111a7e7a6 100644 --- a/coverage/parser.py +++ b/coverage/parser.py @@ -201,7 +201,7 @@ def _raw_parse(self) -> None: # The first line of modules can lie and say 1 always, even if the first # line of code is later. If so, map 1 to the actual first line of the # module. - if env.PYBEHAVIOR.module_firstline_1 and self._multiline: + if env.PYBEHAVIOR.module_firstline_1 and self._multiline and self.raw_statements: self._multiline[1] = min(self.raw_statements) self.excluded = self.first_lines(self.excluded) diff --git a/tests/test_parser.py b/tests/test_parser.py index 1e9a5db72..49e1f2dd7 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -218,6 +218,10 @@ def g2(): """) assert parser.exit_counts() == {1: 1, 2: 1, 3: 1, 5: 1} + def test_fuzzed_weirdness(self) -> None: + # This used to cause a `min of empty sequence` error: + self.parse_text("\r\\\n\n\t\t\\\n\n") + class ExclusionParserTest(PythonParserTestBase): """Tests for the exclusion code in PythonParser.""" From bc8602d3d05ac460795a3dc368a8d4bf83f53629 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Wed, 11 Jun 2025 06:38:12 -0400 Subject: [PATCH 23/26] fix: issue a warning if the C tracer can't be imported --- CHANGES.rst | 7 +++++-- coverage/core.py | 7 ++++++- doc/cmd.rst | 5 +++++ tests/test_process.py | 7 +++++-- 4 files changed, 21 insertions(+), 5 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 71aa699f8..ba9a58079 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -30,8 +30,11 @@ Unreleased - Fixed incorrect rendering of f-strings with doubled braces, closing `issue 1980`_. -- The C extension module now conforms to `PEP 489`_, closing `issue 1977`_. - Thanks, `Adam Turner `_. +- The C tracer core extension module now conforms to `PEP 489`_, closing `issue + 1977`_. Thanks, `Adam Turner `_. + +- If the C tracer core can't be imported, a warning ("no-ctracer") is issued + with the reason. - Fixed a "ValueError: min() arg is an empty sequence" error caused by strange empty modules, found by `oss-fuzz`_. diff --git a/coverage/core.py b/coverage/core.py index b7c509fb9..86acbaed5 100644 --- a/coverage/core.py +++ b/coverage/core.py @@ -25,11 +25,13 @@ os = isolate_module(os) +IMPORT_ERROR: str = "" + try: # Use the C extension code when we can, for speed. import coverage.tracer CTRACER_FILE: str | None = coverage.tracer.__file__ -except ImportError: +except ImportError as imp_err: # Couldn't import the C extension, maybe it isn't built. # We still need to check the environment variable directly here, # as this code runs before configuration is loaded. @@ -42,6 +44,7 @@ # exception here causes all sorts of other noise in unittest. sys.stderr.write("*** COVERAGE_CORE is 'ctrace' but can't import CTracer!\n") sys.exit(1) + IMPORT_ERROR = str(imp_err) CTRACER_FILE = None @@ -89,6 +92,8 @@ def __init__( if CTRACER_FILE: core_name = "ctrace" else: + if env.CPYTHON and IMPORT_ERROR: + warn(f"Couldn't import C tracer: {IMPORT_ERROR}", slug="no-ctracer", once=True) core_name = "pytrace" self.tracer_kwargs = {} diff --git a/doc/cmd.rst b/doc/cmd.rst index f20c8e18c..eab2ca6a4 100644 --- a/doc/cmd.rst +++ b/doc/cmd.rst @@ -274,6 +274,11 @@ 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. +Couldn't import C tracer (no-ctracer) + The core tracer implemented in C should have been used, but couldn't be + imported. The reason is included in the warning message. The Python tracer + will be used instead. + 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 diff --git a/tests/test_process.py b/tests/test_process.py index faa004c66..4e64371ae 100644 --- a/tests/test_process.py +++ b/tests/test_process.py @@ -1136,10 +1136,13 @@ def test_core_default(self) -> None: core = re_line(r" core:", out).strip() # if env.PYBEHAVIOR.pep669: # assert core == "core: SysMonitor" + warns = re_lines(r"\(no-ctracer\)", out) if self.has_ctracer: assert core == "core: CTracer" + assert not warns else: assert core == "core: PyTracer" + assert bool(warns) == env.CPYTHON @pytest.mark.skipif(not has_ctracer, reason="No CTracer to request") def test_core_request_ctrace(self) -> None: @@ -1164,12 +1167,12 @@ def test_core_request_sysmon(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() - warns = re_lines(r"CoverageWarning: sys.monitoring isn't available", out) + warns = re_lines(r"\(no-sysmon\)", out) if env.PYBEHAVIOR.pep669: assert core == "core: SysMonitor" assert not warns else: - assert core in ("core: CTracer", "core: PyTracer") + assert core in ["core: CTracer", "core: PyTracer"] assert warns def test_core_request_nosuchcore(self) -> None: From 3b0cb870f6abac8e4a7607094c467a7d766a44f4 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Wed, 11 Jun 2025 06:47:05 -0400 Subject: [PATCH 24/26] build: windows 3.14 is fixed --- .github/workflows/testsuite.yml | 7 ------- 1 file changed, 7 deletions(-) diff --git a/.github/workflows/testsuite.yml b/.github/workflows/testsuite.yml index 38c88183c..7ed39beed 100644 --- a/.github/workflows/testsuite.yml +++ b/.github/workflows/testsuite.yml @@ -91,13 +91,6 @@ jobs: - "pypy-3.9" - "pypy-3.10" - "pypy-3.11" - exclude: - # Windows 3.14.0b1 seems confused somehow about t vs not-t: - # https://github.com/python/cpython/issues/133779 - - os: windows - python-version: "3.14" - - os: windows - python-version: "3.14t" # # If we need to exclude any combinations, do it like this: # exclude: From a670927ae6d248f369d54fbe22d9546b6a25c25e Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Wed, 11 Jun 2025 18:41:39 -0400 Subject: [PATCH 25/26] docs: prep for 7.9.0 --- .github/workflows/kit.yml | 2 +- CHANGES.rst | 16 +++++++++------- README.rst | 3 ++- coverage/htmlfiles/style.css | 4 ++-- coverage/version.py | 4 ++-- doc/conf.py | 6 +++--- doc/index.rst | 2 +- tests/gold/html/styled/style.css | 4 ++-- 8 files changed, 22 insertions(+), 19 deletions(-) diff --git a/.github/workflows/kit.yml b/.github/workflows/kit.yml index 7415ba33f..0fd0534ea 100644 --- a/.github/workflows/kit.yml +++ b/.github/workflows/kit.yml @@ -149,7 +149,7 @@ jobs: - {"os": "windows", "py": "cp311", "arch": "ARM64", "os-version": "11-arm", "minpy": "3.11"} - {"os": "windows", "py": "cp312", "arch": "ARM64", "os-version": "11-arm", "minpy": "3.11"} - {"os": "windows", "py": "cp313", "arch": "ARM64", "os-version": "11-arm", "minpy": "3.11"} - # [[[end]]] (checksum: ce8e88f33d7db22f1e21a767e3256a00) + # [[[end]]] (sum: zo6I8z19si) # ^^^^^^^^ # If a check fails and points to this checksum line, it means you edited # the matrix directly instead of editing the Python code in the comment diff --git a/CHANGES.rst b/CHANGES.rst index ba9a58079..7d73620a7 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-9-0: + +Version 7.9.0 — 2025-06-11 +-------------------------- - Added a ``[run] core`` configuration setting to specify the measurement core, which was previously only available through the COVERAGE_CORE environment @@ -30,12 +34,12 @@ Unreleased - Fixed incorrect rendering of f-strings with doubled braces, closing `issue 1980`_. -- The C tracer core extension module now conforms to `PEP 489`_, closing `issue - 1977`_. Thanks, `Adam Turner `_. - - If the C tracer core can't be imported, a warning ("no-ctracer") is issued with the reason. +- The C tracer core extension module now conforms to `PEP 489`_, closing `issue + 1977`_. Thanks, `Adam Turner `_. + - Fixed a "ValueError: min() arg is an empty sequence" error caused by strange empty modules, found by `oss-fuzz`_. @@ -47,8 +51,6 @@ Unreleased .. _oss-fuzz: https://google.github.io/oss-fuzz/ -.. start-releases - .. _changes_7-8-2: Version 7.8.2 — 2025-05-23 diff --git a/README.rst b/README.rst index 79e0c5cb6..ba436e037 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 beta 1, including free-threading. +* Python 3.9 through 3.14 beta 2, 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 @@ -36,6 +36,7 @@ Documentation is on `Read the Docs`_. Code repository and issue tracker are on .. _GitHub: https://github.com/nedbat/coveragepy **New in 7.x:** +``[run] core`` setting; ``[run] source_dirs`` setting; ``Coverage.branch_stats()``; multi-line exclusion patterns; diff --git a/coverage/htmlfiles/style.css b/coverage/htmlfiles/style.css index 3cdaf05a3..e54e87aed 100644 --- a/coverage/htmlfiles/style.css +++ b/coverage/htmlfiles/style.css @@ -200,9 +200,9 @@ kbd { border: 1px solid black; border-color: #888 #333 #333 #888; padding: .1em #source p .t .key { font-weight: bold; line-height: 1px; } -#source p .t .str { color: #0451a5; } +#source p .t .str, #source p .t .fst { color: #0451a5; } -@media (prefers-color-scheme: dark) { #source p .t .str { color: #9cdcfe; } } +@media (prefers-color-scheme: dark) { #source p .t .str, #source p .t .fst { color: #9cdcfe; } } #source p.mis .t { border-left: 0.2em solid #ff0000; } diff --git a/coverage/version.py b/coverage/version.py index fb4a43fc6..e0f012a0e 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, 9, 0, "alpha", 0) -_dev = 1 +version_info = (7, 9, 0, "final", 0) +_dev = 0 def _make_version( diff --git a/doc/conf.py b/doc/conf.py index 09cc5e06e..04b18c429 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.8.2" +version = "7.9.0" # The full version, including alpha/beta/rc tags. -release = "7.8.2" +release = "7.9.0" # The date of release, in "monthname day, year" format. -release_date = "May 23, 2025" +release_date = "June 11, 2025" # @@@ end rst_epilog = f""" diff --git a/doc/index.rst b/doc/index.rst index dd4489b7c..5efa10570 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -18,7 +18,7 @@ supported on: .. PYVERSIONS -* Python 3.9 through 3.14 beta 1, including free-threading. +* Python 3.9 through 3.14 beta 2, including free-threading. * PyPy3 versions 3.9, 3.10, and 3.11. .. ifconfig:: prerelease diff --git a/tests/gold/html/styled/style.css b/tests/gold/html/styled/style.css index 3cdaf05a3..e54e87aed 100644 --- a/tests/gold/html/styled/style.css +++ b/tests/gold/html/styled/style.css @@ -200,9 +200,9 @@ kbd { border: 1px solid black; border-color: #888 #333 #333 #888; padding: .1em #source p .t .key { font-weight: bold; line-height: 1px; } -#source p .t .str { color: #0451a5; } +#source p .t .str, #source p .t .fst { color: #0451a5; } -@media (prefers-color-scheme: dark) { #source p .t .str { color: #9cdcfe; } } +@media (prefers-color-scheme: dark) { #source p .t .str, #source p .t .fst { color: #9cdcfe; } } #source p.mis .t { border-left: 0.2em solid #ff0000; } From 452d86ffa16cd0c1b729c7d206f59bc1010aee94 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Wed, 11 Jun 2025 18:41:54 -0400 Subject: [PATCH 26/26] docs: sample HTML for 7.9.0 --- doc/sample_html/class_index.html | 224 +- doc/sample_html/function_index.html | 1026 ++-- doc/sample_html/index.html | 47 +- doc/sample_html/status.json | 2 +- ..._cb_8e611ae1.css => style_cb_81f8c14c.css} | 4 +- .../z_7b071bdc2a35fa80___init___py.html | 10 +- .../z_7b071bdc2a35fa80___main___py.html | 10 +- .../z_7b071bdc2a35fa80_cogapp_py.html | 1642 ++--- .../z_7b071bdc2a35fa80_hashhandler_py.html | 274 + .../z_7b071bdc2a35fa80_makefiles_py.html | 14 +- .../z_7b071bdc2a35fa80_test_cogapp_py.html | 5302 +++++++++-------- .../z_7b071bdc2a35fa80_test_makefiles_py.html | 10 +- ...z_7b071bdc2a35fa80_test_whiteutils_py.html | 10 +- .../z_7b071bdc2a35fa80_utils_py.html | 112 +- .../z_7b071bdc2a35fa80_whiteutils_py.html | 10 +- 15 files changed, 4689 insertions(+), 4008 deletions(-) rename doc/sample_html/{style_cb_8e611ae1.css => style_cb_81f8c14c.css} (98%) create mode 100644 doc/sample_html/z_7b071bdc2a35fa80_hashhandler_py.html diff --git a/doc/sample_html/class_index.html b/doc/sample_html/class_index.html index 5563aaf11..5002ef892 100644 --- a/doc/sample_html/class_index.html +++ b/doc/sample_html/class_index.html @@ -4,14 +4,14 @@ Cog coverage - +

Cog coverage: - 38.58% + 37.79%

@@ -97,8 +97,8 @@

0.00% - cogapp/cogapp.py - CogError + cogapp/cogapp.py + CogError 3 0 0 @@ -107,8 +107,8 @@

100.00% - cogapp/cogapp.py - CogUsageError + cogapp/cogapp.py + CogUsageError 0 0 0 @@ -117,8 +117,8 @@

100.00% - cogapp/cogapp.py - CogInternalError + cogapp/cogapp.py + CogInternalError 0 0 0 @@ -127,8 +127,8 @@

100.00% - cogapp/cogapp.py - CogGeneratedError + cogapp/cogapp.py + CogGeneratedError 0 0 0 @@ -137,8 +137,8 @@

100.00% - cogapp/cogapp.py - CogUserException + cogapp/cogapp.py + CogUserException 0 0 0 @@ -147,8 +147,8 @@

100.00% - cogapp/cogapp.py - CogCheckFailed + cogapp/cogapp.py + CogCheckFailed 0 0 0 @@ -157,8 +157,8 @@

100.00% - cogapp/cogapp.py - CogGenerator + cogapp/cogapp.py + CogGenerator 58 6 0 @@ -167,34 +167,54 @@

88.46% - cogapp/cogapp.py - CogOptions - 80 - 58 + cogapp/cogapp.py + CogOptions + 85 + 62 1 - 44 + 48 0 - 17.74% + 17.29% - cogapp/cogapp.py - Cog + cogapp/cogapp.py + Cog 256 - 155 + 159 0 - 116 - 22 - 36.83% + 114 + 21 + 35.68% cogapp/cogapp.py (no class) - 86 + 88 9 0 8 2 - 86.17% + 86.46% + + + cogapp/hashhandler.py + HashHandler + 51 + 35 + 0 + 24 + 2 + 26.67% + + + cogapp/hashhandler.py + (no class) + 12 + 0 + 0 + 0 + 0 + 100.00% cogapp/makefiles.py @@ -207,8 +227,8 @@

11.11% - cogapp/test_cogapp.py - CogTestsInMemory + cogapp/test_cogapp.py + CogTestsInMemory 73 0 0 @@ -217,8 +237,8 @@

100.00% - cogapp/test_cogapp.py - CogOptionsTests + cogapp/test_cogapp.py + CogOptionsTests 31 31 0 @@ -227,8 +247,8 @@

0.00% - cogapp/test_cogapp.py - FileStructureTests + cogapp/test_cogapp.py + FileStructureTests 29 29 0 @@ -237,8 +257,8 @@

0.00% - cogapp/test_cogapp.py - CogErrorTests + cogapp/test_cogapp.py + CogErrorTests 11 11 0 @@ -247,8 +267,8 @@

0.00% - cogapp/test_cogapp.py - CogGeneratorGetCodeTests + cogapp/test_cogapp.py + CogGeneratorGetCodeTests 37 37 0 @@ -257,8 +277,8 @@

0.00% - cogapp/test_cogapp.py - TestCaseWithTempDir + cogapp/test_cogapp.py + TestCaseWithTempDir 19 19 0 @@ -267,18 +287,18 @@

0.00% - cogapp/test_cogapp.py - ArgumentHandlingTests - 43 - 43 + cogapp/test_cogapp.py + ArgumentHandlingTests + 47 + 47 0 0 0 - 0.00% + 0.00% - cogapp/test_cogapp.py - TestMain + cogapp/test_cogapp.py + TestMain 27 27 0 @@ -287,18 +307,18 @@

0.00% - cogapp/test_cogapp.py - TestFileHandling - 73 - 73 + cogapp/test_cogapp.py + TestFileHandling + 79 + 79 0 2 0 - 0.00% + 0.00% - cogapp/test_cogapp.py - CogTestLineEndings + cogapp/test_cogapp.py + CogTestLineEndings 12 12 0 @@ -307,8 +327,8 @@

0.00% - cogapp/test_cogapp.py - CogTestCharacterEncoding + cogapp/test_cogapp.py + CogTestCharacterEncoding 12 12 0 @@ -317,8 +337,8 @@

0.00% - cogapp/test_cogapp.py - TestCaseWithImports + cogapp/test_cogapp.py + TestCaseWithImports 6 6 0 @@ -327,8 +347,8 @@

0.00% - cogapp/test_cogapp.py - CogIncludeTests + cogapp/test_cogapp.py + CogIncludeTests 46 46 0 @@ -337,8 +357,8 @@

0.00% - cogapp/test_cogapp.py - CogTestsInFiles + cogapp/test_cogapp.py + CogTestsInFiles 122 122 2 @@ -347,18 +367,18 @@

0.00% - cogapp/test_cogapp.py - CheckTests - 40 - 40 + cogapp/test_cogapp.py + CheckTests + 56 + 56 0 6 0 - 0.00% + 0.00% - cogapp/test_cogapp.py - WritabilityTests + cogapp/test_cogapp.py + WritabilityTests 19 19 0 @@ -367,18 +387,18 @@

0.00% - cogapp/test_cogapp.py - ChecksumTests - 30 - 30 + cogapp/test_cogapp.py + ChecksumTests + 34 + 34 0 0 0 - 0.00% + 0.00% - cogapp/test_cogapp.py - CustomMarkerTests + cogapp/test_cogapp.py + CustomMarkerTests 12 12 0 @@ -387,8 +407,8 @@

0.00% - cogapp/test_cogapp.py - BlakeTests + cogapp/test_cogapp.py + BlakeTests 15 15 0 @@ -397,8 +417,8 @@

0.00% - cogapp/test_cogapp.py - ErrorCallTests + cogapp/test_cogapp.py + ErrorCallTests 12 12 0 @@ -406,15 +426,25 @@

0 0.00% + + cogapp/test_cogapp.py + HashHandlerTests + 10 + 10 + 0 + 0 + 0 + 0.00% + cogapp/test_cogapp.py (no class) - 185 + 196 2 0 2 1 - 98.40% + 98.48% cogapp/test_makefiles.py @@ -477,8 +507,8 @@

100.00% - cogapp/utils.py - Redirectable + cogapp/utils.py + Redirectable 8 3 0 @@ -487,8 +517,8 @@

58.33% - cogapp/utils.py - NumberedFileReader + cogapp/utils.py + NumberedFileReader 7 0 0 @@ -521,12 +551,12 @@

Total   - 1580 - 961 + 1701 + 1044 3 - 268 - 34 - 38.58% + 294 + 35 + 37.79% @@ -537,8 +567,8 @@

- coverage.py v7.8.2, - created at 2025-05-23 06:43 -0400 + coverage.py v7.9.0, + created at 2025-06-11 18:41 -0400

- 483 statements   - - + 490 statements   + + - +

« prev     ^ index     - » next + » next       - coverage.py v7.8.2, - created at 2025-05-23 06:43 -0400 + coverage.py v7.9.0, + created at 2025-06-11 18:41 -0400

diff --git a/doc/sample_html/z_7b071bdc2a35fa80_test_whiteutils_py.html b/doc/sample_html/z_7b071bdc2a35fa80_test_whiteutils_py.html index 6d13a8b8f..1cb8c354f 100644 --- a/doc/sample_html/z_7b071bdc2a35fa80_test_whiteutils_py.html +++ b/doc/sample_html/z_7b071bdc2a35fa80_test_whiteutils_py.html @@ -4,7 +4,7 @@ Coverage for cogapp/test_whiteutils.py: 26.47% - + @@ -66,8 +66,8 @@

^ index     » next       - coverage.py v7.8.2, - created at 2025-05-23 06:43 -0400 + coverage.py v7.9.0, + created at 2025-06-11 18:41 -0400