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/coverage.yml b/.github/workflows/coverage.yml index 4863574a6..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: @@ -49,6 +50,8 @@ jobs: filters: | run_coverage: - "**.py" + - "**.h" + - "**.c" - ".github/workflows/coverage.yml" - "tox.ini" - "requirements/*.pip" @@ -248,8 +251,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 @@ -265,9 +269,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' 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/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/.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: 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 diff --git a/.github/workflows/testsuite.yml b/.github/workflows/testsuite.yml index a27e41f99..7ed39beed 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" @@ -89,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: diff --git a/CHANGES.rst b/CHANGES.rst index e2264ab36..7d73620a7 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -22,6 +22,35 @@ upgrading your version of coverage.py. .. 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 + variable. Finishes `issue 1746`_. + +- Fixed incorrect rendering of f-strings with doubled braces, closing `issue + 1980`_. + +- 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`_. + +.. _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/ + + .. _changes_7-8-2: Version 7.8.2 — 2025-05-23 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 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/config.py b/coverage/config.py index 94831e070..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) @@ -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/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/coverage/core.py b/coverage/core.py index c46fd241e..86acbaed5 100644 --- a/coverage/core.py +++ b/coverage/core.py @@ -25,12 +25,16 @@ 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. 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 @@ -40,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 @@ -74,7 +79,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") @@ -87,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/coverage/ctracer/module.c b/coverage/ctracer/module.c index 33b50e64f..ee8f2599e 100644 --- a/coverage/ctracer/module.c +++ b/coverage/ctracer/module.c @@ -9,65 +9,71 @@ #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 -}; - +static BOOL module_inited = FALSE; -PyObject * -PyInit_tracer(void) +static int +tracer_exec(PyObject *mod) { - PyObject * mod = PyModule_Create(&moduledef); - if (mod == NULL) { - return NULL; + if (module_inited) { + return 0; } -#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; + module_inited = TRUE; + return 0; +} + +static PyModuleDef_Slot tracer_slots[] = { + {Py_mod_exec, tracer_exec}, +#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 = { + .m_base = PyModuleDef_HEAD_INIT, + .m_name = "coverage.tracer", + .m_doc = MODULE_DOC, + .m_size = 0, + .m_slots = tracer_slots, +}; + +PyMODINIT_FUNC +PyInit_tracer(void) +{ + return PyModuleDef_Init(&moduledef); } 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.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/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/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/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/coverage/phystokens.py b/coverage/phystokens.py index 8c29402ad..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 @@ -152,19 +155,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 diff --git a/coverage/version.py b/coverage/version.py index d6a2eb793..e0f012a0e 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, 2, "final", 0) +version_info = (7, 9, 0, "final", 0) _dev = 0 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/doc/cmd.rst b/doc/cmd.rst index 74113549b..eab2ca6a4 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. @@ -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, @@ -273,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 @@ -327,7 +333,7 @@ file: [coverage:run] disable_warnings = no-data-collected -.. [[[end]]] (checksum: 489285bcfa173b69a286f03fe13e4554) +.. [[[end]]] (sum: SJKFvPoXO2) .. _cmd_datafile: @@ -427,7 +433,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: @@ -476,7 +482,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. @@ -574,7 +580,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:: @@ -705,7 +711,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 @@ -782,7 +788,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. @@ -871,7 +877,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. @@ -917,7 +923,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] `. @@ -986,7 +992,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`. @@ -1028,7 +1034,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/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/config.rst b/doc/config.rst index 7a02d6a04..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 @@ -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 @@ -556,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/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/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/requirements.pip b/doc/requirements.pip index 535525f49..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 @@ -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/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