diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 1cdec3b21..c6841b18a 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -7,5 +7,11 @@ updates: - package-ecosystem: "github-actions" directory: "/" schedule: - # Check for updates to GitHub Actions every weekday - interval: "daily" + # Check for updates to GitHub Actions once a week + interval: "weekly" + groups: + action-dependencies: + patterns: + - "*" + commit-message: + prefix: "chore" diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 742ea7af7..fbb600a5d 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@2d790406f505036ef40ecba973cc774a50395aac # 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@2d790406f505036ef40ecba973cc774a50395aac # 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@2d790406f505036ef40ecba973cc774a50395aac # v3 diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index cfcae0a69..ae7314ac6 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -84,7 +84,7 @@ jobs: uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: "Set up Python" - uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.1.0 + uses: actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f # v5.1.1 with: python-version: "${{ matrix.python-version }}" allow-prereleases: true @@ -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.* @@ -144,7 +144,7 @@ jobs: uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: "Set up Python" - uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.1.0 + uses: actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f # v5.1.1 with: python-version: "3.8" # Minimum of PYVERSIONS # At a certain point, installing dependencies failed on pypy 3.9 and @@ -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 @@ -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 @@ -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/dependency-review.yml b/.github/workflows/dependency-review.yml index 912e215f5..c578fafec 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -26,7 +26,7 @@ jobs: uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: 'Dependency Review' - uses: actions/dependency-review-action@72eb03d02c7872a771aacd928f3123ac62ad6d3a # v4.3.3 + uses: actions/dependency-review-action@5a2ce3f5b92ee19cbb1541a4984c76d921601d7c # v4.3.4 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 93e744625..d9e485bab 100644 --- a/.github/workflows/kit.yml +++ b/.github/workflows/kit.yml @@ -77,10 +77,9 @@ jobs: # "macos": ["arm64", "x86_64"], # "windows": ["x86", "AMD64"], # } - # # PYVERSIONS. Available versions: - # # https://github.com/actions/python-versions/blob/main/versions-manifest.json + # # PYVERSIONS. Available versions: https://pypi.org/project/cibuildwheel/ # # PyPy versions are handled further below in the "pypy" step. - # pys = ["cp38", "cp39", "cp310", "cp311", "cp312"] + # pys = ["cp38", "cp39", "cp310", "cp311", "cp312", "cp313"] # # # Some OS/arch combinations need overrides for the Python versions: # os_arch_pys = { @@ -107,43 +106,50 @@ jobs: - {"os": "ubuntu", "py": "cp310", "arch": "x86_64"} - {"os": "ubuntu", "py": "cp311", "arch": "x86_64"} - {"os": "ubuntu", "py": "cp312", "arch": "x86_64"} + - {"os": "ubuntu", "py": "cp313", "arch": "x86_64"} - {"os": "ubuntu", "py": "cp38", "arch": "i686"} - {"os": "ubuntu", "py": "cp39", "arch": "i686"} - {"os": "ubuntu", "py": "cp310", "arch": "i686"} - {"os": "ubuntu", "py": "cp311", "arch": "i686"} - {"os": "ubuntu", "py": "cp312", "arch": "i686"} + - {"os": "ubuntu", "py": "cp313", "arch": "i686"} - {"os": "ubuntu", "py": "cp38", "arch": "aarch64"} - {"os": "ubuntu", "py": "cp39", "arch": "aarch64"} - {"os": "ubuntu", "py": "cp310", "arch": "aarch64"} - {"os": "ubuntu", "py": "cp311", "arch": "aarch64"} - {"os": "ubuntu", "py": "cp312", "arch": "aarch64"} + - {"os": "ubuntu", "py": "cp313", "arch": "aarch64"} - {"os": "macos", "py": "cp38", "arch": "arm64", "os-version": "13"} - {"os": "macos", "py": "cp39", "arch": "arm64", "os-version": "13"} - {"os": "macos", "py": "cp310", "arch": "arm64", "os-version": "13"} - {"os": "macos", "py": "cp311", "arch": "arm64", "os-version": "13"} - {"os": "macos", "py": "cp312", "arch": "arm64", "os-version": "13"} + - {"os": "macos", "py": "cp313", "arch": "arm64", "os-version": "13"} - {"os": "macos", "py": "cp38", "arch": "x86_64", "os-version": "13"} - {"os": "macos", "py": "cp39", "arch": "x86_64", "os-version": "13"} - {"os": "macos", "py": "cp310", "arch": "x86_64", "os-version": "13"} - {"os": "macos", "py": "cp311", "arch": "x86_64", "os-version": "13"} - {"os": "macos", "py": "cp312", "arch": "x86_64", "os-version": "13"} + - {"os": "macos", "py": "cp313", "arch": "x86_64", "os-version": "13"} - {"os": "windows", "py": "cp38", "arch": "x86"} - {"os": "windows", "py": "cp39", "arch": "x86"} - {"os": "windows", "py": "cp310", "arch": "x86"} - {"os": "windows", "py": "cp311", "arch": "x86"} - {"os": "windows", "py": "cp312", "arch": "x86"} + - {"os": "windows", "py": "cp313", "arch": "x86"} - {"os": "windows", "py": "cp38", "arch": "AMD64"} - {"os": "windows", "py": "cp39", "arch": "AMD64"} - {"os": "windows", "py": "cp310", "arch": "AMD64"} - {"os": "windows", "py": "cp311", "arch": "AMD64"} - {"os": "windows", "py": "cp312", "arch": "AMD64"} - # [[[end]]] (checksum: 16ed28c185d540b2d9972a0217864472) + - {"os": "windows", "py": "cp313", "arch": "AMD64"} + # [[[end]]] (checksum: e0cd49f4a0028c4fdf1036e9bc843075) fail-fast: false steps: - name: "Setup QEMU" if: matrix.os == 'ubuntu' - uses: docker/setup-qemu-action@68827325e0b33c7199eb31dd4e31fbe9023e06e3 # v3.0.0 + uses: docker/setup-qemu-action@49b3bc8e6bdd4a60e6116a5414239cba5943d3cf # v3.2.0 with: platforms: arm64 @@ -151,7 +157,7 @@ jobs: uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: "Install Python 3.8" - uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.1.0 + uses: actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f # v5.1.1 with: # PYVERSIONS python-version: "3.8" @@ -164,10 +170,11 @@ jobs: - name: "Build wheels" env: - CIBW_BUILD: ${{ matrix.py }}-* + CIBW_BUILD: ${{ matrix.py }}*-* CIBW_ARCHS: ${{ matrix.arch }} CIBW_ENVIRONMENT: PIP_DISABLE_PIP_VERSION_CHECK=1 CIBW_PRERELEASE_PYTHONS: True + CIBW_FREE_THREADED_SUPPORT: True CIBW_TEST_COMMAND: python -c "from coverage.tracer import CTracer; print('CTracer OK!')" run: | python -m cibuildwheel --output-dir wheelhouse @@ -181,7 +188,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 @@ -195,7 +202,7 @@ jobs: uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: "Install Python 3.8" - uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.1.0 + uses: actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f # v5.1.1 with: # PYVERSIONS python-version: "3.8" @@ -219,7 +226,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 @@ -233,7 +240,7 @@ jobs: uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: "Install PyPy" - uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.1.0 + uses: actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f # v5.1.1 with: python-version: "pypy-3.8" # Minimum of PyPy PYVERSIONS cache: pip @@ -261,7 +268,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 @@ -280,13 +287,19 @@ 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 + - name: "List distributions" + run: | + ls -alR + echo "Number of dists, there should be 72:" + ls -1 coverage-* | wc -l + - name: "Sign artifacts" - uses: sigstore/gh-action-sigstore-python@61f6a500bbfdd9a2a339cf033e5421951fbc1cd2 # v2.1.1 + uses: sigstore/gh-action-sigstore-python@f514d46b907ebcd5bedc05145c03b69c1edd8b46 # v3.0.0 with: inputs: coverage-*.* @@ -295,11 +308,9 @@ 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: | - *.crt - *.sig - *.sigstore + *.sigstore.json retention-days: 7 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 090adeef2..35ae00e28 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: @@ -45,7 +46,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" @@ -54,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}} @@ -66,11 +68,16 @@ jobs: - name: "What did we get?" run: | ls -alR - echo "Number of dists:" + echo "Number of dists, should be 72:" ls -1 dist | wc -l + - name: "Generate attestations" + uses: actions/attest-build-provenance@5e9cb68e95676991667494a6a4e59b8a2f13e1d0 # v1.3.3 + with: + subject-path: "dist/*" + - 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/ @@ -78,7 +85,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" @@ -87,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}} @@ -99,8 +107,13 @@ jobs: - name: "What did we get?" run: | ls -alR - echo "Number of dists:" + echo "Number of dists, should be 72:" ls -1 dist | wc -l + - name: "Generate attestations" + uses: actions/attest-build-provenance@5e9cb68e95676991667494a6a4e59b8a2f13e1d0 # v1.3.3 + with: + subject-path: "dist/*" + - 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 diff --git a/.github/workflows/python-nightly.yml b/.github/workflows/python-nightly.yml index a2dc763b8..a91e58c41 100644 --- a/.github/workflows/python-nightly.yml +++ b/.github/workflows/python-nightly.yml @@ -94,7 +94,7 @@ jobs: nogil: "${{ matrix.nogil || false }}" - name: "Install ${{ matrix.python-version }} with setup-python" - uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.1.0 + uses: actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f # v5.1.1 if: "startsWith(matrix.python-version, 'pypy-')" with: python-version: "${{ matrix.python-version }}" @@ -104,6 +104,7 @@ jobs: set -xe python -VV python -m site + python -m sysconfig python -c "import sys; print('GIL:', getattr(sys, '_is_gil_enabled', lambda: True)())" python -m coverage debug sys python -m coverage debug pybehave diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index 2b0274275..7609becab 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -40,7 +40,7 @@ jobs: uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: "Install Python" - uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.1.0 + uses: actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f # v5.1.1 with: python-version: "3.8" # Minimum of PYVERSIONS cache: pip @@ -63,7 +63,7 @@ jobs: uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: "Install Python" - uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.1.0 + uses: actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f # v5.1.1 with: python-version: "3.8" # Minimum of PYVERSIONS, but at least 3.8 cache: pip @@ -86,7 +86,7 @@ jobs: uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: "Install Python" - uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.1.0 + uses: actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f # v5.1.1 with: python-version: "3.11" # Doc version from PYVERSIONS cache: pip diff --git a/.github/workflows/testsuite.yml b/.github/workflows/testsuite.yml index 0f9ab8296..52d9c117c 100644 --- a/.github/workflows/testsuite.yml +++ b/.github/workflows/testsuite.yml @@ -77,7 +77,7 @@ jobs: uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: "Set up Python" - uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.1.0 + uses: actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f # v5.1.1 with: python-version: "${{ matrix.python-version }}" allow-prereleases: true @@ -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) }} diff --git a/CHANGES.rst b/CHANGES.rst index 633f418c1..573cbdefe 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -22,6 +22,55 @@ upgrading your version of coverage.py. .. scriv-start-here +.. _changes_7-6-1: + +Version 7.6.1 — 2024-08-04 +-------------------------- + +- Fix: coverage used to fail when measuring code using :func:`runpy.run_path + ` with a :class:`Path ` argument. + This is now fixed, thanks to `Ask Hjorth Larsen `_. + +- Fix: backslashes preceding a multi-line backslashed string could confuse the + HTML report. This is now fixed, thanks to `LiuYinCarl `_. + +- Now we publish wheels for Python 3.13, both regular and free-threaded. + +.. _pull 1819: https://github.com/nedbat/coveragepy/pull/1819 +.. _pull 1828: https://github.com/nedbat/coveragepy/pull/1828 + + +.. _changes_7-6-0: + +Version 7.6.0 — 2024-07-11 +-------------------------- + +- 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. + +- The JSON report now includes per-function and per-class coverage information. + Thanks to `Daniel Diniz `_ for getting the work started. This + closes `issue 1793`_ and `issue 1532`_. + +- Fixed an incorrect calculation of "(no class)" lines in the HTML classes + report. + +- 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 1532: https://github.com/nedbat/coveragepy/issues/1532 +.. _issue 1741: https://github.com/nedbat/coveragepy/issues/1741 +.. _issue 1793: https://github.com/nedbat/coveragepy/issues/1793 +.. _issue 1803: https://github.com/nedbat/coveragepy/issues/1803 +.. _pull 1807: https://github.com/nedbat/coveragepy/pull/1807 +.. _pull 1809: https://github.com/nedbat/coveragepy/pull/1809 + .. _changes_7-5-4: Version 7.5.4 — 2024-06-22 diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 728f2fb1d..9097546ea 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -25,6 +25,7 @@ Artem Dayneko Arthur Deygin Arthur Rio Asher Foa +Ask Hjorth Larsen Ben Carlsson Ben Finney Benjamin Parzella @@ -134,6 +135,7 @@ Leonardo Pistone Lewis Gaul Lex Berezhny Liam DeVoe +LiuYinCarl Loïc Dachary Lorenzo Micò Louis Heredero 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/README.rst b/README.rst index a48d35133..203f8357c 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.0rc1, including free-threading. * PyPy3 versions 3.8 through 3.10. Documentation is on `Read the Docs`_. Code repository and issue tracker are on @@ -35,7 +35,8 @@ Documentation is on `Read the Docs`_. Code repository and issue tracker are on .. _GitHub: https://github.com/nedbat/coveragepy **New in 7.x:** -initial function/class reporting; +multi-line exclusion patterns; +function/class reporting; experimental support for sys.monitoring; dropped support for Python 3.7; added ``Coverage.collect()`` context manager; diff --git a/benchmark/benchmark.py b/benchmark/benchmark.py index d4649d265..9ec932e5d 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 @@ -279,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: @@ -553,47 +555,37 @@ 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") - 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 @@ -820,7 +812,7 @@ class CoverageSource(Coverage): def __init__( self, - directory_name: str, + directory_name: str = "..", slug: str = "source", tweaks: TweaksType = None, env_vars: Env_VarsType = None, @@ -950,6 +942,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: @@ -957,6 +950,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: diff --git a/benchmark/run.py b/benchmark/run.py index e1ce85c6f..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, @@ -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"}, ), ], diff --git a/coverage/cmdline.py b/coverage/cmdline.py index 9f9c06559..7c01ccbf0 100644 --- a/coverage/cmdline.py +++ b/coverage/cmdline.py @@ -19,9 +19,9 @@ import coverage from coverage import Coverage from coverage import env -from coverage.collector import HAS_CTRACER from coverage.config import CoverageConfig from coverage.control import DEFAULT_DATAFILE +from coverage.core import HAS_CTRACER from coverage.data import combinable_files, debug_data_file from coverage.debug import info_header, short_stack, write_formatted_info from coverage.exceptions import _BaseCoverageException, _ExceptionDuringRun, NoSource diff --git a/coverage/collector.py b/coverage/collector.py index 93860e43c..470d3a7e3 100644 --- a/coverage/collector.py +++ b/coverage/collector.py @@ -17,38 +17,27 @@ from coverage import env from coverage.config import CoverageConfig +from coverage.core import Core from coverage.data import CoverageData from coverage.debug import short_stack -from coverage.disposition import FileDisposition from coverage.exceptions import ConfigError from coverage.misc import human_sorted_items, isolate_module from coverage.plugin import CoveragePlugin -from coverage.pytracer import PyTracer -from coverage.sysmon import SysMonitor from coverage.types import ( - TArc, TFileDisposition, TTraceData, TTraceFn, TracerCore, TWarnFn, + TArc, + TCheckIncludeFn, + TFileDisposition, + TShouldStartContextFn, + TShouldTraceFn, + TTraceData, + TTraceFn, + Tracer, + TWarnFn, ) os = isolate_module(os) -try: - # Use the C extension code when we can, for speed. - from coverage.tracer import CTracer, CFileDisposition - HAS_CTRACER = True -except ImportError: - # Couldn't import the C extension, maybe it isn't built. - 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 - # fallback code. If we thought we had a C tracer, but couldn't import - # it, then exit quickly and clearly instead of dribbling confusing - # errors. I'm using sys.exit here instead of an exception because an - # 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) - HAS_CTRACER = False - T = TypeVar("T") @@ -78,15 +67,14 @@ class Collector: def __init__( self, - should_trace: Callable[[str, FrameType], TFileDisposition], - check_include: Callable[[str, FrameType], bool], - should_start_context: Callable[[FrameType], str | None] | None, + core: Core, + should_trace: TShouldTraceFn, + check_include: TCheckIncludeFn, + should_start_context: TShouldStartContextFn | None, file_mapper: Callable[[str], str], - timid: bool, branch: bool, warn: TWarnFn, concurrency: list[str], - metacov: bool, ) -> None: """Create a collector. @@ -105,11 +93,6 @@ def __init__( filename. The result is the name that will be recorded in the data file. - If `timid` is true, then a slower simpler trace function will be - used. This is important for some environments where manipulation of - tracing functions make the faster more sophisticated trace function not - operate properly. - If `branch` is true, then branches will be measured. This involves collecting data on which statements followed each other (arcs). Use `get_arc_data` to get the arc data. @@ -124,6 +107,7 @@ def __init__( Other values are ignored. """ + self.core = core self.should_trace = should_trace self.check_include = check_include self.should_start_context = should_start_context @@ -143,52 +127,6 @@ def __init__( self.concur_id_func = None - self._trace_class: type[TracerCore] - self.file_disposition_class: type[TFileDisposition] - - core: str | None - if timid: - core = "pytrace" - else: - core = os.getenv("COVERAGE_CORE") - - if core == "sysmon" and not env.PYBEHAVIOR.pep669: - self.warn("sys.monitoring isn't available, using default core", slug="no-sysmon") - core = None - - if not core: - # Once we're comfortable with sysmon as a default: - # if env.PYBEHAVIOR.pep669 and self.should_start_context is None: - # core = "sysmon" - if HAS_CTRACER: - core = "ctrace" - else: - core = "pytrace" - - if core == "sysmon": - self._trace_class = SysMonitor - self._core_kwargs = {"tool_id": 3 if metacov else 1} - self.file_disposition_class = FileDisposition - self.supports_plugins = False - self.packed_arcs = False - self.systrace = False - elif core == "ctrace": - self._trace_class = CTracer - self._core_kwargs = {} - self.file_disposition_class = CFileDisposition - self.supports_plugins = True - self.packed_arcs = True - self.systrace = True - elif core == "pytrace": - self._trace_class = PyTracer - self._core_kwargs = {} - self.file_disposition_class = FileDisposition - self.supports_plugins = False - self.packed_arcs = False - self.systrace = True - else: - raise ConfigError(f"Unknown core value: {core!r}") - # We can handle a few concurrency options here, but only one at a time. concurrencies = set(self.concurrency) unknown = concurrencies - CoverageConfig.CONCURRENCY_CHOICES @@ -222,7 +160,7 @@ def __init__( msg = f"Couldn't trace with concurrency={tried}, the module isn't installed." raise ConfigError(msg) from ex - if self.concur_id_func and not hasattr(self._trace_class, "concur_id_func"): + if self.concur_id_func and not hasattr(core.tracer_class, "concur_id_func"): raise ConfigError( "Can't support concurrency={} with {}, only threads are supported.".format( tried, self.tracer_name(), @@ -249,7 +187,7 @@ def use_data(self, covdata: CoverageData, context: str | None) -> None: def tracer_name(self) -> str: """Return the class name of the tracer we're using.""" - return self._trace_class.__name__ + return self.core.tracer_class.__name__ def _clear_data(self) -> None: """Clear out existing data, but stay ready for more collection.""" @@ -305,7 +243,7 @@ def reset(self) -> None: self.should_trace_cache = {} # Our active Tracers. - self.tracers: list[TracerCore] = [] + self.tracers: list[Tracer] = [] self._clear_data() @@ -321,7 +259,7 @@ def unlock_data(self) -> None: def _start_tracer(self) -> TTraceFn | None: """Start a new Tracer object, and store it in self.tracers.""" - tracer = self._trace_class(**self._core_kwargs) + tracer = self.core.tracer_class(**self.core.tracer_kwargs) tracer.data = self.data tracer.lock_data = self.lock_data tracer.unlock_data = self.unlock_data @@ -403,7 +341,7 @@ def start(self) -> None: # Install our installation tracer in threading, to jump-start other # threads. - if self.systrace and self.threading: + if self.core.systrace and self.threading: self.threading.settrace(self._installation_trace) def stop(self) -> None: @@ -440,7 +378,7 @@ def resume(self) -> None: """Resume tracing after a `pause`.""" for tracer in self.tracers: tracer.start() - if self.systrace: + if self.core.systrace: if self.threading: self.threading.settrace(self._installation_trace) else: @@ -523,7 +461,7 @@ def flush_data(self) -> bool: return False if self.branch: - if self.packed_arcs: + if self.core.packed_arcs: # Unpack the line number pairs packed into integers. See # tracer.c:CTracer_record_pair for the C code that creates # these packed ints. 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/coverage/context.py b/coverage/context.py index c8ee71271..630d3d25a 100644 --- a/coverage/context.py +++ b/coverage/context.py @@ -6,12 +6,14 @@ from __future__ import annotations from types import FrameType -from typing import cast, Callable, Sequence +from typing import cast, Sequence + +from coverage.types import TShouldStartContextFn def combine_context_switchers( - context_switchers: Sequence[Callable[[FrameType], str | None]], -) -> Callable[[FrameType], str | None] | None: + context_switchers: Sequence[TShouldStartContextFn], +) -> TShouldStartContextFn | None: """Create a single context switcher from multiple switchers. `context_switchers` is a list of functions that take a frame as an diff --git a/coverage/control.py b/coverage/control.py index 4e1d359ed..ca757e9e1 100644 --- a/coverage/control.py +++ b/coverage/control.py @@ -26,9 +26,10 @@ from coverage import env from coverage.annotate import AnnotateReporter -from coverage.collector import Collector, HAS_CTRACER +from coverage.collector import Collector from coverage.config import CoverageConfig, read_coverage_config from coverage.context import should_start_context_test_function, combine_context_switchers +from coverage.core import Core, HAS_CTRACER from coverage.data import CoverageData, combine_parallel_data from coverage.debug import ( DebugControl, NoDebugging, short_stack, write_formatted_info, relevant_environment_display, @@ -262,6 +263,7 @@ def __init__( # pylint: disable=too-many-arguments self._inorout: InOrOut | None = None self._plugins: Plugins = Plugins() self._data: CoverageData | None = None + self._core: Core | None = None self._collector: Collector | None = None self._metacov = False @@ -532,16 +534,20 @@ def _init_for_start(self) -> None: should_start_context = combine_context_switchers(context_switchers) + self._core = Core( + warn=self._warn, + timid=self.config.timid, + metacov=self._metacov, + ) self._collector = Collector( + core=self._core, should_trace=self._should_trace, check_include=self._check_include_omit_etc, should_start_context=should_start_context, file_mapper=self._file_mapper, - timid=self.config.timid, branch=self.config.branch, warn=self._warn, concurrency=concurrency, - metacov=self._metacov, ) suffix = self._data_suffix_specified @@ -563,7 +569,7 @@ def _init_for_start(self) -> None: self._collector.use_data(self._data, self.config.context) # Early warning if we aren't going to be able to support plugins. - if self._plugins.file_tracers and not self._collector.supports_plugins: + if self._plugins.file_tracers and not self._core.supports_plugins: self._warn( "Plugin file tracers ({}) aren't supported with {}".format( ", ".join( @@ -584,7 +590,7 @@ def _init_for_start(self) -> None: include_namespace_packages=self.config.include_namespace_packages, ) self._inorout.plugins = self._plugins - self._inorout.disp_class = self._collector.file_disposition_class + self._inorout.disp_class = self._core.file_disposition_class # It's useful to write debug info after initing for start. self._should_write_debug = True @@ -675,7 +681,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.""" diff --git a/coverage/core.py b/coverage/core.py new file mode 100644 index 000000000..89e49c49b --- /dev/null +++ b/coverage/core.py @@ -0,0 +1,90 @@ +# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt + +"""Management of core choices.""" + +from __future__ import annotations + +import os +import sys +from typing import Any + +from coverage import env +from coverage.disposition import FileDisposition +from coverage.exceptions import ConfigError +from coverage.pytracer import PyTracer +from coverage.sysmon import SysMonitor +from coverage.types import TFileDisposition, Tracer, TWarnFn + + +try: + # Use the C extension code when we can, for speed. + from coverage.tracer import CTracer, CFileDisposition + HAS_CTRACER = True +except ImportError: + # Couldn't import the C extension, maybe it isn't built. + 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 + # fallback code. If we thought we had a C tracer, but couldn't import + # it, then exit quickly and clearly instead of dribbling confusing + # errors. I'm using sys.exit here instead of an exception because an + # 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) + HAS_CTRACER = False + + +class Core: + """Information about the central technology enabling execution measurement.""" + + tracer_class: type[Tracer] + tracer_kwargs: dict[str, Any] + file_disposition_class: type[TFileDisposition] + supports_plugins: bool + packed_arcs: bool + systrace: bool + + def __init__(self, warn: TWarnFn, timid: bool, metacov: bool) -> None: + core_name: str | None + if timid: + core_name = "pytrace" + else: + core_name = os.getenv("COVERAGE_CORE") + + if core_name == "sysmon" and not env.PYBEHAVIOR.pep669: + warn("sys.monitoring isn't available, using default core", slug="no-sysmon") + core_name = None + + if not core_name: + # Once we're comfortable with sysmon as a default: + # if env.PYBEHAVIOR.pep669 and self.should_start_context is None: + # core_name = "sysmon" + if HAS_CTRACER: + core_name = "ctrace" + else: + core_name = "pytrace" + + if core_name == "sysmon": + self.tracer_class = SysMonitor + self.tracer_kwargs = {"tool_id": 3 if metacov else 1} + self.file_disposition_class = FileDisposition + self.supports_plugins = False + self.packed_arcs = False + self.systrace = False + elif core_name == "ctrace": + self.tracer_class = CTracer + self.tracer_kwargs = {} + self.file_disposition_class = CFileDisposition + self.supports_plugins = True + self.packed_arcs = True + self.systrace = True + elif core_name == "pytrace": + self.tracer_class = PyTracer + self.tracer_kwargs = {} + self.file_disposition_class = FileDisposition + self.supports_plugins = False + self.packed_arcs = False + self.systrace = True + else: + raise ConfigError(f"Unknown core value: {core_name!r}") diff --git a/coverage/html.py b/coverage/html.py index 8669076d3..2ee3f7d6c 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.""" @@ -533,11 +542,11 @@ def write_region_index_pages(self, files_to_report: Iterable[FileToReport]) -> N for ftr in files_to_report: region_nouns = [pair[0] for pair in ftr.fr.code_region_kinds()] num_lines = len(ftr.fr.source().splitlines()) - outside_lines = set(range(1, num_lines + 1)) regions = ftr.fr.code_regions() for noun in region_nouns: page_data = self.index_pages[noun] + outside_lines = set(range(1, num_lines + 1)) for region in regions: if region.kind != noun: diff --git a/coverage/inorout.py b/coverage/inorout.py index 5ea29edf1..4df54b2b4 100644 --- a/coverage/inorout.py +++ b/coverage/inorout.py @@ -321,7 +321,8 @@ def nope(disp: TFileDisposition, reason: str) -> TFileDisposition: # co_filename value. dunder_file = frame.f_globals and frame.f_globals.get("__file__") if dunder_file: - filename = source_for_file(dunder_file) + # Danger: __file__ can (rarely?) be of type Path. + filename = source_for_file(str(dunder_file)) if original_filename and not original_filename.startswith("<"): orig = os.path.basename(original_filename) if orig != os.path.basename(filename): diff --git a/coverage/jsonreport.py b/coverage/jsonreport.py index 9e515c202..ad5294df6 100644 --- a/coverage/jsonreport.py +++ b/coverage/jsonreport.py @@ -9,7 +9,7 @@ import json import sys -from typing import Any, IO, Iterable, TYPE_CHECKING +from typing import Any, Dict, IO, Iterable, TYPE_CHECKING from coverage import __version__ from coverage.report_core import get_analysis_to_report @@ -19,11 +19,16 @@ if TYPE_CHECKING: from coverage import Coverage from coverage.data import CoverageData + from coverage.plugin import FileReporter +# A type for data that can be JSON-serialized. +JsonObj = Dict[str, Any] + # "Version 1" had no format number at all. # 2: add the meta.format field. -FORMAT_VERSION = 2 +# 3: add region information (functions, classes) +FORMAT_VERSION = 3 class JsonReporter: """A reporter for writing JSON coverage results.""" @@ -34,7 +39,27 @@ def __init__(self, coverage: Coverage) -> None: self.coverage = coverage self.config = self.coverage.config self.total = Numbers(self.config.precision) - self.report_data: dict[str, Any] = {} + self.report_data: JsonObj = {} + + def make_summary(self, nums: Numbers) -> JsonObj: + """Create a dict summarizing `nums`.""" + return { + "covered_lines": nums.n_executed, + "num_statements": nums.n_statements, + "percent_covered": nums.pc_covered, + "percent_covered_display": nums.pc_covered_str, + "missing_lines": nums.n_missing, + "excluded_lines": nums.n_excluded, + } + + def make_branch_summary(self, nums: Numbers) -> JsonObj: + """Create a dict summarizing the branch info in `nums`.""" + return { + "num_branches": nums.n_branches, + "num_partial_branches": nums.n_partial_branches, + "covered_branches": nums.n_executed_branches, + "missing_branches": nums.n_missing_branches, + } def report(self, morfs: Iterable[TMorf] | None, outfile: IO[str]) -> float: """Generate a json report for `morfs`. @@ -60,26 +85,14 @@ def report(self, morfs: Iterable[TMorf] | None, outfile: IO[str]) -> float: measured_files[file_reporter.relative_filename()] = self.report_one_file( coverage_data, analysis, + file_reporter, ) self.report_data["files"] = measured_files - - self.report_data["totals"] = { - "covered_lines": self.total.n_executed, - "num_statements": self.total.n_statements, - "percent_covered": self.total.pc_covered, - "percent_covered_display": self.total.pc_covered_str, - "missing_lines": self.total.n_missing, - "excluded_lines": self.total.n_excluded, - } + self.report_data["totals"] = self.make_summary(self.total) if coverage_data.has_arcs(): - self.report_data["totals"].update({ - "num_branches": self.total.n_branches, - "num_partial_branches": self.total.n_partial_branches, - "covered_branches": self.total.n_executed_branches, - "missing_branches": self.total.n_missing_branches, - }) + self.report_data["totals"].update(self.make_branch_summary(self.total)) json.dump( self.report_data, @@ -89,19 +102,14 @@ def report(self, morfs: Iterable[TMorf] | None, outfile: IO[str]) -> float: return self.total.n_statements and self.total.pc_covered - def report_one_file(self, coverage_data: CoverageData, analysis: Analysis) -> dict[str, Any]: + def report_one_file( + self, coverage_data: CoverageData, analysis: Analysis, file_reporter: FileReporter + ) -> JsonObj: """Extract the relevant report data for a single file.""" nums = analysis.numbers self.total += nums - summary = { - "covered_lines": nums.n_executed, - "num_statements": nums.n_statements, - "percent_covered": nums.pc_covered, - "percent_covered_display": nums.pc_covered_str, - "missing_lines": nums.n_missing, - "excluded_lines": nums.n_excluded, - } - reported_file = { + summary = self.make_summary(nums) + reported_file: JsonObj = { "executed_lines": sorted(analysis.executed), "summary": summary, "missing_lines": sorted(analysis.missing), @@ -110,20 +118,56 @@ def report_one_file(self, coverage_data: CoverageData, analysis: Analysis) -> di if self.config.json_show_contexts: reported_file["contexts"] = coverage_data.contexts_by_lineno(analysis.filename) if coverage_data.has_arcs(): - summary.update({ - "num_branches": nums.n_branches, - "num_partial_branches": nums.n_partial_branches, - "covered_branches": nums.n_executed_branches, - "missing_branches": nums.n_missing_branches, - }) + summary.update(self.make_branch_summary(nums)) reported_file["executed_branches"] = list( _convert_branch_arcs(analysis.executed_branch_arcs()), ) reported_file["missing_branches"] = list( _convert_branch_arcs(analysis.missing_branch_arcs()), ) + + num_lines = len(file_reporter.source().splitlines()) + for noun, plural in file_reporter.code_region_kinds(): + reported_file[plural] = region_data = {} + outside_lines = set(range(1, num_lines + 1)) + for region in file_reporter.code_regions(): + if region.kind != noun: + continue + outside_lines -= region.lines + region_data[region.name] = self.make_region_data( + coverage_data, + analysis.narrow(region.lines), + ) + + region_data[""] = self.make_region_data( + coverage_data, + analysis.narrow(outside_lines), + ) return reported_file + def make_region_data(self, coverage_data: CoverageData, narrowed_analysis: Analysis) -> JsonObj: + """Create the data object for one region of a file.""" + narrowed_nums = narrowed_analysis.numbers + narrowed_summary = self.make_summary(narrowed_nums) + this_region = { + "executed_lines": sorted(narrowed_analysis.executed), + "summary": narrowed_summary, + "missing_lines": sorted(narrowed_analysis.missing), + "excluded_lines": sorted(narrowed_analysis.excluded), + } + if self.config.json_show_contexts: + contexts = coverage_data.contexts_by_lineno(narrowed_analysis.filename) + this_region["contexts"] = contexts + if coverage_data.has_arcs(): + narrowed_summary.update(self.make_branch_summary(narrowed_nums)) + this_region["executed_branches"] = list( + _convert_branch_arcs(narrowed_analysis.executed_branch_arcs()), + ) + this_region["missing_branches"] = list( + _convert_branch_arcs(narrowed_analysis.missing_branch_arcs()), + ) + return this_region + def _convert_branch_arcs( branch_arcs: dict[TLineNo, list[TLineNo]], diff --git a/coverage/parser.py b/coverage/parser.py index 00ccbf10a..0f7e353a3 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: @@ -670,7 +678,7 @@ class NodeList(ast.AST): """ def __init__(self, body: Sequence[ast.AST]) -> None: self.body = body - self.lineno = body[0].lineno + self.lineno = body[0].lineno # type: ignore[attr-defined] # TODO: Shouldn't the cause messages join with "and" instead of "or"? @@ -816,7 +824,7 @@ def line_for_node(self, node: ast.AST) -> TLineNo: if handler is not None: return handler(node) else: - return node.lineno + return node.lineno # type: ignore[attr-defined, no-any-return] # First lines: _line__* # @@ -1012,10 +1020,10 @@ def _missing__While(self, node: ast.While) -> ast.AST | None: if not body_nodes: return None # Make a synthetic While-true node. - new_while = ast.While() - new_while.lineno = body_nodes.lineno - new_while.test = ast.Name() - new_while.test.lineno = body_nodes.lineno + new_while = ast.While() # type: ignore[call-arg] + new_while.lineno = body_nodes.lineno # type: ignore[attr-defined] + new_while.test = ast.Name() # type: ignore[call-arg] + new_while.test.lineno = body_nodes.lineno # type: ignore[attr-defined] new_while.test.id = "True" assert hasattr(body_nodes, "body") new_while.body = body_nodes.body diff --git a/coverage/phystokens.py b/coverage/phystokens.py index ec8959fb6..9fc36ecda 100644 --- a/coverage/phystokens.py +++ b/coverage/phystokens.py @@ -57,7 +57,15 @@ def _phys_tokens(toks: TokenInfos) -> TokenInfos: if last_ttext.endswith("\\"): inject_backslash = False elif ttype == token.STRING: - if "\n" in ttext and ttext.split("\n", 1)[0][-1] == "\\": + if last_line.endswith(last_ttext + "\\\n"): + # Deal with special cases like such code:: + # + # a = ["aaa",\ + # "bbb \ + # ccc"] + # + inject_backslash = True + elif "\n" in ttext and ttext.split("\n", 1)[0][-1] == "\\": # 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 diff --git a/coverage/pytracer.py b/coverage/pytracer.py index 00cacea15..8144e6566 100644 --- a/coverage/pytracer.py +++ b/coverage/pytracer.py @@ -16,8 +16,16 @@ from coverage import env from coverage.types import ( - TArc, TFileDisposition, TLineNo, TTraceData, TTraceFileData, TTraceFn, - TracerCore, TWarnFn, + TArc, + TFileDisposition, + TLineNo, + TShouldStartContextFn, + TShouldTraceFn, + TTraceData, + TTraceFileData, + TTraceFn, + TWarnFn, + Tracer, ) # We need the YIELD_VALUE opcode below, in a comparison-friendly form. @@ -36,7 +44,7 @@ THIS_FILE = __file__.rstrip("co") -class PyTracer(TracerCore): +class PyTracer(Tracer): """Python implementation of the raw data tracer.""" # Because of poor implementations of trace-function-manipulating tools, @@ -64,9 +72,9 @@ def __init__(self) -> None: # Attributes set from the collector: self.data: TTraceData self.trace_arcs = False - self.should_trace: Callable[[str, FrameType], TFileDisposition] + self.should_trace: TShouldTraceFn self.should_trace_cache: dict[str, TFileDisposition | None] - self.should_start_context: Callable[[FrameType], str | None] | None = None + self.should_start_context: TShouldStartContextFn | None = None self.switch_context: Callable[[str | None], None] | None = None self.lock_data: Callable[[], None] self.unlock_data: Callable[[], None] diff --git a/coverage/sysmon.py b/coverage/sysmon.py index 0c9e795c6..ef54292f3 100644 --- a/coverage/sysmon.py +++ b/coverage/sysmon.py @@ -29,9 +29,11 @@ TArc, TFileDisposition, TLineNo, + TShouldStartContextFn, + TShouldTraceFn, TTraceData, TTraceFileData, - TracerCore, + Tracer, TWarnFn, ) @@ -171,7 +173,7 @@ def bytes_to_lines(code: CodeType) -> dict[int, int]: return b2l -class SysMonitor(TracerCore): +class SysMonitor(Tracer): """Python implementation of the raw data tracer for PEP669 implementations.""" # One of these will be used across threads. Be careful. @@ -180,11 +182,11 @@ def __init__(self, tool_id: int) -> None: # Attributes set from the collector: self.data: TTraceData self.trace_arcs = False - self.should_trace: Callable[[str, FrameType], TFileDisposition] + self.should_trace: TShouldTraceFn self.should_trace_cache: dict[str, TFileDisposition | None] # TODO: should_start_context and switch_context are unused! # Change tests/testenv.py:DYN_CONTEXTS when this is updated. - self.should_start_context: Callable[[FrameType], str | None] | None = None + self.should_start_context: TShouldStartContextFn | None = None self.switch_context: Callable[[str | None], None] | None = None self.lock_data: Callable[[], None] self.unlock_data: Callable[[], None] diff --git a/coverage/tracer.pyi b/coverage/tracer.pyi index 58dd69f94..d850493ed 100644 --- a/coverage/tracer.pyi +++ b/coverage/tracer.pyi @@ -5,7 +5,7 @@ from typing import Any, Dict -from coverage.types import TFileDisposition, TTraceData, TTraceFn, TracerCore +from coverage.types import TFileDisposition, TTraceData, TTraceFn, Tracer class CFileDisposition(TFileDisposition): """CFileDisposition is in ctracer/filedisp.c""" @@ -18,7 +18,7 @@ class CFileDisposition(TFileDisposition): trace: Any def __init__(self) -> None: ... -class CTracer(TracerCore): +class CTracer(Tracer): """CTracer is in ctracer/tracer.c""" check_include: Any concur_id_func: Any diff --git a/coverage/types.py b/coverage/types.py index ff5c59cda..bfd2a4d15 100644 --- a/coverage/types.py +++ b/coverage/types.py @@ -78,14 +78,19 @@ class TFileDisposition(Protocol): TTraceData = Dict[str, TTraceFileData] -class TracerCore(Protocol): +# Functions passed into collectors. +TShouldTraceFn = Callable[[str, FrameType], TFileDisposition] +TCheckIncludeFn = Callable[[str, FrameType], bool] +TShouldStartContextFn = Callable[[FrameType], Union[str, None]] + +class Tracer(Protocol): """Anything that can report on Python execution.""" data: TTraceData trace_arcs: bool - should_trace: Callable[[str, FrameType], TFileDisposition] + should_trace: TShouldTraceFn should_trace_cache: Mapping[str, TFileDisposition | None] - should_start_context: Callable[[FrameType], str | None] | None + should_start_context: TShouldStartContextFn | None switch_context: Callable[[str | None], None] | None lock_data: Callable[[], None] unlock_data: Callable[[], None] diff --git a/coverage/version.py b/coverage/version.py index 77743953a..ffefd8e00 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, 4, "final", 0) +version_info = (7, 6, 1, "final", 0) _dev = 0 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..d30030875 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. """ @@ -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() @@ -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/conf.py b/doc/conf.py index 60f84570f..030a1a1c1 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -67,11 +67,11 @@ # @@@ editable copyright = "2009–2024, Ned Batchelder" # pylint: disable=redefined-builtin # The short X.Y.Z version. -version = "7.5.4" +version = "7.6.1" # The full version, including alpha/beta/rc tags. -release = "7.5.4" +release = "7.6.1" # The date of release, in "monthname day, year" format. -release_date = "June 22, 2024" +release_date = "August 4, 2024" # @@@ end rst_epilog = f""" 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..7831f7d9f 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. @@ -123,13 +127,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 +207,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 +222,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 @@ -232,6 +236,110 @@ 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 = + ; 1. Exclude an except clause of a specific form: + except ValueError:\n\s*assume\(False\) + ; 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 = [ + # 1. Exclude an except clause of a specific form: + "except ValueError:\\n\\s*assume\\(False\\)", + # 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", + ] + """, + ) +.. ]]] + +.. tabs:: + + .. code-tab:: ini + :caption: .coveragerc + + [report] + exclude_also = + ; 1. Exclude an except clause of a specific form: + except ValueError:\n\s*assume\(False\) + ; 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 + :caption: pyproject.toml + + [tool.coverage.report] + exclude_also = [ + # 1. Exclude an except clause of a specific form: + "except ValueError:\\n\\s*assume\\(False\\)", + # 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:: ini + :caption: setup.cfg or tox.ini + + [coverage:report] + exclude_also = + ; 1. Exclude an except clause of a specific form: + except ValueError:\n\s*assume\(False\) + ; 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: 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 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 +newline. + + Excluding source files ---------------------- diff --git a/doc/index.rst b/doc/index.rst index 9dd939690..100823ada 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.0rc1, including free-threading. * PyPy3 versions 3.8 through 3.10. .. ifconfig:: prerelease diff --git a/doc/other.rst b/doc/other.rst index d0998b37f..8c8f71003 100644 --- a/doc/other.rst +++ b/doc/other.rst @@ -53,9 +53,9 @@ Language plugins Coverage.py plugins to enable coverage measurement of other languages. -* `django-coverage`__ measures the coverage of Django templates. +* `django-coverage-plugin`__ measures the coverage of Django templates. - __ https://pypi.org/project/django-coverage/ + __ https://pypi.org/project/django-coverage-plugin/ * `Cython`__ provides a plugin for measuring Cythonized code. 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/sample_html/class_index.html b/doc/sample_html/class_index.html index 44c660ac8..244ce8666 100644 --- a/doc/sample_html/class_index.html +++ b/doc/sample_html/class_index.html @@ -11,7 +11,7 @@

