From 4f7a2079da969b355ce7976b1d3b34bbf003de75 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 22 Jun 2024 18:02:41 -0400 Subject: [PATCH 01/37] build: bump version --- CHANGES.rst | 6 ++++++ coverage/version.py | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 633f418c1..c2e2b1215 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. + + .. scriv-start-here .. _changes_7-5-4: diff --git a/coverage/version.py b/coverage/version.py index 77743953a..15be60ad2 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, 5, 4, "final", 0) -_dev = 0 +version_info = (7, 5, 5, "alpha", 0) +_dev = 1 def _make_version( From 8636249878ceca417f239c4eea8d616e54fe8d89 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 23 Jun 2024 08:06:31 -0400 Subject: [PATCH 02/37] benchmark: simplify the -k args for mypy --- benchmark/benchmark.py | 48 ++++++++++++++++-------------------------- 1 file changed, 18 insertions(+), 30 deletions(-) diff --git a/benchmark/benchmark.py b/benchmark/benchmark.py index d4649d265..c9748c5a4 100644 --- a/benchmark/benchmark.py +++ b/benchmark/benchmark.py @@ -553,45 +553,32 @@ def run_with_coverage(self, env: Env, cov_ver: Coverage) -> float: class ProjectMypy(ToxProject): git_url = "https://github.com/python/mypy" - # Slow test suites - CMDLINE = "PythonCmdline" - PEP561 = "PEP561Suite" - EVALUATION = "PythonEvaluation" - DAEMON = "testdaemon" - STUBGEN_CMD = "StubgenCmdLine" - STUBGEN_PY = "StubgenPythonSuite" - MYPYC_RUN = "TestRun" - MYPYC_RUN_MULTI = "TestRunMultiFile" - MYPYC_EXTERNAL = "TestExternal" - MYPYC_COMMAND_LINE = "TestCommandLine" - ERROR_STREAM = "ErrorStreamSuite" - - ALL_NON_FAST = ( - CMDLINE, - PEP561, - EVALUATION, - DAEMON, - STUBGEN_CMD, - STUBGEN_PY, - MYPYC_RUN, - MYPYC_RUN_MULTI, - MYPYC_EXTERNAL, - MYPYC_COMMAND_LINE, - ERROR_STREAM, - ) - - FAST = "pytest", "-k", f"\"not ({' or '.join(ALL_NON_FAST)})\"" + SLOW_TESTS = " or ".join([ + "PythonCmdline", + "PEP561Suite", + "PythonEvaluation", + "testdaemon", + "StubgenCmdLine", + "StubgenPythonSuite", + "TestRun", + "TestRunMultiFile", + "TestExternal", + "TestCommandLine", + "ErrorStreamSuite", + ]) + + FAST = f"-k 'not ({SLOW_TESTS})'" def prep_environment(self, env: Env) -> None: env.shell.run_command(f"{env.python} -m pip install -r test-requirements.txt") def run_no_coverage(self, env: Env) -> float: - env.shell.run_command(f"{env.python} -m {' '.join(self.FAST)} --no-cov") + env.shell.run_command(f"{env.python} -m pytest {self.FAST} --no-cov") return env.shell.last_duration def run_with_coverage(self, env: Env, cov_ver: Coverage) -> float: env.shell.run_command(f"{env.python} -m pip install {cov_ver.pip_args}") - env.shell.run_command(f"{env.python} -m {' '.join(self.FAST)} --cov") + env.shell.run_command(f"{env.python} -m pytest {self.FAST} --cov") duration = env.shell.last_duration report = env.shell.run_command(f"{env.python} -m coverage report --precision=6") print("Results:", report.splitlines()[-1]) @@ -957,6 +944,7 @@ def run(self, num_runs: int = 3) -> None: self.result_data[result_key].append(dur) run_data[result_key].append(dur) self.save_results() + # Summarize and collect the data. print("# Results") for proj in self.projects: From ec1acd985bf47258e9d209db037fbf6373270b1c Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 23 Jun 2024 09:41:47 -0400 Subject: [PATCH 03/37] feat: secret COVERAGE_FORCE_CONFIG environment variable This is for our use in our benchmarks to force third-party test suites to run the way we want. For example, we can use it to force branch=True or branch=False. --- coverage/config.py | 5 +++++ tests/test_config.py | 18 ++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/coverage/config.py b/coverage/config.py index 7aa2471bd..5468ca490 100644 --- a/coverage/config.py +++ b/coverage/config.py @@ -612,6 +612,11 @@ def read_coverage_config( # 4) from constructor arguments: config.from_args(**kwargs) + # 5) for our benchmark, force settings using a secret environment variable: + force_file = os.getenv("COVERAGE_FORCE_CONFIG") + if force_file: + config.from_file(force_file, warn, our_file=True) + # Once all the config has been collected, there's a little post-processing # to do. config.post_process() diff --git a/tests/test_config.py b/tests/test_config.py index e2ab90e88..062c69324 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -167,6 +167,24 @@ def test_missing_rcfile_from_environment(self) -> None: with pytest.raises(ConfigError, match=msg): coverage.Coverage() + @pytest.mark.parametrize("force", [False, True]) + def test_force_environment(self, force: bool) -> None: + self.make_file(".coveragerc", """\ + [run] + debug = dataio, pids + """) + self.make_file("force.ini", """\ + [run] + debug = callers, fooey + """) + if force: + self.set_environ("COVERAGE_FORCE_CONFIG", "force.ini") + cov = coverage.Coverage() + if force: + assert cov.config.debug == ["callers", "fooey"] + else: + assert cov.config.debug == ["dataio", "pids"] + @pytest.mark.parametrize("bad_config, msg", [ ("[run]\ntimid = maybe?\n", r"maybe[?]"), ("timid = 1\n", r"no section headers"), From 2f1b28fb89278be5c3fcd3f0f27a15900e77720d Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 23 Jun 2024 09:44:51 -0400 Subject: [PATCH 04/37] benchmark: if an exception happens, put it in the log file --- benchmark/benchmark.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/benchmark/benchmark.py b/benchmark/benchmark.py index c9748c5a4..7a68b2087 100644 --- a/benchmark/benchmark.py +++ b/benchmark/benchmark.py @@ -16,6 +16,7 @@ import subprocess import sys import time +import traceback from pathlib import Path @@ -937,6 +938,7 @@ def run(self, num_runs: int = 3) -> None: dur = proj.run_with_coverage(env, cov_ver) except Exception as exc: print(f"!!! {exc = }") + traceback.print_exc(file=env.shell.foutput) dur = float("NaN") print(f"Tests took {dur:.3f}s") if result_key not in self.result_data: From f572e3aadfb80b844a0c3b21a6a0df70e05c2a37 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 23 Jun 2024 09:45:28 -0400 Subject: [PATCH 05/37] benchmark: CoverageSource defaults to the current working tree --- benchmark/benchmark.py | 2 +- benchmark/run.py | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/benchmark/benchmark.py b/benchmark/benchmark.py index 7a68b2087..cd53de1ad 100644 --- a/benchmark/benchmark.py +++ b/benchmark/benchmark.py @@ -808,7 +808,7 @@ class CoverageSource(Coverage): def __init__( self, - directory_name: str, + directory_name: str = "..", slug: str = "source", tweaks: TweaksType = None, env_vars: Env_VarsType = None, diff --git a/benchmark/run.py b/benchmark/run.py index e1ce85c6f..8c5f50c26 100644 --- a/benchmark/run.py +++ b/benchmark/run.py @@ -136,7 +136,7 @@ ], cov_versions=[ Coverage("pip", "coverage"), - CoverageSource("../..", "latest"), + CoverageSource(slug="latest"), ], projects=[ ProjectMashumaro(), @@ -160,7 +160,6 @@ Coverage("732", "coverage==7.3.2"), CoverageSource( slug="sysmon", - directory_name="../..", env_vars={"COVERAGE_CORE": "sysmon"}, ), ], @@ -187,7 +186,6 @@ Coverage("732", "coverage==7.3.2"), CoverageSource( slug="sysmon", - directory_name="/Users/nbatchelder/coverage/trunk", env_vars={"COVERAGE_CORE": "sysmon"}, ), ], From b8739dd5ef58beb671e0f957ff3cecbe3fe1f8e1 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 23 Jun 2024 16:17:16 -0400 Subject: [PATCH 06/37] benchmark: force mypy to measure line coverage --- benchmark/benchmark.py | 12 ++++++++---- benchmark/run.py | 8 ++++---- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/benchmark/benchmark.py b/benchmark/benchmark.py index cd53de1ad..9ec932e5d 100644 --- a/benchmark/benchmark.py +++ b/benchmark/benchmark.py @@ -280,7 +280,8 @@ class ToxProject(ProjectToTest): env_vars: Env_VarsType = { **(ProjectToTest.env_vars or {}), # Allow some environment variables into the tox execution. - "TOX_OVERRIDE": "testenv.pass_env+=COVERAGE_DEBUG,COVERAGE_CORE", + "TOX_OVERRIDE": "testenv.pass_env+=COVERAGE_DEBUG,COVERAGE_CORE,COVERAGE_FORCE_CONFIG", + "COVERAGE_DEBUG": "config,sys", } def prep_environment(self, env: Env) -> None: @@ -579,9 +580,12 @@ def run_no_coverage(self, env: Env) -> float: def run_with_coverage(self, env: Env, cov_ver: Coverage) -> float: env.shell.run_command(f"{env.python} -m pip install {cov_ver.pip_args}") - env.shell.run_command(f"{env.python} -m pytest {self.FAST} --cov") - duration = env.shell.last_duration - report = env.shell.run_command(f"{env.python} -m coverage report --precision=6") + pforce = Path("force.ini") + pforce.write_text("[run]\nbranch=false\n") + with env.shell.set_env({"COVERAGE_FORCE_CONFIG": str(pforce.resolve())}): + env.shell.run_command(f"{env.python} -m pytest {self.FAST} --cov") + duration = env.shell.last_duration + report = env.shell.run_command(f"{env.python} -m coverage report --precision=6") print("Results:", report.splitlines()[-1]) return duration diff --git a/benchmark/run.py b/benchmark/run.py index 8c5f50c26..2c4263f2c 100644 --- a/benchmark/run.py +++ b/benchmark/run.py @@ -104,8 +104,8 @@ ], cov_versions=[ NoCoverage("nocov"), - Coverage("753", "coverage==7.5.3"), - Coverage("sysmon", "coverage==7.5.3", env_vars={"COVERAGE_CORE": "sysmon"}), + CoverageSource(slug="ctrace", env_vars={"COVERAGE_CORE": "ctrace"}), + CoverageSource(slug="sysmon", env_vars={"COVERAGE_CORE": "sysmon"}), ], projects=[ # ProjectSphinx(), # Works, slow @@ -115,14 +115,14 @@ # ProjectDulwich(), # Works # ProjectBlack(), # Works, slow # ProjectMpmath(), # Works, slow - # ProjectMypy(), # Works, slow + ProjectMypy(), # Works, slow # ProjectHtml5lib(), # Works # ProjectUrllib3(), # Works ], rows=["pyver", "proj"], column="cov", ratios=[ - (f"753%", "753", "nocov"), + (f"ctrace%", "ctrace", "nocov"), (f"sysmon%", "sysmon", "nocov"), ], load=True, From 627227a60c0565d6fd2d886860fdafbf51112c46 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Jun 2024 11:45:08 -0400 Subject: [PATCH 07/37] build(deps): bump github/codeql-action (#1800) Bumps [github/codeql-action](https://github.com/github/codeql-action) from 412ab5c4176178930892df540237c587c71786c9 to 23acc5c183826b7a8a97bce3cecc52db901f8251. - [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/412ab5c4176178930892df540237c587c71786c9...23acc5c183826b7a8a97bce3cecc52db901f8251) --- updated-dependencies: - dependency-name: github/codeql-action dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codeql-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 742ea7af7..a9eb4b915 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -49,7 +49,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@412ab5c4176178930892df540237c587c71786c9 # v3 + uses: github/codeql-action/init@23acc5c183826b7a8a97bce3cecc52db901f8251 # v3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -60,7 +60,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@412ab5c4176178930892df540237c587c71786c9 # v3 + uses: github/codeql-action/autobuild@23acc5c183826b7a8a97bce3cecc52db901f8251 # v3 # â„šī¸ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -74,4 +74,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@412ab5c4176178930892df540237c587c71786c9 # v3 + uses: github/codeql-action/analyze@23acc5c183826b7a8a97bce3cecc52db901f8251 # v3 From 4cee9052e58bf992b14b48fab71887a9649788c1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Jun 2024 16:00:47 +0000 Subject: [PATCH 08/37] build(deps): bump re-actors/alls-green (#1802) --- .github/workflows/testsuite.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/testsuite.yml b/.github/workflows/testsuite.yml index 0f9ab8296..6bccad215 100644 --- a/.github/workflows/testsuite.yml +++ b/.github/workflows/testsuite.yml @@ -123,6 +123,6 @@ jobs: runs-on: ubuntu-latest steps: - name: Decide whether the needed jobs succeeded or failed - uses: re-actors/alls-green@afee1c1eac2a506084c274e9c02c8e0687b48d9e # v1.2.2 + uses: re-actors/alls-green@05ac9388f0aebcb5727afa17fcccfecd6f8ec5fe # v1.2.2 with: jobs: ${{ toJSON(needs) }} From fa36ebddb92853c0b154f415277fe79566b8fc2b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Jun 2024 16:17:20 +0000 Subject: [PATCH 09/37] build(deps): bump pypa/gh-action-pypi-publish (#1801) --- .github/workflows/publish.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 090adeef2..360978688 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -70,7 +70,7 @@ jobs: ls -1 dist | wc -l - name: "Publish dists to Test PyPI" - uses: pypa/gh-action-pypi-publish@4bb033805d9e19112d8c697528791ff53f6c2f74 # v1.9.0 + uses: pypa/gh-action-pypi-publish@ec4db0b4ddc65acdf4bff5fa45ac92d78b56bdf0 # v1.9.0 with: repository-url: https://test.pypi.org/legacy/ @@ -103,4 +103,4 @@ jobs: ls -1 dist | wc -l - name: "Publish dists to PyPI" - uses: pypa/gh-action-pypi-publish@4bb033805d9e19112d8c697528791ff53f6c2f74 # v1.9.0 + uses: pypa/gh-action-pypi-publish@ec4db0b4ddc65acdf4bff5fa45ac92d78b56bdf0 # v1.9.0 From 40c5a4a2313410b8631ca4e420f04290dfd72438 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Mon, 24 Jun 2024 15:49:51 -0400 Subject: [PATCH 10/37] build: make dependabot talk like we do --- .github/dependabot.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 1cdec3b21..3408e4455 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -9,3 +9,5 @@ updates: schedule: # Check for updates to GitHub Actions every weekday interval: "daily" + commit-message: + prefix: "chore" From 69a034f59c9812a0ed6b2831981b5417f5ecd9d5 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Mon, 24 Jun 2024 21:52:58 -0400 Subject: [PATCH 11/37] build: generate attestations --- .github/workflows/publish.yml | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 360978688..386b7284f 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -45,7 +45,8 @@ jobs: name: "Publish to Test PyPI" if: ${{ github.event.action == 'publish-testpypi' }} permissions: - id-token: write # Needed for trusted publishing to PyPI. + id-token: write + attestations: write runs-on: "ubuntu-latest" environment: name: "testpypi" @@ -69,6 +70,11 @@ jobs: echo "Number of dists:" ls -1 dist | wc -l + - name: "Generate attestations" + uses: actions/attest-build-provenance@bdd51370e0416ac948727f861e03c2f05d32d78e # v1.3.2 + with: + subject-path: "dist/*" + - name: "Publish dists to Test PyPI" uses: pypa/gh-action-pypi-publish@ec4db0b4ddc65acdf4bff5fa45ac92d78b56bdf0 # v1.9.0 with: @@ -78,7 +84,8 @@ jobs: name: "Publish to PyPI" if: ${{ github.event.action == 'publish-pypi' }} permissions: - id-token: write # Needed for trusted publishing to PyPI. + id-token: write + attestations: write runs-on: "ubuntu-latest" environment: name: "pypi" @@ -102,5 +109,10 @@ jobs: echo "Number of dists:" ls -1 dist | wc -l + - name: "Generate attestations" + uses: actions/attest-build-provenance@bdd51370e0416ac948727f861e03c2f05d32d78e # v1.3.2 + with: + subject-path: "dist/*" + - name: "Publish dists to PyPI" uses: pypa/gh-action-pypi-publish@ec4db0b4ddc65acdf4bff5fa45ac92d78b56bdf0 # v1.9.0 From c654ec06e05467cfa72e7f763ab2bf2a833d5c01 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 25 Jun 2024 06:36:52 -0400 Subject: [PATCH 12/37] build: kit_upload -> pypi_upload --- .github/workflows/publish.yml | 5 +++-- Makefile | 4 ++-- howto.txt | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 386b7284f..c76e43e3f 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -5,9 +5,10 @@ name: "Publish" on: repository_dispatch: + # Triggered with `make` targets: types: - - publish-testpypi - - publish-pypi + - publish-testpypi # `make test_upload` + - publish-pypi # `make pypi_upload` defaults: run: diff --git a/Makefile b/Makefile index ac2433fdc..9a58205a9 100644 --- a/Makefile +++ b/Makefile @@ -177,7 +177,7 @@ sample_html_beta: _sample_cog_html ## Generate sample HTML report for a beta rel ##@ Kitting: making releases .PHONY: edit_for_release cheats relbranch relcommit1 relcommit2 -.PHONY: kit kit_upload test_upload kit_local build_kits +.PHONY: kit pypi_upload test_upload kit_local build_kits .PHONY: tag bump_version REPO_OWNER = nedbat/coveragepy @@ -201,7 +201,7 @@ relcommit2: #: Commit the latest sample HTML report (see howto.txt). kit: ## Make the source distribution. python -m build -kit_upload: ## Upload the built distributions to PyPI. +pypi_upload: ## Upload the built distributions to PyPI. python ci/trigger_action.py $(REPO_OWNER) publish-pypi test_upload: ## Upload the distributions to PyPI's testing server. diff --git a/howto.txt b/howto.txt index e540f6355..6cba5ba98 100644 --- a/howto.txt +++ b/howto.txt @@ -57,7 +57,7 @@ $ make test_upload - you'll need to approve the action - upload kits: - $ make kit_upload + $ make pypi_upload - you'll need to approve the action - Tag the tree $ make tag From da5f4c994261f5ed6180c1a5244ce46516f1ac77 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 30 Jun 2024 17:01:11 -0400 Subject: [PATCH 13/37] docs: a small tweak to how we label the tabs for sample configs --- doc/cmd.rst | 4 ++-- doc/cog_helpers.py | 4 ++-- doc/config.rst | 8 ++++---- doc/contexts.rst | 4 ++-- doc/excluding.rst | 8 ++++---- doc/plugins.rst | 8 ++++---- doc/source.rst | 4 ++-- 7 files changed, 20 insertions(+), 20 deletions(-) diff --git a/doc/cmd.rst b/doc/cmd.rst index 439927d9c..e2f717882 100644 --- a/doc/cmd.rst +++ b/doc/cmd.rst @@ -310,12 +310,12 @@ collected," add this to your configuration file: disable_warnings = ["no-data-collected"] .. code-tab:: ini - :caption: setup.cfg, tox.ini + :caption: setup.cfg or tox.ini [coverage:run] disable_warnings = no-data-collected -.. [[[end]]] (checksum: 66c0c28e863c2a44218190a8a6a3f707) +.. [[[end]]] (checksum: 489285bcfa173b69a286f03fe13e4554) .. _cmd_datafile: diff --git a/doc/cog_helpers.py b/doc/cog_helpers.py index 56e4630a9..8cdb65f09 100644 --- a/doc/cog_helpers.py +++ b/doc/cog_helpers.py @@ -73,7 +73,7 @@ def show_configs(ini, toml): `ini` is the ini-file syntax, `toml` is the equivalent TOML syntax. The equivalence is checked for accuracy, and the process fails if there's - a mismtach. + a mismatch. A three-tabbed box will be produced. """ @@ -89,7 +89,7 @@ def show_configs(ini, toml): for name, syntax, text in [ (".coveragerc", "ini", ini), ("pyproject.toml", "toml", toml), - ("setup.cfg, tox.ini", "ini", ini2), + ("setup.cfg or tox.ini", "ini", ini2), ]: print(f" .. code-tab:: {syntax}") print(f" :caption: {name}") diff --git a/doc/config.rst b/doc/config.rst index 858f4bba0..62c051c5d 100644 --- a/doc/config.rst +++ b/doc/config.rst @@ -229,7 +229,7 @@ Here's a sample configuration file, in each syntax: directory = "coverage_html_report" .. code-tab:: ini - :caption: setup.cfg, tox.ini + :caption: setup.cfg or tox.ini [coverage:run] branch = True @@ -257,7 +257,7 @@ Here's a sample configuration file, in each syntax: [coverage:html] directory = coverage_html_report -.. [[[end]]] (checksum: 75c6c0c2ee170424cc1c18710e2b4919) +.. [[[end]]] (checksum: 1d4d59eb69af44aacb77c9ebad869b65) The specific configuration settings are described below. Many sections and @@ -535,7 +535,7 @@ equivalent when combining data from different machines: ] .. code-tab:: ini - :caption: setup.cfg, tox.ini + :caption: setup.cfg or tox.ini [coverage:paths] source = @@ -543,7 +543,7 @@ equivalent when combining data from different machines: /jenkins/build/*/src c:\myproj\src -.. [[[end]]] (checksum: cf06ac36436db0c87be15a85223900d0) +.. [[[end]]] (checksum: a074a5f121a23135dcb6733bca3e20bd) 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 a277dd8e8..75080f0cb 100644 --- a/doc/contexts.rst +++ b/doc/contexts.rst @@ -103,12 +103,12 @@ The ``[run] dynamic_context`` setting has only one option now. Set it to dynamic_context = "test_function" .. code-tab:: ini - :caption: setup.cfg, tox.ini + :caption: setup.cfg or tox.ini [coverage:run] dynamic_context = test_function -.. [[[end]]] (checksum: 5c5d120ee876e5fe26e573e1a5e8551d) +.. [[[end]]] (checksum: 7594c36231f0ef52b554aad8c835ccf4) 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/excluding.rst b/doc/excluding.rst index a5766676e..80f0c79dd 100644 --- a/doc/excluding.rst +++ b/doc/excluding.rst @@ -123,13 +123,13 @@ all of them by adding a regex to the exclusion list: ] .. code-tab:: ini - :caption: setup.cfg, tox.ini + :caption: setup.cfg or tox.ini [coverage:report] exclude_also = def __repr__ -.. [[[end]]] (checksum: adc6406467518c89a5a6fe2c4b999416) +.. [[[end]]] (checksum: e3194120285bcbac38a92b109edaa20c) For example, here's a list of exclusions I've used: @@ -203,7 +203,7 @@ For example, here's a list of exclusions I've used: ] .. code-tab:: ini - :caption: setup.cfg, tox.ini + :caption: setup.cfg or tox.ini [coverage:report] exclude_also = @@ -218,7 +218,7 @@ For example, here's a list of exclusions I've used: class .*\bProtocol\): @(abc\.)?abstractmethod -.. [[[end]]] (checksum: ef1947821b8224c4f02d27f9514e5c5e) +.. [[[end]]] (checksum: 91f09828a1e6d0e92543e14a8ea3ba39) 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 diff --git a/doc/plugins.rst b/doc/plugins.rst index 7be4e4b6b..147fb1db4 100644 --- a/doc/plugins.rst +++ b/doc/plugins.rst @@ -78,13 +78,13 @@ a coverage.py plug-in called ``something.plugin``. plugins = [ "something.plugin" ] .. code-tab:: ini - :caption: setup.cfg, tox.ini + :caption: setup.cfg or tox.ini [coverage:run] plugins = something.plugin - .. [[[end]]] (checksum: 788b15abb3c53370ccae3d9348e65385) + .. [[[end]]] (checksum: 6e866323d4bc319d42e3199b08615111) #. 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: @@ -121,13 +121,13 @@ a coverage.py plug-in called ``something.plugin``. option2 = "abc.foo" .. code-tab:: ini - :caption: setup.cfg, tox.ini + :caption: setup.cfg or tox.ini [coverage:something.plugin] option1 = True option2 = abc.foo - .. [[[end]]] (checksum: 71aa2ad856e03d228758fd5026fd3a52) + .. [[[end]]] (checksum: b690115dbe7f6c7806567e009b5715c4) 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 3357ae73d..bcf7a8af9 100644 --- a/doc/source.rst +++ b/doc/source.rst @@ -128,7 +128,7 @@ current directory: ] .. code-tab:: ini - :caption: setup.cfg, tox.ini + :caption: setup.cfg or tox.ini [coverage:run] omit = @@ -139,7 +139,7 @@ current directory: # omit this single file utils/tirefire.py -.. [[[end]]] (checksum: 9fa764509b4c484ea613298a20d4b577) +.. [[[end]]] (checksum: 84ad2743cc0c7a077770e50fcedab29d) The ``source``, ``include``, and ``omit`` values all work together to determine the source that will be measured. From 7272390e892f44b0eb68c13a6c2ff04e681b1716 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 30 Jun 2024 17:03:36 -0400 Subject: [PATCH 14/37] docs: Python 3.13.0b3 --- CHANGES.rst | 2 +- README.rst | 2 +- doc/index.rst | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index c2e2b1215..070098aa5 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -23,7 +23,7 @@ upgrading your version of coverage.py. Unreleased ---------- -Nothing yet. +- Python 3.13.0b3 is supported. .. scriv-start-here diff --git a/README.rst b/README.rst index a48d35133..a9c8eb6a7 100644 --- a/README.rst +++ b/README.rst @@ -25,7 +25,7 @@ Coverage.py runs on these versions of Python: .. PYVERSIONS -* Python 3.8 through 3.12, and 3.13.0b2. +* Python 3.8 through 3.12, and 3.13.0b3. * PyPy3 versions 3.8 through 3.10. Documentation is on `Read the Docs`_. Code repository and issue tracker are on diff --git a/doc/index.rst b/doc/index.rst index 9dd939690..748b963af 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -18,7 +18,7 @@ supported on: .. PYVERSIONS -* Python 3.8 through 3.12, and 3.13.0b2. +* Python 3.8 through 3.12, and 3.13.0b3. * PyPy3 versions 3.8 through 3.10. .. ifconfig:: prerelease From c97272ed5ab059bea8f449c15bac0c0ca1aac0a1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Jul 2024 08:35:09 -0400 Subject: [PATCH 15/37] chore: bump github/codeql-action from 3.25.10 to 3.25.11 (#1805) Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.25.10 to 3.25.11. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/23acc5c183826b7a8a97bce3cecc52db901f8251...b611370bb5703a7efb587f9d136a52ea24c5c38c) --- updated-dependencies: - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codeql-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index a9eb4b915..0934c2528 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -49,7 +49,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@23acc5c183826b7a8a97bce3cecc52db901f8251 # v3 + uses: github/codeql-action/init@b611370bb5703a7efb587f9d136a52ea24c5c38c # v3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -60,7 +60,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@23acc5c183826b7a8a97bce3cecc52db901f8251 # v3 + uses: github/codeql-action/autobuild@b611370bb5703a7efb587f9d136a52ea24c5c38c # v3 # â„šī¸ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -74,4 +74,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@23acc5c183826b7a8a97bce3cecc52db901f8251 # v3 + uses: github/codeql-action/analyze@b611370bb5703a7efb587f9d136a52ea24c5c38c # v3 From de3b92dbfa7b9eb0f5b43b480732201ab7a95ea6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 3 Jul 2024 14:07:21 -0400 Subject: [PATCH 16/37] chore: bump docker/setup-qemu-action from 3.0.0 to 3.1.0 (#1808) Bumps [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) from 3.0.0 to 3.1.0. - [Release notes](https://github.com/docker/setup-qemu-action/releases) - [Commits](https://github.com/docker/setup-qemu-action/compare/68827325e0b33c7199eb31dd4e31fbe9023e06e3...5927c834f5b4fdf503fca6f4c7eccda82949e1ee) --- updated-dependencies: - dependency-name: docker/setup-qemu-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/kit.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/kit.yml b/.github/workflows/kit.yml index 93e744625..0351c89be 100644 --- a/.github/workflows/kit.yml +++ b/.github/workflows/kit.yml @@ -143,7 +143,7 @@ jobs: steps: - name: "Setup QEMU" if: matrix.os == 'ubuntu' - uses: docker/setup-qemu-action@68827325e0b33c7199eb31dd4e31fbe9023e06e3 # v3.0.0 + uses: docker/setup-qemu-action@5927c834f5b4fdf503fca6f4c7eccda82949e1ee # v3.1.0 with: platforms: arm64 From 21d276abeaceea899edfdfb92484f83a7b9a0579 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Wed, 3 Jul 2024 18:58:44 -0400 Subject: [PATCH 17/37] test(coverage): self.stop() will never be covered; nested coverage --- coverage/control.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coverage/control.py b/coverage/control.py index 4e1d359ed..583fed946 100644 --- a/coverage/control.py +++ b/coverage/control.py @@ -675,7 +675,7 @@ def collect(self) -> Iterator[None]: try: yield finally: - self.stop() + self.stop() # pragma: nested def _atexit(self, event: str = "atexit") -> None: """Clean up on process shutdown.""" From fd4f83c31420876ecebe5b212484333a4fd049c2 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Wed, 3 Jul 2024 19:07:25 -0400 Subject: [PATCH 18/37] test(coverage): this plugin is used ad-hoc during debugging, it won't be covered --- tests/select_plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/select_plugin.py b/tests/select_plugin.py index 9ca48ff35..4239608fc 100644 --- a/tests/select_plugin.py +++ b/tests/select_plugin.py @@ -25,7 +25,7 @@ def pytest_addoption(parser): ) -def pytest_collection_modifyitems(config, items): +def pytest_collection_modifyitems(config, items): # pragma: debugging """Run an external command to get a list of tests to run.""" select_cmd = config.getoption("--select-cmd") if select_cmd: From 1fe897d740ebc67bd1f6493bcc889846f16abf10 Mon Sep 17 00:00:00 2001 From: devdanzin <74280297+devdanzin@users.noreply.github.com> Date: Thu, 4 Jul 2024 17:40:49 -0300 Subject: [PATCH 19/37] feat: multiline exclusion regexes (fix #996) (#1807) * Support excluding multiline regexes, O(N) algorithm by nedbat. * Fix test_parser.py. * Actually fix tests and mypy check. * Format multiline exclusion tests. * Apply suggestions from code review by @nedbat. Co-authored-by: Ned Batchelder * Improve and add new tests to test_parser.py. * Skip test_multiline_exclusion_block2 if Python version < 3.10. * Add tests for exclusion of a whole module or from a marker until the end of the file. * tweak the regexes in the tests * test: add one more test for the specific #996 use * docs: explain multi-line exclusion regexes * build: the next version will be 7.6.0 * better: no whitespace in regexes --------- Co-authored-by: Ned Batchelder --- CHANGES.rst | 14 ++++ coverage/parser.py | 20 +++-- coverage/version.py | 2 +- doc/excluding.rst | 96 ++++++++++++++++++++- tests/test_parser.py | 195 +++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 317 insertions(+), 10 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 070098aa5..f6fb6f5b2 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -23,8 +23,22 @@ upgrading your version of coverage.py. Unreleased ---------- +- Exclusion patterns can now be multi-line, thanks to `Daniel Diniz `_. This enables many interesting exclusion use-cases, including those + requested in issues `118 `_ (entire files), `996 + `_ (multiple lines only when appearing together), `1741 + `_ (remainder of a function), and `1803 `_ + (arbitrary sequence of marked lines). See the :ref:`multi_line_exclude` + section of the docs for more details and examples. + - Python 3.13.0b3 is supported. +.. _issue 118: https://github.com/nedbat/coveragepy/issues/118 +.. _issue 996: https://github.com/nedbat/coveragepy/issues/996 +.. _issue 1741: https://github.com/nedbat/coveragepy/issues/1741 +.. _issue 1803: https://github.com/nedbat/coveragepy/issues/1803 + +.. _pull 1807: https://github.com/nedbat/coveragepy/pull/1807 .. scriv-start-here diff --git a/coverage/parser.py b/coverage/parser.py index 00ccbf10a..19267a718 100644 --- a/coverage/parser.py +++ b/coverage/parser.py @@ -105,14 +105,22 @@ def lines_matching(self, regex: str) -> set[TLineNo]: """Find the lines matching a regex. Returns a set of line numbers, the lines that contain a match for - `regex`. The entire line needn't match, just a part of it. + `regex`. The entire line needn't match, just a part of it. + Handles multiline regex patterns. """ - regex_c = re.compile(regex) - matches = set() - for i, ltext in enumerate(self.text.split("\n"), start=1): - if regex_c.search(ltext): - matches.add(self._multiline.get(i, i)) + regex_c = re.compile(regex, re.MULTILINE) + matches: set[TLineNo] = set() + + last_start = 0 + last_start_line = 0 + for match in regex_c.finditer(self.text): + start, end = match.span() + start_line = last_start_line + self.text.count('\n', last_start, start) + end_line = last_start_line + self.text.count('\n', last_start, end) + matches.update(self._multiline.get(i, i) for i in range(start_line + 1, end_line + 2)) + last_start = start + last_start_line = start_line return matches def _raw_parse(self) -> None: diff --git a/coverage/version.py b/coverage/version.py index 15be60ad2..fae238809 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, 5, 5, "alpha", 0) +version_info = (7, 6, 0, "alpha", 0) _dev = 1 diff --git a/doc/excluding.rst b/doc/excluding.rst index 80f0c79dd..3ade282fc 100644 --- a/doc/excluding.rst +++ b/doc/excluding.rst @@ -73,13 +73,17 @@ line, so it isn't considered a branch at all. Advanced exclusion ------------------ -Coverage.py identifies exclusions by matching lines against a list of regular -expressions. Using :ref:`configuration files ` or the coverage +Coverage.py identifies exclusions by matching source code against a list of +regular expressions. Using :ref:`configuration files ` or the coverage :ref:`API `, you can add to that list. This is useful if you have often-used constructs to exclude that can be matched with a regex. You can exclude them all at once without littering your code with exclusion pragmas. -If the matched line introduces a block, the entire block is excluded from +Before coverage.py 7.6.0, the regexes were matched against single lines of your +source code. Now they can be multi-line regexes that find matches across +lines. See :ref:`multi_line_exclude`. + +If a matched line introduces a block, the entire block is excluded from reporting. Matching a ``def`` line or decorator line will exclude an entire function. @@ -232,6 +236,92 @@ A similar pragma, "no branch", can be used to tailor branch coverage measurement. See :ref:`branch` for details. +.. _multi_line_exclude: + +Multi-line exclusion regexes +---------------------------- + +.. versionadded:: 7.6.0 + +Exclusion regexes can match multi-line regions. All of the lines in a matched +region will be excluded. If part of the region introduces a block, the entire +block is excluded even if part of it is outside the matched region. + +When writing regexes to match multiple lines, remember that ``"."`` won't match +a newline character, but ``"\n"`` or ``"(?s:.)"`` will. Using the ``"(?s)"`` +flag in your regex will also make dot match a newline. + +Here are some examples: + +.. [[[cog + show_configs( + ini=r""" + [report] + exclude_also = + ; Exclude an except clause of a specific form: + except ValueError:\n\s*assume\(False\) + ; A pragma comment that excludes an entire file: + (?s)\A.*# pragma: exclude file.*\Z + """, + toml=r""" + [tool.coverage.report] + exclude_also = [ + # Exclude an except clause of a specific form: + "except ValueError:\\n\\s*assume\\(False\\)", + # A pragma comment that excludes an entire file: + "(?s)\\A.*# pragma: exclude file.*\\Z", + ] + """, + ) +.. ]]] + +.. tabs:: + + .. code-tab:: ini + :caption: .coveragerc + + [report] + exclude_also = + ; Exclude an except clause of a specific form: + except ValueError:\n\s*assume\(False\) + ; A pragma comment that excludes an entire file: + (?s)\A.*# pragma: exclude file.*\Z + + .. code-tab:: toml + :caption: pyproject.toml + + [tool.coverage.report] + exclude_also = [ + # Exclude an except clause of a specific form: + "except ValueError:\\n\\s*assume\\(False\\)", + # A pragma comment that excludes an entire file: + "(?s)\\A.*# pragma: exclude file.*\\Z", + ] + + .. code-tab:: ini + :caption: setup.cfg or tox.ini + + [coverage:report] + exclude_also = + ; Exclude an except clause of a specific form: + except ValueError:\n\s*assume\(False\) + ; A pragma comment that excludes an entire file: + (?s)\A.*# pragma: exclude file.*\Z + +.. [[[end]]] (checksum: 8892a4efef9da67fb0080d15811e1c19) + +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 +the regex uses ``"\n\s*"`` to match the newline and the indentation of the +second line. Without these, the regex won't match. + +The second regex matches the entire text of a file containing the comment ``# +pragma: exclude file``. This lets you exclude files from coverage measurement +with an internal comment instead of naming them in a settings file. This regex +uses the ``"(?s)"`` regex flag to let a dot match any character including a +newline. + + Excluding source files ---------------------- diff --git a/tests/test_parser.py b/tests/test_parser.py index e9ad91a05..f5891e127 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -781,6 +781,201 @@ def function() -> int: assert parser.raw_statements == {1, 3, 4, 5, 6, 8, 9} assert parser.statements == {1, 8, 9} + def test_multiline_exclusion_single_line(self) -> None: + regex = r"print\('.*'\)" + parser = self.parse_text("""\ + def foo(): + print('Hello, world!') + """, regex) + assert parser.lines_matching(regex) == {2} + assert parser.raw_statements == {1, 2} + assert parser.statements == {1} + + def test_multiline_exclusion_suite(self) -> None: + # A multi-line exclusion that matches a colon line still excludes the entire block. + regex = r"if T:\n\s+print\('Hello, world!'\)" + parser = self.parse_text("""\ + def foo(): + if T: + print('Hello, world!') + print('This is a multiline regex test.') + a = 5 + """, regex) + assert parser.lines_matching(regex) == {2, 3} + assert parser.raw_statements == {1, 2, 3, 4, 5} + assert parser.statements == {1, 5} + + def test_multiline_exclusion_no_match(self) -> None: + regex = r"nonexistent" + parser = self.parse_text("""\ + def foo(): + print('Hello, world!') + """, regex) + assert parser.lines_matching(regex) == set() + assert parser.raw_statements == {1, 2} + assert parser.statements == {1, 2} + + def test_multiline_exclusion_no_source(self) -> None: + regex = r"anything" + parser = PythonParser(text="", filename="dummy.py", exclude=regex) + assert parser.lines_matching(regex) == set() + assert parser.raw_statements == set() + assert parser.statements == set() + + def test_multiline_exclusion_all_lines_must_match(self) -> None: + # https://github.com/nedbat/coveragepy/issues/996 + regex = r"except ValueError:\n\s*print\('false'\)" + parser = self.parse_text("""\ + try: + a = 2 + print('false') + except ValueError: + print('false') + except ValueError: + print('something else') + except IndexError: + print('false') + """, regex) + assert parser.lines_matching(regex) == {4, 5} + assert parser.raw_statements == {1, 2, 3, 4, 5, 6, 7, 8, 9} + assert parser.statements == {1, 2, 3, 6, 7, 8, 9} + + def test_multiline_exclusion_multiple_matches(self) -> None: + regex = r"print\('.*'\)\n\s+. = \d" + parser = self.parse_text("""\ + def foo(): + print('Hello, world!') + a = 5 + def bar(): + print('Hello again!') + b = 6 + """, regex) + assert parser.lines_matching(regex) == {2, 3, 5, 6} + assert parser.raw_statements == {1, 2, 3, 4, 5, 6} + assert parser.statements == {1, 4} + + def test_multiline_exclusion_suite2(self) -> None: + regex = r"print\('Hello, world!'\)\n\s+if T:" + parser = self.parse_text("""\ + def foo(): + print('Hello, world!') + if T: + print('This is a test.') + """, regex) + assert parser.lines_matching(regex) == {2, 3} + assert parser.raw_statements == {1, 2, 3, 4} + assert parser.statements == {1} + + def test_multiline_exclusion_match_all(self) -> None: + regex = ( + r"def foo\(\):\n\s+print\('Hello, world!'\)\n" + + r"\s+if T:\n\s+print\('This is a test\.'\)" + ) + parser = self.parse_text("""\ + def foo(): + print('Hello, world!') + if T: + print('This is a test.') + """, regex) + assert parser.lines_matching(regex) == {1, 2, 3, 4} + assert parser.raw_statements == {1, 2, 3, 4} + assert parser.statements == set() + + def test_multiline_exclusion_block(self) -> None: + # https://github.com/nedbat/coveragepy/issues/1803 + regex = "# no cover: start(?s:.)*# no cover: stop" + parser = self.parse_text("""\ + a = my_function1() + if debug: + msg = "blah blah" + # no cover: start + log_message(msg, a) + b = my_function2() + # no cover: stop + """, regex) + assert parser.lines_matching(regex) == {4, 5, 6, 7} + assert parser.raw_statements == {1, 2, 3, 5, 6} + assert parser.statements == {1, 2, 3} + + @pytest.mark.skipif(not env.PYBEHAVIOR.match_case, reason="Match-case is new in 3.10") + def test_multiline_exclusion_block2(self) -> None: + # https://github.com/nedbat/coveragepy/issues/1797 + regex = r"case _:\n\s+assert_never\(" + parser = self.parse_text("""\ + match something: + case type_1(): + logic_1() + case type_2(): + logic_2() + case _: + assert_never(something) + match something: + case type_1(): + logic_1() + case type_2(): + logic_2() + case _: + print("Default case") + """, regex) + assert parser.lines_matching(regex) == {6, 7} + assert parser.raw_statements == {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14} + assert parser.statements == {1, 2, 3, 4, 5, 8, 9, 10, 11, 12, 13, 14} + + def test_multiline_exclusion_block3(self) -> None: + # https://github.com/nedbat/coveragepy/issues/1741 + # This will only work if there's exactly one return statement in the rest of the function + regex = r"# no cover: to return(?s:.)*return" + parser = self.parse_text("""\ + def my_function(args, j): + if args.command == Commands.CMD.value: + return cmd_handler(j, args) + # no cover: to return + print(f"Command '{args.command}' was not handled.", file=sys.stderr) + parser.print_help(file=sys.stderr) + + return os.EX_USAGE + print("not excluded") + """, regex) + assert parser.lines_matching(regex) == {4, 5, 6, 7, 8} + assert parser.raw_statements == {1, 2, 3, 5, 6, 8, 9} + assert parser.statements == {1, 2, 3, 9} + + def test_multiline_exclusion_whole_source(self) -> None: + # https://github.com/nedbat/coveragepy/issues/118 + regex = r"(?s)\A.*# pragma: exclude file.*\Z" + parser = self.parse_text("""\ + import coverage + # pragma: exclude file + def the_void() -> None: + if "py" not in __file__: + print("Not a Python file.") + print("Everything here is excluded.") + + return + print("Excluded too") + """, regex) + assert parser.lines_matching(regex) == {1, 2, 3, 4, 5, 6, 7, 8, 9, 10} + assert parser.raw_statements == {1, 3, 4, 5, 6, 8, 9} + assert parser.statements == set() + + def test_multiline_exclusion_from_marker(self) -> None: + # https://github.com/nedbat/coveragepy/issues/118 + regex = r"# pragma: rest of file(?s:.)*\Z" + parser = self.parse_text("""\ + import coverage + # pragma: rest of file + def the_void() -> None: + if "py" not in __file__: + print("Not a Python file.") + print("Everything here is excluded.") + + return + print("Excluded too") + """, regex) + assert parser.lines_matching(regex) == {2, 3, 4, 5, 6, 7, 8, 9, 10} + assert parser.raw_statements == {1, 3, 4, 5, 6, 8, 9} + assert parser.statements == {1} + class ParserMissingArcDescriptionTest(PythonParserTestBase): """Tests for PythonParser.missing_arc_description.""" From 7ea6c39365d719ce2049f902975a79b9925264d5 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Thu, 4 Jul 2024 17:11:45 -0400 Subject: [PATCH 20/37] docs: explain the need for non-greedy wildcards --- doc/cog_helpers.py | 2 +- doc/excluding.rst | 42 ++++++++++++++++++++++++++++++------------ tests/test_parser.py | 4 ++-- 3 files changed, 33 insertions(+), 15 deletions(-) diff --git a/doc/cog_helpers.py b/doc/cog_helpers.py index 8cdb65f09..d30030875 100644 --- a/doc/cog_helpers.py +++ b/doc/cog_helpers.py @@ -81,7 +81,7 @@ def show_configs(ini, toml): toml, toml_vals = _read_config(toml, "covrc.toml") for key, val in ini_vals.items(): if val != toml_vals[key]: - cog.error(f"Mismatch! {key}: {val!r} vs {toml_vals[key]!r}") + cog.error(f"Mismatch! {key}:\nini: {val!r}\ntoml: {toml_vals[key]!r}") ini2 = re.sub(r"(?m)^\[", "[coverage:", ini) print() diff --git a/doc/excluding.rst b/doc/excluding.rst index 3ade282fc..7831f7d9f 100644 --- a/doc/excluding.rst +++ b/doc/excluding.rst @@ -258,17 +258,21 @@ Here are some examples: ini=r""" [report] exclude_also = - ; Exclude an except clause of a specific form: + ; 1. Exclude an except clause of a specific form: except ValueError:\n\s*assume\(False\) - ; A pragma comment that excludes an entire file: + ; 2. Comments to turn coverage on and off: + no cover: start(?s:.)*?no cover: stop + ; 3. A pragma comment that excludes an entire file: (?s)\A.*# pragma: exclude file.*\Z """, toml=r""" [tool.coverage.report] exclude_also = [ - # Exclude an except clause of a specific form: + # 1. Exclude an except clause of a specific form: "except ValueError:\\n\\s*assume\\(False\\)", - # A pragma comment that excludes an entire file: + # 2. Comments to turn coverage on and off: + "no cover: start(?s:.)*?no cover: stop", + # 3. A pragma comment that excludes an entire file: "(?s)\\A.*# pragma: exclude file.*\\Z", ] """, @@ -282,9 +286,11 @@ Here are some examples: [report] exclude_also = - ; Exclude an except clause of a specific form: + ; 1. Exclude an except clause of a specific form: except ValueError:\n\s*assume\(False\) - ; A pragma comment that excludes an entire file: + ; 2. Comments to turn coverage on and off: + no cover: start(?s:.)*?no cover: stop + ; 3. A pragma comment that excludes an entire file: (?s)\A.*# pragma: exclude file.*\Z .. code-tab:: toml @@ -292,9 +298,11 @@ Here are some examples: [tool.coverage.report] exclude_also = [ - # Exclude an except clause of a specific form: + # 1. Exclude an except clause of a specific form: "except ValueError:\\n\\s*assume\\(False\\)", - # A pragma comment that excludes an entire file: + # 2. Comments to turn coverage on and off: + "no cover: start(?s:.)*?no cover: stop", + # 3. A pragma comment that excludes an entire file: "(?s)\\A.*# pragma: exclude file.*\\Z", ] @@ -303,19 +311,29 @@ Here are some examples: [coverage:report] exclude_also = - ; Exclude an except clause of a specific form: + ; 1. Exclude an except clause of a specific form: except ValueError:\n\s*assume\(False\) - ; A pragma comment that excludes an entire file: + ; 2. Comments to turn coverage on and off: + no cover: start(?s:.)*?no cover: stop + ; 3. A pragma comment that excludes an entire file: (?s)\A.*# pragma: exclude file.*\Z -.. [[[end]]] (checksum: 8892a4efef9da67fb0080d15811e1c19) +.. [[[end]]] (checksum: 22ff0a1433f00d3b4d13544623aaf884) 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 the regex uses ``"\n\s*"`` to match the newline and the indentation of the second line. Without these, the regex won't match. -The second regex matches the entire text of a file containing the comment ``# +The second regex creates a pair of comments that can be used to exclude +statements between them. All lines between ``# no cover: start`` and ``# no +cover: stop`` will be excluded. The regex doesn't start with ``#`` because +that's a comment in a .coveragerc file. Be careful with wildcards: we've used +the non-greedy ``*?`` to match the fewest possible characters between the +comments. If you used the greedy ``*`` instead, the star would match as many +as possible, and you could accidentally exclude large swaths of code. + +The third regex matches the entire text of a file containing the comment ``# pragma: exclude file``. This lets you exclude files from coverage measurement with an internal comment instead of naming them in a settings file. This regex uses the ``"(?s)"`` regex flag to let a dot match any character including a diff --git a/tests/test_parser.py b/tests/test_parser.py index f5891e127..c19e378cb 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -883,7 +883,7 @@ def foo(): def test_multiline_exclusion_block(self) -> None: # https://github.com/nedbat/coveragepy/issues/1803 - regex = "# no cover: start(?s:.)*# no cover: stop" + regex = "# no cover: start(?s:.)*?# no cover: stop" parser = self.parse_text("""\ a = my_function1() if debug: @@ -924,7 +924,7 @@ def test_multiline_exclusion_block2(self) -> None: def test_multiline_exclusion_block3(self) -> None: # https://github.com/nedbat/coveragepy/issues/1741 # This will only work if there's exactly one return statement in the rest of the function - regex = r"# no cover: to return(?s:.)*return" + regex = r"# no cover: to return(?s:.)*?return" parser = self.parse_text("""\ def my_function(args, j): if args.command == Commands.CMD.value: From b113eee8858206df07b6fc9568b5dca125ed85fc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Jul 2024 16:00:25 -0400 Subject: [PATCH 21/37] chore: bump actions/download-artifact from 4.1.7 to 4.1.8 (#1810) Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 4.1.7 to 4.1.8. - [Release notes](https://github.com/actions/download-artifact/releases) - [Commits](https://github.com/actions/download-artifact/compare/65a9edc5881444af0b9093a5e628f2fe47ea3b2e...fa0a91b85d4f404e444e00e005971372dc801d16) --- updated-dependencies: - dependency-name: actions/download-artifact dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/coverage.yml | 4 ++-- .github/workflows/kit.yml | 2 +- .github/workflows/publish.yml | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index cfcae0a69..7707b3ed2 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -167,7 +167,7 @@ jobs: python igor.py zip_mods - name: "Download coverage data" - uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7 + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 with: pattern: metacov-* merge-multiple: true @@ -234,7 +234,7 @@ jobs: - name: "Download coverage HTML report" if: ${{ github.ref == 'refs/heads/master' }} - uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7 + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 with: name: html_report path: reports_repo/${{ env.report_dir }} diff --git a/.github/workflows/kit.yml b/.github/workflows/kit.yml index 0351c89be..0dbf69f91 100644 --- a/.github/workflows/kit.yml +++ b/.github/workflows/kit.yml @@ -280,7 +280,7 @@ jobs: id-token: write steps: - name: "Download artifacts" - uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7 + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 with: pattern: dist-* merge-multiple: true diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index c76e43e3f..183c8f702 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -56,7 +56,7 @@ jobs: steps: - name: "Download dists" - uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7 + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 with: repository: "nedbat/coveragepy" run-id: ${{needs.find-run.outputs.run-id}} @@ -95,7 +95,7 @@ jobs: steps: - name: "Download dists" - uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7 + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 with: repository: "nedbat/coveragepy" run-id: ${{needs.find-run.outputs.run-id}} From c1d17734521f618185466dcaedde322909e5be13 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Jul 2024 16:23:00 -0400 Subject: [PATCH 22/37] chore: bump actions/upload-artifact from 4.3.3 to 4.3.4 (#1811) Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4.3.3 to 4.3.4. - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/65462800fd760344b1a7b4382951275a0abb4808...0b2256b8c012f0828dc542b3febcab082c67f72b) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Ned Batchelder --- .github/workflows/coverage.yml | 4 ++-- .github/workflows/kit.yml | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 7707b3ed2..b9c251b91 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -125,7 +125,7 @@ jobs: mv .metacov .metacov.$MATRIX_ID - name: "Upload coverage data" - uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 + uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4 with: name: metacov-${{ env.MATRIX_ID }} path: .metacov.* @@ -181,7 +181,7 @@ jobs: python igor.py combine_html - name: "Upload HTML report" - uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 + uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4 with: name: html_report path: htmlcov diff --git a/.github/workflows/kit.yml b/.github/workflows/kit.yml index 0dbf69f91..c8b99b2e4 100644 --- a/.github/workflows/kit.yml +++ b/.github/workflows/kit.yml @@ -181,7 +181,7 @@ jobs: python -m twine check wheelhouse/* - name: "Upload wheels" - uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 + uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4 with: name: dist-${{ env.MATRIX_ID }} path: wheelhouse/*.whl @@ -219,7 +219,7 @@ jobs: python -m twine check dist/* - name: "Upload sdist" - uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 + uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4 with: name: dist-sdist path: dist/*.tar.gz @@ -261,7 +261,7 @@ jobs: python -m twine check dist/* - name: "Upload wheels" - uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 + uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4 with: name: dist-pypy path: dist/*.whl @@ -295,7 +295,7 @@ jobs: ls -alR - name: "Upload signatures" - uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 + uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4 with: name: signatures path: | From 41ae7bd309ee44a75a5300dfa0088281145f2df1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 10 Jul 2024 16:25:43 -0400 Subject: [PATCH 23/37] chore: bump actions/attest-build-provenance from 1.3.2 to 1.3.3 (#1813) Bumps [actions/attest-build-provenance](https://github.com/actions/attest-build-provenance) from 1.3.2 to 1.3.3. - [Release notes](https://github.com/actions/attest-build-provenance/releases) - [Changelog](https://github.com/actions/attest-build-provenance/blob/main/RELEASE.md) - [Commits](https://github.com/actions/attest-build-provenance/compare/bdd51370e0416ac948727f861e03c2f05d32d78e...5e9cb68e95676991667494a6a4e59b8a2f13e1d0) --- updated-dependencies: - dependency-name: actions/attest-build-provenance dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/publish.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 183c8f702..c4d5746d1 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -72,7 +72,7 @@ jobs: ls -1 dist | wc -l - name: "Generate attestations" - uses: actions/attest-build-provenance@bdd51370e0416ac948727f861e03c2f05d32d78e # v1.3.2 + uses: actions/attest-build-provenance@5e9cb68e95676991667494a6a4e59b8a2f13e1d0 # v1.3.3 with: subject-path: "dist/*" @@ -111,7 +111,7 @@ jobs: ls -1 dist | wc -l - name: "Generate attestations" - uses: actions/attest-build-provenance@bdd51370e0416ac948727f861e03c2f05d32d78e # v1.3.2 + uses: actions/attest-build-provenance@5e9cb68e95676991667494a6a4e59b8a2f13e1d0 # v1.3.3 with: subject-path: "dist/*" From 56212288274ed6383f02854d2c8774f1dbb8584d Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Wed, 10 Jul 2024 14:27:03 -0400 Subject: [PATCH 24/37] build(test): gold files now have cache hashes in them --- coverage/html.py | 29 ++++++++++++------ igor.py | 8 +++++ tests/gold/README.rst | 2 +- tests/gold/html/Makefile | 6 +++- tests/gold/html/a/a_py.html | 10 +++--- tests/gold/html/a/class_index.html | 12 ++++---- tests/gold/html/a/function_index.html | 12 ++++---- tests/gold/html/a/index.html | 12 ++++---- tests/gold/html/b_branch/b_py.html | 8 ++--- tests/gold/html/b_branch/class_index.html | 12 ++++---- tests/gold/html/b_branch/function_index.html | 12 ++++---- tests/gold/html/b_branch/index.html | 12 ++++---- tests/gold/html/bom/bom_py.html | 10 +++--- tests/gold/html/bom/class_index.html | 12 ++++---- tests/gold/html/bom/function_index.html | 12 ++++---- tests/gold/html/bom/index.html | 12 ++++---- tests/gold/html/isolatin1/class_index.html | 12 ++++---- tests/gold/html/isolatin1/function_index.html | 12 ++++---- tests/gold/html/isolatin1/index.html | 12 ++++---- tests/gold/html/isolatin1/isolatin1_py.html | 10 +++--- tests/gold/html/omit_1/class_index.html | 12 ++++---- tests/gold/html/omit_1/function_index.html | 12 ++++---- tests/gold/html/omit_1/index.html | 12 ++++---- tests/gold/html/omit_1/m1_py.html | 10 +++--- tests/gold/html/omit_1/m2_py.html | 10 +++--- tests/gold/html/omit_1/m3_py.html | 10 +++--- tests/gold/html/omit_1/main_py.html | 10 +++--- tests/gold/html/omit_2/class_index.html | 12 ++++---- tests/gold/html/omit_2/function_index.html | 12 ++++---- tests/gold/html/omit_2/index.html | 12 ++++---- tests/gold/html/omit_2/m2_py.html | 10 +++--- tests/gold/html/omit_2/m3_py.html | 10 +++--- tests/gold/html/omit_2/main_py.html | 10 +++--- tests/gold/html/omit_3/class_index.html | 12 ++++---- tests/gold/html/omit_3/function_index.html | 12 ++++---- tests/gold/html/omit_3/index.html | 12 ++++---- tests/gold/html/omit_3/m3_py.html | 10 +++--- tests/gold/html/omit_3/main_py.html | 10 +++--- tests/gold/html/omit_4/class_index.html | 12 ++++---- tests/gold/html/omit_4/function_index.html | 12 ++++---- tests/gold/html/omit_4/index.html | 12 ++++---- tests/gold/html/omit_4/m1_py.html | 10 +++--- tests/gold/html/omit_4/m3_py.html | 10 +++--- tests/gold/html/omit_4/main_py.html | 10 +++--- tests/gold/html/omit_5/class_index.html | 12 ++++---- tests/gold/html/omit_5/function_index.html | 12 ++++---- tests/gold/html/omit_5/index.html | 12 ++++---- tests/gold/html/omit_5/m1_py.html | 10 +++--- tests/gold/html/omit_5/main_py.html | 10 +++--- tests/gold/html/other/blah_blah_other_py.html | 20 ++++++------ tests/gold/html/other/class_index.html | 16 +++++----- tests/gold/html/other/function_index.html | 16 +++++----- tests/gold/html/other/here_py.html | 20 ++++++------ tests/gold/html/other/index.html | 16 +++++----- tests/gold/html/partial_626/class_index.html | 12 ++++---- .../gold/html/partial_626/function_index.html | 12 ++++---- tests/gold/html/partial_626/index.html | 12 ++++---- tests/gold/html/partial_626/partial_py.html | 8 ++--- tests/gold/html/styled/a_py.html | 10 +++--- tests/gold/html/styled/class_index.html | 12 ++++---- tests/gold/html/styled/function_index.html | 12 ++++---- tests/gold/html/styled/index.html | 12 ++++---- ...e_html.js => coverage_html_cb_6fb7b396.js} | 0 ...icon_32.png => favicon_32_cb_58284776.png} | Bin ...losed.png => keybd_closed_cb_ce680311.png} | Bin .../{style.css => style_cb_8e611ae1.css} | 0 tests/gold/html/unicode/class_index.html | 12 ++++---- tests/gold/html/unicode/function_index.html | 12 ++++---- tests/gold/html/unicode/index.html | 12 ++++---- tests/gold/html/unicode/unicode_py.html | 10 +++--- tests/goldtest.py | 2 ++ 71 files changed, 398 insertions(+), 375 deletions(-) rename tests/gold/html/support/{coverage_html.js => coverage_html_cb_6fb7b396.js} (100%) rename tests/gold/html/support/{favicon_32.png => favicon_32_cb_58284776.png} (100%) rename tests/gold/html/support/{keybd_closed.png => keybd_closed_cb_ce680311.png} (100%) rename tests/gold/html/support/{style.css => style_cb_8e611ae1.css} (100%) diff --git a/coverage/html.py b/coverage/html.py index 8669076d3..860327b64 100644 --- a/coverage/html.py +++ b/coverage/html.py @@ -214,6 +214,23 @@ def encode_int(n: int) -> str: return "".join(r) +def copy_with_cache_bust(src: str, dest_dir: str) -> str: + """Copy `src` to `dest_dir`, adding a hash to the name. + + Returns the updated destination file name with hash. + """ + with open(src, "rb") as f: + text = f.read() + h = Hasher() + h.update(text) + cache_bust = h.hexdigest()[:8] + src_base = os.path.basename(src) + dest = src_base.replace(".", f"_cb_{cache_bust}.") + with open(os.path.join(dest_dir, dest), "wb") as f: + f.write(text) + return dest + + class HtmlReporter: """HTML reporting.""" @@ -362,18 +379,10 @@ def make_directory(self) -> None: def copy_static_file(self, src: str, slug: str = "") -> None: """Copy a static file into the output directory with cache busting.""" - with open(src, "rb") as f: - text = f.read() - h = Hasher() - h.update(text) - cache_bust = h.hexdigest()[:8] - src_base = os.path.basename(src) - dest = src_base.replace(".", f"_cb_{cache_bust}.") + dest = copy_with_cache_bust(src, self.directory) if not slug: - slug = src_base.replace(".", "_") + slug = os.path.basename(src).replace(".", "_") self.template_globals["statics"][slug] = dest # type: ignore - with open(os.path.join(self.directory, dest), "wb") as f: - f.write(text) def make_local_static_report_files(self) -> None: """Make local instances of static files for HTML report.""" diff --git a/igor.py b/igor.py index f02ffe68c..5037cb503 100644 --- a/igor.py +++ b/igor.py @@ -475,6 +475,14 @@ def do_cheats(): ) +def do_copy_with_hash(*args): + """Copy files with a cache-busting hash. Used in tests/gold/html/Makefile.""" + from coverage.html import copy_with_cache_bust + *srcs, dest_dir = args + for src in srcs: + copy_with_cache_bust(src, dest_dir) + + def do_help(): """List the available commands""" items = list(globals().items()) diff --git a/tests/gold/README.rst b/tests/gold/README.rst index c90c9eece..00b43ff28 100644 --- a/tests/gold/README.rst +++ b/tests/gold/README.rst @@ -19,7 +19,7 @@ directories, and then add the supporting files so they can be viewed as complete output. For example:: cp tests/actual/html/contexts/* tests/gold/html/contexts - cd tests/actual/html + cd tests/gold/html make complete If the new actual output is correct, you can use "make update-gold" to copy the diff --git a/tests/gold/html/Makefile b/tests/gold/html/Makefile index 1b75f73d5..5ae08b44e 100644 --- a/tests/gold/html/Makefile +++ b/tests/gold/html/Makefile @@ -24,4 +24,8 @@ update-gold: ## Copy actual output files from latest tests to gold files. true update-support: ## Copy latest support files here for posterity. - cp ../../../coverage/htmlfiles/*.{css,js,png} support + python -m pip install -e ../../.. + git rm --ignore-unmatch support/*.{css,js,png} + mkdir -p support + python ../../../igor.py copy_with_hash ../../../coverage/htmlfiles/*.{css,js,png} support + git add support diff --git a/tests/gold/html/a/a_py.html b/tests/gold/html/a/a_py.html index 911e44771..1b294759e 100644 --- a/tests/gold/html/a/a_py.html +++ b/tests/gold/html/a/a_py.html @@ -5,7 +5,7 @@ Coverage for a.py: 67% - +
@@ -64,8 +64,8 @@

^ index     » next       - coverage.py v7.5.1a0.dev1, - created at 2024-04-24 09:22 -0400 + coverage.py v7.6.0a0.dev1, + created at 2024-07-10 12:20 -0400

@@ -2377,8 +2377,8 @@

- coverage.py v7.5.4, - created at 2024-06-22 17:11 -0400 + coverage.py v7.6.0, + created at 2024-07-11 12:38 -0400

diff --git a/doc/sample_html/z_7b071bdc2a35fa80_cogapp_py.html b/doc/sample_html/z_7b071bdc2a35fa80_cogapp_py.html index 8b1b03c84..d4c19de42 100644 --- a/doc/sample_html/z_7b071bdc2a35fa80_cogapp_py.html +++ b/doc/sample_html/z_7b071bdc2a35fa80_cogapp_py.html @@ -66,8 +66,8 @@

^ index     » next       - coverage.py v7.5.4, - created at 2024-06-22 17:11 -0400 + coverage.py v7.6.0, + created at 2024-07-11 12:38 -0400