diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index c6841b18a..764f927e2 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -9,6 +9,7 @@ updates:
schedule:
# Check for updates to GitHub Actions once a week
interval: "weekly"
+ day: "sunday"
groups:
action-dependencies:
patterns:
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index 25330d632..a47292b10 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -44,36 +44,36 @@ jobs:
# Learn more about CodeQL language support at https://git.io/codeql-language-support
steps:
- - name: Checkout repository
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- with:
- persist-credentials: false
+ - name: Checkout repository
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ with:
+ persist-credentials: false
- # Initializes the CodeQL tools for scanning.
- - name: Initialize CodeQL
- uses: github/codeql-action/init@5f8171a638ada777af81d42b55959a643bb29017 # v3
- with:
- languages: ${{ matrix.language }}
- # If you wish to specify custom queries, you can do so here or in a config file.
- # By default, queries listed here will override any specified in a config file.
- # Prefix the list here with "+" to use these queries and those in the config file.
- # queries: ./path/to/local/query, your-org/your-repo/queries@main
+ # Initializes the CodeQL tools for scanning.
+ - name: Initialize CodeQL
+ uses: github/codeql-action/init@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3
+ with:
+ languages: ${{ matrix.language }}
+ # If you wish to specify custom queries, you can do so here or in a config file.
+ # By default, queries listed here will override any specified in a config file.
+ # Prefix the list here with "+" to use these queries and those in the config file.
+ # queries: ./path/to/local/query, your-org/your-repo/queries@main
- # 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@5f8171a638ada777af81d42b55959a643bb29017 # v3
+ # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
+ # If this step fails, then you should remove it and run the build manually (see below)
+ - name: Autobuild
+ uses: github/codeql-action/autobuild@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3
- # ℹ️ Command-line programs to run using the OS shell.
- # 📚 https://git.io/JvXDl
+ # ℹ️ Command-line programs to run using the OS shell.
+ # 📚 https://git.io/JvXDl
- # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
- # and modify them (or add more) to build your code if your project
- # uses a compiled language
+ # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
+ # and modify them (or add more) to build your code if your project
+ # uses a compiled language
- #- run: |
- # make bootstrap
- # make release
+ #- run: |
+ # make bootstrap
+ # make release
- - name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@5f8171a638ada777af81d42b55959a643bb29017 # v3
+ - name: Perform CodeQL Analysis
+ uses: github/codeql-action/analyze@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3
diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml
index 9a3cc59a9..4863574a6 100644
--- a/.github/workflows/coverage.yml
+++ b/.github/workflows/coverage.yml
@@ -28,12 +28,44 @@ concurrency:
cancel-in-progress: true
jobs:
+ changed:
+ name: "Check changed files"
+ runs-on: ubuntu-latest
+ permissions:
+ pull-requests: read
+ outputs:
+ run_coverage: ${{ steps.filter.outputs.run_coverage }}
+ workflow: ${{ steps.filter.outputs.workflow }}
+ steps:
+ - name: "Check out the repo"
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ with:
+ persist-credentials: false
+
+ - name: "Examine changed files"
+ uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
+ id: filter
+ with:
+ filters: |
+ run_coverage:
+ - "**.py"
+ - ".github/workflows/coverage.yml"
+ - "tox.ini"
+ - "requirements/*.pip"
+ - "tests/gold/**"
+
coverage:
name: "${{ matrix.python-version }} on ${{ matrix.os }}"
runs-on: "${{ matrix.os }}-${{ matrix.os-version || 'latest' }}"
timeout-minutes: 30
+
+ # Only run coverage if Python files or this workflow changed.
+ needs: changed
+ if: ${{ needs.changed.outputs.run_coverage == 'true' }}
+
env:
MATRIX_ID: "${{ matrix.python-version }}.${{ matrix.os }}"
+ TOX_GH_MAJOR_MINOR: "${{ matrix.python-version }}"
strategy:
matrix:
@@ -51,7 +83,9 @@ jobs:
- "3.11"
- "3.12"
- "3.13"
+ - "3.13t"
- "3.14"
+ - "3.14t"
- "pypy-3.9"
- "pypy-3.10"
exclude:
@@ -67,6 +101,12 @@ jobs:
python-version: "pypy-3.9"
- os: windows
python-version: "pypy-3.10"
+ # Windows 3.14.0b1 seems confused somehow about t vs not-t:
+ # https://github.com/python/cpython/issues/133779
+ - os: windows
+ python-version: "3.14"
+ - os: windows
+ python-version: "3.14t"
# If we need to tweak the os version we can do it with an include like
# this:
# include:
@@ -84,7 +124,7 @@ jobs:
persist-credentials: false
- name: "Set up Python"
- uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0
+ uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: "${{ matrix.python-version }}"
allow-prereleases: true
@@ -97,22 +137,20 @@ jobs:
- name: "Show environment"
run: |
set -xe
+ echo matrix id: $MATRIX_ID
python -VV
python -m site
- env
+ env | sort
- name: "Install dependencies"
run: |
- echo matrix id: $MATRIX_ID
set -xe
- python -VV
- python -m site
python -m pip install -r requirements/tox.pip
- name: "Run tox coverage for ${{ matrix.python-version }}"
env:
COVERAGE_COVERAGE: "yes"
- COVERAGE_CONTEXT: "${{ matrix.python-version }}.${{ matrix.os }}"
+ COVERAGE_CONTEXT: "${{ env.MATRIX_ID }}"
run: |
set -xe
python -m tox
@@ -147,7 +185,7 @@ jobs:
persist-credentials: false
- name: "Set up Python"
- uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0
+ uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: "3.9" # Minimum of PYVERSIONS
# At a certain point, installing dependencies failed on pypy 3.9 and
@@ -170,7 +208,7 @@ jobs:
python igor.py zip_mods
- name: "Download coverage data"
- uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1
+ uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
pattern: metacov-*
merge-multiple: true
@@ -239,7 +277,7 @@ jobs:
- name: "Download coverage HTML report"
if: ${{ github.ref == 'refs/heads/master' }}
- uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1
+ uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
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 032745854..84a28187a 100644
--- a/.github/workflows/dependency-review.yml
+++ b/.github/workflows/dependency-review.yml
@@ -28,7 +28,7 @@ jobs:
persist-credentials: false
- name: 'Dependency Review'
- uses: actions/dependency-review-action@3b139cfc5fae8b618d3eae3675e383bb1769c019 # v4.5.0
+ uses: actions/dependency-review-action@38ecb5b593bf0eb19e335c03f97670f792489a8b # v4.7.0
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 f5f45ef92..7415ba33f 100644
--- a/.github/workflows/kit.yml
+++ b/.github/workflows/kit.yml
@@ -15,9 +15,6 @@
# $ piprepo build /tmp/pypi
# $ python -m pip install -v coverage --index-url=file:///tmp/pypi/simple
#
-# Note that cibuildwheel recommends not shipping wheels for pre-release versions
-# of Python: https://cibuildwheel.readthedocs.io/en/stable/options/#prerelease-pythons
-# So we don't.
name: "Kits"
@@ -38,8 +35,8 @@ defaults:
env:
PIP_DISABLE_PIP_VERSION_CHECK: 1
# PYVERSIONS: changing the list of versions will change the number of
- # expected distributions.
- EXPECTED: 63
+ # expected distributions. This must match the same number in publish.yml.
+ EXPECTED: 67
permissions:
contents: read
@@ -78,15 +75,19 @@ jobs:
# os_archs = {
# "ubuntu": ["x86_64", "i686", "aarch64"],
# "macos": ["arm64", "x86_64"],
- # "windows": ["x86", "AMD64"],
+ # "windows": ["x86", "AMD64", "ARM64"],
# }
# # PYVERSIONS. Available versions: https://pypi.org/project/cibuildwheel/
# # PyPy versions are handled further below in the "pypy" step.
+ # # Note that cibuildwheel recommends not shipping wheels for pre-release versions
+ # # of Python: https://cibuildwheel.readthedocs.io/en/stable/options/#enable
+ #
# pys = ["cp39", "cp310", "cp311", "cp312", "cp313"]
#
# # Some OS/arch combinations need overrides for the Python versions:
# os_arch_pys = {
# # ("macos", "arm64"): ["cp38", "cp39", "cp310", "cp311", "cp312"],
+ # ("windows", "ARM64"): ["cp311", "cp312", "cp313"],
# }
#
# #----- ^^^ ---------------------- ^^^ -----
@@ -102,6 +103,9 @@ jobs:
# }
# if the_os == "macos":
# them["os-version"] = "13"
+ # if the_os == "windows" and the_arch == "ARM64":
+ # them["os-version"] = "11-arm"
+ # them["minpy"] = "3.11"
# if the_arch == "aarch64":
# # https://github.com/pypa/cibuildwheel/issues/2257
# them["os-version"] = "22.04-arm"
@@ -142,7 +146,16 @@ jobs:
- {"os": "windows", "py": "cp311", "arch": "AMD64"}
- {"os": "windows", "py": "cp312", "arch": "AMD64"}
- {"os": "windows", "py": "cp313", "arch": "AMD64"}
- # [[[end]]] (checksum: 7c3758a4ca41df53d7ebcad68f12d0d0)
+ - {"os": "windows", "py": "cp311", "arch": "ARM64", "os-version": "11-arm", "minpy": "3.11"}
+ - {"os": "windows", "py": "cp312", "arch": "ARM64", "os-version": "11-arm", "minpy": "3.11"}
+ - {"os": "windows", "py": "cp313", "arch": "ARM64", "os-version": "11-arm", "minpy": "3.11"}
+ # [[[end]]] (checksum: ce8e88f33d7db22f1e21a767e3256a00)
+ # ^^^^^^^^
+ # If a check fails and points to this checksum line, it means you edited
+ # the matrix directly instead of editing the Python code in the comment
+ # above it. The matrix is generated by running cog as described at the
+ # top of that comment.
+
fail-fast: false
steps:
@@ -152,12 +165,24 @@ jobs:
persist-credentials: false
- name: "Install Python"
- uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0
+ uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
- python-version: "3.9" # Minimum of PYVERSIONS
+ python-version: "${{ matrix.minpy || '3.9' }}" # Minimum of PYVERSIONS
cache: pip
cache-dependency-path: 'requirements/*.pip'
+ # Rust toolchain is not currently installed on windows arm64 images.
+ # We need it for nh3, needed by readme-renderer, needed by twine.
+ # https://github.com/actions/partner-runner-images/issues/77
+ - if: ${{ matrix.os-version == '11-arm' }}
+ name: Setup rust
+ id: setup-rust
+ shell: pwsh
+ run: |
+ Invoke-WebRequest https://static.rust-lang.org/rustup/dist/aarch64-pc-windows-msvc/rustup-init.exe -OutFile .\rustup-init.exe
+ .\rustup-init.exe -y
+ Add-Content $env:GITHUB_PATH "$env:USERPROFILE\.cargo\bin"
+
- name: "Install tools"
run: |
python -m pip install -r requirements/kit.pip
@@ -198,7 +223,7 @@ jobs:
persist-credentials: false
- name: "Install Python"
- uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0
+ uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: "3.9" # Minimum of PYVERSIONS
cache: pip
@@ -239,7 +264,7 @@ jobs:
persist-credentials: false
- name: "Install PyPy"
- uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0
+ uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: "pypy-3.9" # Minimum of PyPy PYVERSIONS
cache: pip
@@ -286,7 +311,7 @@ jobs:
id-token: write
steps:
- name: "Download artifacts"
- uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1
+ uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
pattern: dist-*
merge-multiple: true
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index 4e6032605..7533c9840 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -16,8 +16,8 @@ defaults:
env:
# PYVERSIONS: changing the list of versions will change the number of
- # expected distributions.
- EXPECTED: 63
+ # expected distributions. This must match the same number in kit.yml.
+ EXPECTED: 67
permissions:
contents: read
@@ -34,21 +34,21 @@ jobs:
run-id: ${{ steps.run-id.outputs.run-id }}
steps:
- - name: "Find latest kit.yml run"
- id: runs
- uses: octokit/request-action@dad4362715b7fb2ddedf9772c8670824af564f0d # v2.4.0
- with:
- route: GET /repos/nedbat/coveragepy/actions/workflows/kit.yml/runs
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-
- - name: "Record run id" # zizmor: ignore[template-injection]
- id: run-id
- run: |
- # There must be a shorter way to write this...
- [ "${{ fromJson(steps.runs.outputs.data).workflow_runs[0].status}}" = "completed" ] || exit 1
- [ "${{ fromJson(steps.runs.outputs.data).workflow_runs[0].conclusion}}" = "success" ] || exit 1
- echo "run-id=${{ fromJson(steps.runs.outputs.data).workflow_runs[0].id }}" >> "$GITHUB_OUTPUT"
+ - name: "Find latest kit.yml run"
+ id: runs
+ uses: octokit/request-action@dad4362715b7fb2ddedf9772c8670824af564f0d # v2.4.0
+ with:
+ route: GET /repos/nedbat/coveragepy/actions/workflows/kit.yml/runs
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: "Record run id" # zizmor: ignore[template-injection]
+ id: run-id
+ run: |
+ # There must be a shorter way to write this...
+ [ "${{ fromJson(steps.runs.outputs.data).workflow_runs[0].status}}" = "completed" ] || exit 1
+ [ "${{ fromJson(steps.runs.outputs.data).workflow_runs[0].conclusion}}" = "success" ] || exit 1
+ echo "run-id=${{ fromJson(steps.runs.outputs.data).workflow_runs[0].id }}" >> "$GITHUB_OUTPUT"
publish-to-test-pypi:
name: "Publish to Test PyPI"
@@ -63,32 +63,32 @@ jobs:
- find-run
steps:
- - name: "Download dists"
- uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1
- with:
- repository: "nedbat/coveragepy"
- run-id: ${{ needs.find-run.outputs.run-id }}
- github-token: ${{ secrets.GITHUB_TOKEN }}
- pattern: "dist-*"
- merge-multiple: true
- path: "dist/"
-
- - name: "What did we get?"
- run: |
- ls -alR
- echo "Number of dists, should be $EXPECTED:"
- ls -1 dist | wc -l
- files=$(ls dist 2>/dev/null | wc -l) && [ "$files" -eq $EXPECTED ] || exit 1
-
- - name: "Generate attestations"
- uses: actions/attest-build-provenance@c074443f1aee8d4aeeae555aebba3282517141b2 # v2.2.3
- with:
- subject-path: "dist/*"
-
- - name: "Publish dists to Test PyPI"
- uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # v1.12.4
- with:
- repository-url: https://test.pypi.org/legacy/
+ - name: "Download dists"
+ uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
+ with:
+ repository: "nedbat/coveragepy"
+ run-id: ${{ needs.find-run.outputs.run-id }}
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ pattern: "dist-*"
+ merge-multiple: true
+ path: "dist/"
+
+ - name: "What did we get?"
+ run: |
+ ls -alR
+ echo "Number of dists, should be $EXPECTED:"
+ ls -1 dist | wc -l
+ files=$(ls dist 2>/dev/null | wc -l) && [ "$files" -eq $EXPECTED ] || exit 1
+
+ - name: "Generate attestations"
+ uses: actions/attest-build-provenance@db473fddc028af60658334401dc6fa3ffd8669fd # v2.3.0
+ with:
+ subject-path: "dist/*"
+
+ - name: "Publish dists to Test PyPI"
+ uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # v1.12.4
+ with:
+ repository-url: https://test.pypi.org/legacy/
publish-to-pypi:
name: "Publish to PyPI"
@@ -103,27 +103,27 @@ jobs:
- find-run
steps:
- - name: "Download dists"
- uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1
- with:
- repository: "nedbat/coveragepy"
- run-id: ${{ needs.find-run.outputs.run-id }}
- github-token: ${{ secrets.GITHUB_TOKEN }}
- pattern: "dist-*"
- merge-multiple: true
- path: "dist/"
-
- - name: "What did we get?"
- run: |
- ls -alR
- echo "Number of dists, should be $EXPECTED:"
- ls -1 dist | wc -l
- files=$(ls dist 2>/dev/null | wc -l) && [ "$files" -eq $EXPECTED ] || exit 1
-
- - name: "Generate attestations"
- uses: actions/attest-build-provenance@c074443f1aee8d4aeeae555aebba3282517141b2 # v2.2.3
- with:
- subject-path: "dist/*"
-
- - name: "Publish dists to PyPI"
- uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # v1.12.4
+ - name: "Download dists"
+ uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
+ with:
+ repository: "nedbat/coveragepy"
+ run-id: ${{ needs.find-run.outputs.run-id }}
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ pattern: "dist-*"
+ merge-multiple: true
+ path: "dist/"
+
+ - name: "What did we get?"
+ run: |
+ ls -alR
+ echo "Number of dists, should be $EXPECTED:"
+ ls -1 dist | wc -l
+ files=$(ls dist 2>/dev/null | wc -l) && [ "$files" -eq $EXPECTED ] || exit 1
+
+ - name: "Generate attestations"
+ uses: actions/attest-build-provenance@db473fddc028af60658334401dc6fa3ffd8669fd # v2.3.0
+ with:
+ subject-path: "dist/*"
+
+ - name: "Publish dists to PyPI"
+ uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # v1.12.4
diff --git a/.github/workflows/python-nightly.yml b/.github/workflows/python-nightly.yml
index 99e19d091..99d407291 100644
--- a/.github/workflows/python-nightly.yml
+++ b/.github/workflows/python-nightly.yml
@@ -37,6 +37,9 @@ jobs:
# hours needlessly.
timeout-minutes: 60
+ env:
+ TOX_GH_MAJOR_MINOR: "${{ matrix.python-version }}${{ matrix.nogil && 't' || '' }}"
+
strategy:
matrix:
os:
@@ -56,24 +59,22 @@ jobs:
# tox.ini so that tox will run properly. PYVERSIONS
# Available versions:
# https://launchpad.net/~deadsnakes/+archive/ubuntu/nightly/+packages
- - "3.12-dev"
- - "3.13-dev"
- - "3.14-dev"
+ - "3.12"
+ - "3.13"
+ - "3.14"
# https://github.com/actions/setup-python#available-versions-of-pypy
- - "pypy-3.10-nightly"
+ - "pypy-3.11"
nogil:
- false
- true
- include:
- - python-version: "pypy-3.10-nightly"
- os: "windows-latest"
- os-short: "windows"
+ # include:
+ # - python-version: "pypy-3.11"
+ # os: "windows-latest"
+ # os-short: "windows"
exclude:
- - python-version: "3.12-dev"
- nogil: true
- - python-version: "pypy-3.9-nightly"
+ - python-version: "3.12"
nogil: true
- - python-version: "pypy-3.10-nightly"
+ - python-version: "pypy-3.11"
nogil: true
fail-fast: false
@@ -88,14 +89,14 @@ jobs:
uses: deadsnakes/action@e640ac8743173a67cca4d7d77cd837e514bf98e8 # v3.2.0
if: "!startsWith(matrix.python-version, 'pypy-')"
with:
- python-version: "${{ matrix.python-version }}"
+ python-version: "${{ matrix.python-version }}-dev"
nogil: "${{ matrix.nogil || false }}"
- name: "Install ${{ matrix.python-version }} with setup-python"
- uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0
+ uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
if: "startsWith(matrix.python-version, 'pypy-')"
with:
- python-version: "${{ matrix.python-version }}"
+ python-version: "${{ matrix.python-version }}-nightly"
- name: "Show diagnostic info"
run: |
@@ -109,7 +110,6 @@ jobs:
env | sort
- name: "Check build recency"
- #if: "!startsWith(matrix.python-version, 'pypy-')"
shell: python
run: |
import platform
@@ -119,10 +119,11 @@ jobs:
built = datetime.strptime(platform.python_build()[1], fmt)
except ValueError:
continue
- now = datetime.now()
- days = (now - built).days
- print(f"Days since Python was built: {days}")
- assert days <= 7
+ days = (datetime.now() - built).days
+ impl = platform.python_implementation()
+ recency = 7 if (impl == "CPython") else 21
+ print(f"Days since {impl} was built: {days}, need within {recency}")
+ assert days <= recency
- name: "Install dependencies"
run: |
@@ -130,4 +131,4 @@ jobs:
- name: "Run tox"
run: |
- python -m tox -- -rfsEX
+ python -m tox -v -- -rfsEX
diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml
index 56f063c0d..3eb783830 100644
--- a/.github/workflows/quality.yml
+++ b/.github/workflows/quality.yml
@@ -26,6 +26,39 @@ concurrency:
cancel-in-progress: true
jobs:
+ changed:
+ name: "Check changed files"
+ runs-on: ubuntu-latest
+ permissions:
+ pull-requests: read
+ outputs:
+ python: ${{ steps.filter.outputs.python }}
+ docs: ${{ steps.filter.outputs.docs }}
+ actions: ${{ steps.filter.outputs.actions }}
+ workflow: ${{ steps.filter.outputs.workflow }}
+ steps:
+ - name: "Check out the repo"
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ with:
+ persist-credentials: false
+
+ - name: "Examine changed files"
+ uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
+ id: filter
+ with:
+ filters: |
+ python:
+ - "**.py"
+ docs:
+ - "doc/**"
+ - "coverage/**.py"
+ actions:
+ - ".github/workflows/**"
+ workflow:
+ - ".github/workflows/quality.yml"
+ - "tox.ini"
+ - "requirements/*.pip"
+
lint:
name: "Pylint etc"
# Because pylint can report different things on different OS's (!)
@@ -35,6 +68,9 @@ jobs:
# https://mastodon.social/@hugovk/112320493602782374
runs-on: macos-13
+ needs: changed
+ if: ${{ needs.changed.outputs.python == 'true' || needs.changed.outputs.actions == 'true' || needs.changed.outputs.workflow == 'true' }}
+
steps:
- name: "Check out the repo"
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
@@ -42,7 +78,7 @@ jobs:
persist-credentials: false
- name: "Install Python"
- uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0
+ uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: "3.9" # Minimum of PYVERSIONS
cache: pip
@@ -60,6 +96,9 @@ jobs:
name: "Check types"
runs-on: ubuntu-latest
+ needs: changed
+ if: ${{ needs.changed.outputs.python == 'true' || needs.changed.outputs.workflow == 'true' }}
+
steps:
- name: "Check out the repo"
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
@@ -67,7 +106,7 @@ jobs:
persist-credentials: false
- name: "Install Python"
- uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0
+ uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: "3.9" # Minimum of PYVERSIONS
cache: pip
@@ -85,6 +124,9 @@ jobs:
name: "Build docs"
runs-on: ubuntu-latest
+ needs: changed
+ if: ${{ needs.changed.outputs.docs == 'true' || needs.changed.outputs.workflow == 'true' }}
+
steps:
- name: "Check out the repo"
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
@@ -92,7 +134,7 @@ jobs:
persist-credentials: false
- name: "Install Python"
- uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0
+ uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: "3.11" # Doc version from PYVERSIONS
cache: pip
@@ -113,3 +155,29 @@ jobs:
- name: "Tox doc"
run: |
python -m tox -e doc
+
+ zizmor:
+ name: "Zizmor GHA security check"
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ actions: read
+
+ needs: changed
+ if: ${{ needs.changed.outputs.actions == 'true' || needs.changed.outputs.workflow == 'true' }}
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ with:
+ persist-credentials: false
+
+ - name: Install the latest version of uv
+ uses: astral-sh/setup-uv@6b9c6063abd6010835644d4c2e1bef4cf5cd0fca #v6.0.1
+ with:
+ enable-cache: false
+
+ - name: Run zizmor
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: uvx zizmor --pedantic .github/workflows
diff --git a/.github/workflows/testsuite.yml b/.github/workflows/testsuite.yml
index 0f831e81d..a27e41f99 100644
--- a/.github/workflows/testsuite.yml
+++ b/.github/workflows/testsuite.yml
@@ -28,12 +28,44 @@ concurrency:
cancel-in-progress: true
jobs:
+ changed:
+ name: "Check changed files"
+ runs-on: ubuntu-latest
+ permissions:
+ pull-requests: read
+ outputs:
+ run_tests: ${{ steps.filter.outputs.run_tests }}
+ steps:
+ - name: "Check out the repo"
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ with:
+ persist-credentials: false
+
+ - name: "Examine changed files"
+ uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
+ id: filter
+ with:
+ filters: |
+ run_tests:
+ - "**.py"
+ - ".github/workflows/testsuite.yml"
+ - "tox.ini"
+ - "requirements/*.pip"
+ - "tests/gold/**"
+
tests:
name: "${{ matrix.python-version }} on ${{ matrix.os }}"
runs-on: "${{ matrix.os }}-${{ matrix.os-version || 'latest' }}"
timeout-minutes: 30
- # Don't run tests if the branch name includes "-notests"
- if: "!contains(github.ref, '-notests')"
+
+ # Don't run tests if the branch name includes "-notests".
+ # Only run tests if files that affect tests have changed.
+ needs: changed
+ if: ${{ needs.changed.outputs.run_tests == 'true' && !contains(github.ref, '-notests') }}
+
+ env:
+ TOX_GH_MAJOR_MINOR: "${{ matrix.python-version }}"
+
strategy:
matrix:
os:
@@ -51,10 +83,19 @@ jobs:
- "3.11"
- "3.12"
- "3.13"
+ - "3.13t"
- "3.14"
+ - "3.14t"
- "pypy-3.9"
- "pypy-3.10"
- "pypy-3.11"
+ exclude:
+ # Windows 3.14.0b1 seems confused somehow about t vs not-t:
+ # https://github.com/python/cpython/issues/133779
+ - os: windows
+ python-version: "3.14"
+ - os: windows
+ python-version: "3.14t"
#
# If we need to exclude any combinations, do it like this:
# exclude:
@@ -78,7 +119,7 @@ jobs:
persist-credentials: false
- name: "Set up Python"
- uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0
+ uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: "${{ matrix.python-version }}"
allow-prereleases: true
@@ -117,8 +158,6 @@ jobs:
# https://github.com/orgs/community/discussions/33579
success:
name: Tests successful
- # The tests didn't run if the branch name includes "-notests"
- if: "!contains(github.ref, '-notests')"
needs:
- tests
runs-on: ubuntu-latest
diff --git a/.github/zizmor.yml b/.github/zizmor.yml
new file mode 100644
index 000000000..9606c3719
--- /dev/null
+++ b/.github/zizmor.yml
@@ -0,0 +1,8 @@
+# Rules for checking workflows
+# https://woodruffw.github.io/zizmor
+
+rules:
+ unpinned-uses:
+ config:
+ policies:
+ actions/*: hash-pin
diff --git a/.ignore b/.ignore
index f37cd43a5..e4a6d1f79 100644
--- a/.ignore
+++ b/.ignore
@@ -1,7 +1,7 @@
# .ignore for coverage: controls what files get searched.
build/
htmlcov/
-html0
+html0/
.tox*
.coverage*
.metacov
@@ -10,7 +10,7 @@ coverage.xml
coverage.lcov
*.min.js
style.css
-gold/
+tests/gold/*/*/*.*
sample_html/
sample_html_beta/
*.so
diff --git a/CHANGES.rst b/CHANGES.rst
index a2b172dc6..e2264ab36 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -22,6 +22,33 @@ upgrading your version of coverage.py.
.. start-releases
+.. _changes_7-8-2:
+
+Version 7.8.2 — 2025-05-23
+--------------------------
+
+- Wheels are provided for Windows ARM64 on Python 3.11, 3.12, and 3.13.
+ Thanks, `Finn Womack `_.
+
+.. _issue 1971: https://github.com/nedbat/coveragepy/pull/1971
+.. _pull 1972: https://github.com/nedbat/coveragepy/pull/1972
+
+.. _changes_7-8-1:
+
+Version 7.8.1 — 2025-05-21
+--------------------------
+
+- A number of EncodingWarnings were fixed that could appear if you've enabled
+ PYTHONWARNDEFAULTENCODING, fixing `issue 1966`_. Thanks, `Henry Schreiner
+ `_.
+
+- Fixed a race condition when using sys.monitoring with free-threading Python,
+ closing `issue 1970`_.
+
+.. _issue 1966: https://github.com/nedbat/coveragepy/issues/1966
+.. _pull 1967: https://github.com/nedbat/coveragepy/pull/1967
+.. _issue 1970: https://github.com/nedbat/coveragepy/issues/1970
+
.. _changes_7-8-0:
Version 7.8.0 — 2025-03-30
@@ -133,7 +160,7 @@ Version 7.6.10 — 2024-12-26
.. _issue 1875: https://github.com/nedbat/coveragepy/issues/1875
.. _issue 1902: https://github.com/nedbat/coveragepy/issues/1902
.. _issue 1908: https://github.com/nedbat/coveragepy/issues/1908
-.. _pep649: https://docs.python.org/3.14/whatsnew/3.14.html#pep-649-deferred-evaluation-of-annotations
+.. _pep649: https://docs.python.org/3.14/whatsnew/3.14.html#whatsnew314-pep649
.. _changes_7-6-9:
diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt
index 12fc1dab5..bd83cfb3f 100644
--- a/CONTRIBUTORS.txt
+++ b/CONTRIBUTORS.txt
@@ -86,6 +86,7 @@ Emil Madsen
Éric Larivière
Federico Bond
Felix Horvat
+Finn Womack
Frazer McLean
Geoff Bache
George Paci
@@ -95,6 +96,7 @@ Greg Rogers
Guido van Rossum
Guillaume Chazarain
Guillaume Pujol
+Henry Schreiner
Holger Krekel
Hugo van Kemenade
Ian Moore
@@ -170,6 +172,7 @@ Michał Górny
Mickie Betz
Mike Fiedler
Min ho Kim
+Nathan Goldbaum
Nathan Land
Naveen Srinivasan
Naveen Yadav
diff --git a/Makefile b/Makefile
index e03d5e5f7..426e43b46 100644
--- a/Makefile
+++ b/Makefile
@@ -8,7 +8,7 @@
##@ Utilities
-.PHONY: help clean_platform clean sterile install
+.PHONY: help _clean_platform debug_clean clean_platform clean sterile install
help: ## Show this help.
@# Adapted from https://www.thapaliya.com/en/writings/well-documented-makefiles/
@@ -50,8 +50,16 @@ sterile: clean ## Remove all non-controlled content, even if expensive.
rm -rf .tox
rm -f cheats.txt
+# For installing development tooling, use uv if it's available, otherwise pip.
+HAS_UV := $(shell command -v uv 2>/dev/null)
+ifdef HAS_UV
+ INSTALL := uv pip sync
+else
+ INSTALL := python -m pip install -r
+endif
+
install: ## Install the developer tools
- python3 -m pip install -r requirements/dev.pip
+ $(INSTALL) requirements/dev.pip
##@ Tests and quality checks
@@ -93,23 +101,21 @@ metasmoke:
# in requirements/pins.pip, and search for "windows" in .in files to find pins
# and extra requirements that have been needed, but might be obsolete.
-.PHONY: upgrade doc_upgrade diff_upgrade
+.PHONY: upgrade upgrade_one _upgrade diff_upgrade
DOCBIN = .tox/doc/bin
-PIP_COMPILE = pip-compile ${COMPILE_OPTS} --allow-unsafe --resolver=backtracking
+PIP_COMPILE = uv pip compile -q ${COMPILE_OPTS}
upgrade: ## Update the *.pip files with the latest packages satisfying *.in files.
$(MAKE) _upgrade COMPILE_OPTS="--upgrade"
-upgrade_one: ## Update the *.pip files for one package. `make upgrade-one package=...`
+upgrade_one: ## Update the *.pip files for one package. `make upgrade_one package=...`
@test -n "$(package)" || { echo "\nUsage: make upgrade-one package=...\n"; exit 1; }
$(MAKE) _upgrade COMPILE_OPTS="--upgrade-package $(package)"
-_upgrade: export CUSTOM_COMPILE_COMMAND=make upgrade
+_upgrade: export UV_CUSTOM_COMPILE_COMMAND=make upgrade
_upgrade:
- pip install -q -r requirements/pip-tools.pip
- $(PIP_COMPILE) -o requirements/pip-tools.pip requirements/pip-tools.in
$(PIP_COMPILE) -o requirements/pip.pip requirements/pip.in
$(PIP_COMPILE) -o requirements/pytest.pip requirements/pytest.in
$(PIP_COMPILE) -o requirements/kit.pip requirements/kit.in
@@ -117,11 +123,7 @@ _upgrade:
$(PIP_COMPILE) -o requirements/dev.pip requirements/dev.in
$(PIP_COMPILE) -o requirements/light-threads.pip requirements/light-threads.in
$(PIP_COMPILE) -o requirements/mypy.pip requirements/mypy.in
-
-doc_upgrade: export CUSTOM_COMPILE_COMMAND=make doc_upgrade
-doc_upgrade: $(DOCBIN) ## Update the doc/requirements.pip file
- $(DOCBIN)/pip install -q -r requirements/pip-tools.pip
- $(DOCBIN)/$(PIP_COMPILE) --upgrade -o doc/requirements.pip doc/requirements.in
+ $(PIP_COMPILE) -p $(DOCBIN)/python3 -o doc/requirements.pip doc/requirements.in
diff_upgrade: ## Summarize the last `make upgrade`
@# The sort flags sort by the package name first, then by the -/+, and
@@ -267,7 +269,7 @@ docspell: $(DOCBIN) ## Run the spell checker on the docs.
##@ Publishing docs
-.PHONY: publish publishbeta relnotes_json github_releases comment_on_fixes
+.PHONY: publish publishbeta github_releases comment_on_fixes
WEBHOME = ~/web/stellated
WEBSAMPLE = $(WEBHOME)/files/sample_coverage_html
@@ -284,19 +286,20 @@ publishbeta:
cp doc/sample_html_beta/*.* $(WEBSAMPLEBETA)
CHANGES_MD = tmp/rst_rst/changes.md
-RELNOTES_JSON = tmp/relnotes.json
+SCRIV_SOURCE = tmp/only-changes.md
$(CHANGES_MD): CHANGES.rst $(DOCBIN)
$(SPHINXBUILD) -b rst doc tmp/rst_rst
- pandoc --version
pandoc -frst -tmarkdown_strict --markdown-headings=atx --wrap=none tmp/rst_rst/changes.rst > $(CHANGES_MD)
-relnotes_json: $(RELNOTES_JSON) ## Convert changelog to JSON for further parsing.
-$(RELNOTES_JSON): $(CHANGES_MD)
- $(DOCBIN)/python ci/parse_relnotes.py tmp/rst_rst/changes.md $(RELNOTES_JSON)
+$(SCRIV_SOURCE): $(CHANGES_MD)
+ @# Trim parts of the file that aren't changelog entries.
+ sed -n -e '/## Version/,/## Earlier/p' < $(CHANGES_MD) > tmp/trimmed.md
+ @# Replace sphinx references with published URLs.
+ sed -r -e 's@]\(([a-zA-Z0-9_]+)\.rst#([^)]+)\)@](https://coverage.readthedocs.io/en/latest/\1.html#\2)@g' < tmp/trimmed.md > $(SCRIV_SOURCE)
-github_releases: $(RELNOTES_JSON) ## Update GitHub releases.
- $(DOCBIN)/python ci/github_releases.py $(RELNOTES_JSON) $(REPO_OWNER)
+github_releases: $(SCRIV_SOURCE) ## Update GitHub releases.
+ $(DOCBIN)/python -m scriv github-release --all
-comment_on_fixes: $(RELNOTES_JSON) ## Add a comment to issues that were fixed.
+comment_on_fixes: $(SCRIV_SOURCE) ## Add a comment to issues that were fixed.
python ci/comment_on_fixes.py $(REPO_OWNER)
diff --git a/README.rst b/README.rst
index cf1e856f5..79e0c5cb6 100644
--- a/README.rst
+++ b/README.rst
@@ -16,6 +16,7 @@ Code coverage measurement for Python.
| |kit| |license| |versions|
| |test-status| |quality-status| |docs| |metacov|
| |tidelift| |sponsor| |stars| |mastodon-coveragepy| |mastodon-nedbat|
+ |bluesky-nedbat|
Coverage.py measures code coverage, typically during test execution. It uses
the code analysis tools and tracing hooks provided in the Python standard
@@ -25,7 +26,7 @@ Coverage.py runs on these versions of Python:
.. PYVERSIONS
-* Python 3.9 through 3.14 alpha 6, including free-threading.
+* Python 3.9 through 3.14 beta 1, including free-threading.
* PyPy3 versions 3.9, 3.10, and 3.11.
Documentation is on `Read the Docs`_. Code repository and issue tracker are on
@@ -150,7 +151,7 @@ Licensed under the `Apache 2.0 License`_. For details, see `NOTICE.txt`_.
.. |tidelift| image:: https://tidelift.com/badges/package/pypi/coverage
:target: https://tidelift.com/subscription/pkg/pypi-coverage?utm_source=pypi-coverage&utm_medium=referral&utm_campaign=readme
:alt: Tidelift
-.. |stars| image:: https://img.shields.io/github/stars/nedbat/coveragepy.svg?logo=github
+.. |stars| image:: https://img.shields.io/github/stars/nedbat/coveragepy.svg?logo=github&style=flat
:target: https://github.com/nedbat/coveragepy/stargazers
:alt: GitHub stars
.. |mastodon-nedbat| image:: https://img.shields.io/badge/dynamic/json?style=flat&labelColor=450657&logo=mastodon&logoColor=ffffff&label=@nedbat&query=followers_count&url=https%3A%2F%2Fhachyderm.io%2Fapi%2Fv1%2Faccounts%2Flookup%3Facct=nedbat
@@ -159,6 +160,9 @@ Licensed under the `Apache 2.0 License`_. For details, see `NOTICE.txt`_.
.. |mastodon-coveragepy| image:: https://img.shields.io/badge/dynamic/json?style=flat&labelColor=450657&logo=mastodon&logoColor=ffffff&label=@coveragepy&query=followers_count&url=https%3A%2F%2Fhachyderm.io%2Fapi%2Fv1%2Faccounts%2Flookup%3Facct=coveragepy
:target: https://hachyderm.io/@coveragepy
:alt: coveragepy on Mastodon
+.. |bluesky-nedbat| image:: https://img.shields.io/badge/dynamic/json?style=flat&color=96a3b0&labelColor=3686f7&logo=icloud&logoColor=white&label=@nedbat&url=https%3A%2F%2Fpublic.api.bsky.app%2Fxrpc%2Fapp.bsky.actor.getProfile%3Factor=nedbat.com&query=followersCount
+ :target: https://bsky.app/profile/nedbat.com
+ :alt: nedbat on Bluesky
.. |sponsor| image:: https://img.shields.io/badge/%E2%9D%A4-Sponsor%20me-brightgreen?style=flat&logo=GitHub
:target: https://github.com/sponsors/nedbat
:alt: Sponsor me on GitHub
diff --git a/benchmark/benchmark.py b/benchmark/benchmark.py
index 35cf938c7..726f686ff 100644
--- a/benchmark/benchmark.py
+++ b/benchmark/benchmark.py
@@ -148,16 +148,16 @@ def file_replace(file_name: Path, old_text: str, new_text: str) -> Iterator[None
"""
file_text = ""
if old_text:
- file_text = file_name.read_text()
+ file_text = file_name.read_text(encoding="utf-8")
if old_text not in file_text:
raise Exception("Old text {old_text!r} not found in {file_name}")
updated_text = file_text.replace(old_text, new_text)
- file_name.write_text(updated_text)
+ file_name.write_text(updated_text, encoding="utf-8")
try:
yield
finally:
if old_text:
- file_name.write_text(file_text)
+ file_name.write_text(file_text, encoding="utf-8")
def file_must_exist(file_name: str, kind: str = "file") -> Path:
@@ -624,7 +624,7 @@ 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}")
pforce = Path("force.ini")
- pforce.write_text("[run]\nbranch=false\n")
+ pforce.write_text("[run]\nbranch=false\n", encoding="utf-8")
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
@@ -907,13 +907,13 @@ def __init__(
def save_results(self) -> None:
"""Save current results to the JSON file."""
- with self.results_file.open("w") as f:
+ with self.results_file.open("w", encoding="utf-8") as f:
json.dump({" ".join(k): v for k, v in self.result_data.items()}, f)
def load_results(self) -> dict[ResultKey, list[float]]:
"""Load results from the JSON file if it exists."""
if self.results_file.exists():
- with self.results_file.open("r") as f:
+ with self.results_file.open("r", encoding="utf-8") as f:
data: dict[str, list[float]] = json.load(f)
return {
(k.split()[0], k.split()[1], k.split()[2]): v for k, v in data.items()
diff --git a/ci/comment_on_fixes.py b/ci/comment_on_fixes.py
index 7debf0bfe..d159dd157 100644
--- a/ci/comment_on_fixes.py
+++ b/ci/comment_on_fixes.py
@@ -3,17 +3,23 @@
"""Add a release comment to all the issues mentioned in the latest release."""
-import json
import re
import sys
+from scriv.scriv import Scriv
+
from session import get_session
-with open("tmp/relnotes.json") as frn:
- relnotes = json.load(frn)
+scriv = Scriv()
+changelog = scriv.changelog()
+changelog.read()
+
+# Get the first entry in the changelog:
+for etitle, sections in changelog.entries().items():
+ version = etitle.split()[1] # particular to our title format.
+ text = "\n".join(sections)
+ break
-latest = relnotes[0]
-version = latest["version"]
comment = (
f"This is now released as part of [coverage {version}]" +
f"(https://pypi.org/project/coverage/{version})."
@@ -21,7 +27,7 @@
print(f"Comment will be:\n\n{comment}\n")
repo_owner = sys.argv[1]
-url_matches = re.finditer(fr"https://github.com/{repo_owner}/(issues|pull)/(\d+)", latest["text"])
+url_matches = re.finditer(fr"https://github.com/{repo_owner}/(issues|pull)/(\d+)", text)
urls = set((m[0], m[1], m[2]) for m in url_matches)
for url, kind, number in urls:
diff --git a/ci/github_releases.py b/ci/github_releases.py
deleted file mode 100644
index eec267bc9..000000000
--- a/ci/github_releases.py
+++ /dev/null
@@ -1,168 +0,0 @@
-# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
-# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt
-
-"""Upload release notes into GitHub releases."""
-
-import json
-import os
-import shlex
-import subprocess
-import sys
-import time
-
-import pkg_resources
-import requests
-
-
-RELEASES_URL = "https://api.github.com/repos/{repo}/releases"
-
-def run_command(cmd):
- """
- Run a command line (with no shell).
-
- Returns a tuple:
- bool: true if the command succeeded.
- str: the output of the command.
-
- """
- proc = subprocess.run(
- shlex.split(cmd),
- shell=False,
- check=False,
- stdout=subprocess.PIPE,
- stderr=subprocess.STDOUT,
- )
- output = proc.stdout.decode("utf-8")
- succeeded = proc.returncode == 0
- return succeeded, output
-
-def does_tag_exist(tag_name):
- """
- Does `tag_name` exist as a tag in git?
- """
- return run_command(f"git rev-parse --verify {tag_name}")[0]
-
-def check_ok(resp):
- """
- Check that the Requests response object was successful.
-
- Raise an exception if not.
- """
- if not resp:
- print(f"text: {resp.text!r}")
- resp.raise_for_status()
-
-def get_session():
- """
- Get an authenticated GitHub requests session.
- """
- gh_session = requests.Session()
- token = os.environ.get("GITHUB_TOKEN", "")
- if token:
- gh_session.headers["Authorization"] = f"Bearer {token}"
- return gh_session
-
-def github_paginated(session, url):
- """
- Get all the results from a paginated GitHub url.
- """
- while True:
- resp = session.get(url)
- check_ok(resp)
- yield from resp.json()
- next_link = resp.links.get("next", None)
- if not next_link:
- break
- url = next_link["url"]
-
-def get_releases(session, repo):
- """
- Get all the releases from a name/project repo.
-
- Returns:
- A dict mapping tag names to release dictionaries.
- """
- url = RELEASES_URL.format(repo=repo)
- releases = { r['tag_name']: r for r in github_paginated(session, url) }
- return releases
-
-RELEASE_BODY_FMT = """\
-## Version {version} \N{EM DASH} {when}
-
-{relnote_text}
-
-:arrow_right:\xa0 PyPI page: [coverage {version}](https://pypi.org/project/coverage/{version}).
-:arrow_right:\xa0 To install: `python3 -m pip install coverage=={version}`
-"""
-
-def release_for_relnote(relnote, tag):
- """
- Turn a release note dict into the data needed by GitHub for a release.
- """
- relnote_text = relnote["text"]
- version = relnote["version"]
- body = RELEASE_BODY_FMT.format(
- relnote_text=relnote_text,
- version=version,
- when=relnote["when"],
- )
- return {
- "tag_name": tag,
- "name": version,
- "body": body,
- "draft": False,
- "prerelease": relnote["prerelease"],
- }
-
-def create_release(session, repo, release_data):
- """
- Create a new GitHub release.
- """
- print(f"Creating {release_data['name']}")
- resp = session.post(RELEASES_URL.format(repo=repo), json=release_data)
- check_ok(resp)
-
-def update_release(session, url, release_data):
- """
- Update an existing GitHub release.
- """
- print(f"Updating {release_data['name']}")
- resp = session.patch(url, json=release_data)
- check_ok(resp)
-
-def update_github_releases(json_filename, repo):
- """
- Read the json file, and create or update releases in GitHub.
- """
- gh_session = get_session()
- releases = get_releases(gh_session, repo)
- if 0: # if you need to delete all the releases!
- for release in releases.values():
- print(release["tag_name"])
- resp = gh_session.delete(release["url"])
- check_ok(resp)
- return
-
- with open(json_filename) as jf:
- relnotes = json.load(jf)
- relnotes.sort(key=lambda rel: pkg_resources.parse_version(rel["version"]))
- for relnote in relnotes:
- tag = relnote["version"]
- if not does_tag_exist(tag):
- tag = f"coverage-{tag}"
- if not does_tag_exist(tag):
- continue
- release_data = release_for_relnote(relnote, tag)
- exists = tag in releases
- if not exists:
- create_release(gh_session, repo, release_data)
- time.sleep(3)
- else:
- release = releases[tag]
- if release["body"] != release_data["body"]:
- url = release["url"]
- update_release(gh_session, url, release_data)
- time.sleep(3)
-
-if __name__ == "__main__":
- update_github_releases(*sys.argv[1:3])
diff --git a/ci/parse_relnotes.py b/ci/parse_relnotes.py
deleted file mode 100644
index 6ba32e6f0..000000000
--- a/ci/parse_relnotes.py
+++ /dev/null
@@ -1,123 +0,0 @@
-# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
-# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt
-
-"""
-Parse CHANGES.md into a JSON structure.
-
-Run with two arguments: the .md file to parse, and the JSON file to write:
-
- python parse_relnotes.py CHANGES.md relnotes.json
-
-Every section that has something that looks like a version number in it will
-be recorded as the release notes for that version.
-
-"""
-
-import json
-import re
-import sys
-
-
-class TextChunkBuffer:
- """Hold onto text chunks until needed."""
- def __init__(self):
- self.buffer = []
-
- def append(self, text):
- """Add `text` to the buffer."""
- self.buffer.append(text)
-
- def clear(self):
- """Clear the buffer."""
- self.buffer = []
-
- def flush(self):
- """Produce a ("text", text) tuple if there's anything here."""
- buffered = "".join(self.buffer).strip()
- if buffered:
- yield ("text", buffered)
- self.clear()
-
-
-def parse_md(lines):
- """Parse markdown lines, producing (type, text) chunks."""
- buffer = TextChunkBuffer()
-
- for line in lines:
- if header_match := re.search(r"^(#+) (.+)$", line):
- yield from buffer.flush()
- hashes, text = header_match.groups()
- yield (f"h{len(hashes)}", text)
- else:
- buffer.append(line)
-
- yield from buffer.flush()
-
-
-def sections(parsed_data):
- """Convert a stream of parsed tokens into sections with text and notes.
-
- Yields a stream of:
- ('h-level', 'header text', 'text')
-
- """
- header = None
- text = []
- for ttype, ttext in parsed_data:
- if ttype.startswith('h'):
- if header:
- yield (*header, "\n".join(text))
- text = []
- header = (ttype, ttext)
- elif ttype == "text":
- text.append(ttext)
- else:
- raise RuntimeError(f"Don't know ttype {ttype!r}")
- yield (*header, "\n".join(text))
-
-
-def refind(regex, text):
- """Find a regex in some text, and return the matched text, or None."""
- if m := re.search(regex, text):
- return m.group()
- else:
- return None
-
-
-def fix_ref_links(text, version):
- """Find links to .rst files, and make them full RTFD links."""
- def new_link(m):
- return f"](https://coverage.readthedocs.io/en/{version}/{m[1]}.html{m[2]})"
- return re.sub(r"\]\((\w+)\.rst(#.*?)\)", new_link, text)
-
-
-def relnotes(mdlines):
- r"""Yield (version, text) pairs from markdown lines.
-
- Each tuple is a separate version mentioned in the release notes.
-
- A version is any section with \d\.\d in the header text.
-
- """
- for _, htext, text in sections(parse_md(mdlines)):
- version = refind(r"\d+\.\d[^ ]*", htext)
- if version:
- prerelease = any(c in version for c in "abc")
- when = refind(r"\d+-\d+-\d+", htext)
- text = fix_ref_links(text, version)
- yield {
- "version": version,
- "text": text,
- "prerelease": prerelease,
- "when": when,
- }
-
-def parse(md_filename, json_filename):
- """Main function: parse markdown and write JSON."""
- with open(md_filename) as mf:
- markdown = mf.read()
- with open(json_filename, "w") as jf:
- json.dump(list(relnotes(markdown.splitlines(True))), jf, indent=4)
-
-if __name__ == "__main__":
- parse(*sys.argv[1:3])
diff --git a/coverage/cmdline.py b/coverage/cmdline.py
index 783345e01..998e6b398 100644
--- a/coverage/cmdline.py
+++ b/coverage/cmdline.py
@@ -21,7 +21,7 @@
from coverage import env
from coverage.config import CoverageConfig
from coverage.control import DEFAULT_DATAFILE
-from coverage.core import HAS_CTRACER
+from coverage.core import CTRACER_FILE
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
@@ -574,7 +574,7 @@ def show_help(
help_params = dict(coverage.__dict__)
help_params["__url__"] = __url__
help_params["program_name"] = program_name
- if HAS_CTRACER:
+ if CTRACER_FILE:
help_params["extension_modifier"] = "with C extension"
else:
help_params["extension_modifier"] = "without C extension"
diff --git a/coverage/control.py b/coverage/control.py
index 16c99f7f0..281c3aec9 100644
--- a/coverage/control.py
+++ b/coverage/control.py
@@ -27,7 +27,7 @@
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.core import Core, CTRACER_FILE
from coverage.data import CoverageData, combine_parallel_data
from coverage.debug import (
DebugControl, NoDebugging, short_stack, write_formatted_info, relevant_environment_display,
@@ -1336,7 +1336,7 @@ def plugin_info(plugins: list[Any]) -> list[str]:
("coverage_version", covmod.__version__),
("coverage_module", covmod.__file__),
("core", self._collector.tracer_name() if self._collector is not None else "-none-"),
- ("CTracer", "available" if HAS_CTRACER else "unavailable"),
+ ("CTracer", f"available from {CTRACER_FILE}" if CTRACER_FILE else "unavailable"),
("plugins.file_tracers", plugin_info(self._plugins.file_tracers)),
("plugins.configurers", plugin_info(self._plugins.configurers)),
("plugins.context_switchers", plugin_info(self._plugins.context_switchers)),
diff --git a/coverage/core.py b/coverage/core.py
index 38c27578b..c46fd241e 100644
--- a/coverage/core.py
+++ b/coverage/core.py
@@ -27,8 +27,8 @@
try:
# Use the C extension code when we can, for speed.
- from coverage.tracer import CTracer, CFileDisposition
- HAS_CTRACER = True
+ import coverage.tracer
+ CTRACER_FILE: str | None = coverage.tracer.__file__
except ImportError:
# Couldn't import the C extension, maybe it isn't built.
if os.getenv("COVERAGE_CORE") == "ctrace": # pragma: part covered
@@ -40,7 +40,7 @@
# exception here causes all sorts of other noise in unittest.
sys.stderr.write("*** COVERAGE_CORE is 'ctrace' but can't import CTracer!\n")
sys.exit(1)
- HAS_CTRACER = False
+ CTRACER_FILE = None
class Core:
@@ -84,7 +84,7 @@ def __init__(
# Someday we will default to sysmon, but it's still experimental:
# if not reason_no_sysmon:
# core_name = "sysmon"
- if HAS_CTRACER:
+ if CTRACER_FILE:
core_name = "ctrace"
else:
core_name = "pytrace"
@@ -99,8 +99,8 @@ def __init__(
self.packed_arcs = False
self.systrace = False
elif core_name == "ctrace":
- self.tracer_class = CTracer
- self.file_disposition_class = CFileDisposition
+ self.tracer_class = coverage.tracer.CTracer
+ self.file_disposition_class = coverage.tracer.CFileDisposition
self.supports_plugins = True
self.packed_arcs = True
self.systrace = True
diff --git a/coverage/ctracer/tracer.c b/coverage/ctracer/tracer.c
index 4fca2fee7..c05dff643 100644
--- a/coverage/ctracer/tracer.c
+++ b/coverage/ctracer/tracer.c
@@ -289,7 +289,7 @@ CTracer_set_pdata_stack(CTracer *self)
}
// Thanks for the idea, memray!
-inline PyCodeObject*
+static inline PyCodeObject*
MyFrame_BorrowCode(PyFrameObject* frame)
{
// Return a borrowed reference.
diff --git a/coverage/env.py b/coverage/env.py
index a88161b38..365da7f93 100644
--- a/coverage/env.py
+++ b/coverage/env.py
@@ -40,6 +40,9 @@
else:
PYPYVERSION = (0,)
+# Do we have a GIL?
+GIL = getattr(sys, '_is_gil_enabled', lambda: True)()
+
# Python behavior.
class PYBEHAVIOR:
"""Flags indicating this Python's behavior."""
diff --git a/coverage/execfile.py b/coverage/execfile.py
index b44c95280..23928c02e 100644
--- a/coverage/execfile.py
+++ b/coverage/execfile.py
@@ -17,7 +17,6 @@
from types import CodeType, ModuleType
from typing import Any
-from coverage import env
from coverage.exceptions import CoverageException, _ExceptionDuringRun, NoCode, NoSource
from coverage.files import canonical_filename, python_reported_file
from coverage.misc import isolate_module
@@ -90,7 +89,7 @@ def prepare(self) -> None:
This needs to happen before any importing, and without importing anything.
"""
path0: str | None
- if env.PYVERSION >= (3, 11) and getattr(sys.flags, "safe_path"):
+ if getattr(sys.flags, "safe_path", False):
# See https://docs.python.org/3/using/cmdline.html#cmdoption-P
path0 = None
elif self.as_module:
diff --git a/coverage/html.py b/coverage/html.py
index 2cc68ac1d..55dd32e2e 100644
--- a/coverage/html.py
+++ b/coverage/html.py
@@ -51,7 +51,7 @@ def data_filename(fname: str) -> str:
def read_data(fname: str) -> str:
"""Return the contents of a data file of ours."""
- with open(data_filename(fname)) as data_file:
+ with open(data_filename(fname), encoding="utf-8") as data_file:
return data_file.read()
@@ -412,7 +412,7 @@ def make_local_static_report_files(self) -> None:
# .gitignore can't be copied from the source tree because if it was in
# the source tree, it would stop the static files from being checked in.
if self.directory_was_empty:
- with open(os.path.join(self.directory, ".gitignore"), "w") as fgi:
+ with open(os.path.join(self.directory, ".gitignore"), "w", encoding="utf-8") as fgi:
fgi.write("# Created by coverage.py\n*\n")
def should_report(self, analysis: Analysis, index_page: IndexPage) -> bool:
@@ -706,7 +706,7 @@ def read(self) -> None:
"""Read the information we stored last time."""
try:
status_file = os.path.join(self.directory, self.STATUS_FILE)
- with open(status_file) as fstatus:
+ with open(status_file, encoding="utf-8") as fstatus:
status = json.load(fstatus)
except (OSError, ValueError):
# Status file is missing or malformed.
@@ -747,7 +747,7 @@ def write(self) -> None:
for fname, finfo in self.files.items()
},
}
- with open(status_file, "w") as fout:
+ with open(status_file, "w", encoding="utf-8") as fout:
json.dump(status_data, fout, separators=(",", ":"))
def check_global_data(self, *data: Any) -> None:
diff --git a/coverage/misc.py b/coverage/misc.py
index c5ce7f4ae..32c91cca1 100644
--- a/coverage/misc.py
+++ b/coverage/misc.py
@@ -244,13 +244,13 @@ def substitute_variables(text: str, variables: Mapping[str, str]) -> str:
dollar_pattern = r"""(?x) # Use extended regex syntax
\$ # A dollar sign,
(?: # then
- (?P\$) | # a dollar sign, or
- (?P\w+) | # a plain word, or
- { # a {-wrapped
- (?P\w+) # word,
- (?:
- (?P\?) | # with a strict marker
- -(?P[^}]*) # or a default value
+ (?P \$ ) | # a dollar sign, or
+ (?P \w+ ) | # a plain word, or
+ \{ # a {-wrapped
+ (?P \w+ ) # word,
+ (?: # either
+ (?P \? ) | # with a strict marker
+ -(?P [^}]* ) # or a default value
)? # maybe.
}
)
diff --git a/coverage/pytracer.py b/coverage/pytracer.py
index 7c119c070..31b4866cb 100644
--- a/coverage/pytracer.py
+++ b/coverage/pytracer.py
@@ -126,7 +126,7 @@ def __repr__(self) -> str:
def log(self, marker: str, *args: Any) -> None:
"""For hard-core logging of what this tracer is doing."""
- with open("/tmp/debug_trace.txt", "a") as f:
+ with open("/tmp/debug_trace.txt", "a", encoding="utf-8") as f:
f.write(f"{marker} {self.id}[{len(self.data_stack)}]")
if 0: # if you want thread ids..
f.write(".{:x}.{:x}".format( # type: ignore[unreachable]
@@ -147,8 +147,8 @@ def _trace(
self,
frame: FrameType,
event: str,
- arg: Any, # pylint: disable=unused-argument
- lineno: TLineNo | None = None, # pylint: disable=unused-argument
+ arg: Any, # pylint: disable=unused-argument
+ lineno: TLineNo | None = None, # pylint: disable=unused-argument
) -> TTraceFn | None:
"""The trace function passed to sys.settrace."""
@@ -162,7 +162,7 @@ def _trace(
# The PyTrace.stop() method has been called, possibly by another
# thread, let's deactivate ourselves now.
if 0:
- f = frame # type: ignore[unreachable]
+ f = frame # type: ignore[unreachable]
self.log("---\nX", f.f_code.co_filename, f.f_lineno)
while f:
self.log(">", f.f_code.co_filename, f.f_lineno, f.f_code.co_name, f.f_trace)
@@ -306,6 +306,7 @@ def _trace(
assert self.switch_context is not None
self.context = None
self.switch_context(None) # pylint: disable=not-callable
+
return self._cached_bound_method_trace
def start(self) -> TTraceFn:
diff --git a/coverage/sysmon.py b/coverage/sysmon.py
index 8e5376cf0..2c4602765 100644
--- a/coverage/sysmon.py
+++ b/coverage/sysmon.py
@@ -104,7 +104,7 @@ def log(msg: str) -> None:
# f"{root}-{pid}.out",
# f"{root}-{pid}-{tslug}.out",
]:
- with open(filename, "a") as f:
+ with open(filename, "a", encoding="utf-8") as f:
try:
print(f"{pid}:{tslug}: {msg}", file=f, flush=True)
except UnicodeError:
@@ -233,7 +233,6 @@ def __init__(self, tool_id: int) -> None:
0,
)
- self.stopped = False
self._activity = False
def __repr__(self) -> str:
@@ -244,43 +243,43 @@ def __repr__(self) -> str:
@panopticon()
def start(self) -> None:
"""Start this Tracer."""
- self.stopped = False
-
- assert sys_monitoring is not None
- sys_monitoring.use_tool_id(self.myid, "coverage.py")
- register = functools.partial(sys_monitoring.register_callback, self.myid)
- events = sys.monitoring.events
-
- sys_monitoring.set_events(self.myid, events.PY_START)
- register(events.PY_START, self.sysmon_py_start)
- if self.trace_arcs:
- register(events.PY_RETURN, self.sysmon_py_return)
- register(events.LINE, self.sysmon_line_arcs)
- if env.PYBEHAVIOR.branch_right_left:
- register(
- events.BRANCH_RIGHT, # type:ignore[attr-defined]
- self.sysmon_branch_either,
- )
- register(
- events.BRANCH_LEFT, # type:ignore[attr-defined]
- self.sysmon_branch_either,
- )
- else:
- register(events.LINE, self.sysmon_line_lines)
- sys_monitoring.restart_events()
- self.sysmon_on = True
+ with self.lock:
+ assert sys_monitoring is not None
+ sys_monitoring.use_tool_id(self.myid, "coverage.py")
+ register = functools.partial(sys_monitoring.register_callback, self.myid)
+ events = sys.monitoring.events
+
+ sys_monitoring.set_events(self.myid, events.PY_START)
+ register(events.PY_START, self.sysmon_py_start)
+ if self.trace_arcs:
+ register(events.PY_RETURN, self.sysmon_py_return)
+ register(events.LINE, self.sysmon_line_arcs)
+ if env.PYBEHAVIOR.branch_right_left:
+ register(
+ events.BRANCH_RIGHT, # type:ignore[attr-defined]
+ self.sysmon_branch_either,
+ )
+ register(
+ events.BRANCH_LEFT, # type:ignore[attr-defined]
+ self.sysmon_branch_either,
+ )
+ else:
+ register(events.LINE, self.sysmon_line_lines)
+ sys_monitoring.restart_events()
+ self.sysmon_on = True
@panopticon()
def stop(self) -> None:
"""Stop this Tracer."""
- if not self.sysmon_on:
- # In forking situations, we might try to stop when we are not
- # started. Do nothing in that case.
- return
- assert sys_monitoring is not None
- sys_monitoring.set_events(self.myid, 0)
- self.sysmon_on = False
- sys_monitoring.free_tool_id(self.myid)
+ with self.lock:
+ if not self.sysmon_on:
+ # In forking situations, we might try to stop when we are not
+ # started. Do nothing in that case.
+ return
+ assert sys_monitoring is not None
+ sys_monitoring.set_events(self.myid, 0)
+ self.sysmon_on = False
+ sys_monitoring.free_tool_id(self.myid)
@panopticon()
def post_fork(self) -> None:
diff --git a/coverage/version.py b/coverage/version.py
index fe08b5f98..d6a2eb793 100644
--- a/coverage/version.py
+++ b/coverage/version.py
@@ -8,7 +8,7 @@
# version_info: same semantics as sys.version_info.
# _dev: the .devN suffix if any.
-version_info = (7, 8, 0, "final", 0)
+version_info = (7, 8, 2, "final", 0)
_dev = 0
diff --git a/doc/api.rst b/doc/api.rst
index 7d04f03ee..04c52d639 100644
--- a/doc/api.rst
+++ b/doc/api.rst
@@ -37,11 +37,17 @@ If you want to access the data that coverage.py has collected, the
:class:`coverage.CoverageData` class provides an API to read coverage.py data
files.
-.. note::
+.. warning::
- Only the documented portions of the API are supported. Other names you may
- find in modules or objects can change their behavior at any time. Please
- limit yourself to documented methods to avoid problems.
+ Only the published documented portions of the API are supported. Other
+ names you may find in modules or objects can change their behavior at any
+ time. Please limit yourself to documented methods to avoid problems.
+
+ All internal code in coverage.py has docstrings; this does not make them
+ part of the public supported API. Many internal names have no leading
+ underscore; this does not make them part of the public supported API. If
+ classes or functions are not documented in this published documentation,
+ they are not supported.
For more intensive data use, you might want to access the coverage.py database
file directly. The schema is subject to change, so this is for advanced uses
diff --git a/doc/cog_helpers.py b/doc/cog_helpers.py
index d30030875..70161ab0d 100644
--- a/doc/cog_helpers.py
+++ b/doc/cog_helpers.py
@@ -52,7 +52,7 @@ def _read_config(text, fname):
text = textwrap.dedent(text[1:])
os.makedirs("tmp", exist_ok=True)
- with open(f"tmp/{fname}", "w") as f:
+ with open(f"tmp/{fname}", "w", encoding="utf-8") as f:
f.write(text)
config = read_coverage_config(f"tmp/{fname}", warn=cog.error)
diff --git a/doc/conf.py b/doc/conf.py
index 57a1ffd00..09cc5e06e 100644
--- a/doc/conf.py
+++ b/doc/conf.py
@@ -67,11 +67,11 @@
# @@@ editable
copyright = "2009–2025, Ned Batchelder" # pylint: disable=redefined-builtin
# The short X.Y.Z version.
-version = "7.8.0"
+version = "7.8.2"
# The full version, including alpha/beta/rc tags.
-release = "7.8.0"
+release = "7.8.2"
# The date of release, in "monthname day, year" format.
-release_date = "March 30, 2025"
+release_date = "May 23, 2025"
# @@@ end
rst_epilog = f"""
@@ -215,7 +215,7 @@
# missing, so only use the extension if we are specifically spell-checking.
extensions += ['sphinxcontrib.spelling']
names_file = tempfile.NamedTemporaryFile(mode='w', prefix="coverage_names_", suffix=".txt")
- with open("../CONTRIBUTORS.txt") as contributors:
+ with open("../CONTRIBUTORS.txt", encoding="utf-8") as contributors:
names = set(re.split(r"[^\w']", contributors.read()))
names = [n for n in names if len(n) >= 2 and n[0].isupper()]
names_file.write("\n".join(names))
diff --git a/doc/contributing.rst b/doc/contributing.rst
index 368029081..8abdad53f 100644
--- a/doc/contributing.rst
+++ b/doc/contributing.rst
@@ -73,6 +73,7 @@ Running the tests
-----------------
.. To get the test output:
+ # Use the lowest of the PYVERSIONS
# Resize terminal width to 95
% make sterile
@@ -81,66 +82,65 @@ Running the tests
The tests are written mostly as standard unittest-style tests, and are run with
pytest running under `tox`_::
- $ python3 -m tox -e py38
- py38: wheel-0.43.0-py3-none-any.whl already present in /Users/ned/Library/Application Support/virtualenv/wheel/3.8/embed/3/wheel.json
- py38: pip-24.0-py3-none-any.whl already present in /Users/ned/Library/Application Support/virtualenv/wheel/3.8/embed/3/pip.json
- py38: setuptools-69.2.0-py3-none-any.whl already present in /Users/ned/Library/Application Support/virtualenv/wheel/3.8/embed/3/setuptools.json
- py38: install_deps> python -m pip install -U -r requirements/pip.pip -r requirements/pytest.pip -r requirements/light-threads.pip
+ % python3 -m tox -e py39
+ py39: wheel-0.45.1-py3-none-any.whl already present in /Users/ned/Library/Application Support/virtualenv/wheel/3.9/embed/3/wheel.json
+ py39: pip-25.0.1-py3-none-any.whl already present in /Users/ned/Library/Application Support/virtualenv/wheel/3.9/embed/3/pip.json
+ py39: setuptools-78.1.0-py3-none-any.whl already present in /Users/ned/Library/Application Support/virtualenv/wheel/3.9/embed/3/setuptools.json
+ py39: install_deps> python -m pip install -U -r requirements/pip.pip -r requirements/pytest.pip -r requirements/light-threads.pip
.pkg: install_requires> python -I -m pip install setuptools
- .pkg: _optional_hooks> python /usr/local/virtualenvs/coverage/lib/python3.8/site-packages/pyproject_api/_backend.py True setuptools.build_meta
- .pkg: get_requires_for_build_editable> python /usr/local/virtualenvs/coverage/lib/python3.8/site-packages/pyproject_api/_backend.py True setuptools.build_meta
- .pkg: install_requires_for_build_editable> python -I -m pip install wheel
- .pkg: build_editable> python /usr/local/virtualenvs/coverage/lib/python3.8/site-packages/pyproject_api/_backend.py True setuptools.build_meta
- py38: install_package_deps> python -m pip install -U 'tomli; python_full_version <= "3.11.0a6"'
- py38: install_package> python -m pip install -U --force-reinstall --no-deps .tox/.tmp/package/1/coverage-7.4.5a0.dev1-0.editable-cp38-cp38-macosx_14_0_arm64.whl
- py38: commands[0]> python igor.py zip_mods
- py38: commands[1]> python setup.py --quiet build_ext --inplace
- ld: warning: duplicate -rpath '/usr/local/pyenv/pyenv/versions/3.8.18/lib' ignored
- ld: warning: duplicate -rpath '/opt/homebrew/lib' ignored
- py38: commands[2]> python -m pip install -q -e .
- py38: commands[3]> python igor.py test_with_core ctrace
- === CPython 3.8.18 with C tracer (.tox/py38/bin/python) ===
+ .pkg: _optional_hooks> python /usr/local/virtualenvs/coverage/lib/python3.9/site-packages/pyproject_api/_backend.py True setuptools.build_meta
+ .pkg: get_requires_for_build_editable> python /usr/local/virtualenvs/coverage/lib/python3.9/site-packages/pyproject_api/_backend.py True setuptools.build_meta
+ .pkg: build_editable> python /usr/local/virtualenvs/coverage/lib/python3.9/site-packages/pyproject_api/_backend.py True setuptools.build_meta
+ py39: install_package_deps> python -m pip install -U 'tomli; python_full_version <= "3.11.0a6"'
+ py39: install_package> python -m pip install -U --force-reinstall --no-deps .tox/.tmp/package/1/coverage-7.8.1a0.dev1-0.editable-cp39-cp39-macosx_15_0_arm64.whl
+ py39: commands[0]> python igor.py zip_mods
+ py39: commands[1]> python igor.py remove_extension
+ py39: commands[2]> python igor.py test_with_core pytrace
+ === CPython 3.9.21 (gil) with Python tracer (.tox/py39/bin/python) ===
bringing up nodes...
- ....................................................................................... [ 6%]
- .....................................................x...x............s......s.s....s.. [ 12%]
- ....................................................................................... [ 18%]
- ....................................................................................... [ 25%]
- ....................................................................................... [ 31%]
- ....................................................................................... [ 37%]
- ....................................................................................... [ 44%]
- ....................................................................................... [ 50%]
- ....................................................................................... [ 56%]
- ........................s...........s.................................................. [ 63%]
- ...........................................................................s........... [ 69%]
- .................................s............s.s.................s.................... [ 75%]
- ...........................................s........................................s.. [ 81%]
- ................................s...................................................... [ 88%]
+ ....................................................................................... [ 5%]
+ ..................................................................x................s... [ 11%]
+ ......s...s.....s....s......s.s.s.s.................................................... [ 17%]
+ ...........................................s..ss...ss.ss.ss............................ [ 23%]
+ ....................................................................................... [ 29%]
+ ....................................................................................... [ 35%]
+ ....................................................................................... [ 41%]
+ ................................................s...................................... [ 47%]
+ ....................................................................................... [ 53%]
+ ...................................................s..........s........................ [ 59%]
+ ....................................................................................... [ 65%]
+ ..........................ssss......................................................... [ 71%]
+ ..s.....s.ss..........................ss...................s.s..sssssss.ssssss.sssss... [ 77%]
+ .........ss..........................s...s.s......s........s........................s.. [ 83%]
+ .............................s......................................................... [ 88%]
....................................................................................... [ 94%]
- ............................................................s................... [100%]
- 1368 passed, 15 skipped, 2 xfailed in 13.10s
- py38: commands[4]> python igor.py remove_extension
- py38: commands[5]> python igor.py test_with_core pytrace
- === CPython 3.8.18 with Python tracer (.tox/py38/bin/python) ===
+ .............................................s.......................ss.... [100%]
+ 1403 passed, 63 skipped, 1 xfailed in 15.05s
+ py39: commands[3]> python setup.py --quiet build_ext --inplace
+ py39: commands[4]> python -m pip install -q -e .
+ py39: commands[5]> python igor.py test_with_core ctrace
+ === CPython 3.9.21 (gil) with C tracer (.tox/py39/bin/python) ===
bringing up nodes...
- ....................................................................................... [ 6%]
- ....................x..x.............................................s.ss...s.......... [ 12%]
- ..........................................................................s.ss.s..s.... [ 18%]
- s........s........s..s...s............................................................. [ 25%]
- ................s...................................................................... [ 31%]
- ...................s......ss..........................ssss...........................s. [ 37%]
- ....................................................................................... [ 43%]
- ....................................................................................... [ 50%]
- .................................................................s..................... [ 56%]
- ........s..s.........sss.s............................................................. [ 62%]
- ...................................................................ss.................. [ 69%]
- ..............................................ss...........s.s......................... [ 75%]
- ................................ssssss................................................. [ 81%]
- ......s...ss........ss................................................................. [ 88%]
- .............................................s......................................... [ 94%]
- .......................................................................ss....... [100%]
- 1333 passed, 50 skipped, 2 xfailed in 11.17s
- py38: OK (37.60=setup[9.10]+cmd[0.11,0.49,2.83,13.59,0.11,11.39] seconds)
- congratulations :) (37.91 seconds)
+ ....................................................................................... [ 5%]
+ ..........................................sx................................s.......... [ 11%]
+ ..........ss........s....................................s............................. [ 17%]
+ ..............................sss...................................................... [ 23%]
+ ..............................................................s........................ [ 29%]
+ ....................................................................................... [ 35%]
+ ....................................................................................... [ 41%]
+ ......................................................s................................ [ 47%]
+ .............................................s......................................... [ 53%]
+ .......s..................s............................................................ [ 59%]
+ ....................................................................................s.. [ 65%]
+ .......................................................ss.......................s...... [ 71%]
+ ....................................................s............................ss.... [ 77%]
+ ..........................s...................s........................................ [ 83%]
+ ....................................................................................... [ 88%]
+ ............................s......s................................................... [ 94%]
+ .................................................................s......... [100%]
+ 1440 passed, 26 skipped, 1 xfailed in 12.38s
+ py39: OK (40.04=setup[9.03]+cmd[0.17,0.09,15.40,0.13,2.47,12.77] seconds)
+ congratulations :) (40.61 seconds)
Tox runs the complete test suite a few times for each version of Python you
have installed. The first run uses the C implementation of the trace function,
@@ -165,15 +165,17 @@ respectively. The pytest ``-k`` option selects tests based on a word in their
name, which can be very convenient for ad-hoc test selection. Of course you
can combine tox and pytest options::
- $ python3 -m tox -q -e py310 -- -n 0 -vv -k hash
- ================================== test session starts ===================================
- platform darwin -- Python 3.10.13, pytest-8.1.1, pluggy-1.4.0 -- /Users/ned/coverage/trunk/.tox/py310/bin/python
+ % python3 -m tox -q -e py310 -- -n 0 -vv -k hash
+ Skipping tests with Python tracer: Only one core: not running pytrace
+ === CPython 3.10.16 (gil) with C tracer (.tox/py310/bin/python) ===
+ ===================================== test session starts =====================================
+ platform darwin -- Python 3.10.16, pytest-8.3.5, pluggy-1.5.0 -- /Users/ned/coverage/trunk/.tox/py310/bin/python
cachedir: .tox/py310/.pytest_cache
hypothesis profile 'default' -> database=DirectoryBasedExampleDatabase(PosixPath('/Users/ned/coverage/trunk/.hypothesis/examples'))
rootdir: /Users/ned/coverage/trunk
configfile: pyproject.toml
- plugins: flaky-3.8.1, xdist-3.5.0, hypothesis-6.99.6
- collected 1385 items / 1375 deselected / 10 selected
+ plugins: flaky-3.8.1, hypothesis-6.128.1, xdist-3.6.1
+ collected 1467 items / 1457 deselected / 10 selected
run-last-failure: no previously failed tests, not deselecting items.
tests/test_data.py::CoverageDataTest::test_add_to_hash_with_lines PASSED [ 10%]
@@ -187,11 +189,9 @@ can combine tox and pytest options::
tests/test_misc.py::HasherTest::test_dict_hashing PASSED [ 90%]
tests/test_misc.py::HasherTest::test_dict_collision PASSED [100%]
- ========================== 10 passed, 1375 deselected in 0.60s ===========================
- Skipping tests with Python tracer: Only one core: not running pytrace
- py310: OK (6.41 seconds)
- congratulations :) (6.72 seconds)
-
+ ============================= 10 passed, 1457 deselected in 3.13s =============================
+ py310: OK (16.62 seconds)
+ congratulations :) (16.97 seconds)
You can also affect the test runs with environment variables:
@@ -237,7 +237,8 @@ Other style questions are best answered by looking at the existing code.
Formatting of docstrings, comments, long lines, and so on, should match the
code that already exists.
-Many people love `black`_, but I would prefer not to run it on coverage.py.
+Many people love auto-formatting with `black`_ or `ruff`_, but I would prefer
+not to on coverage.py.
Continuous integration
@@ -299,6 +300,7 @@ fixes. If you need help writing tests, please ask.
.. _editorconfig.org: http://editorconfig.org
.. _tox: https://tox.readthedocs.io/
.. _black: https://pypi.org/project/black/
+.. _ruff: https://pypi.org/project/ruff/
.. _set_env.py: https://nedbatchelder.com/blog/201907/set_envpy.html
.. _pytest test selectors: https://doc.pytest.org/en/stable/usage.html#specifying-which-tests-to-run
.. _sys.monitoring: https://docs.python.org/3/library/sys.monitoring.html
diff --git a/doc/index.rst b/doc/index.rst
index ea3486e12..dd4489b7c 100644
--- a/doc/index.rst
+++ b/doc/index.rst
@@ -18,7 +18,7 @@ supported on:
.. PYVERSIONS
-* Python 3.9 through 3.14 alpha 6, including free-threading.
+* Python 3.9 through 3.14 beta 1, including free-threading.
* PyPy3 versions 3.9, 3.10, and 3.11.
.. ifconfig:: prerelease
@@ -90,7 +90,7 @@ Getting started is easy:
.. tab:: unittest
- Change "python" to "coverage run", so this::
+ Change your python command name to "coverage run", so this::
$ python3 -m unittest discover
@@ -134,20 +134,21 @@ Getting started is easy:
listings detailing missed lines::
$ coverage html
+ Wrote HTML report to htmlcov/index.html
.. ifconfig:: not prerelease
- Then open htmlcov/index.html in your browser, to see a
- `report like this`_.
+ Then open `htmlcov/index.html `__ in your browser
+ to see a `report like this `__.
.. ifconfig:: prerelease
- Then open htmlcov/index.html in your browser, to see a
- `report like this one`_.
+ Then open `htmlcov/index.html `__ in your browser
+ to see a `report like this `__.
-.. _report like this: https://nedbatchelder.com/files/sample_coverage_html/index.html
-.. _report like this one: https://nedbatchelder.com/files/sample_coverage_html_beta/index.html
+.. _htmlreport: https://nedbatchelder.com/files/sample_coverage_html/index.html
+.. _betahtmlreport: https://nedbatchelder.com/files/sample_coverage_html_beta/index.html
.. _nose state: https://github.com/nose-devs/nose/commit/0f40fa995384afad77e191636c89eb7d5b8870ca
.. _include tests: https://nedbatchelder.com/blog/202008/you_should_include_your_tests_in_coverage.html
diff --git a/doc/requirements.in b/doc/requirements.in
index 4486c06ac..3c77e0f7e 100644
--- a/doc/requirements.in
+++ b/doc/requirements.in
@@ -2,13 +2,14 @@
# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt
# PyPI requirements input for building documentation for coverage.py
-# "make doc_upgrade" turns this into doc/requirements.pip
+# "make upgrade" turns this into doc/requirements.pip
-c ../requirements/pins.pip
cogapp
doc8
pyenchant
+scriv
sphinx
sphinx-autobuild
sphinx_rtd_theme
diff --git a/doc/requirements.pip b/doc/requirements.pip
index ae5de5420..535525f49 100644
--- a/doc/requirements.pip
+++ b/doc/requirements.pip
@@ -1,23 +1,26 @@
-#
-# This file is autogenerated by pip-compile with Python 3.11
-# by the following command:
-#
-# make doc_upgrade
-#
+# This file was autogenerated by uv via the following command:
+# make upgrade
alabaster==1.0.0
# via sphinx
-anyio==4.8.0
+anyio==4.9.0
# via
# starlette
# watchfiles
+attrs==25.3.0
+ # via scriv
babel==2.17.0
# via sphinx
-certifi==2025.1.31
+certifi==2025.4.26
# via requests
-charset-normalizer==3.4.1
+charset-normalizer==3.4.2
# via requests
-click==8.1.8
- # via uvicorn
+click==8.2.1
+ # via
+ # click-log
+ # scriv
+ # uvicorn
+click-log==0.4.0
+ # via scriv
cogapp==3.4.1
# via -r doc/requirements.in
colorama==0.4.6
@@ -30,7 +33,7 @@ docutils==0.21.2
# restructuredtext-lint
# sphinx
# sphinx-rtd-theme
-h11==0.14.0
+h11==0.16.0
# via uvicorn
idna==3.10
# via
@@ -39,10 +42,16 @@ idna==3.10
imagesize==1.4.1
# via sphinx
jinja2==3.1.6
- # via sphinx
+ # via
+ # scriv
+ # sphinx
+markdown-it-py==3.0.0
+ # via scriv
markupsafe==3.0.2
# via jinja2
-packaging==24.2
+mdurl==0.1.2
+ # via markdown-it-py
+packaging==25.0
# via sphinx
pbr==6.1.1
# via stevedore
@@ -60,15 +69,20 @@ regex==2024.11.6
# via sphinx-lint
requests==2.32.3
# via
+ # scriv
# sphinx
# sphinxcontrib-spelling
restructuredtext-lint==1.4.0
# via doc8
roman-numerals-py==3.1.0
# via sphinx
+scriv==1.7.0
+ # via -r doc/requirements.in
+setuptools==80.8.0
+ # via pbr
sniffio==1.3.1
# via anyio
-snowballstemmer==2.2.0
+snowballstemmer==3.0.1
# via sphinx
sphinx==8.2.3
# via
@@ -105,21 +119,17 @@ sphinxcontrib-serializinghtml==2.0.0
# via sphinx
sphinxcontrib-spelling==8.0.1
# via -r doc/requirements.in
-starlette==0.46.1
+starlette==0.46.2
# via sphinx-autobuild
stevedore==5.4.1
# via doc8
-typing-extensions==4.12.2
+typing-extensions==4.13.2
# via anyio
-urllib3==2.3.0
+urllib3==2.4.0
# via requests
-uvicorn==0.34.0
+uvicorn==0.34.2
# via sphinx-autobuild
-watchfiles==1.0.4
+watchfiles==1.0.5
# via sphinx-autobuild
websockets==15.0.1
# via sphinx-autobuild
-
-# The following packages are considered to be unsafe in a requirements file:
-setuptools==76.0.0
- # via pbr
diff --git a/doc/sample_html/class_index.html b/doc/sample_html/class_index.html
index c4f4afb5e..5563aaf11 100644
--- a/doc/sample_html/class_index.html
+++ b/doc/sample_html/class_index.html
@@ -56,8 +56,8 @@
- coverage.py v7.8.0,
- created at 2025-03-30 15:44 -0400
+ coverage.py v7.8.2,
+ created at 2025-05-23 06:43 -0400
@@ -537,8 +537,8 @@
@@ -97,8 +97,8 @@
^ index
» next
- coverage.py v7.8.0,
- created at 2025-03-30 15:44 -0400
+ coverage.py v7.8.2,
+ created at 2025-05-23 06:43 -0400
diff --git a/doc/sample_html/z_7b071bdc2a35fa80___main___py.html b/doc/sample_html/z_7b071bdc2a35fa80___main___py.html
index 156a8b29c..ea87d1d8a 100644
--- a/doc/sample_html/z_7b071bdc2a35fa80___main___py.html
+++ b/doc/sample_html/z_7b071bdc2a35fa80___main___py.html
@@ -66,8 +66,8 @@
^ index
» next
- coverage.py v7.8.0,
- created at 2025-03-30 15:44 -0400
+ coverage.py v7.8.2,
+ created at 2025-05-23 06:43 -0400
@@ -97,8 +97,8 @@
^ index
» next
- coverage.py v7.8.0,
- created at 2025-03-30 15:44 -0400
+ coverage.py v7.8.2,
+ created at 2025-05-23 06:43 -0400
diff --git a/doc/sample_html/z_7b071bdc2a35fa80_cogapp_py.html b/doc/sample_html/z_7b071bdc2a35fa80_cogapp_py.html
index 4e1ebbd83..772d350a8 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.8.0,
- created at 2025-03-30 15:44 -0400
+ coverage.py v7.8.2,
+ created at 2025-05-23 06:43 -0400
@@ -928,8 +928,8 @@
^ index
» next
- coverage.py v7.8.0,
- created at 2025-03-30 15:44 -0400
+ coverage.py v7.8.2,
+ created at 2025-05-23 06:43 -0400
diff --git a/doc/sample_html/z_7b071bdc2a35fa80_makefiles_py.html b/doc/sample_html/z_7b071bdc2a35fa80_makefiles_py.html
index 43a29b1e3..25a0a3086 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.8.0,
- created at 2025-03-30 15:44 -0400
+ coverage.py v7.8.2,
+ created at 2025-05-23 06:43 -0400
@@ -127,8 +127,8 @@
^ index
» next
- coverage.py v7.8.0,
- created at 2025-03-30 15:44 -0400
+ coverage.py v7.8.2,
+ created at 2025-05-23 06:43 -0400
diff --git a/doc/sample_html/z_7b071bdc2a35fa80_test_cogapp_py.html b/doc/sample_html/z_7b071bdc2a35fa80_test_cogapp_py.html
index 45d3d3ccb..8f97dcc21 100644
--- a/doc/sample_html/z_7b071bdc2a35fa80_test_cogapp_py.html
+++ b/doc/sample_html/z_7b071bdc2a35fa80_test_cogapp_py.html
@@ -66,8 +66,8 @@
^ index
» next
- coverage.py v7.8.0,
- created at 2025-03-30 15:44 -0400
+ coverage.py v7.8.2,
+ created at 2025-05-23 06:43 -0400
@@ -2135,7 +2135,7 @@
2051 d[f"f{i}.cog"] = (
2052 "x\n" * i
2053 + "[[[cog\n"
- 2054 + f"assert cog.firstLineNum == int(FIRST) == {i+1}\n"
+ 2054 + f"assert cog.firstLineNum == int(FIRST) == {i + 1}\n"
2055 + "]]]\n"
2056 + "[[[end]]]\n"
2057 )
@@ -2146,7 +2146,7 @@
2062 def thread_main(num):
2063 try:
2064 ret = Cog().main(
- 2065 ["cog.py", "-r", "-D", f"FIRST={num+1}", f"f{num}.cog"]
+ 2065 ["cog.py", "-r", "-D", f"FIRST={num + 1}", f"f{num}.cog"]
2066 )
2067 assert ret == 0
2068 except Exception as exc: # pragma: no cover (only happens on test failure)
@@ -2737,8 +2737,8 @@
^ index
» next
- coverage.py v7.8.0,
- created at 2025-03-30 15:44 -0400
+ coverage.py v7.8.2,
+ created at 2025-05-23 06:43 -0400
diff --git a/doc/sample_html/z_7b071bdc2a35fa80_test_makefiles_py.html b/doc/sample_html/z_7b071bdc2a35fa80_test_makefiles_py.html
index 43d9b519d..bd750f15b 100644
--- a/doc/sample_html/z_7b071bdc2a35fa80_test_makefiles_py.html
+++ b/doc/sample_html/z_7b071bdc2a35fa80_test_makefiles_py.html
@@ -66,8 +66,8 @@
^ index
» next
- coverage.py v7.8.0,
- created at 2025-03-30 15:44 -0400
+ coverage.py v7.8.2,
+ created at 2025-05-23 06:43 -0400
@@ -205,8 +205,8 @@
^ index
» next
- coverage.py v7.8.0,
- created at 2025-03-30 15:44 -0400
+ coverage.py v7.8.2,
+ created at 2025-05-23 06:43 -0400
diff --git a/doc/sample_html/z_7b071bdc2a35fa80_test_whiteutils_py.html b/doc/sample_html/z_7b071bdc2a35fa80_test_whiteutils_py.html
index 55500d4a5..6d13a8b8f 100644
--- a/doc/sample_html/z_7b071bdc2a35fa80_test_whiteutils_py.html
+++ b/doc/sample_html/z_7b071bdc2a35fa80_test_whiteutils_py.html
@@ -66,8 +66,8 @@
^ index
» next
- coverage.py v7.8.0,
- created at 2025-03-30 15:44 -0400
+ coverage.py v7.8.2,
+ created at 2025-05-23 06:43 -0400
@@ -186,8 +186,8 @@
^ index
» next
- coverage.py v7.8.0,
- created at 2025-03-30 15:44 -0400
+ coverage.py v7.8.2,
+ created at 2025-05-23 06:43 -0400
diff --git a/doc/sample_html/z_7b071bdc2a35fa80_utils_py.html b/doc/sample_html/z_7b071bdc2a35fa80_utils_py.html
index ef5f617af..06c232685 100644
--- a/doc/sample_html/z_7b071bdc2a35fa80_utils_py.html
+++ b/doc/sample_html/z_7b071bdc2a35fa80_utils_py.html
@@ -66,8 +66,8 @@
^ index
» next
- coverage.py v7.8.0,
- created at 2025-03-30 15:44 -0400
+ coverage.py v7.8.2,
+ created at 2025-05-23 06:43 -0400
@@ -159,8 +159,8 @@
^ index
» next
- coverage.py v7.8.0,
- created at 2025-03-30 15:44 -0400
+ coverage.py v7.8.2,
+ created at 2025-05-23 06:43 -0400
diff --git a/doc/sample_html/z_7b071bdc2a35fa80_whiteutils_py.html b/doc/sample_html/z_7b071bdc2a35fa80_whiteutils_py.html
index c8b77d090..72f49e1d6 100644
--- a/doc/sample_html/z_7b071bdc2a35fa80_whiteutils_py.html
+++ b/doc/sample_html/z_7b071bdc2a35fa80_whiteutils_py.html
@@ -66,8 +66,8 @@
^ index
» next
- coverage.py v7.8.0,
- created at 2025-03-30 15:44 -0400
+ coverage.py v7.8.2,
+ created at 2025-05-23 06:43 -0400
@@ -159,8 +159,8 @@
^ index
» next
- coverage.py v7.8.0,
- created at 2025-03-30 15:44 -0400
+ coverage.py v7.8.2,
+ created at 2025-05-23 06:43 -0400
diff --git a/howto.txt b/howto.txt
index 70a209339..5b3955e19 100644
--- a/howto.txt
+++ b/howto.txt
@@ -1,6 +1,8 @@
* Release checklist
- Check that the current virtualenv matches the current coverage branch.
+ $ python -V # should match lowest PYVERSIONS
+ $ make install
- start branch for release work
$ make relbranch
- check version number in coverage/version.py
@@ -33,9 +35,7 @@
- IF PRE-RELEASE:
$ make sample_html_beta
- IF NOT PRE-RELEASE:
- $ make sample_html
- - check in the new sample html
- $ make relcommit2
+ $ make sample_html relcommit2
- Build and publish docs:
- IF PRE-RELEASE:
$ make publishbeta
diff --git a/igor.py b/igor.py
index aecd46299..8bc0c7086 100644
--- a/igor.py
+++ b/igor.py
@@ -8,12 +8,12 @@
"""
-import contextlib
import datetime
import glob
import inspect
import itertools
import os
+import os.path
import platform
import pprint
import re
@@ -22,7 +22,6 @@
import sysconfig
import textwrap
import types
-import warnings
import zipfile
try:
@@ -39,15 +38,8 @@
PYPY = platform.python_implementation() == "PyPy"
-@contextlib.contextmanager
-def ignore_warnings():
- """Context manager to ignore warning within the with statement."""
- with warnings.catch_warnings():
- warnings.simplefilter("ignore")
- yield
-
-
-VERBOSITY = int(os.getenv("COVERAGE_IGOR_VERBOSE", "0"))
+# $set_env.py: COVERAGE_IGOR_VERBOSE - How much chatter from igor.py (default 1)
+VERBOSITY = int(os.getenv("COVERAGE_IGOR_VERBOSE", "1"))
# Functions named do_* are executable from the command line: do_blah is run
# by "python igor.py blah".
@@ -86,21 +78,26 @@ def do_remove_extension(*args):
)
roots = [root]
else:
- roots = ["coverage", "build/*/coverage"]
+ roots = [
+ "coverage",
+ "build/*/coverage",
+ ".tox/*/[Ll]ib/*/site-packages/coverage",
+ ".tox/*/[Ll]ib/site-packages/coverage",
+ ]
for root, pattern in itertools.product(roots, so_patterns):
- pattern = os.path.join(root, pattern.strip())
- if VERBOSITY:
- print(f"Searching for {pattern}")
+ pattern = os.path.join(root, pattern)
+ if VERBOSITY > 1:
+ print(f"Searching for {pattern} from {os.getcwd()}")
for filename in glob.glob(pattern):
if os.path.exists(filename):
- if VERBOSITY:
- print(f"Removing {filename}")
+ if VERBOSITY > 1:
+ print(f"Removing {os.path.abspath(filename)}")
try:
os.remove(filename)
except OSError as exc:
- if VERBOSITY:
- print(f"Couldn't remove {filename}: {exc}")
+ if VERBOSITY > 1:
+ print(f"Couldn't remove {os.path.abspath(filename)}: {exc}")
def label_for_core(core):
@@ -143,13 +140,9 @@ def should_skip(core):
skipper = f"No C core for {platform.python_implementation()}"
if skipper:
- msg = "Skipping tests " + label_for_core(core)
- if len(skipper) > 1:
- msg += ": " + skipper
+ return f"Skipping tests {label_for_core(core)}: {skipper}"
else:
- msg = ""
-
- return msg
+ return ""
def make_env_id(core):
@@ -189,7 +182,7 @@ def run_tests_with_coverage(core, *runner_args):
# There's an entry in "make clean" to get rid of this file.
pth_dir = sysconfig.get_path("purelib")
pth_path = os.path.join(pth_dir, "zzz_metacov.pth")
- with open(pth_path, "w") as pth_file:
+ with open(pth_path, "w", encoding="utf-8") as pth_file:
pth_file.write("import coverage; coverage.process_startup()\n")
suffix = f"{make_env_id(core)}_{platform.platform()}"
@@ -256,7 +249,8 @@ def do_test_with_core(core, *runner_args):
# If we should skip these tests, skip them.
skip_msg = should_skip(core)
if skip_msg:
- print(skip_msg)
+ if VERBOSITY > 0:
+ print(skip_msg)
return None
os.environ["COVERAGE_CORE"] = core
@@ -367,14 +361,14 @@ def get_release_facts():
def update_file(fname, pattern, replacement):
"""Update the contents of a file, replacing pattern with replacement."""
- with open(fname) as fobj:
+ with open(fname, encoding="utf-8") as fobj:
old_text = fobj.read()
new_text = re.sub(pattern, replacement, old_text, count=1)
if new_text != old_text:
print(f"Updating {fname}")
- with open(fname, "w") as fobj:
+ with open(fname, "w", encoding="utf-8") as fobj:
fobj.write(new_text)
diff --git a/lab/branch_trace.py b/lab/branch_trace.py
index 7e8e88f9a..c2623c477 100644
--- a/lab/branch_trace.py
+++ b/lab/branch_trace.py
@@ -11,7 +11,7 @@ def trace(frame, event, arg):
last = this
return trace
-code = open(sys.argv[1]).read()
+code = open(sys.argv[1], encoding="utf-8").read()
sys.settrace(trace)
exec(code)
print(sorted(pairs))
diff --git a/lab/extract_code.py b/lab/extract_code.py
index 3940a2042..cf32c1730 100644
--- a/lab/extract_code.py
+++ b/lab/extract_code.py
@@ -52,7 +52,7 @@ def f(a, b):
fname, lineno = sys.argv[1:]
lineno = int(lineno)
-with open(fname) as code_file:
+with open(fname, encoding="utf-8") as code_file:
lines = ["", *code_file]
# Find opening triple-quote
diff --git a/lab/goals.py b/lab/goals.py
index 4bda0f0f5..13f3f68a5 100644
--- a/lab/goals.py
+++ b/lab/goals.py
@@ -64,7 +64,7 @@ def main(argv):
print("Need either --file or --group")
return 1
- with open("coverage.json") as j:
+ with open("coverage.json", encoding="utf-8") as j:
data = json.load(j)
all_files = list(data["files"].keys())
selected = select_files(all_files, args.pattern)
diff --git a/lab/run_sysmon.py b/lab/run_sysmon.py
index f88988bbe..fa7d44d7b 100644
--- a/lab/run_sysmon.py
+++ b/lab/run_sysmon.py
@@ -9,7 +9,7 @@
print(sys.version)
the_program = sys.argv[1]
-code = compile(open(the_program).read(), filename=the_program, mode="exec")
+code = compile(open(the_program, encoding="utf-8").read(), filename=the_program, mode="exec")
my_id = sys.monitoring.COVERAGE_ID
sys.monitoring.use_tool_id(my_id, "run_sysmon.py")
diff --git a/lab/run_trace.py b/lab/run_trace.py
index 54e6a53f0..7e8354877 100644
--- a/lab/run_trace.py
+++ b/lab/run_trace.py
@@ -32,6 +32,6 @@ def trace(frame, event, arg):
print(sys.version)
the_program = sys.argv[1]
-code = open(the_program).read()
+code = open(the_program, encoding="utf-8").read()
sys.settrace(trace)
exec(code)
diff --git a/lab/show_pyc.py b/lab/show_pyc.py
index 0585b937f..a044eca2c 100644
--- a/lab/show_pyc.py
+++ b/lab/show_pyc.py
@@ -44,7 +44,7 @@ def show_pyc_file(fname):
show_code(code)
def show_py_file(fname):
- text = open(fname).read().replace('\r\n', '\n')
+ text = open(fname, encoding="utf-8").read().replace('\r\n', '\n')
show_py_text(text, fname=fname)
def show_py_text(text, fname=""):
diff --git a/pyproject.toml b/pyproject.toml
index 1d3199d12..54114d1db 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -147,6 +147,20 @@ balanced_clumps = [
"GetZipBytesTest",
]
+## Scriv
+
+[tool.scriv]
+changelog = "tmp/only-changes.md"
+ghrel_template = """
+## {{title}}
+
+{{body}}
+
+:arrow_right:\u00a0 PyPI page: [coverage {{version}}](https://pypi.org/project/coverage/{{version}}).
+:arrow_right:\u00a0 To install: `python3 -m pip install coverage=={{version}}`
+
+"""
+
## RUFF
# We aren't using ruff for real yet...
diff --git a/requirements/dev.in b/requirements/dev.in
index 3e5c45c2c..366206ed9 100644
--- a/requirements/dev.in
+++ b/requirements/dev.in
@@ -19,9 +19,10 @@ pylint
readme_renderer
# for kitting.
+libsass
requests
+scriv
twine
-libsass
# Just so I have a debugger if I want it.
pudb
diff --git a/requirements/dev.pip b/requirements/dev.pip
index 13e6b0d81..afac0d62a 100644
--- a/requirements/dev.pip
+++ b/requirements/dev.pip
@@ -1,66 +1,70 @@
-#
-# This file is autogenerated by pip-compile with Python 3.9
-# by the following command:
-#
+# This file was autogenerated by uv via the following command:
# make upgrade
-#
-astroid==3.3.9
+astroid==3.3.10
# via pylint
-attrs==25.1.0
- # via hypothesis
+attrs==25.3.0
+ # via
+ # hypothesis
+ # scriv
backports-tarfile==1.2.0
# via jaraco-context
build==1.2.2.post1
# via check-manifest
cachetools==5.5.2
# via tox
-certifi==2025.1.31
+certifi==2025.4.26
# via requests
chardet==5.2.0
# via tox
-charset-normalizer==3.4.1
+charset-normalizer==3.4.2
# via requests
check-manifest==0.50
# via -r requirements/dev.in
+click==8.1.8
+ # via
+ # click-log
+ # scriv
+click-log==0.4.0
+ # via scriv
cogapp==3.4.1
# via -r requirements/dev.in
colorama==0.4.6
# via
- # -r /Users/ned/coverage/trunk/requirements/pytest.in
- # -r /Users/ned/coverage/trunk/requirements/tox.in
+ # -r requirements/pytest.in
+ # -r requirements/tox.in
# tox
-dill==0.3.9
+dill==0.4.0
# via pylint
distlib==0.3.9
# via virtualenv
docutils==0.21.2
# via readme-renderer
-exceptiongroup==1.2.2
+exceptiongroup==1.3.0
# via
# hypothesis
# pytest
execnet==2.1.1
# via pytest-xdist
-filelock==3.17.0
+filelock==3.18.0
# via
# tox
# virtualenv
flaky==3.8.1
- # via -r /Users/ned/coverage/trunk/requirements/pytest.in
-greenlet==3.1.1
+ # via -r requirements/pytest.in
+greenlet==3.2.2
# via -r requirements/dev.in
-hypothesis==6.128.1
- # via -r /Users/ned/coverage/trunk/requirements/pytest.in
+hypothesis==6.131.21
+ # via -r requirements/pytest.in
id==1.5.0
# via twine
idna==3.10
# via requests
-importlib-metadata==8.6.1
+importlib-metadata==8.7.0
# via
# build
# keyring
# twine
-iniconfig==2.0.0
+iniconfig==2.1.0
# via pytest
isort==6.0.1
# via pylint
@@ -72,23 +76,29 @@ jaraco-functools==4.1.0
# via keyring
jedi==0.19.2
# via pudb
+jinja2==3.1.6
+ # via scriv
keyring==25.6.0
# via twine
libsass==0.23.0
# via -r requirements/dev.in
markdown-it-py==3.0.0
- # via rich
+ # via
+ # rich
+ # scriv
+markupsafe==3.0.2
+ # via jinja2
mccabe==0.7.0
# via pylint
mdurl==0.1.2
# via markdown-it-py
-more-itertools==10.6.0
+more-itertools==10.7.0
# via
# jaraco-classes
# jaraco-functools
nh3==0.2.21
# via readme-renderer
-packaging==24.2
+packaging==25.0
# via
# build
# pudb
@@ -98,35 +108,37 @@ packaging==24.2
# twine
parso==0.8.4
# via jedi
-platformdirs==4.3.6
+pip==25.1.1
+ # via -r requirements/pip.in
+platformdirs==4.3.8
# via
# pylint
# tox
# virtualenv
-pluggy==1.5.0
+pluggy==1.6.0
# via
# pytest
# tox
-pudb==2024.1.3
+pudb==2025.1
# via -r requirements/dev.in
pygments==2.19.1
# via
- # -r /Users/ned/coverage/trunk/requirements/pytest.in
+ # -r requirements/pytest.in
# pudb
# readme-renderer
# rich
-pylint==3.3.5
+pylint==3.3.7
# via -r requirements/dev.in
-pyproject-api==1.9.0
+pyproject-api==1.9.1
# via tox
pyproject-hooks==1.2.0
# via build
pytest==8.3.5
# via
- # -r /Users/ned/coverage/trunk/requirements/pytest.in
+ # -r requirements/pytest.in
# pytest-xdist
pytest-xdist==3.6.1
- # via -r /Users/ned/coverage/trunk/requirements/pytest.in
+ # via -r requirements/pytest.in
readme-renderer==44.0
# via
# -r requirements/dev.in
@@ -136,13 +148,20 @@ requests==2.32.3
# -r requirements/dev.in
# id
# requests-toolbelt
+ # scriv
# twine
requests-toolbelt==1.0.0
# via twine
rfc3986==2.0.0
# via twine
-rich==13.9.4
+rich==14.0.0
# via twine
+scriv==1.7.0
+ # via -r requirements/dev.in
+setuptools==80.8.0
+ # via
+ # -r requirements/pip.in
+ # check-manifest
sortedcontainers==2.4.0
# via hypothesis
tabulate==0.9.0
@@ -157,45 +176,36 @@ tomli==2.2.1
# tox
tomlkit==0.13.2
# via pylint
-tox==4.24.1
+tox==4.26.0
# via
- # -r /Users/ned/coverage/trunk/requirements/tox.in
+ # -r requirements/tox.in
# tox-gh
tox-gh==1.5.0
- # via -r /Users/ned/coverage/trunk/requirements/tox.in
+ # via -r requirements/tox.in
twine==6.1.0
# via -r requirements/dev.in
-typing-extensions==4.12.2
+typing-extensions==4.13.2
# via
# astroid
+ # exceptiongroup
# pylint
# rich
# tox
- # urwid
-urllib3==2.3.0
+urllib3==2.4.0
# via
# requests
# twine
-urwid==2.6.16
+urwid==3.0.2
# via
# pudb
# urwid-readline
urwid-readline==0.15.1
# via pudb
-virtualenv==20.28.1
+virtualenv==20.31.2
# via
- # -c /Users/ned/coverage/trunk/requirements/pins.pip
- # -r /Users/ned/coverage/trunk/requirements/pip.in
+ # -r requirements/pip.in
# tox
wcwidth==0.2.13
# via urwid
zipp==3.21.0
# via importlib-metadata
-
-# The following packages are considered to be unsafe in a requirements file:
-pip==25.0.1
- # via -r /Users/ned/coverage/trunk/requirements/pip.in
-setuptools==76.0.0
- # via
- # -r /Users/ned/coverage/trunk/requirements/pip.in
- # check-manifest
diff --git a/requirements/kit.pip b/requirements/kit.pip
index 7b7c8a6a3..df39ec8bc 100644
--- a/requirements/kit.pip
+++ b/requirements/kit.pip
@@ -1,10 +1,6 @@
-#
-# This file is autogenerated by pip-compile with Python 3.9
-# by the following command:
-#
+# This file was autogenerated by uv via the following command:
# make upgrade
-#
-auditwheel==6.2.0
+auditwheel==6.3.0
# via -r requirements/kit.in
backports-tarfile==1.2.0
# via jaraco-context
@@ -14,27 +10,27 @@ bracex==2.5.post1
# via cibuildwheel
build==1.2.2.post1
# via -r requirements/kit.in
-certifi==2025.1.31
+certifi==2025.4.26
# via
# cibuildwheel
# requests
-charset-normalizer==3.4.1
+charset-normalizer==3.4.2
# via requests
-cibuildwheel==2.23.0
+cibuildwheel==2.23.3
# via -r requirements/kit.in
colorama==0.4.6
# via -r requirements/kit.in
-dependency-groups==1.3.0
+dependency-groups==1.3.1
# via cibuildwheel
docutils==0.21.2
# via readme-renderer
-filelock==3.17.0
+filelock==3.18.0
# via cibuildwheel
id==1.5.0
# via twine
idna==3.10
# via requests
-importlib-metadata==8.6.1
+importlib-metadata==8.7.0
# via
# build
# keyring
@@ -51,20 +47,20 @@ markdown-it-py==3.0.0
# via rich
mdurl==0.1.2
# via markdown-it-py
-more-itertools==10.6.0
+more-itertools==10.7.0
# via
# jaraco-classes
# jaraco-functools
nh3==0.2.21
# via readme-renderer
-packaging==24.2
+packaging==25.0
# via
# auditwheel
# build
# cibuildwheel
# dependency-groups
# twine
-platformdirs==4.3.6
+platformdirs==4.3.8
# via cibuildwheel
pyelftools==0.32
# via auditwheel
@@ -85,8 +81,10 @@ requests-toolbelt==1.0.0
# via twine
rfc3986==2.0.0
# via twine
-rich==13.9.4
+rich==14.0.0
# via twine
+setuptools==80.8.0
+ # via -r requirements/kit.in
tomli==2.2.1
# via
# build
@@ -94,11 +92,11 @@ tomli==2.2.1
# dependency-groups
twine==6.1.0
# via -r requirements/kit.in
-typing-extensions==4.12.2
+typing-extensions==4.13.2
# via
# cibuildwheel
# rich
-urllib3==2.3.0
+urllib3==2.4.0
# via
# requests
# twine
@@ -106,7 +104,3 @@ wheel==0.45.1
# via -r requirements/kit.in
zipp==3.21.0
# via importlib-metadata
-
-# The following packages are considered to be unsafe in a requirements file:
-setuptools==76.0.0
- # via -r requirements/kit.in
diff --git a/requirements/light-threads.pip b/requirements/light-threads.pip
index aff5c3b49..393dbe6b2 100644
--- a/requirements/light-threads.pip
+++ b/requirements/light-threads.pip
@@ -1,31 +1,25 @@
-#
-# This file is autogenerated by pip-compile with Python 3.9
-# by the following command:
-#
+# This file was autogenerated by uv via the following command:
# make upgrade
-#
cffi==1.17.1
# via -r requirements/light-threads.in
dnspython==2.7.0
# via eventlet
-eventlet==0.39.1
+eventlet==0.40.0
# via -r requirements/light-threads.in
-gevent==24.11.1
+gevent==25.5.1
# via -r requirements/light-threads.in
-greenlet==3.1.1
+greenlet==3.2.2
# via
# -r requirements/light-threads.in
# eventlet
# gevent
pycparser==2.22
# via cffi
+setuptools==80.8.0
+ # via
+ # zope-event
+ # zope-interface
zope-event==5.0
# via gevent
zope-interface==7.2
# via gevent
-
-# The following packages are considered to be unsafe in a requirements file:
-setuptools==76.0.0
- # via
- # zope-event
- # zope-interface
diff --git a/requirements/mypy.pip b/requirements/mypy.pip
index 6d63badc7..f9175ec13 100644
--- a/requirements/mypy.pip
+++ b/requirements/mypy.pip
@@ -1,52 +1,50 @@
-#
-# This file is autogenerated by pip-compile with Python 3.9
-# by the following command:
-#
+# This file was autogenerated by uv via the following command:
# make upgrade
-#
-attrs==25.1.0
+attrs==25.3.0
# via hypothesis
colorama==0.4.6
- # via -r /Users/ned/coverage/trunk/requirements/pytest.in
-exceptiongroup==1.2.2
+ # via -r requirements/pytest.in
+exceptiongroup==1.3.0
# via
# hypothesis
# pytest
execnet==2.1.1
# via pytest-xdist
flaky==3.8.1
- # via -r /Users/ned/coverage/trunk/requirements/pytest.in
-hypothesis==6.128.1
- # via -r /Users/ned/coverage/trunk/requirements/pytest.in
-iniconfig==2.0.0
+ # via -r requirements/pytest.in
+hypothesis==6.131.21
+ # via -r requirements/pytest.in
+iniconfig==2.1.0
# via pytest
mypy==1.15.0
# via -r requirements/mypy.in
-mypy-extensions==1.0.0
+mypy-extensions==1.1.0
# via mypy
-packaging==24.2
+packaging==25.0
# via pytest
-pluggy==1.5.0
+pluggy==1.6.0
# via pytest
pygments==2.19.1
- # via -r /Users/ned/coverage/trunk/requirements/pytest.in
+ # via -r requirements/pytest.in
pytest==8.3.5
# via
- # -r /Users/ned/coverage/trunk/requirements/pytest.in
+ # -r requirements/pytest.in
# pytest-xdist
pytest-xdist==3.6.1
- # via -r /Users/ned/coverage/trunk/requirements/pytest.in
+ # via -r requirements/pytest.in
sortedcontainers==2.4.0
# via hypothesis
tomli==2.2.1
# via
# mypy
# pytest
-types-requests==2.32.0.20250306
+types-requests==2.32.0.20250515
# via -r requirements/mypy.in
types-tabulate==0.9.0.20241207
# via -r requirements/mypy.in
-typing-extensions==4.12.2
- # via mypy
-urllib3==2.3.0
+typing-extensions==4.13.2
+ # via
+ # exceptiongroup
+ # mypy
+urllib3==2.4.0
# via types-requests
diff --git a/requirements/pins.pip b/requirements/pins.pip
index 8c2147b76..584cd28bd 100644
--- a/requirements/pins.pip
+++ b/requirements/pins.pip
@@ -2,6 +2,3 @@
# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt
# Version pins, for use as a constraints file.
-
-# https://github.com/pypa/virtualenv/issues/2829
-virtualenv<20.29.0
diff --git a/requirements/pip-tools.in b/requirements/pip-tools.in
deleted file mode 100644
index 4a6755620..000000000
--- a/requirements/pip-tools.in
+++ /dev/null
@@ -1,8 +0,0 @@
-# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
-# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt
-
--c pins.pip
-
-# "make upgrade" turns this into requirements/pip-tools.pip.
-
-pip-tools
diff --git a/requirements/pip-tools.pip b/requirements/pip-tools.pip
deleted file mode 100644
index 72ccd349b..000000000
--- a/requirements/pip-tools.pip
+++ /dev/null
@@ -1,34 +0,0 @@
-#
-# This file is autogenerated by pip-compile with Python 3.9
-# by the following command:
-#
-# make upgrade
-#
-build==1.2.2.post1
- # via pip-tools
-click==8.1.8
- # via pip-tools
-importlib-metadata==8.6.1
- # via build
-packaging==24.2
- # via build
-pip-tools==7.4.1
- # via -r requirements/pip-tools.in
-pyproject-hooks==1.2.0
- # via
- # build
- # pip-tools
-tomli==2.2.1
- # via
- # build
- # pip-tools
-wheel==0.45.1
- # via pip-tools
-zipp==3.21.0
- # via importlib-metadata
-
-# The following packages are considered to be unsafe in a requirements file:
-pip==25.0.1
- # via pip-tools
-setuptools==76.0.0
- # via pip-tools
diff --git a/requirements/pip.pip b/requirements/pip.pip
index 4827fc3c1..5a3b2c451 100644
--- a/requirements/pip.pip
+++ b/requirements/pip.pip
@@ -1,22 +1,14 @@
-#
-# This file is autogenerated by pip-compile with Python 3.9
-# by the following command:
-#
+# This file was autogenerated by uv via the following command:
# make upgrade
-#
distlib==0.3.9
# via virtualenv
-filelock==3.17.0
+filelock==3.18.0
# via virtualenv
-platformdirs==4.3.6
+pip==25.1.1
+ # via -r requirements/pip.in
+platformdirs==4.3.8
# via virtualenv
-virtualenv==20.28.1
- # via
- # -c /Users/ned/coverage/trunk/requirements/pins.pip
- # -r requirements/pip.in
-
-# The following packages are considered to be unsafe in a requirements file:
-pip==25.0.1
+setuptools==80.8.0
# via -r requirements/pip.in
-setuptools==76.0.0
+virtualenv==20.31.2
# via -r requirements/pip.in
diff --git a/requirements/pytest.pip b/requirements/pytest.pip
index 4d12a374a..ae1d0ac18 100644
--- a/requirements/pytest.pip
+++ b/requirements/pytest.pip
@@ -1,14 +1,10 @@
-#
-# This file is autogenerated by pip-compile with Python 3.9
-# by the following command:
-#
+# This file was autogenerated by uv via the following command:
# make upgrade
-#
-attrs==25.1.0
+attrs==25.3.0
# via hypothesis
colorama==0.4.6
# via -r requirements/pytest.in
-exceptiongroup==1.2.2
+exceptiongroup==1.3.0
# via
# hypothesis
# pytest
@@ -16,13 +12,13 @@ execnet==2.1.1
# via pytest-xdist
flaky==3.8.1
# via -r requirements/pytest.in
-hypothesis==6.128.1
+hypothesis==6.131.21
# via -r requirements/pytest.in
-iniconfig==2.0.0
+iniconfig==2.1.0
# via pytest
-packaging==24.2
+packaging==25.0
# via pytest
-pluggy==1.5.0
+pluggy==1.6.0
# via pytest
pygments==2.19.1
# via -r requirements/pytest.in
@@ -36,3 +32,5 @@ sortedcontainers==2.4.0
# via hypothesis
tomli==2.2.1
# via pytest
+typing-extensions==4.13.2
+ # via exceptiongroup
diff --git a/requirements/tox.pip b/requirements/tox.pip
index a2387c529..3017d6bb3 100644
--- a/requirements/tox.pip
+++ b/requirements/tox.pip
@@ -1,9 +1,5 @@
-#
-# This file is autogenerated by pip-compile with Python 3.9
-# by the following command:
-#
+# This file was autogenerated by uv via the following command:
# make upgrade
-#
cachetools==5.5.2
# via tox
chardet==5.2.0
@@ -14,35 +10,33 @@ colorama==0.4.6
# tox
distlib==0.3.9
# via virtualenv
-filelock==3.17.0
+filelock==3.18.0
# via
# tox
# virtualenv
-packaging==24.2
+packaging==25.0
# via
# pyproject-api
# tox
-platformdirs==4.3.6
+platformdirs==4.3.8
# via
# tox
# virtualenv
-pluggy==1.5.0
+pluggy==1.6.0
# via tox
-pyproject-api==1.9.0
+pyproject-api==1.9.1
# via tox
tomli==2.2.1
# via
# pyproject-api
# tox
-tox==4.24.1
+tox==4.26.0
# via
# -r requirements/tox.in
# tox-gh
tox-gh==1.5.0
# via -r requirements/tox.in
-typing-extensions==4.12.2
+typing-extensions==4.13.2
+ # via tox
+virtualenv==20.31.2
# via tox
-virtualenv==20.28.1
- # via
- # -c /Users/ned/coverage/trunk/requirements/pins.pip
- # tox
diff --git a/setup.py b/setup.py
index 8fcec5e2d..92fad6502 100644
--- a/setup.py
+++ b/setup.py
@@ -35,7 +35,7 @@
"""
cov_ver_py = os.path.join(os.path.split(__file__)[0], "coverage/version.py")
-with open(cov_ver_py) as version_file:
+with open(cov_ver_py, encoding="utf-8") as version_file:
# __doc__ will be overwritten by version.py.
doc = __doc__
# Keep pylint happy.
@@ -43,7 +43,7 @@
# Execute the code in version.py.
exec(compile(version_file.read(), cov_ver_py, "exec", dont_inherit=True))
-with open("README.rst") as readme:
+with open("README.rst", encoding="utf-8") as readme:
readme_text = readme.read()
temp_url = __url__.replace("readthedocs", "@@")
diff --git a/tests/balance_xdist_plugin.py b/tests/balance_xdist_plugin.py
index 64a8c85f1..114d4ad3b 100644
--- a/tests/balance_xdist_plugin.py
+++ b/tests/balance_xdist_plugin.py
@@ -71,7 +71,7 @@ def pytest_sessionstart(self, session):
if self.worker == "none":
if tests_csv_dir.exists():
for csv_file in tests_csv_dir.iterdir():
- with csv_file.open(newline="") as fcsv:
+ with csv_file.open(newline="", encoding="utf-8") as fcsv:
reader = csv.reader(fcsv)
for row in reader:
self.times[row[1]] += float(row[3])
@@ -81,7 +81,7 @@ def write_duration_row(self, item, phase, duration):
"""Helper to write a row to the tracked-test csv file."""
if self.running_all:
self.tests_csv.parent.mkdir(parents=True, exist_ok=True)
- with self.tests_csv.open("a", newline="") as fcsv:
+ with self.tests_csv.open("a", newline="", encoding="utf-8") as fcsv:
csv.writer(fcsv).writerow([self.worker, item.nodeid, phase, duration])
@pytest.hookimpl(hookwrapper=True)
@@ -171,7 +171,7 @@ def show_worker_times(): # pragma: debugging
tests_csv_dir = Path("tmp/tests_csv")
for csv_file in tests_csv_dir.iterdir():
- with csv_file.open(newline="") as fcsv:
+ with csv_file.open(newline="", encoding="utf-8") as fcsv:
reader = csv.reader(fcsv)
for row in reader:
worker = row[0]
diff --git a/tests/conftest.py b/tests/conftest.py
index 876dc0827..9cd953bbd 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -60,6 +60,9 @@ def set_warnings() -> None:
warnings.filterwarnings("ignore", r".*no-sysmon")
+ # We have a test that has a return in a finally: test_bug_1891.
+ warnings.filterwarnings("ignore", "'return' in a 'finally' block", category=SyntaxWarning)
+
@pytest.fixture(autouse=True)
def reset_sys_path() -> Iterator[None]:
@@ -79,10 +82,17 @@ def reset_environment() -> Iterator[None]:
@pytest.fixture(autouse=True)
-def reset_filesdotpy_globals() -> Iterator[None]:
+def reset_filesdotpy_globals() -> None:
"""coverage/files.py has some unfortunate globals. Reset them every test."""
set_relative_directory()
- yield
+
+@pytest.fixture(autouse=True)
+def force_local_pyc_files() -> None:
+ """Ensure that .pyc files are written next to source files."""
+ # For some tests, we need .pyc files written in the current directory,
+ # so override any local setting.
+ sys.pycache_prefix = None
+
WORKER = os.getenv("PYTEST_XDIST_WORKER", "none")
@@ -94,7 +104,8 @@ def pytest_sessionstart() -> None:
# Create a .pth file for measuring subprocess coverage.
pth_dir = find_writable_pth_directory()
assert pth_dir
- (pth_dir / "subcover.pth").write_text("import coverage; coverage.process_startup()\n")
+ sub_dir = pth_dir / "subcover.pth"
+ sub_dir.write_text("import coverage; coverage.process_startup()\n", encoding="utf-8")
# subcover.pth is deleted by pytest_sessionfinish below.
@@ -127,7 +138,7 @@ def find_writable_pth_directory() -> Path | None:
for pth_dir in possible_pth_dirs(): # pragma: part covered
try_it = pth_dir / f"touch_{WORKER}.it"
try:
- try_it.write_text("foo")
+ try_it.write_text("foo", encoding="utf-8")
except OSError: # pragma: cant happen
continue
diff --git a/tests/goldtest.py b/tests/goldtest.py
index 7ca4af159..4ff8276f2 100644
--- a/tests/goldtest.py
+++ b/tests/goldtest.py
@@ -59,8 +59,8 @@ def save_mismatch(f: str) -> None:
save_path = expected_dir.replace(os_sep("/gold/"), os_sep("/actual/"))
os.makedirs(save_path, exist_ok=True)
save_file = os.path.join(save_path, f)
- with open(save_file, "w") as savef:
- with open(os.path.join(actual_dir, f)) as readf:
+ with open(save_file, "w", encoding="utf-8") as savef:
+ with open(os.path.join(actual_dir, f), encoding="utf-8") as readf:
savef.write(readf.read())
print(os_sep(f"Saved actual output to '{save_file}': see tests/gold/README.rst"))
@@ -70,13 +70,13 @@ def save_mismatch(f: str) -> None:
text_diff = []
for f in diff_files:
expected_file = os.path.join(expected_dir, f)
- with open(expected_file) as fobj:
+ with open(expected_file, encoding="utf-8") as fobj:
expected = fobj.read()
if expected_file.endswith(".xml"):
expected = canonicalize_xml(expected)
actual_file = os.path.join(actual_dir, f)
- with open(actual_file) as fobj:
+ with open(actual_file, encoding="utf-8") as fobj:
actual = fobj.read()
if actual_file.endswith(".xml"):
actual = canonicalize_xml(actual)
@@ -114,7 +114,7 @@ def contains(filename: str, *strlist: str) -> None:
"""
__tracebackhide__ = True # pytest, please don't show me this function.
- with open(filename) as fobj:
+ with open(filename, encoding="utf-8") as fobj:
text = fobj.read()
for s in strlist:
assert s in text, f"Missing content in {filename}: {s!r}"
@@ -128,7 +128,7 @@ def contains_rx(filename: str, *rxlist: str) -> None:
"""
__tracebackhide__ = True # pytest, please don't show me this function.
- with open(filename) as fobj:
+ with open(filename, encoding="utf-8") as fobj:
lines = fobj.readlines()
for rx in rxlist:
assert any(re.search(rx, line) for line in lines), (
@@ -144,7 +144,7 @@ def contains_any(filename: str, *strlist: str) -> None:
"""
__tracebackhide__ = True # pytest, please don't show me this function.
- with open(filename) as fobj:
+ with open(filename, encoding="utf-8") as fobj:
text = fobj.read()
for s in strlist:
if s in text:
@@ -161,7 +161,7 @@ def doesnt_contain(filename: str, *strlist: str) -> None:
"""
__tracebackhide__ = True # pytest, please don't show me this function.
- with open(filename) as fobj:
+ with open(filename, encoding="utf-8") as fobj:
text = fobj.read()
for s in strlist:
assert s not in text, f"Forbidden content in {filename}: {s!r}"
diff --git a/tests/helpers.py b/tests/helpers.py
index d1412afcd..b3ab529cf 100644
--- a/tests/helpers.py
+++ b/tests/helpers.py
@@ -15,6 +15,7 @@
import re
import shutil
import subprocess
+import sys
import textwrap
import warnings
@@ -42,10 +43,18 @@ def run_command(cmd: str) -> tuple[int, str]:
# Subprocesses are expensive, but convenient, and so may be over-used in
# the test suite. Use these lines to get a list of the tests using them:
if 0: # pragma: debugging
- with open("/tmp/processes.txt", "a") as proctxt: # type: ignore[unreachable]
+ pth = "/tmp/processes.txt" # type: ignore[unreachable]
+ with open(pth, "a", encoding="utf-8") as proctxt:
print(os.getenv("PYTEST_CURRENT_TEST", "unknown"), file=proctxt, flush=True)
- encoding = os.device_encoding(1) or locale.getpreferredencoding()
+ # Type checking trick due to "unreachable" being set
+ _locale_type_erased: Any = locale
+
+ encoding = os.device_encoding(1) or (
+ _locale_type_erased.getpreferredencoding()
+ if sys.version_info < (3, 11)
+ else _locale_type_erased.getencoding()
+ )
# In some strange cases (PyPy3 in a virtualenv!?) the stdout encoding of
# the subprocess is set incorrectly to ascii. Use an environment variable
@@ -113,7 +122,7 @@ def make_file(
if text and basename.endswith(".py") and SHOW_DIS: # pragma: debugging
os.makedirs("/tmp/dis", exist_ok=True)
- with open(f"/tmp/dis/{basename}.dis", "w") as fdis:
+ with open(f"/tmp/dis/{basename}.dis", "w", encoding="utf-8") as fdis:
print(f"# {os.path.abspath(filename)}", file=fdis)
cur_test = os.getenv("PYTEST_CURRENT_TEST", "unknown")
print(f"# PYTEST_CURRENT_TEST = {cur_test}", file=fdis)
@@ -379,9 +388,19 @@ def _decorator(fn: TestMethod) -> TestMethod:
def all_our_source_files() -> Iterator[tuple[Path, str]]:
"""Iterate over all of our own source files.
+ This is used in tests that need a bunch of Python code to analyze, so we
+ might as well use our own source code as the subject.
+
Produces a stream of (filename, file contents) tuples.
"""
+ print(f"all_our_source_files: {coverage.__file__ = }")
cov_dir = Path(coverage.__file__).parent.parent
+ if ".tox" in cov_dir.parts:
+ # We are in a tox-installed environment, look above the .tox dir to
+ # also find the uninstalled source files.
+ cov_dir = Path(os.fspath(cov_dir).partition(".tox")[0])
+
+ print(f"all_our_source_files: {os.path.abspath(cov_dir) = }")
# To run against all the files in the tox venvs:
# for source_file in cov_dir.rglob("*.py"):
for sub in [".", "benchmark", "ci", "coverage", "lab", "tests"]:
diff --git a/tests/osinfo.py b/tests/osinfo.py
index f55fe88c1..e90d5dcf2 100644
--- a/tests/osinfo.py
+++ b/tests/osinfo.py
@@ -64,7 +64,7 @@ def _VmB(key: str) -> int:
"""Read the /proc/PID/status file to find memory use."""
try:
# Get pseudo file /proc//status
- with open(f"/proc/{os.getpid()}/status") as t:
+ with open(f"/proc/{os.getpid()}/status", encoding="utf-8") as t:
v = t.read()
except OSError: # pragma: cant happen
return 0 # non-Linux?
diff --git a/tests/test_arcs.py b/tests/test_arcs.py
index 7ec0e663a..966f80241 100644
--- a/tests/test_arcs.py
+++ b/tests/test_arcs.py
@@ -188,7 +188,7 @@ class WithTest(CoverageTest):
def test_with(self) -> None:
self.check_coverage("""\
def example():
- with open("test", "w") as f:
+ with open("test", "w", encoding="utf-8") as f:
f.write("3")
a = 4
@@ -201,7 +201,7 @@ def example():
def test_with_return(self) -> None:
self.check_coverage("""\
def example():
- with open("test", "w") as f:
+ with open("test", "w", encoding="utf-8") as f:
f.write("3")
return 4
@@ -215,7 +215,7 @@ def test_bug_146(self) -> None:
# https://github.com/nedbat/coveragepy/issues/146
self.check_coverage("""\
for i in range(2):
- with open("test", "w") as f:
+ with open("test", "w", encoding="utf-8") as f:
print(3)
print(4)
print(5)
@@ -228,9 +228,9 @@ def test_bug_146(self) -> None:
def test_nested_with_return(self) -> None:
self.check_coverage("""\
def example(x):
- with open("test", "w") as f2:
+ with open("test", "w", encoding="utf-8") as f2:
a = 3
- with open("test2", "w") as f4:
+ with open("test2", "w", encoding="utf-8") as f4:
f2.write("5")
return 6
@@ -243,7 +243,7 @@ def example(x):
def test_break_through_with(self) -> None:
self.check_coverage("""\
for i in range(1+1):
- with open("test", "w") as f:
+ with open("test", "w", encoding="utf-8") as f:
print(3)
break
print(5)
@@ -255,7 +255,7 @@ def test_break_through_with(self) -> None:
def test_continue_through_with(self) -> None:
self.check_coverage("""\
for i in range(1+1):
- with open("test", "w") as f:
+ with open("test", "w", encoding="utf-8") as f:
print(3)
continue
print(5)
diff --git a/tests/test_concurrency.py b/tests/test_concurrency.py
index 98f72f77a..c9c25d934 100644
--- a/tests/test_concurrency.py
+++ b/tests/test_concurrency.py
@@ -653,7 +653,7 @@ def test_thread_safe_save_data(tmp_path: pathlib.Path) -> None:
modules_dir.mkdir()
module_names = [f"m{i:03d}" for i in range(1000)]
for module_name in module_names:
- (modules_dir / (module_name + ".py")).write_text("def f(): pass\n")
+ (modules_dir / (module_name + ".py")).write_text("def f(): pass\n", encoding="utf-8")
# Shared variables for threads
should_run = [True]
diff --git a/tests/test_debug.py b/tests/test_debug.py
index 651c7d7ea..d1ab6f57c 100644
--- a/tests/test_debug.py
+++ b/tests/test_debug.py
@@ -199,10 +199,9 @@ def test_debug_sys_ctracer(self) -> None:
out_text = self.f1_debug_output(["sys"])
tracer_line = re_line(r"CTracer:", out_text).strip()
if testenv.C_TRACER or testenv.SYS_MON:
- expected = "CTracer: available"
+ assert tracer_line.startswith("CTracer: available from ")
else:
- expected = "CTracer: unavailable"
- assert expected == tracer_line
+ assert tracer_line == "CTracer: unavailable"
def test_debug_pybehave(self) -> None:
out_text = self.f1_debug_output(["pybehave"])
@@ -269,14 +268,14 @@ def test_envvar(self) -> None:
self.set_environ("COVERAGE_DEBUG_FILE", "debug.out")
self.debug_sys()
assert ("", "") == self.stdouterr()
- with open("debug.out") as f:
+ with open("debug.out", encoding="utf-8") as f:
assert_good_debug_sys(f.read())
def test_config_file(self) -> None:
self.make_file(".coveragerc", "[run]\ndebug_file = lotsa_info.txt")
self.debug_sys()
assert ("", "") == self.stdouterr()
- with open("lotsa_info.txt") as f:
+ with open("lotsa_info.txt", encoding="utf-8") as f:
assert_good_debug_sys(f.read())
def test_stdout_alias(self) -> None:
diff --git a/tests/test_execfile.py b/tests/test_execfile.py
index cd12dea99..930a31a0d 100644
--- a/tests/test_execfile.py
+++ b/tests/test_execfile.py
@@ -89,7 +89,7 @@ def test_missing_final_newline(self) -> None:
a = 1
print(f"a is {a!r}")
#""")
- with open("abrupt.py") as f:
+ with open("abrupt.py", encoding="utf-8") as f:
abrupt = f.read()
assert abrupt[-1] == '#'
run_python_file(["abrupt.py"])
diff --git a/tests/test_goldtest.py b/tests/test_goldtest.py
index f4972ab1c..113ecc8c6 100644
--- a/tests/test_goldtest.py
+++ b/tests/test_goldtest.py
@@ -80,7 +80,7 @@ def test_bad(self) -> None:
assert " D/D/D, Gxxx, Pennsylvania" in stdout
# The actual file was saved.
- with open(ACTUAL_GETTY_FILE) as f:
+ with open(ACTUAL_GETTY_FILE, encoding="utf-8") as f:
saved = f.read()
assert saved == BAD_GETTY
diff --git a/tests/test_html.py b/tests/test_html.py
index 67b8933aa..627690bd7 100644
--- a/tests/test_html.py
+++ b/tests/test_html.py
@@ -72,7 +72,7 @@ def get_html_report_content(self, module: str) -> str:
"""Return the content of the HTML report for `module`."""
filename = flat_rootname(module) + ".html"
filename = os.path.join("htmlcov", filename)
- with open(filename) as f:
+ with open(filename, encoding="utf-8") as f:
return f.read()
def get_html_index_content(self) -> str:
@@ -81,7 +81,7 @@ def get_html_index_content(self) -> str:
Time stamps are replaced with a placeholder so that clocks don't matter.
"""
- with open("htmlcov/index.html") as f:
+ with open("htmlcov/index.html", encoding="utf-8") as f:
index = f.read()
index = re.sub(
r"created at \d{4}-\d{2}-\d{2} \d{2}:\d{2} \+\d{4}",
@@ -122,7 +122,7 @@ def assert_valid_hrefs(self, directory: str = "htmlcov") -> None:
"""
hrefs = collections.defaultdict(set)
for fname in glob.glob(f"{directory}/*.html"):
- with open(fname) as fhtml:
+ with open(fname, encoding="utf-8") as fhtml:
html = fhtml.read()
for href in re.findall(r""" href=['"]([^'"]*)['"]""", html):
if href.startswith("#"):
@@ -182,11 +182,11 @@ class FileWriteTracker:
def __init__(self, written: set[str]) -> None:
self.written = written
- def open(self, filename: str, mode: str = "r") -> IO[str]:
+ def open(self, filename: str, mode: str = "r", encoding: str | None = None) -> IO[str]:
"""Be just like `open`, but write written file names to `self.written`."""
if mode.startswith("w"):
self.written.add(filename.replace('\\', '/'))
- return open(filename, mode)
+ return open(filename, mode, encoding=encoding)
class HtmlDeltaTest(HtmlTestHelpers, CoverageTest):
@@ -361,12 +361,12 @@ def test_status_format_change(self) -> None:
self.create_initial_files()
self.run_coverage()
- with open("htmlcov/status.json") as status_json:
+ with open("htmlcov/status.json", encoding="utf-8") as status_json:
status_data = json.load(status_json)
assert status_data['format'] == 5
status_data['format'] = 99
- with open("htmlcov/status.json", "w") as status_json:
+ with open("htmlcov/status.json", "w", encoding="utf-8") as status_json:
json.dump(status_data, status_json)
self.run_coverage()
@@ -382,7 +382,7 @@ def test_dont_overwrite_gitignore(self) -> None:
self.create_initial_files()
self.make_file("htmlcov/.gitignore", "# ignore nothing")
self.run_coverage()
- with open("htmlcov/.gitignore") as fgi:
+ with open("htmlcov/.gitignore", encoding="utf-8") as fgi:
assert fgi.read() == "# ignore nothing"
def test_dont_write_gitignore_into_existing_directory(self) -> None:
@@ -609,9 +609,9 @@ def test_has_date_stamp_in_files(self) -> None:
self.create_initial_files()
self.run_coverage()
- with open("htmlcov/index.html") as f:
+ with open("htmlcov/index.html", encoding="utf-8") as f:
self.assert_correct_timestamp(f.read())
- with open("htmlcov/main_file_py.html") as f:
+ with open("htmlcov/main_file_py.html", encoding="utf-8") as f:
self.assert_correct_timestamp(f.read())
def test_reporting_on_unmeasured_file(self) -> None:
@@ -1259,7 +1259,7 @@ def test_accented_dot_py(self) -> None:
cov.load()
cov.html_report()
self.assert_exists("htmlcov/h\xe2t_py.html")
- with open("htmlcov/index.html") as indexf:
+ with open("htmlcov/index.html", encoding="utf-8") as indexf:
index = indexf.read()
assert 'hât.py' in index
@@ -1273,7 +1273,7 @@ def test_accented_directory(self) -> None:
cov.load()
cov.html_report()
self.assert_exists("htmlcov/z_5786906b6f0ffeb4_accented_py.html")
- with open("htmlcov/index.html") as indexf:
+ with open("htmlcov/index.html", encoding="utf-8") as indexf:
index = indexf.read()
expected = 'â%saccented.py'
assert expected % os.sep in index
diff --git a/tests/test_json.py b/tests/test_json.py
index e5b116ac4..aeeb81c35 100644
--- a/tests/test_json.py
+++ b/tests/test_json.py
@@ -83,7 +83,7 @@ def _compare_json_reports(
mod = self.start_import_stop(cov, mod_name)
output_path = os.path.join(self.temp_dir, f"{mod_name}.json")
cov.json_report(mod, outfile=output_path)
- with open(output_path) as result_file:
+ with open(output_path, encoding="utf-8") as result_file:
parsed_result = json.load(result_file)
self.assert_recent_datetime(
datetime.strptime(parsed_result['meta']['timestamp'], "%Y-%m-%dT%H:%M:%S.%f"),
diff --git a/tests/test_lcov.py b/tests/test_lcov.py
index 76e99e91d..f4dff3801 100644
--- a/tests/test_lcov.py
+++ b/tests/test_lcov.py
@@ -43,7 +43,7 @@ def test_volume(self):
def get_lcov_report_content(self, filename: str = "coverage.lcov") -> str:
"""Return the content of an LCOV report."""
- with open(filename) as file:
+ with open(filename, encoding="utf-8") as file:
return file.read()
def test_lone_file(self) -> None:
diff --git a/tests/test_oddball.py b/tests/test_oddball.py
index 21d1fc421..eabaa9758 100644
--- a/tests/test_oddball.py
+++ b/tests/test_oddball.py
@@ -8,6 +8,7 @@
import os.path
import re
import sys
+import warnings
from flaky import flaky
import pytest
@@ -210,9 +211,16 @@ def once(x): # line 301
if fails > 8:
pytest.fail("RAM grew by %d" % (ram_growth)) # pragma: only failure
- @pytest.mark.skipif(not testenv.C_TRACER, reason="Only the C tracer has refcounting issues")
- # In fact, sysmon explicitly holds onto all code objects,
- # so this will definitely fail with sysmon.
+ @pytest.mark.skipif(
+ not testenv.C_TRACER,
+ reason="Only the C tracer has refcounting issues",
+ # In fact, sysmon explicitly holds onto all code objects,
+ # so this will definitely fail with sysmon.
+ )
+ @pytest.mark.skipif(
+ env.PYVERSION[:2] == (3, 13) and not env.GIL,
+ reason = "3.13t never frees code objects: https://github.com/python/cpython/pull/131989",
+ )
@pytest.mark.parametrize("branch", [False, True])
def test_eval_codeobject_leak(self, branch: bool) -> None:
# https://github.com/nedbat/coveragepy/issues/1924
@@ -225,7 +233,7 @@ def test_eval_codeobject_leak(self, branch: bool) -> None:
# one of our loops only increased the footprint by a small amount.
base = osinfo.process_ram()
deltas = []
- for _ in range(10):
+ for _ in range(30):
self.check_coverage(code, lines=[1, 2, 3], missing="", branch=branch)
now = osinfo.process_ram()
deltas.append(now - base)
@@ -458,7 +466,11 @@ def return_arg_or_void(arg):
doctest.testmod(sys.modules[__name__]) # we're not __main__ :(
''')
cov = coverage.Coverage()
- self.start_import_stop(cov, "the_doctest")
+ with warnings.catch_warnings():
+ # Doctest calls pdb which opens ~/.pdbrc without an encoding argument,
+ # but we don't care. PYVERSIONS: this was needed for 3.10 only.
+ warnings.filterwarnings("ignore", r".*'encoding' argument not specified.*")
+ self.start_import_stop(cov, "the_doctest")
data = cov.get_data()
assert len(data.measured_files()) == 1
lines = sorted_lines(data, data.measured_files().pop())
@@ -592,7 +604,7 @@ def test_correct_filename(self) -> None:
""")
self.make_file("main.py", """\
namespace = {'var': 17}
- with open("to_exec.py") as to_exec_py:
+ with open("to_exec.py", encoding="utf-8") as to_exec_py:
code = compile(to_exec_py.read(), 'to_exec.py', 'exec')
exec(code, globals(), namespace)
\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
diff --git a/tests/test_parser.py b/tests/test_parser.py
index a9a247ffd..1e9a5db72 100644
--- a/tests/test_parser.py
+++ b/tests/test_parser.py
@@ -207,6 +207,8 @@ def test_bug_1891(self) -> None:
)
""")
assert parser.exit_counts() == {1: 1}
+ # In conftest.py, we silence the SyntaxWarning this code causes. If
+ # we remove this code, we can probably remove that warning.
parser = self.parse_text("""\
def g2():
try:
@@ -1181,7 +1183,7 @@ def test_missing_line_ending(self) -> None:
stderr=subprocess.PIPE).communicate()""") # no final newline.
# Double-check that some test helper wasn't being helpful.
- with open("abrupt.py") as f:
+ with open("abrupt.py", encoding="utf-8") as f:
assert f.read()[-1] == ")"
parser = self.parse_file("abrupt.py")
diff --git a/tests/test_phystokens.py b/tests/test_phystokens.py
index 0a863ab6e..49d8d25a7 100644
--- a/tests/test_phystokens.py
+++ b/tests/test_phystokens.py
@@ -128,7 +128,7 @@ def test_stress(self, fname: str) -> None:
stress = os.path.join(TESTS_DIR, fname)
self.check_file_tokenization(stress)
- with open(stress) as fstress:
+ with open(stress, encoding="utf-8") as fstress:
assert re.search(r"(?m) $", fstress.read()), f"{stress} needs a trailing space."
diff --git a/tests/test_plugins.py b/tests/test_plugins.py
index 44b139e57..a94104a01 100644
--- a/tests/test_plugins.py
+++ b/tests/test_plugins.py
@@ -171,7 +171,7 @@ class Plugin(CoveragePlugin):
pass
def coverage_init(reg, options):
reg.add_noop(Plugin())
- with open("evidence.out", "w") as f:
+ with open("evidence.out", "w", encoding="utf-8") as f:
f.write("we are here!")
""")
@@ -181,7 +181,7 @@ def coverage_init(reg, options):
cov.start()
cov.stop() # pragma: nested
- with open("evidence.out") as f:
+ with open("evidence.out", encoding="utf-8") as f:
assert f.read() == "we are here!"
def test_missing_plugin_raises_import_error(self) -> None:
diff --git a/tests/test_process.py b/tests/test_process.py
index 2466de081..d2c95bbda 100644
--- a/tests/test_process.py
+++ b/tests/test_process.py
@@ -410,7 +410,7 @@ def test_fork(self) -> None:
data.read()
assert line_counts(data)["fork.py"] == total_lines
- debug_text = Path("debug.out").read_text()
+ debug_text = Path("debug.out").read_text(encoding="utf-8")
ppid = pids["parent"]
cpid = pids["child"]
assert ppid != cpid
@@ -670,14 +670,14 @@ def assert_tryexecfile_output(self, expected: str, actual: str) -> None:
assert actual == expected
def test_coverage_run_is_like_python(self) -> None:
- with open(TRY_EXECFILE) as f:
+ with open(TRY_EXECFILE, encoding="utf-8") as f:
self.make_file("run_me.py", f.read())
expected = self.run_command("python run_me.py")
actual = self.run_command("coverage run run_me.py")
self.assert_tryexecfile_output(expected, actual)
def test_coverage_run_far_away_is_like_python(self) -> None:
- with open(TRY_EXECFILE) as f:
+ with open(TRY_EXECFILE, encoding="utf-8") as f:
self.make_file("sub/overthere/prog.py", f.read())
expected = self.run_command("python sub/overthere/prog.py")
actual = self.run_command("coverage run sub/overthere/prog.py")
@@ -685,7 +685,7 @@ def test_coverage_run_far_away_is_like_python(self) -> None:
@pytest.mark.skipif(not env.WINDOWS, reason="This is about Windows paths")
def test_coverage_run_far_away_is_like_python_windows(self) -> None:
- with open(TRY_EXECFILE) as f:
+ with open(TRY_EXECFILE, encoding="utf-8") as f:
self.make_file("sub/overthere/prog.py", f.read())
expected = self.run_command("python sub\\overthere\\prog.py")
actual = self.run_command("coverage run sub\\overthere\\prog.py")
@@ -698,7 +698,7 @@ def test_coverage_run_dashm_is_like_python_dashm(self) -> None:
self.assert_tryexecfile_output(expected, actual)
def test_coverage_run_dir_is_like_python_dir(self) -> None:
- with open(TRY_EXECFILE) as f:
+ with open(TRY_EXECFILE, encoding="utf-8") as f:
self.make_file("with_main/__main__.py", f.read())
expected = self.run_command("python with_main")
@@ -706,7 +706,7 @@ def test_coverage_run_dir_is_like_python_dir(self) -> None:
self.assert_tryexecfile_output(expected, actual)
def test_coverage_run_dashm_dir_no_init_is_like_python(self) -> None:
- with open(TRY_EXECFILE) as f:
+ with open(TRY_EXECFILE, encoding="utf-8") as f:
self.make_file("with_main/__main__.py", f.read())
expected = self.run_command("python -m with_main")
@@ -714,7 +714,7 @@ def test_coverage_run_dashm_dir_no_init_is_like_python(self) -> None:
self.assert_tryexecfile_output(expected, actual)
def test_coverage_run_dashm_dir_with_init_is_like_python(self) -> None:
- with open(TRY_EXECFILE) as f:
+ with open(TRY_EXECFILE, encoding="utf-8") as f:
self.make_file("with_main/__main__.py", f.read())
self.make_file("with_main/__init__.py", "")
@@ -782,7 +782,7 @@ def test_coverage_run_script_imports_doubledashsource(self) -> None:
def test_coverage_run_dashm_is_like_python_dashm_off_path(self) -> None:
# https://github.com/nedbat/coveragepy/issues/242
self.make_file("sub/__init__.py", "")
- with open(TRY_EXECFILE) as f:
+ with open(TRY_EXECFILE, encoding="utf-8") as f:
self.make_file("sub/run_me.py", f.read())
expected = self.run_command("python -m sub.run_me")
@@ -801,7 +801,7 @@ def test_coverage_zip_is_like_python(self) -> None:
# Test running coverage from a zip file itself. Some environments
# (windows?) zip up the coverage main to be used as the coverage
# command.
- with open(TRY_EXECFILE) as f:
+ with open(TRY_EXECFILE, encoding="utf-8") as f:
self.make_file("run_me.py", f.read())
expected = self.run_command("python run_me.py")
cov_main = os.path.join(TESTS_DIR, "covmain.zip")
@@ -814,7 +814,7 @@ def test_coverage_zip_is_like_python(self) -> None:
reason="Windows gets this wrong: https://github.com/python/cpython/issues/131484",
)
def test_pythonsafepath(self) -> None:
- with open(TRY_EXECFILE) as f:
+ with open(TRY_EXECFILE, encoding="utf-8") as f:
self.make_file("run_me.py", f.read())
self.set_environ("PYTHONSAFEPATH", "1")
expected = self.run_command("python run_me.py")
@@ -823,7 +823,7 @@ def test_pythonsafepath(self) -> None:
@pytest.mark.skipif(env.PYVERSION < (3, 11), reason="PYTHONSAFEPATH is new in 3.11")
def test_pythonsafepath_dashm_runme(self) -> None:
- with open(TRY_EXECFILE) as f:
+ with open(TRY_EXECFILE, encoding="utf-8") as f:
self.make_file("run_me.py", f.read())
self.set_environ("PYTHONSAFEPATH", "1")
expected = self.run_command("python run_me.py")
@@ -832,7 +832,7 @@ def test_pythonsafepath_dashm_runme(self) -> None:
@pytest.mark.skipif(env.PYVERSION < (3, 11), reason="PYTHONSAFEPATH is new in 3.11")
def test_pythonsafepath_dashm(self) -> None:
- with open(TRY_EXECFILE) as f:
+ with open(TRY_EXECFILE, encoding="utf-8") as f:
self.make_file("with_main/__main__.py", f.read())
self.set_environ("PYTHONSAFEPATH", "1")
@@ -1256,7 +1256,7 @@ def setUp(self) -> None:
""")
# sub.py will write a few lines.
self.make_file("sub.py", """\
- f = open("out.txt", "w")
+ f = open("out.txt", "w", encoding="utf-8")
f.write("Hello, world!\\n")
f.close()
""")
@@ -1276,7 +1276,7 @@ def test_subprocess_with_pth_files(self) -> None:
self.set_environ("COVERAGE_PROCESS_START", "coverage.ini")
import main # pylint: disable=unused-import, import-error
- with open("out.txt") as f:
+ with open("out.txt", encoding="utf-8") as f:
assert f.read() == "Hello, world!\n"
# Read the data from .coverage
@@ -1295,7 +1295,7 @@ def test_subprocess_with_pth_files_and_parallel(self) -> None:
self.set_environ("COVERAGE_PROCESS_START", "coverage.ini")
self.run_command("coverage run main.py")
- with open("out.txt") as f:
+ with open("out.txt", encoding="utf-8") as f:
assert f.read() == "Hello, world!\n"
self.run_command("coverage combine")
@@ -1368,7 +1368,7 @@ def path(basename: str) -> str:
self.make_file(path("__init__.py"), "")
# sub.py will write a few lines.
self.make_file(path("sub.py"), """\
- f = open("out.txt", "w")
+ f = open("out.txt", "w", encoding="utf-8")
f.write("Hello, world!")
f.close()
""")
@@ -1386,7 +1386,7 @@ def path(basename: str) -> str:
self.run_command(cmd)
- with open("out.txt") as f:
+ with open("out.txt", encoding="utf-8") as f:
assert f.read() == "Hello, world!"
# Read the data from .coverage
diff --git a/tests/test_python.py b/tests/test_python.py
index 6a8362919..7c8666ac5 100644
--- a/tests/test_python.py
+++ b/tests/test_python.py
@@ -57,11 +57,11 @@ def test_source_for_file_windows(tmp_path: pathlib.Path) -> None:
# On windows if a pyw exists, it is an acceptable source
path_windows = tmp_path / "a.pyw"
- path_windows.write_text("")
+ path_windows.write_text("", encoding="utf-8")
assert str(path_windows) == source_for_file(src + 'c')
# If both pyw and py exist, py is preferred
- a_py.write_text("")
+ a_py.write_text("", encoding="utf-8")
assert source_for_file(src + 'c') == src
@@ -81,6 +81,6 @@ def test_runpy_path(self, convert_to: str) -> None:
import runpy
from pathlib import Path
pyfile = Path('script.py')
- pyfile.write_text('')
+ pyfile.write_text('', encoding='utf-8')
runpy.run_path({convert_to}(pyfile))
""")
diff --git a/tests/test_report.py b/tests/test_report.py
index fca027f9b..06d095e81 100644
--- a/tests/test_report.py
+++ b/tests/test_report.py
@@ -787,7 +787,7 @@ def test_report_with_chdir(self) -> None:
print("Line One")
os.chdir("subdir")
print("Line Two")
- print(open("something").read())
+ print(open("something", encoding="utf-8").read())
""")
self.make_file("subdir/something", "hello")
out = self.run_command("coverage run --source=. chdir.py")
diff --git a/tests/test_report_common.py b/tests/test_report_common.py
index 20c54e323..4583fe17d 100644
--- a/tests/test_report_common.py
+++ b/tests/test_report_common.py
@@ -265,7 +265,7 @@ def test_lcov(self) -> None:
cov = coverage.Coverage()
cov.load()
cov.lcov_report()
- with open("coverage.lcov") as lcov:
+ with open("coverage.lcov", encoding="utf-8") as lcov:
actual = lcov.read()
expected = textwrap.dedent("""\
SF:good.j2
diff --git a/tests/test_venv.py b/tests/test_venv.py
index 70b2e4c4d..bd30b0e1b 100644
--- a/tests/test_venv.py
+++ b/tests/test_venv.py
@@ -62,7 +62,7 @@ def third(x):
return 3 * x
""")
# Use plugin2.py as third.plugin
- with open(os.path.join(os.path.dirname(__file__), "plugin2.py")) as f:
+ with open(os.path.join(os.path.dirname(__file__), "plugin2.py"), encoding="utf-8") as f:
make_file("third_pkg/third/plugin.py", f.read())
# A render function for plugin2 to use for dynamic file names.
make_file("third_pkg/third/render.py", """\
@@ -194,7 +194,7 @@ def in_venv_world_fixture(self, venv_world: Path) -> Iterator[None]:
def get_trace_output(self) -> str:
"""Get the debug output of coverage.py"""
- with open("debug_out.txt") as f:
+ with open("debug_out.txt", encoding="utf-8") as f:
return f.read()
@pytest.mark.parametrize('install_source_in_venv', [True, False])
diff --git a/tox.ini b/tox.ini
index 330a967a4..eb6a0a20e 100644
--- a/tox.ini
+++ b/tox.ini
@@ -4,12 +4,11 @@
[tox]
# When changing this list, be sure to check the [gh] list below.
# PYVERSIONS
-envlist = py3{9,10,11,12,13,14}, pypy3, anypy, doc, lint, mypy
+envlist = py3{9,10,11,12,13,14}, py3{13,14}t, pypy3, anypy, doc, lint, mypy
skip_missing_interpreters = {env:COVERAGE_SKIP_MISSING_INTERPRETERS:True}
toxworkdir = {env:TOXWORKDIR:.tox}
[testenv]
-usedevelop = True
download = True
extras =
toml
@@ -27,11 +26,16 @@ install_command = python -m pip install -U {opts} {packages}
passenv = *
setenv =
pypy3{,9,10,11}: COVERAGE_TEST_CORES=pytrace
- # For some tests, we need .pyc files written in the current directory,
- # so override any local setting.
- PYTHONPYCACHEPREFIX=
# If we ever need a stronger way to suppress warnings:
#PYTHONWARNINGS=ignore:removed in Python 3.14; use ast.Constant:DeprecationWarning
+ # We want to know about missing encoding arguments, but we need to silence
+ # some warnings that aren't ours. We can't silence them in 3.9 because
+ # EncodingWarning doesn't exist yet, and it's hard to suppress them in some
+ # environments and not others. So by default, don't warn, and we'll enable
+ # the warning in a handful of environments to catch the problems.
+ PYTHONWARNDEFAULTENCODING=
+ py3{10,11,12,13,14}: PYTHONWARNDEFAULTENCODING=1
+ py3{10,11,12,13,14}: PYTHONWARNINGS=ignore::EncodingWarning:pip._internal.utils.subprocess
# Disable CPython's color output
PYTHON_COLORS=0
@@ -52,13 +56,21 @@ commands =
python -m pip install {env:COVERAGE_PIP_ARGS} -q -e .
python igor.py test_with_core ctrace {posargs}
- py3{12,13,14},anypy: python igor.py test_with_core sysmon {posargs}
+ py3{12,13,14}{,t},anypy: python igor.py test_with_core sysmon {posargs}
+
+# Until tox properly supports no-gil interpreter selection
+[testenv:py313t]
+basepython = python3.13t
+
+[testenv:py314t]
+basepython = python3.14t
[testenv:anypy]
# $set_env.py: COVERAGE_ANYPY - The custom Python for "tox -e anypy"
# For running against my own builds of CPython, or any other specific Python.
basepython = {env:COVERAGE_ANYPY}
+
[testenv:doc]
# One of the PYVERSIONS, that's currently supported by Sphinx. Make sure it
# matches the `python:version:` in the .readthedocs.yml file, and the
@@ -128,5 +140,8 @@ python =
3.11 = py311
3.12 = py312
3.13 = py313
+ 3.13t = py313t
3.14 = py314
+ 3.14t = py314t
pypy-3 = pypy3
+ pypy-3.11 = pypy3