Cog coverage: - 37.34% + 38.68%

@@ -189,22 +189,22 @@

cogapp/cogapp.py (no class) - 71 + 86 + 9 0 - 0 - 0 - 0 - 100.00% + 8 + 2 + 86.17% cogapp/makefiles.py (no class) - 4 - 0 - 0 + 22 + 18 0 + 14 0 - 100.00% + 11.11% cogapp/test_cogapp.py @@ -499,34 +499,34 @@

cogapp/utils.py (no class) - 17 - 0 + 22 + 5 0 0 0 - 100.00% + 77.27% cogapp/whiteutils.py (no class) - 4 - 0 - 0 - 0 + 44 + 5 0 - 100.00% + 34 + 4 + 88.46% Total   - 1502 - 924 + 1580 + 961 3 - 236 - 29 - 37.34% + 292 + 35 + 38.68% @@ -537,8 +537,8 @@

- coverage.py v7.5.4, - created at 2024-06-22 17:11 -0400 + coverage.py v7.6.1, + created at 2024-08-04 15:03 -0400

diff --git a/doc/sample_html/z_7b071bdc2a35fa80_makefiles_py.html b/doc/sample_html/z_7b071bdc2a35fa80_makefiles_py.html index add597110..01df01b73 100644 --- a/doc/sample_html/z_7b071bdc2a35fa80_makefiles_py.html +++ b/doc/sample_html/z_7b071bdc2a35fa80_makefiles_py.html @@ -66,8 +66,8 @@

^ index     » next       - coverage.py v7.5.4, - created at 2024-06-22 17:11 -0400 + coverage.py v7.6.1, + created at 2024-08-04 15:03 -0400