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 @@

Classes

- 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 @@

- 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