From 1402ac74aa3f0529a15d31aab4f392c4ece3db97 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Jul 2026 11:36:43 +0000 Subject: [PATCH 01/10] Bump the `actions/{github-script,checkout}` and `j178/prek-action` actions (#152760) Bumps the actions group with 3 updates: [actions/github-script](https://github.com/actions/github-script), [actions/checkout](https://github.com/actions/checkout) and [j178/prek-action](https://github.com/j178/prek-action). Updates `actions/github-script` from 8.0.0 to 9.0.0 - [Release notes](https://github.com/actions/github-script/releases) - [Commits](https://github.com/actions/github-script/compare/ed597411d8f924073f98dfc5c65a23a2325f34cd...3a2844b7e9c422d3c10d287c895573f7108da1b3) Updates `actions/checkout` from 6.0.2 to 7.0.0 - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/de0fac2e4500dabe0009e67214ff5f5447ce83dd...9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0) Updates `j178/prek-action` from 1.1.1 to 2.0.4 - [Release notes](https://github.com/j178/prek-action/releases) - [Commits](https://github.com/j178/prek-action/compare/0bb87d7f00b0c99306c8bcb8b8beba1eb581c037...bdca6f102f98e2b4c7029491a53dfd366469e33d) --- updated-dependencies: - dependency-name: actions/github-script dependency-version: 9.0.0 dependency-type: direct:production update-type: version-update:semver-major dependency-group: actions - dependency-name: actions/checkout dependency-version: 7.0.0 dependency-type: direct:production update-type: version-update:semver-major dependency-group: actions - dependency-name: j178/prek-action dependency-version: 2.0.4 dependency-type: direct:production update-type: version-update:semver-major dependency-group: actions ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/add-issue-header.yml | 2 +- .github/workflows/build.yml | 16 ++++++++-------- .github/workflows/jit.yml | 10 +++++----- .github/workflows/lint.yml | 4 ++-- .github/workflows/mypy.yml | 2 +- .github/workflows/new-bugs-announce-notifier.yml | 2 +- .github/workflows/reusable-check-c-api-docs.yml | 2 +- .github/workflows/reusable-check-html-ids.yml | 2 +- .github/workflows/reusable-context.yml | 2 +- .github/workflows/reusable-docs.yml | 6 +++--- .github/workflows/reusable-emscripten.yml | 2 +- .github/workflows/reusable-macos.yml | 2 +- .github/workflows/reusable-san.yml | 2 +- .github/workflows/reusable-ubuntu.yml | 2 +- .github/workflows/reusable-wasi.yml | 2 +- .github/workflows/reusable-windows-msi.yml | 2 +- .github/workflows/reusable-windows.yml | 2 +- .github/workflows/tail-call.yml | 4 ++-- .github/workflows/verify-ensurepip-wheels.yml | 2 +- .github/workflows/verify-expat.yml | 2 +- 20 files changed, 35 insertions(+), 35 deletions(-) diff --git a/.github/workflows/add-issue-header.yml b/.github/workflows/add-issue-header.yml index 4c25976b9c24f7..09da61a470ff95 100644 --- a/.github/workflows/add-issue-header.yml +++ b/.github/workflows/add-issue-header.yml @@ -22,7 +22,7 @@ jobs: issues: write timeout-minutes: 5 steps: - - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: # language=JavaScript script: | diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d9a956a6bf5303..0edf4602bfaf97 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -64,7 +64,7 @@ jobs: run: | apt update && apt install git -yq git config --global --add safe.directory "$GITHUB_WORKSPACE" - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: fetch-depth: 1 persist-credentials: false @@ -101,7 +101,7 @@ jobs: needs: build-context if: needs.build-context.outputs.run-tests == 'true' steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 @@ -291,7 +291,7 @@ jobs: SSLLIB_DIR: ${{ github.workspace }}/multissl/${{ matrix.ssllib.name }}/${{ matrix.ssllib.version }} LD_LIBRARY_PATH: ${{ github.workspace }}/multissl/${{ matrix.ssllib.name }}/${{ matrix.ssllib.version }}/lib steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - name: Runner image version @@ -350,7 +350,7 @@ jobs: runs-on: ${{ matrix.runs-on }} steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - name: Build and test @@ -363,7 +363,7 @@ jobs: timeout-minutes: 60 runs-on: macos-14 steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false @@ -401,7 +401,7 @@ jobs: OPENSSL_VER: 3.5.7 PYTHONSTRICTEXTENSIONBUILD: 1 steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - name: Register gcc problem matcher @@ -510,7 +510,7 @@ jobs: PYTHONSTRICTEXTENSIONBUILD: 1 ASAN_OPTIONS: detect_leaks=0:allocator_may_return_null=1:handle_segv=0 steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - name: Runner image version @@ -577,7 +577,7 @@ jobs: needs: build-context if: needs.build-context.outputs.run-ubuntu == 'true' steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - name: Runner image version diff --git a/.github/workflows/jit.yml b/.github/workflows/jit.yml index 025ff7ecbeeaff..846c90d2525d25 100644 --- a/.github/workflows/jit.yml +++ b/.github/workflows/jit.yml @@ -32,7 +32,7 @@ jobs: runs-on: ubuntu-24.04 timeout-minutes: 60 steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - name: Install dependencies @@ -72,7 +72,7 @@ jobs: architecture: ARM64 runner: windows-11-arm steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 @@ -106,7 +106,7 @@ jobs: - target: aarch64-apple-darwin/clang runner: macos-15 steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 @@ -149,7 +149,7 @@ jobs: - target: aarch64-unknown-linux-gnu/gcc runner: ubuntu-24.04-arm steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 @@ -191,7 +191,7 @@ jobs: use_clang: true run_tests: false steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index e9a4eb2b0808cb..8a79ea20d5f50b 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -19,7 +19,7 @@ jobs: timeout-minutes: 10 steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - - uses: j178/prek-action@0bb87d7f00b0c99306c8bcb8b8beba1eb581c037 # v1.1.1 + - uses: j178/prek-action@bdca6f102f98e2b4c7029491a53dfd366469e33d # v2.0.4 diff --git a/.github/workflows/mypy.yml b/.github/workflows/mypy.yml index d748b6ff63e68a..3cdce4f5952e3d 100644 --- a/.github/workflows/mypy.yml +++ b/.github/workflows/mypy.yml @@ -66,7 +66,7 @@ jobs: "Tools/peg_generator", ] steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 diff --git a/.github/workflows/new-bugs-announce-notifier.yml b/.github/workflows/new-bugs-announce-notifier.yml index 1267361040c81b..9c19adb7ffda70 100644 --- a/.github/workflows/new-bugs-announce-notifier.yml +++ b/.github/workflows/new-bugs-announce-notifier.yml @@ -20,7 +20,7 @@ jobs: node-version: 20 - run: npm install mailgun.js form-data - name: Send notification - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: MAILGUN_API_KEY: ${{ secrets.MAILGUN_PYTHON_ORG_MAILGUN_KEY }} with: diff --git a/.github/workflows/reusable-check-c-api-docs.yml b/.github/workflows/reusable-check-c-api-docs.yml index 49e5ef7f768b79..db030c80008b1e 100644 --- a/.github/workflows/reusable-check-c-api-docs.yml +++ b/.github/workflows/reusable-check-c-api-docs.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 5 steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 diff --git a/.github/workflows/reusable-check-html-ids.yml b/.github/workflows/reusable-check-html-ids.yml index 4f827c55cacd06..41ba1288be1ecf 100644 --- a/.github/workflows/reusable-check-html-ids.yml +++ b/.github/workflows/reusable-check-html-ids.yml @@ -16,7 +16,7 @@ jobs: timeout-minutes: 30 steps: - name: 'Check out PR head' - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false ref: ${{ github.event.pull_request.head.sha }} diff --git a/.github/workflows/reusable-context.yml b/.github/workflows/reusable-context.yml index b8a9e2960eca59..6595aef0377e50 100644 --- a/.github/workflows/reusable-context.yml +++ b/.github/workflows/reusable-context.yml @@ -84,7 +84,7 @@ jobs: - run: >- echo '${{ github.event_name }}' - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false ref: >- diff --git a/.github/workflows/reusable-docs.yml b/.github/workflows/reusable-docs.yml index 7b524569f85c9e..cac481d26a8d0c 100644 --- a/.github/workflows/reusable-docs.yml +++ b/.github/workflows/reusable-docs.yml @@ -27,7 +27,7 @@ jobs: refspec_pr: '+${{ github.event.pull_request.head.sha }}:remotes/origin/${{ github.event.pull_request.head.ref }}' steps: - name: 'Check out latest PR branch commit' - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false ref: >- @@ -98,7 +98,7 @@ jobs: runs-on: ubuntu-24.04 timeout-minutes: 60 steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 @@ -124,7 +124,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 30 steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - name: 'Set up Python' diff --git a/.github/workflows/reusable-emscripten.yml b/.github/workflows/reusable-emscripten.yml index 38e6dcceb8f47c..46664522aa6d79 100644 --- a/.github/workflows/reusable-emscripten.yml +++ b/.github/workflows/reusable-emscripten.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-24.04 timeout-minutes: 40 steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - name: "Read Emscripten config" diff --git a/.github/workflows/reusable-macos.yml b/.github/workflows/reusable-macos.yml index 65a7f857fc4c77..5393e9e34040b9 100644 --- a/.github/workflows/reusable-macos.yml +++ b/.github/workflows/reusable-macos.yml @@ -31,7 +31,7 @@ jobs: PYTHONSTRICTEXTENSIONBUILD: 1 TERM: linux steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - name: Runner image version diff --git a/.github/workflows/reusable-san.yml b/.github/workflows/reusable-san.yml index ef36447964cf41..4c76952386095e 100644 --- a/.github/workflows/reusable-san.yml +++ b/.github/workflows/reusable-san.yml @@ -29,7 +29,7 @@ jobs: runs-on: ubuntu-24.04 timeout-minutes: 60 steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - name: Runner image version diff --git a/.github/workflows/reusable-ubuntu.yml b/.github/workflows/reusable-ubuntu.yml index f4321cefa1b598..b8f264ff13b2ce 100644 --- a/.github/workflows/reusable-ubuntu.yml +++ b/.github/workflows/reusable-ubuntu.yml @@ -39,7 +39,7 @@ jobs: PYTHONSTRICTEXTENSIONBUILD: 1 TERM: linux steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - name: Register gcc problem matcher diff --git a/.github/workflows/reusable-wasi.yml b/.github/workflows/reusable-wasi.yml index 4b4888c3844409..9675c39b86a24c 100644 --- a/.github/workflows/reusable-wasi.yml +++ b/.github/workflows/reusable-wasi.yml @@ -18,7 +18,7 @@ jobs: WASMTIME_VERSION: 38.0.3 CROSS_BUILD_WASI: cross-build/wasm32-wasip1 steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false # No problem resolver registered as one doesn't currently exist for Clang. diff --git a/.github/workflows/reusable-windows-msi.yml b/.github/workflows/reusable-windows-msi.yml index d07b4f7f29e487..d57046ae5ca8f0 100644 --- a/.github/workflows/reusable-windows-msi.yml +++ b/.github/workflows/reusable-windows-msi.yml @@ -23,7 +23,7 @@ jobs: ARCH: ${{ inputs.arch }} IncludeFreethreaded: true steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - name: Build CPython installer diff --git a/.github/workflows/reusable-windows.yml b/.github/workflows/reusable-windows.yml index dbb192fb8819a4..707b8d7efb5e0d 100644 --- a/.github/workflows/reusable-windows.yml +++ b/.github/workflows/reusable-windows.yml @@ -31,7 +31,7 @@ jobs: env: ARCH: ${{ inputs.arch }} steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - name: Register MSVC problem matcher diff --git a/.github/workflows/tail-call.yml b/.github/workflows/tail-call.yml index 656a14906b3cb7..f5b174d87d8448 100644 --- a/.github/workflows/tail-call.yml +++ b/.github/workflows/tail-call.yml @@ -36,7 +36,7 @@ jobs: - target: aarch64-apple-darwin/clang runner: macos-15 steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 @@ -75,7 +75,7 @@ jobs: runner: ubuntu-24.04-arm configure_flags: --with-pydebug steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 diff --git a/.github/workflows/verify-ensurepip-wheels.yml b/.github/workflows/verify-ensurepip-wheels.yml index cb40f6abc0b3b7..7def8c9e78abb8 100644 --- a/.github/workflows/verify-ensurepip-wheels.yml +++ b/.github/workflows/verify-ensurepip-wheels.yml @@ -25,7 +25,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 10 steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 diff --git a/.github/workflows/verify-expat.yml b/.github/workflows/verify-expat.yml index 472a11db2da5fb..bae93a4d9ac0a2 100644 --- a/.github/workflows/verify-expat.yml +++ b/.github/workflows/verify-expat.yml @@ -23,7 +23,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 5 steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - name: Download and verify bundled libexpat files From 34503f39532279efb12653754bb3a7e535fb66cc Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Wed, 1 Jul 2026 13:47:00 +0200 Subject: [PATCH 02/10] gh-144473: Add "steal" term to glossary; clarify "stealing" on error (GH-144502) With one exception, all "stealing" functions also steal on error, but it makes sense to note this in each case. Co-authored-by: Peter Bierma --- Doc/c-api/bytes.rst | 9 +++++---- Doc/c-api/dict.rst | 4 ++-- Doc/c-api/exceptions.rst | 15 +++++++++------ Doc/c-api/gen.rst | 13 +++++++------ Doc/c-api/intro.rst | 9 ++++++--- Doc/c-api/list.rst | 8 +++++--- Doc/c-api/module.rst | 8 ++++---- Doc/c-api/sequence.rst | 2 +- Doc/c-api/threads.rst | 2 +- Doc/c-api/tuple.rst | 9 +++++---- Doc/c-api/unicode.rst | 3 ++- Doc/glossary.rst | 8 ++++++++ 12 files changed, 55 insertions(+), 35 deletions(-) diff --git a/Doc/c-api/bytes.rst b/Doc/c-api/bytes.rst index fa77d3d38ff89f..ff68ecafcda4d0 100644 --- a/Doc/c-api/bytes.rst +++ b/Doc/c-api/bytes.rst @@ -184,10 +184,11 @@ called with a non-bytes parameter. .. c:function:: void PyBytes_Concat(PyObject **bytes, PyObject *newpart) Create a new bytes object in *\*bytes* containing the contents of *newpart* - appended to *bytes*; the caller will own the new reference. The reference to - the old value of *bytes* will be stolen. If the new object cannot be - created, the old reference to *bytes* will still be discarded and the value - of *\*bytes* will be set to ``NULL``; the appropriate exception will be set. + appended to *bytes*; the caller will own the new reference. + The reference to the old value of *bytes* will be ":term:`stolen `". + If the new object cannot be created, the old reference to *bytes* will still + be "stolen", the value of *\*bytes* will be set to ``NULL``, and + the appropriate exception will be set. .. note:: If *newpart* implements the buffer protocol, then the buffer diff --git a/Doc/c-api/dict.rst b/Doc/c-api/dict.rst index 556113a97bf772..87d09ad2412e40 100644 --- a/Doc/c-api/dict.rst +++ b/Doc/c-api/dict.rst @@ -107,8 +107,8 @@ Dictionary objects Insert *val* into the dictionary *p* with a key of *key*. *key* must be :term:`hashable`; if it isn't, :exc:`TypeError` will be raised. Return - ``0`` on success or ``-1`` on failure. This function *does not* steal a - reference to *val*. + ``0`` on success or ``-1`` on failure. + This function *does not* ":term:`steal`" a reference to *val*. .. note:: diff --git a/Doc/c-api/exceptions.rst b/Doc/c-api/exceptions.rst index 82f594e11300a7..78c95791ab3104 100644 --- a/Doc/c-api/exceptions.rst +++ b/Doc/c-api/exceptions.rst @@ -503,7 +503,8 @@ Querying the error indicator .. warning:: - This call steals a reference to *exc*, which must be a valid exception. + This call ":term:`steals `" a reference to *exc*, + which must be a valid exception. .. versionadded:: 3.12 @@ -641,7 +642,8 @@ Querying the error indicator Set the exception info, as known from ``sys.exc_info()``. This refers to an exception that was *already caught*, not to an exception that was - freshly raised. This function steals the references of the arguments. + freshly raised. This function ":term:`steals `" the references + of the arguments. To clear the exception state, pass ``NULL`` for all three arguments. This function is kept for backwards compatibility. Prefer using :c:func:`PyErr_SetHandledException`. @@ -658,8 +660,8 @@ Querying the error indicator .. versionchanged:: 3.11 The ``type`` and ``traceback`` arguments are no longer used and can be NULL. The interpreter now derives them from the exception - instance (the ``value`` argument). The function still steals - references of all three arguments. + instance (the ``value`` argument). The function still + ":term:`steals `" references of all three arguments. Signal Handling @@ -869,7 +871,7 @@ Exception Objects Set the context associated with the exception to *ctx*. Use ``NULL`` to clear it. There is no type check to make sure that *ctx* is an exception instance. - This steals a reference to *ctx*. + This ":term:`steals `" a reference to *ctx*. .. c:function:: PyObject* PyException_GetCause(PyObject *ex) @@ -884,7 +886,8 @@ Exception Objects Set the cause associated with the exception to *cause*. Use ``NULL`` to clear it. There is no type check to make sure that *cause* is either an exception - instance or ``None``. This steals a reference to *cause*. + instance or ``None``. + This ":term:`steals `" a reference to *cause*. The :attr:`~BaseException.__suppress_context__` attribute is implicitly set to ``True`` by this function. diff --git a/Doc/c-api/gen.rst b/Doc/c-api/gen.rst index 7713ba2ee4f804..88c3cc31186826 100644 --- a/Doc/c-api/gen.rst +++ b/Doc/c-api/gen.rst @@ -35,8 +35,8 @@ than explicitly calling :c:func:`PyGen_New` or :c:func:`PyGen_NewWithQualName`. .. c:function:: PyObject* PyGen_New(PyFrameObject *frame) Create and return a new generator object based on the *frame* object. - A reference to *frame* is stolen by this function. The argument must not be - ``NULL``. + A reference to *frame* is ":term:`stolen `" by this function (even + on error). The argument must not be ``NULL``. .. deprecated-removed:: 3.16 3.18 @@ -48,8 +48,8 @@ than explicitly calling :c:func:`PyGen_New` or :c:func:`PyGen_NewWithQualName`. Create and return a new generator object based on the *frame* object, with ``__name__`` and ``__qualname__`` set to *name* and *qualname*. - A reference to *frame* is stolen by this function. The *frame* argument - must not be ``NULL``. + A reference to *frame* is ":term:`stolen `" by this function (even + on error). The *frame* argument must not be ``NULL``. .. deprecated-removed:: 3.16 3.18 @@ -80,8 +80,9 @@ Asynchronous Generator Objects .. c:function:: PyObject *PyAsyncGen_New(PyFrameObject *frame, PyObject *name, PyObject *qualname) Create a new asynchronous generator wrapping *frame*, with ``__name__`` and - ``__qualname__`` set to *name* and *qualname*. *frame* is stolen by this - function and must not be ``NULL``. + ``__qualname__`` set to *name* and *qualname*. + *frame* is ":term:`stolen `" by this function (even on error) and + must not be ``NULL``. On success, this function returns a :term:`strong reference` to the new asynchronous generator. On failure, this function returns ``NULL`` diff --git a/Doc/c-api/intro.rst b/Doc/c-api/intro.rst index 500f2818e2e40a..4c0c9af45e8360 100644 --- a/Doc/c-api/intro.rst +++ b/Doc/c-api/intro.rst @@ -737,9 +737,12 @@ the caller is said to *borrow* the reference. Nothing needs to be done for a Conversely, when a calling function passes in a reference to an object, there are two possibilities: the function *steals* a reference to the object, or it -does not. *Stealing a reference* means that when you pass a reference to a -function, that function assumes that it now owns that reference, and you are not -responsible for it any longer. +does not. + +*Stealing a reference* means that when you pass a reference to a +function, that function assumes that it now owns that reference. +Since the new owner can use :c:func:`!Py_DECREF` at its discretion, +you (the caller) must not use that reference after the call. .. index:: single: PyList_SetItem (C function) diff --git a/Doc/c-api/list.rst b/Doc/c-api/list.rst index 8f560699d355e4..21769c4c5c869c 100644 --- a/Doc/c-api/list.rst +++ b/Doc/c-api/list.rst @@ -102,8 +102,10 @@ List Objects .. note:: - This function "steals" a reference to *item* and discards a reference to - an item already in the list at the affected position. + This function ":term:`steals `" a reference to *item*, + even on error. + On success, it discards a reference to an item already in the list + at the affected position (unless it was ``NULL``). .. c:function:: void PyList_SET_ITEM(PyObject *list, Py_ssize_t i, PyObject *o) @@ -117,7 +119,7 @@ List Objects .. note:: - This macro "steals" a reference to *item*, and, unlike + This macro ":term:`steals `" a reference to *item*, and, unlike :c:func:`PyList_SetItem`, does *not* discard a reference to any item that is being replaced; any reference in *list* at position *i* will be leaked. diff --git a/Doc/c-api/module.rst b/Doc/c-api/module.rst index 9f68abba66bc5d..435583e05469c6 100644 --- a/Doc/c-api/module.rst +++ b/Doc/c-api/module.rst @@ -974,8 +974,8 @@ or code that creates modules dynamically. .. c:function:: int PyModule_Add(PyObject *module, const char *name, PyObject *value) - Similar to :c:func:`PyModule_AddObjectRef`, but "steals" a reference - to *value*. + Similar to :c:func:`PyModule_AddObjectRef`, but ":term:`steals `" + a reference to *value* (even on error). It can be called with a result of function that returns a new reference without bothering to check its result or even saving it to a variable. @@ -990,8 +990,8 @@ or code that creates modules dynamically. .. c:function:: int PyModule_AddObject(PyObject *module, const char *name, PyObject *value) - Similar to :c:func:`PyModule_AddObjectRef`, but steals a reference to - *value* on success (if it returns ``0``). + Similar to :c:func:`PyModule_AddObjectRef`, but :term:`steals ` + a reference to *value* on success (if it returns ``0``). The new :c:func:`PyModule_Add` or :c:func:`PyModule_AddObjectRef` functions are recommended, since it is diff --git a/Doc/c-api/sequence.rst b/Doc/c-api/sequence.rst index 6bae8f25ad75d1..90490cf6749b59 100644 --- a/Doc/c-api/sequence.rst +++ b/Doc/c-api/sequence.rst @@ -67,7 +67,7 @@ Sequence Protocol Assign object *v* to the *i*\ th element of *o*. Raise an exception and return ``-1`` on failure; return ``0`` on success. This is the equivalent of the Python statement ``o[i] = v``. This function *does - not* steal a reference to *v*. + not* ":term:`steal`" a reference to *v*. If *v* is ``NULL``, the element is deleted, but this feature is deprecated in favour of using :c:func:`PySequence_DelItem`. diff --git a/Doc/c-api/threads.rst b/Doc/c-api/threads.rst index 67a9ac54909727..f48af654e7422b 100644 --- a/Doc/c-api/threads.rst +++ b/Doc/c-api/threads.rst @@ -924,7 +924,7 @@ pointer and a void pointer argument. To prevent naive misuse, you must write your own C extension to call this. This function must be called with an :term:`attached thread state`. - This function does not steal any references to *exc*. + This function does not :term:`steal` any references to *exc*. This function does not necessarily interrupt system calls such as :py:func:`~time.sleep`. diff --git a/Doc/c-api/tuple.rst b/Doc/c-api/tuple.rst index ba4c6b93de4c11..e8be4762dc33a1 100644 --- a/Doc/c-api/tuple.rst +++ b/Doc/c-api/tuple.rst @@ -104,8 +104,9 @@ Tuple Objects .. note:: - This function "steals" a reference to *o* and discards a reference to - an item already in the tuple at the affected position. + This function ":term:`steals `" a reference to *o* and discards + a reference to an item already in the tuple at the affected position + (unless it was NULL). .. c:function:: void PyTuple_SET_ITEM(PyObject *p, Py_ssize_t pos, PyObject *o) @@ -118,7 +119,7 @@ Tuple Objects .. note:: - This function "steals" a reference to *o*, and, unlike + This function ":term:`steals `" a reference to *o*, and, unlike :c:func:`PyTuple_SetItem`, does *not* discard a reference to any item that is being replaced; any reference in the tuple at position *pos* will be leaked. @@ -263,7 +264,7 @@ type. .. note:: - This function "steals" a reference to *o*. + This function ":term:`steals `" a reference to *o*. .. c:function:: void PyStructSequence_SET_ITEM(PyObject *p, Py_ssize_t *pos, PyObject *o) diff --git a/Doc/c-api/unicode.rst b/Doc/c-api/unicode.rst index 634dcbce7a5791..9bf801ad608c77 100644 --- a/Doc/c-api/unicode.rst +++ b/Doc/c-api/unicode.rst @@ -676,7 +676,8 @@ APIs: Append the string *right* to the end of *p_left*. *p_left* must point to a :term:`strong reference` to a Unicode object; - :c:func:`!PyUnicode_Append` releases ("steals") this reference. + :c:func:`!PyUnicode_Append` releases (":term:`steals `") + this reference. On error, set *\*p_left* to ``NULL`` and set an exception. diff --git a/Doc/glossary.rst b/Doc/glossary.rst index b25532d2d63412..bb00a4f02f0efd 100644 --- a/Doc/glossary.rst +++ b/Doc/glossary.rst @@ -1515,6 +1515,14 @@ Glossary stdlib An abbreviation of :term:`standard library`. + steal + In Python's C API, "*stealing*" an argument means that ownership of the + argument is transferred to the called function. + The caller must not use that reference after the call. + Generally, functions that "steal" an argument do so even if they fail. + + See :ref:`api-refcountdetails` for a full explanation. + strong reference In Python's C API, a strong reference is a reference to an object which is owned by the code holding the reference. The strong From b52bc564513d720fb0c6470871c1f5c24c7ce1d8 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Wed, 1 Jul 2026 14:22:34 +0200 Subject: [PATCH 03/10] Fix copy paste error with Py_mod_state_traverse (GH-152776) Co-authored-by: hydrogen-mvm --- Doc/c-api/module.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/c-api/module.rst b/Doc/c-api/module.rst index 435583e05469c6..31df4451f15e66 100644 --- a/Doc/c-api/module.rst +++ b/Doc/c-api/module.rst @@ -488,7 +488,7 @@ defining the module state. .. versionadded:: 3.15 - Use :c:member:`PyModuleDef.m_size` instead to support previous versions. + Use :c:member:`PyModuleDef.m_traverse` instead to support previous versions. .. c:macro:: Py_mod_state_clear From c6982439ee0dd11f8014f78205132df0a8a85027 Mon Sep 17 00:00:00 2001 From: Mark Shannon Date: Wed, 1 Jul 2026 15:15:33 +0100 Subject: [PATCH 04/10] GH-148874: Make sure that mngr.__exit__() is always called in a with statement (GH-150911) * Even if there is an interrupt during the call to mngr.__enter__() --- Lib/test/test_with.py | 17 +++++ ...-06-04-12-53-10.gh-issue-148874.r121cG.rst | 3 + Modules/_testinternalcapi.c | 65 +++++++++++++++++++ Modules/_testinternalcapi/test_cases.c.h | 34 +++++----- Python/bytecodes.c | 2 +- Python/ceval_macros.h | 16 +++++ Python/generated_cases.c.h | 34 +++++----- Python/optimizer.c | 9 ++- Tools/c-analyzer/cpython/ignored.tsv | 1 + 9 files changed, 144 insertions(+), 37 deletions(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-06-04-12-53-10.gh-issue-148874.r121cG.rst diff --git a/Lib/test/test_with.py b/Lib/test/test_with.py index f16611b29a2658..60aaa5cd548cbf 100644 --- a/Lib/test/test_with.py +++ b/Lib/test/test_with.py @@ -10,6 +10,7 @@ import unittest from collections import deque from contextlib import _GeneratorContextManager, contextmanager, nullcontext +from _testinternalcapi import SelfInterruptingContextManager def do_with(obj): @@ -850,5 +851,21 @@ def exit_raises(): expected) +class InterruptDuringEnter(unittest.TestCase): + + def test_exit_called_after_interrupt(self): + cm = SelfInterruptingContextManager() + self.assertFalse(cm.within()) + try: + with cm: + self.assertTrue(cm.within()) + except KeyboardInterrupt: + self.assertFalse(cm.within()) + return + except: + self.fail("Wrong exception raised") + self.fail("No exception raised") + + if __name__ == '__main__': unittest.main() diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-06-04-12-53-10.gh-issue-148874.r121cG.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-06-04-12-53-10.gh-issue-148874.r121cG.rst new file mode 100644 index 00000000000000..95f93338beb912 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-06-04-12-53-10.gh-issue-148874.r121cG.rst @@ -0,0 +1,3 @@ +Ignore interrupts immediately after calling the ``__enter__`` method of a +context menager in a ``with`` statement. This ensures that the ``__exit__`` +method is always called in a ``with`` statement. diff --git a/Modules/_testinternalcapi.c b/Modules/_testinternalcapi.c index f6ff7820821ce1..ea3ad2b81c2866 100644 --- a/Modules/_testinternalcapi.c +++ b/Modules/_testinternalcapi.c @@ -3207,6 +3207,66 @@ test_thread_state_ensure_from_view_interp_switch(PyObject *self, PyObject *unuse Py_RETURN_NONE; } +/* Self interrupting context manager */ + +typedef struct { + PyObject_HEAD + int within; +} SelfInterruptingContextManagerObject; + +static PyObject * +new_self_interrupting(PyTypeObject *type, PyObject *args, PyObject *kwds) +{ + SelfInterruptingContextManagerObject *self = + (SelfInterruptingContextManagerObject *)type->tp_alloc(type, 0); + if (self != NULL) { + self->within = 0; + } + return (PyObject *)self; +} + +static PyObject * +self_interrupting_enter(PyObject *op, PyObject *Py_UNUSED(dummy)) +{ + ((SelfInterruptingContextManagerObject *)op)->within = 1; + PyThreadState *tstate = PyThreadState_Get(); + PyObject *ki = Py_NewRef(PyExc_KeyboardInterrupt); + PyObject *old_exc = _Py_atomic_exchange_ptr(&tstate->async_exc, ki); + _Py_set_eval_breaker_bit(tstate, _PY_ASYNC_EXCEPTION_BIT); + Py_XDECREF(old_exc); + + return Py_NewRef(op); +} + +static PyObject * +self_interrupting_within(PyObject *op, PyObject *Py_UNUSED(dummy)) +{ + return PyBool_FromLong(((SelfInterruptingContextManagerObject *)op)->within); +} + +static PyObject * +self_interrupting_exit(PyObject *op, PyObject *Py_UNUSED(args)) { + ((SelfInterruptingContextManagerObject *)op)->within = 0; + Py_RETURN_NONE; +} + +static PyMethodDef self_interrupting_methods[] = { + {"__enter__", self_interrupting_enter, METH_NOARGS, NULL}, + {"within", self_interrupting_within, METH_NOARGS, NULL}, + {"__exit__", self_interrupting_exit, METH_VARARGS, NULL}, + {NULL, NULL} /* sentinel */ +}; + +static PyTypeObject SelfInterruptingContextManager_Type = { + PyVarObject_HEAD_INIT(NULL, 0) + "_testcapi.SelfInterruptingContextManager", + sizeof(SelfInterruptingContextManagerObject), + .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_IMMUTABLETYPE, + .tp_new = new_self_interrupting, + .tp_methods = self_interrupting_methods, +}; + + static PyMethodDef module_functions[] = { {"get_configs", get_configs, METH_NOARGS}, {"get_eval_frame_stats", get_eval_frame_stats, METH_NOARGS, NULL}, @@ -3429,6 +3489,11 @@ module_exec(PyObject *module) } #endif + if (PyType_Ready(&SelfInterruptingContextManager_Type) < 0) { + return 1; + } + PyModule_AddObject(module, "SelfInterruptingContextManager", (PyObject *)&SelfInterruptingContextManager_Type); + return 0; } diff --git a/Modules/_testinternalcapi/test_cases.c.h b/Modules/_testinternalcapi/test_cases.c.h index f36c8192ff2662..5f2b1ae5d978aa 100644 --- a/Modules/_testinternalcapi/test_cases.c.h +++ b/Modules/_testinternalcapi/test_cases.c.h @@ -1927,7 +1927,7 @@ ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); _PyFrame_SetStackPointer(frame, stack_pointer); _PyFrame_StackPointerValidate(frame); - int err = check_periodics(tstate); + int err = check_periodics_at_end(tstate, frame); _PyFrame_StackPointerInvalidate(frame); if (err != 0) { JUMP_TO_LABEL(error); @@ -2453,7 +2453,7 @@ { assert(stack_pointer == _PyFrame_GetStackPointer(frame)); _PyFrame_StackPointerValidate(frame); - int err = check_periodics(tstate); + int err = check_periodics_at_end(tstate, frame); _PyFrame_StackPointerInvalidate(frame); if (err != 0) { JUMP_TO_LABEL(error); @@ -2546,7 +2546,7 @@ { assert(stack_pointer == _PyFrame_GetStackPointer(frame)); _PyFrame_StackPointerValidate(frame); - int err = check_periodics(tstate); + int err = check_periodics_at_end(tstate, frame); _PyFrame_StackPointerInvalidate(frame); if (err != 0) { JUMP_TO_LABEL(error); @@ -2635,7 +2635,7 @@ { assert(stack_pointer == _PyFrame_GetStackPointer(frame)); _PyFrame_StackPointerValidate(frame); - int err = check_periodics(tstate); + int err = check_periodics_at_end(tstate, frame); _PyFrame_StackPointerInvalidate(frame); if (err != 0) { JUMP_TO_LABEL(error); @@ -2746,7 +2746,7 @@ { assert(stack_pointer == _PyFrame_GetStackPointer(frame)); _PyFrame_StackPointerValidate(frame); - int err = check_periodics(tstate); + int err = check_periodics_at_end(tstate, frame); _PyFrame_StackPointerInvalidate(frame); if (err != 0) { JUMP_TO_LABEL(error); @@ -2860,7 +2860,7 @@ ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); _PyFrame_SetStackPointer(frame, stack_pointer); _PyFrame_StackPointerValidate(frame); - int err = check_periodics(tstate); + int err = check_periodics_at_end(tstate, frame); _PyFrame_StackPointerInvalidate(frame); if (err != 0) { JUMP_TO_LABEL(error); @@ -3191,7 +3191,7 @@ ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); _PyFrame_SetStackPointer(frame, stack_pointer); _PyFrame_StackPointerValidate(frame); - int err = check_periodics(tstate); + int err = check_periodics_at_end(tstate, frame); _PyFrame_StackPointerInvalidate(frame); if (err != 0) { JUMP_TO_LABEL(error); @@ -3703,7 +3703,7 @@ ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); _PyFrame_SetStackPointer(frame, stack_pointer); _PyFrame_StackPointerValidate(frame); - int err = check_periodics(tstate); + int err = check_periodics_at_end(tstate, frame); _PyFrame_StackPointerInvalidate(frame); if (err != 0) { JUMP_TO_LABEL(error); @@ -4122,7 +4122,7 @@ { assert(stack_pointer == _PyFrame_GetStackPointer(frame)); _PyFrame_StackPointerValidate(frame); - int err = check_periodics(tstate); + int err = check_periodics_at_end(tstate, frame); _PyFrame_StackPointerInvalidate(frame); if (err != 0) { JUMP_TO_LABEL(error); @@ -4242,7 +4242,7 @@ { assert(stack_pointer == _PyFrame_GetStackPointer(frame)); _PyFrame_StackPointerValidate(frame); - int err = check_periodics(tstate); + int err = check_periodics_at_end(tstate, frame); _PyFrame_StackPointerInvalidate(frame); if (err != 0) { JUMP_TO_LABEL(error); @@ -4364,7 +4364,7 @@ { assert(stack_pointer == _PyFrame_GetStackPointer(frame)); _PyFrame_StackPointerValidate(frame); - int err = check_periodics(tstate); + int err = check_periodics_at_end(tstate, frame); _PyFrame_StackPointerInvalidate(frame); if (err != 0) { JUMP_TO_LABEL(error); @@ -4499,7 +4499,7 @@ { assert(stack_pointer == _PyFrame_GetStackPointer(frame)); _PyFrame_StackPointerValidate(frame); - int err = check_periodics(tstate); + int err = check_periodics_at_end(tstate, frame); _PyFrame_StackPointerInvalidate(frame); if (err != 0) { JUMP_TO_LABEL(error); @@ -4576,7 +4576,7 @@ ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); _PyFrame_SetStackPointer(frame, stack_pointer); _PyFrame_StackPointerValidate(frame); - int err = check_periodics(tstate); + int err = check_periodics_at_end(tstate, frame); _PyFrame_StackPointerInvalidate(frame); if (err != 0) { JUMP_TO_LABEL(error); @@ -4880,7 +4880,7 @@ { assert(stack_pointer == _PyFrame_GetStackPointer(frame)); _PyFrame_StackPointerValidate(frame); - int err = check_periodics(tstate); + int err = check_periodics_at_end(tstate, frame); _PyFrame_StackPointerInvalidate(frame); if (err != 0) { JUMP_TO_LABEL(error); @@ -4958,7 +4958,7 @@ { assert(stack_pointer == _PyFrame_GetStackPointer(frame)); _PyFrame_StackPointerValidate(frame); - int err = check_periodics(tstate); + int err = check_periodics_at_end(tstate, frame); _PyFrame_StackPointerInvalidate(frame); if (err != 0) { JUMP_TO_LABEL(error); @@ -7221,7 +7221,7 @@ ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); _PyFrame_SetStackPointer(frame, stack_pointer); _PyFrame_StackPointerValidate(frame); - int err = check_periodics(tstate); + int err = check_periodics_at_end(tstate, frame); _PyFrame_StackPointerInvalidate(frame); if (err != 0) { JUMP_TO_LABEL(error); @@ -7403,7 +7403,7 @@ ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); _PyFrame_SetStackPointer(frame, stack_pointer); _PyFrame_StackPointerValidate(frame); - int err = check_periodics(tstate); + int err = check_periodics_at_end(tstate, frame); _PyFrame_StackPointerInvalidate(frame); if (err != 0) { JUMP_TO_LABEL(error); diff --git a/Python/bytecodes.c b/Python/bytecodes.c index 753530b0dabff3..8ac397081e2738 100644 --- a/Python/bytecodes.c +++ b/Python/bytecodes.c @@ -161,7 +161,7 @@ dummy_func( } replaced op(_CHECK_PERIODIC_AT_END, (--)) { - int err = check_periodics(tstate); + int err = check_periodics_at_end(tstate, frame); ERROR_IF(err != 0); } diff --git a/Python/ceval_macros.h b/Python/ceval_macros.h index b13884bf8214d4..f19adfa0cfcfc1 100644 --- a/Python/ceval_macros.h +++ b/Python/ceval_macros.h @@ -528,6 +528,22 @@ check_periodics(PyThreadState *tstate) { return 0; } +static inline int +check_periodics_at_end(PyThreadState *tstate, _PyInterpreterFrame *frame) { + _Py_CHECK_EMSCRIPTEN_SIGNALS_PERIODICALLY(); + QSBR_QUIESCENT_STATE(tstate); + if (_Py_atomic_load_uintptr_relaxed(&tstate->eval_breaker) & _PY_EVAL_EVENTS_MASK) { + // Do not handle pending interrupts if the previous instruction was LOAD_SPECIAL + // This may also not handle interrupts if a cache looks like LOAD_SPECIAL, + // but this is benign as we won't skip periodic checks indefinitely. + if (frame->instr_ptr[-1].op.code == LOAD_SPECIAL) { + return 0; + } + return _Py_HandlePending(tstate); + } + return 0; +} + // Mark the generator as executing. Returns true if the state was changed, // false if it was already executing or finished. static inline bool diff --git a/Python/generated_cases.c.h b/Python/generated_cases.c.h index 88678f14a99585..cc95179ccaab17 100644 --- a/Python/generated_cases.c.h +++ b/Python/generated_cases.c.h @@ -1927,7 +1927,7 @@ ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); _PyFrame_SetStackPointer(frame, stack_pointer); _PyFrame_StackPointerValidate(frame); - int err = check_periodics(tstate); + int err = check_periodics_at_end(tstate, frame); _PyFrame_StackPointerInvalidate(frame); if (err != 0) { JUMP_TO_LABEL(error); @@ -2453,7 +2453,7 @@ { assert(stack_pointer == _PyFrame_GetStackPointer(frame)); _PyFrame_StackPointerValidate(frame); - int err = check_periodics(tstate); + int err = check_periodics_at_end(tstate, frame); _PyFrame_StackPointerInvalidate(frame); if (err != 0) { JUMP_TO_LABEL(error); @@ -2546,7 +2546,7 @@ { assert(stack_pointer == _PyFrame_GetStackPointer(frame)); _PyFrame_StackPointerValidate(frame); - int err = check_periodics(tstate); + int err = check_periodics_at_end(tstate, frame); _PyFrame_StackPointerInvalidate(frame); if (err != 0) { JUMP_TO_LABEL(error); @@ -2635,7 +2635,7 @@ { assert(stack_pointer == _PyFrame_GetStackPointer(frame)); _PyFrame_StackPointerValidate(frame); - int err = check_periodics(tstate); + int err = check_periodics_at_end(tstate, frame); _PyFrame_StackPointerInvalidate(frame); if (err != 0) { JUMP_TO_LABEL(error); @@ -2746,7 +2746,7 @@ { assert(stack_pointer == _PyFrame_GetStackPointer(frame)); _PyFrame_StackPointerValidate(frame); - int err = check_periodics(tstate); + int err = check_periodics_at_end(tstate, frame); _PyFrame_StackPointerInvalidate(frame); if (err != 0) { JUMP_TO_LABEL(error); @@ -2860,7 +2860,7 @@ ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); _PyFrame_SetStackPointer(frame, stack_pointer); _PyFrame_StackPointerValidate(frame); - int err = check_periodics(tstate); + int err = check_periodics_at_end(tstate, frame); _PyFrame_StackPointerInvalidate(frame); if (err != 0) { JUMP_TO_LABEL(error); @@ -3191,7 +3191,7 @@ ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); _PyFrame_SetStackPointer(frame, stack_pointer); _PyFrame_StackPointerValidate(frame); - int err = check_periodics(tstate); + int err = check_periodics_at_end(tstate, frame); _PyFrame_StackPointerInvalidate(frame); if (err != 0) { JUMP_TO_LABEL(error); @@ -3703,7 +3703,7 @@ ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); _PyFrame_SetStackPointer(frame, stack_pointer); _PyFrame_StackPointerValidate(frame); - int err = check_periodics(tstate); + int err = check_periodics_at_end(tstate, frame); _PyFrame_StackPointerInvalidate(frame); if (err != 0) { JUMP_TO_LABEL(error); @@ -4122,7 +4122,7 @@ { assert(stack_pointer == _PyFrame_GetStackPointer(frame)); _PyFrame_StackPointerValidate(frame); - int err = check_periodics(tstate); + int err = check_periodics_at_end(tstate, frame); _PyFrame_StackPointerInvalidate(frame); if (err != 0) { JUMP_TO_LABEL(error); @@ -4242,7 +4242,7 @@ { assert(stack_pointer == _PyFrame_GetStackPointer(frame)); _PyFrame_StackPointerValidate(frame); - int err = check_periodics(tstate); + int err = check_periodics_at_end(tstate, frame); _PyFrame_StackPointerInvalidate(frame); if (err != 0) { JUMP_TO_LABEL(error); @@ -4364,7 +4364,7 @@ { assert(stack_pointer == _PyFrame_GetStackPointer(frame)); _PyFrame_StackPointerValidate(frame); - int err = check_periodics(tstate); + int err = check_periodics_at_end(tstate, frame); _PyFrame_StackPointerInvalidate(frame); if (err != 0) { JUMP_TO_LABEL(error); @@ -4499,7 +4499,7 @@ { assert(stack_pointer == _PyFrame_GetStackPointer(frame)); _PyFrame_StackPointerValidate(frame); - int err = check_periodics(tstate); + int err = check_periodics_at_end(tstate, frame); _PyFrame_StackPointerInvalidate(frame); if (err != 0) { JUMP_TO_LABEL(error); @@ -4576,7 +4576,7 @@ ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); _PyFrame_SetStackPointer(frame, stack_pointer); _PyFrame_StackPointerValidate(frame); - int err = check_periodics(tstate); + int err = check_periodics_at_end(tstate, frame); _PyFrame_StackPointerInvalidate(frame); if (err != 0) { JUMP_TO_LABEL(error); @@ -4880,7 +4880,7 @@ { assert(stack_pointer == _PyFrame_GetStackPointer(frame)); _PyFrame_StackPointerValidate(frame); - int err = check_periodics(tstate); + int err = check_periodics_at_end(tstate, frame); _PyFrame_StackPointerInvalidate(frame); if (err != 0) { JUMP_TO_LABEL(error); @@ -4958,7 +4958,7 @@ { assert(stack_pointer == _PyFrame_GetStackPointer(frame)); _PyFrame_StackPointerValidate(frame); - int err = check_periodics(tstate); + int err = check_periodics_at_end(tstate, frame); _PyFrame_StackPointerInvalidate(frame); if (err != 0) { JUMP_TO_LABEL(error); @@ -7221,7 +7221,7 @@ ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); _PyFrame_SetStackPointer(frame, stack_pointer); _PyFrame_StackPointerValidate(frame); - int err = check_periodics(tstate); + int err = check_periodics_at_end(tstate, frame); _PyFrame_StackPointerInvalidate(frame); if (err != 0) { JUMP_TO_LABEL(error); @@ -7403,7 +7403,7 @@ ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); _PyFrame_SetStackPointer(frame, stack_pointer); _PyFrame_StackPointerValidate(frame); - int err = check_periodics(tstate); + int err = check_periodics_at_end(tstate, frame); _PyFrame_StackPointerInvalidate(frame); if (err != 0) { JUMP_TO_LABEL(error); diff --git a/Python/optimizer.c b/Python/optimizer.c index e95e4b5e24b2c5..c9f6ebdb62f07b 100644 --- a/Python/optimizer.c +++ b/Python/optimizer.c @@ -956,10 +956,15 @@ _PyJit_translate_single_bytecode_to_trace( case OPARG_REPLACED: uop = _PyUOp_Replacements[uop]; assert(uop != 0); - uint32_t next_inst = target + 1 + _PyOpcode_Caches[_PyOpcode_Deopt[opcode]]; if (uop == _TIER2_RESUME_CHECK) { - target = next_inst; + if (this_instr[-1].op.code == LOAD_SPECIAL) { + // Don't check eval breaker immediately after LOAD_SPECIAL + uop = _NOP; + } + else { + target = next_inst; + } } else { int extended_arg = orig_oparg > 255; diff --git a/Tools/c-analyzer/cpython/ignored.tsv b/Tools/c-analyzer/cpython/ignored.tsv index bf08e5568205e7..6e18593ad69857 100644 --- a/Tools/c-analyzer/cpython/ignored.tsv +++ b/Tools/c-analyzer/cpython/ignored.tsv @@ -577,6 +577,7 @@ Modules/_testimportmultiple.c - _testimportmultiple - Modules/_testinternalcapi.c - pending_identify_result - Modules/_testinternalcapi.c - Test_EvalFrame_Resumes - Modules/_testinternalcapi.c - Test_EvalFrame_Loads - +Modules/_testinternalcapi.c - SelfInterruptingContextManager_Type - Modules/_testinternalcapi/interpreter.c - Test_EvalFrame_Resumes - Modules/_testinternalcapi/interpreter.c - Test_EvalFrame_Loads - Modules/_testlimitedcapi/slots.c - TestMethods - From 0f59b2a800d2f91b863c80c92b64966a8ed28515 Mon Sep 17 00:00:00 2001 From: Terry Jan Reedy Date: Wed, 1 Jul 2026 11:08:37 -0400 Subject: [PATCH 05/10] Use tk_scaling() instead of a raw Tcl call in IDLE and tkinter tests (#152788) Replace `tk.call('tk', 'scaling')` with `tk_scaling()`, which returns a float, in idlelib.util and test_tkinter.widget_tests. Co-authored-by: serhiy-storchaka --- Lib/idlelib/util.py | 2 +- Lib/test/test_tkinter/widget_tests.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/idlelib/util.py b/Lib/idlelib/util.py index bf88c905e1d177..bdf02892de68ea 100644 --- a/Lib/idlelib/util.py +++ b/Lib/idlelib/util.py @@ -24,7 +24,7 @@ def fix_scaling(root): # Called in filelist _test, pyshell, and run. """Scale fonts on HiDPI displays, once per process.""" import tkinter.font - scaling = float(root.tk.call('tk', 'scaling')) + scaling = root.tk_scaling() if scaling > 1.4: for name in tkinter.font.names(root): font = tkinter.font.Font(root=root, name=name, exists=True) diff --git a/Lib/test/test_tkinter/widget_tests.py b/Lib/test/test_tkinter/widget_tests.py index 014906cba2902c..05e60f7ee118e3 100644 --- a/Lib/test/test_tkinter/widget_tests.py +++ b/Lib/test/test_tkinter/widget_tests.py @@ -34,7 +34,7 @@ def scaling(self): try: return self._scaling except AttributeError: - self._scaling = float(self.root.call('tk', 'scaling')) + self._scaling = self.root.tk_scaling() return self._scaling def _str(self, value): From 4e16b8d4eef162082c3d409f171802ceccb36521 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Wed, 1 Jul 2026 18:10:49 +0300 Subject: [PATCH 06/10] gh-66331: Set correct WM_CLASS on X11 for IDLE windows (#152733) Set the WM_CLASS of IDLE's long-lived windows (window-list windows and the stack viewers) to "Idle" instead of the default "Toplevel", so that window managers group and label them correctly. --- Lib/idlelib/idle_test/test_stackviewer.py | 2 ++ Lib/idlelib/idle_test/test_window.py | 5 +++++ Lib/idlelib/pyshell.py | 2 +- Lib/idlelib/stackviewer.py | 2 +- Lib/idlelib/window.py | 4 ++++ .../next/IDLE/2026-07-01-00-00-00.gh-issue-66331.wMcLaS.rst | 3 +++ 6 files changed, 16 insertions(+), 2 deletions(-) create mode 100644 Misc/NEWS.d/next/IDLE/2026-07-01-00-00-00.gh-issue-66331.wMcLaS.rst diff --git a/Lib/idlelib/idle_test/test_stackviewer.py b/Lib/idlelib/idle_test/test_stackviewer.py index 2434d38e4ffe83..341f158031d5f4 100644 --- a/Lib/idlelib/idle_test/test_stackviewer.py +++ b/Lib/idlelib/idle_test/test_stackviewer.py @@ -35,6 +35,8 @@ def test_init(self): isi(stackviewer.sc, ScrolledCanvas) isi(stackviewer.item, stackviewer.StackTreeItem) isi(stackviewer.node, TreeNode) + top = stackviewer.sc.frame.winfo_toplevel() + self.assertEqual(top.winfo_class(), 'Idle') if __name__ == '__main__': diff --git a/Lib/idlelib/idle_test/test_window.py b/Lib/idlelib/idle_test/test_window.py index 9b56d321a407d6..d8a77817205dc3 100644 --- a/Lib/idlelib/idle_test/test_window.py +++ b/Lib/idlelib/idle_test/test_window.py @@ -39,6 +39,11 @@ def test_init(self): win = window.ListedToplevel(self.root) self.assertIn(win, window.registry) self.assertEqual(win.focused_widget, win) + self.assertEqual(win.winfo_class(), 'Idle') + + def test_init_class_override(self): + win = window.ListedToplevel(self.root, class_='Other') + self.assertEqual(win.winfo_class(), 'Other') if __name__ == '__main__': diff --git a/Lib/idlelib/pyshell.py b/Lib/idlelib/pyshell.py index 439f98170292b0..f117f0bfb52f2a 100755 --- a/Lib/idlelib/pyshell.py +++ b/Lib/idlelib/pyshell.py @@ -644,7 +644,7 @@ def remote_stack_viewer(self): return item = debugobj_r.StubObjectTreeItem(self.rpcclt, oid) from idlelib.tree import ScrolledCanvas, TreeNode - top = Toplevel(self.tkconsole.root) + top = Toplevel(self.tkconsole.root, class_='Idle') theme = idleConf.CurrentTheme() background = idleConf.GetHighlight(theme, 'normal')['background'] sc = ScrolledCanvas(top, bg=background, highlightthickness=0) diff --git a/Lib/idlelib/stackviewer.py b/Lib/idlelib/stackviewer.py index 95042d4debdc03..268e2f1daeb98c 100644 --- a/Lib/idlelib/stackviewer.py +++ b/Lib/idlelib/stackviewer.py @@ -11,7 +11,7 @@ def StackBrowser(root, exc, flist=None, top=None): global sc, item, node # For testing. if top is None: - top = tk.Toplevel(root) + top = tk.Toplevel(root, class_='Idle') sc = ScrolledCanvas(top, bg="white", highlightthickness=0) sc.frame.pack(expand=1, fill="both") item = StackTreeItem(exc, flist) diff --git a/Lib/idlelib/window.py b/Lib/idlelib/window.py index 460d5b67948dde..a41d69927dcdd5 100644 --- a/Lib/idlelib/window.py +++ b/Lib/idlelib/window.py @@ -61,6 +61,10 @@ def call_callbacks(self): class ListedToplevel(Toplevel): def __init__(self, master, **kw): + # Set the WM_CLASS property so that X11 window managers group and + # label IDLE's windows under 'Idle' instead of the default + # 'Toplevel' (gh-66331). It matches the class name passed to Tk(). + kw.setdefault('class_', 'Idle') Toplevel.__init__(self, master, kw) registry.add(self) self.focused_widget = self diff --git a/Misc/NEWS.d/next/IDLE/2026-07-01-00-00-00.gh-issue-66331.wMcLaS.rst b/Misc/NEWS.d/next/IDLE/2026-07-01-00-00-00.gh-issue-66331.wMcLaS.rst new file mode 100644 index 00000000000000..e00e7c7d8fdc65 --- /dev/null +++ b/Misc/NEWS.d/next/IDLE/2026-07-01-00-00-00.gh-issue-66331.wMcLaS.rst @@ -0,0 +1,3 @@ +Set the ``WM_CLASS`` window property of IDLE's windows to ``Idle`` on X11, +so that window managers group and label them correctly instead of using the +default ``Toplevel``. From ef27e5b310feebb6068f9e798eee7069e1966fc9 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Wed, 1 Jul 2026 18:55:43 +0300 Subject: [PATCH 07/10] gh-71956: Fix IDLE Replace All searching up without wrap around (#152737) When the search direction is "Up" and "Wrap around" is off, Replace All replaced only the first match above the current position (and all matches below it). It now replaces all matches from the start of the text down to the current position, consistently with the "Up" direction. --- Lib/idlelib/idle_test/test_replace.py | 86 +++++++++++++++++-- Lib/idlelib/replace.py | 29 +++++-- ...6-07-01-12-00-00.gh-issue-71956.rPlAlL.rst | 3 + 3 files changed, 105 insertions(+), 13 deletions(-) create mode 100644 Misc/NEWS.d/next/IDLE/2026-07-01-12-00-00.gh-issue-71956.rPlAlL.rst diff --git a/Lib/idlelib/idle_test/test_replace.py b/Lib/idlelib/idle_test/test_replace.py index 6c07389b29ad45..5342a3856ef65d 100644 --- a/Lib/idlelib/idle_test/test_replace.py +++ b/Lib/idlelib/idle_test/test_replace.py @@ -246,32 +246,108 @@ def test_replace_backwards(self): equal(text.get('1.2', '1.5'), 'was') def test_replace_all(self): + # The default mode, forward with wrap around, replaces every + # match, both below and above the current position. + equal = self.assertEqual text = self.text pv = self.engine.patvar rv = self.dialog.replvar replace_all = self.dialog.replace_all - text.insert('insert', '\n') - text.insert('insert', text.get('1.0', 'end')*100) - pv.set('is') - rv.set('was') + text.delete('1.0', 'end') + text.insert('1.0', 'a\na\na\n') + text.mark_set('insert', '2.1') + pv.set('a') + rv.set('b') replace_all() - self.assertNotIn('is', text.get('1.0', 'end')) + equal(text.get('1.0', '3.end'), 'b\nb\nb') # Wrapped around. + # An empty regular expression is reported as an error. self.engine.revar.set(True) pv.set('') replace_all() self.assertIn('error', showerror.title) self.assertIn('Empty', showerror.message) + # An invalid replacement expression is reported as an error, + # and nothing is replaced. + text.delete('1.0', 'end') + text.insert('1.0', 'asT') pv.set('[s][T]') rv.set('\\') replace_all() + self.assertIn('error', showerror.title) + self.assertIn('Invalid Replace Expression', showerror.message) + equal(text.get('1.0', '1.end'), 'asT') + # A pattern that is not present replaces nothing. self.engine.revar.set(False) + text.delete('1.0', 'end') + text.insert('1.0', 'unchanged') pv.set('text which is not present') rv.set('foobar') replace_all() + equal(text.get('1.0', '1.end'), 'unchanged') + + def test_replace_all_backwards_no_wrap(self): + # gh-71956: 'up' without wrap replaces all matches from the start + # of the text down to the current position, not just one up. + equal = self.assertEqual + text = self.text + pv = self.engine.patvar + rv = self.dialog.replvar + replace_all = self.dialog.replace_all + self.engine.backvar.set(True) + self.engine.wrapvar.set(False) + + text.delete('1.0', 'end') + text.insert('1.0', 'a\na\na\n') + text.mark_set('insert', '2.1') + pv.set('a') + rv.set('b') + replace_all() + equal(text.get('1.0', '1.end'), 'b') # Above the cursor. + equal(text.get('2.0', '2.end'), 'b') # At the cursor. + equal(text.get('3.0', '3.end'), 'a') # Below the cursor, untouched. + + def test_replace_all_forwards_no_wrap(self): + # 'down' without wrap replaces all matches from the current + # position to the end of the text, and none before it. + equal = self.assertEqual + text = self.text + pv = self.engine.patvar + rv = self.dialog.replvar + replace_all = self.dialog.replace_all + self.engine.backvar.set(False) + self.engine.wrapvar.set(False) + + text.delete('1.0', 'end') + text.insert('1.0', 'a\na\na\n') + text.mark_set('insert', '2.1') + pv.set('a') + rv.set('b') + replace_all() + equal(text.get('1.0', '1.end'), 'a') # Before the cursor, untouched. + equal(text.get('2.0', '2.end'), 'a') # Before the cursor, untouched. + equal(text.get('3.0', '3.end'), 'b') # After the cursor. + + def test_replace_all_backwards_wrap(self): + # With wrap around, an 'up' search also replaces every match. + equal = self.assertEqual + text = self.text + pv = self.engine.patvar + rv = self.dialog.replvar + replace_all = self.dialog.replace_all + self.engine.backvar.set(True) + self.engine.wrapvar.set(True) + + text.delete('1.0', 'end') + text.insert('1.0', 'a\na\na\n') + text.mark_set('insert', '2.1') + pv.set('a') + rv.set('b') + replace_all() + equal(text.get('1.0', '3.end'), 'b\nb\nb') def test_default_command(self): text = self.text diff --git a/Lib/idlelib/replace.py b/Lib/idlelib/replace.py index 3716d841568d30..93d4f1c0c6c573 100644 --- a/Lib/idlelib/replace.py +++ b/Lib/idlelib/replace.py @@ -122,12 +122,13 @@ def _replace_expand(self, m, repl): def replace_all(self, event=None): """Handle the Replace All button. - Search text for occurrences of the Find value and replace - each of them. The 'wrap around' value controls the start - point for searching. If wrap isn't set, then the searching - starts at the first occurrence after the current selection; - if wrap is set, the replacement starts at the first line. - The replacement is always done top-to-bottom in the text. + Search text for occurrences of the Find value and replace each + of them. The 'wrap around' and direction values control which + occurrences are replaced. With wrap around, every occurrence is + replaced. Without it, a forward search replaces occurrences from + the current position to the end of the text, and a backward search + replaces occurrences from the beginning of the text to the current + position. The replacement is always done top-to-bottom. """ prog = self.engine.getprog() if not prog: @@ -142,9 +143,18 @@ def replace_all(self, event=None): text.tag_remove("hit", "1.0", "end") line = res[0] col = res[1].start() + # For a backward search without wrap, replace top-to-bottom from + # the start of the text down to the first match at or above the + # current position (gh-71956). A mark tracks that stop point. + stop = None if self.engine.iswrap(): line = 1 col = 0 + elif self.engine.isback(): + stop = "replace_all_stop" + text.mark_set(stop, "%d.%d" % (line, res[1].end())) + line = 1 + col = 0 ok = True first = last = None # XXX ought to replace circular instead of top-to-bottom when wrapping @@ -152,12 +162,13 @@ def replace_all(self, event=None): while res := self.engine.search_forward( text, prog, line, col, wrap=False, ok=ok): line, m = res - chars = text.get("%d.0" % line, "%d.0" % (line+1)) + i, j = m.span() + if stop is not None and text.compare("%d.%d" % (line, i), ">=", stop): + break orig = m.group() new = self._replace_expand(m, repl) if new is None: break - i, j = m.span() first = "%d.%d" % (line, i) last = "%d.%d" % (line, j) if new == orig: @@ -170,6 +181,8 @@ def replace_all(self, event=None): text.insert(first, new, self.insert_tags) col = i + len(new) ok = False + if stop is not None: + text.mark_unset(stop) text.undo_block_stop() if first and last: self.show_hit(first, last) diff --git a/Misc/NEWS.d/next/IDLE/2026-07-01-12-00-00.gh-issue-71956.rPlAlL.rst b/Misc/NEWS.d/next/IDLE/2026-07-01-12-00-00.gh-issue-71956.rPlAlL.rst new file mode 100644 index 00000000000000..1a92ed4cd389cf --- /dev/null +++ b/Misc/NEWS.d/next/IDLE/2026-07-01-12-00-00.gh-issue-71956.rPlAlL.rst @@ -0,0 +1,3 @@ +Fix Replace All in the IDLE editor's Replace dialog when the search +direction is "Up" and "Wrap around" is off: it now replaces all matches +above the current position instead of only the first one. From ea7619faeacf310ba1b2ef41ad07966ce2aae11f Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Wed, 1 Jul 2026 19:22:03 +0300 Subject: [PATCH 08/10] gh-89360: Fix ValueError in IDLE MultiCall event_delete (#152738) Deleting a key binding for a sequence not bound to the virtual event no longer raises a ValueError; the discrepancy is now ignored. --- Lib/idlelib/idle_test/test_multicall.py | 16 ++++++++++++++++ Lib/idlelib/multicall.py | 3 ++- ...2026-07-01-14-00-00.gh-issue-89360.mUlTiC.rst | 3 +++ 3 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/IDLE/2026-07-01-14-00-00.gh-issue-89360.mUlTiC.rst diff --git a/Lib/idlelib/idle_test/test_multicall.py b/Lib/idlelib/idle_test/test_multicall.py index 7d73761cfdfee8..fcce2030579f4b 100644 --- a/Lib/idlelib/idle_test/test_multicall.py +++ b/Lib/idlelib/idle_test/test_multicall.py @@ -43,6 +43,22 @@ def test_yview(self): mctext = self.mc(self.root) self.assertIs(mctext.yview.__func__, Text.yview) + def test_event_delete_unbound_sequence(self): + # gh-89360: deleting a sequence that was not added to a virtual + # event is ignored instead of raising ValueError. + mctext = self.mc(self.root) + mctext.event_add('<>', '') + info = mctext.event_info('<>') + self.assertEqual(len(info), 1) + + # A different sequence, never added: a no-op, not an error. + mctext.event_delete('<>', '') + self.assertEqual(mctext.event_info('<>'), info) + + # The added sequence can still be deleted normally. + mctext.event_delete('<>', '') + self.assertNotIn(info[0], mctext.event_info('<>')) + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/Lib/idlelib/multicall.py b/Lib/idlelib/multicall.py index 41f81813113062..95f4fee7fbe740 100644 --- a/Lib/idlelib/multicall.py +++ b/Lib/idlelib/multicall.py @@ -386,10 +386,11 @@ def event_delete(self, virtual, *sequences): if triplet is None: #print("Tkinter event_delete: %s" % seq, file=sys.__stderr__) widget.event_delete(self, virtual, seq) - else: + elif triplet in triplets: if func is not None: self.__binders[triplet[1]].unbind(triplet, func) triplets.remove(triplet) + # Else the sequence is not bound; ignore it (gh-89360). def event_info(self, virtual=None): if virtual is None or virtual not in self.__eventinfo: diff --git a/Misc/NEWS.d/next/IDLE/2026-07-01-14-00-00.gh-issue-89360.mUlTiC.rst b/Misc/NEWS.d/next/IDLE/2026-07-01-14-00-00.gh-issue-89360.mUlTiC.rst new file mode 100644 index 00000000000000..9e3023f8228233 --- /dev/null +++ b/Misc/NEWS.d/next/IDLE/2026-07-01-14-00-00.gh-issue-89360.mUlTiC.rst @@ -0,0 +1,3 @@ +Fix a rare crash in the IDLE editor when the completion window is closed: +deleting a key binding for a sequence that is not bound to the virtual +event is now ignored instead of raising a ``ValueError``. From 3f5491a09223f7371b4b4c92225e8dfdf1c989b7 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Wed, 1 Jul 2026 19:35:57 +0300 Subject: [PATCH 09/10] gh-134300: Remove idlelib from the path of the IDLE user process (#152739) The idlelib directory ends up on sys.path when idle.py is run as a script, and it was passed to the user process, where it let user code import idlelib submodules as top-level modules, such as "import help". --- Lib/idlelib/idle_test/test_pyshell.py | 9 +++++++++ Lib/idlelib/pyshell.py | 12 ++++++++++++ .../2026-07-01-15-00-00.gh-issue-134300.iDlPaT.rst | 3 +++ 3 files changed, 24 insertions(+) create mode 100644 Misc/NEWS.d/next/IDLE/2026-07-01-15-00-00.gh-issue-134300.iDlPaT.rst diff --git a/Lib/idlelib/idle_test/test_pyshell.py b/Lib/idlelib/idle_test/test_pyshell.py index 51f7691eefe9d5..dec81bdbbccd67 100644 --- a/Lib/idlelib/idle_test/test_pyshell.py +++ b/Lib/idlelib/idle_test/test_pyshell.py @@ -2,6 +2,7 @@ # Plus coverage of test_warning. Was 20% with test_openshell. from idlelib import pyshell +import os import unittest from test.support import requires from tkinter import Tk @@ -28,6 +29,14 @@ def test_restart_line_narrow(self): self.assertEqual(pyshell.restart_line(width, ''), expect) self.assertEqual(pyshell.restart_line(taglen+2, ''), expect+' =') + def test_fix_user_path(self): + # gh-134300: the idlelib directory is removed, other entries kept. + eq = self.assertEqual + idlelib_dir = os.path.dirname(os.path.abspath(pyshell.__file__)) + eq(pyshell.fix_user_path(['', '/a', idlelib_dir, '/b']), ['', '/a', '/b']) + eq(pyshell.fix_user_path(['/a', '/b']), ['/a', '/b']) + eq(pyshell.fix_user_path([idlelib_dir]), []) + class PyShellFileListTest(unittest.TestCase): diff --git a/Lib/idlelib/pyshell.py b/Lib/idlelib/pyshell.py index f117f0bfb52f2a..ef3d014d936ce8 100755 --- a/Lib/idlelib/pyshell.py +++ b/Lib/idlelib/pyshell.py @@ -408,6 +408,17 @@ def restart_line(width, filename): # See bpo-38141. return tag[:-2] # Remove ' ='. +def fix_user_path(path): + """Return path without the idlelib directory (gh-134300). + + That directory is on sys.path when idle.py is run as a script. + Otherwise user code could import idlelib submodules as top-level + modules, such as "import help". + """ + idlelib_dir = os.path.dirname(os.path.abspath(__file__)) + return [p for p in path if p != idlelib_dir] + + class ModifiedInterpreter(InteractiveInterpreter): def __init__(self, tkconsole): @@ -568,6 +579,7 @@ def transfer_path(self, with_cwd=False): path.extend(sys.path) else: path = sys.path + path = fix_user_path(path) # gh-134300 self.runcommand("""if 1: import sys as _sys diff --git a/Misc/NEWS.d/next/IDLE/2026-07-01-15-00-00.gh-issue-134300.iDlPaT.rst b/Misc/NEWS.d/next/IDLE/2026-07-01-15-00-00.gh-issue-134300.iDlPaT.rst new file mode 100644 index 00000000000000..791acbbed62aaf --- /dev/null +++ b/Misc/NEWS.d/next/IDLE/2026-07-01-15-00-00.gh-issue-134300.iDlPaT.rst @@ -0,0 +1,3 @@ +Do not add the ``idlelib`` directory to the path of the IDLE user process. +User code run in IDLE can no longer import ``idlelib`` submodules as +top-level modules, such as ``import help``. From f21f338f1f88352d50362271a7f38c01745a65da Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Wed, 1 Jul 2026 20:05:56 +0300 Subject: [PATCH 10/10] gh-80504: Always show the full search path in IDLE Find in Files (#152740) The "In files:" field of the Find in Files dialog now always contains a full directory path, so the grep output shows which directory was searched. --- Lib/idlelib/grep.py | 20 ++++++++++++++----- Lib/idlelib/idle_test/test_grep.py | 19 ++++++++++++++++++ ...026-07-01-16-00-00.gh-issue-80504.gReP.rst | 3 +++ 3 files changed, 37 insertions(+), 5 deletions(-) create mode 100644 Misc/NEWS.d/next/IDLE/2026-07-01-16-00-00.gh-issue-80504.gReP.rst diff --git a/Lib/idlelib/grep.py b/Lib/idlelib/grep.py index 42048ff2395fe1..5d604faec23f32 100644 --- a/Lib/idlelib/grep.py +++ b/Lib/idlelib/grep.py @@ -40,6 +40,20 @@ def grep(text, io=None, flist=None): dialog.open(text, searchphrase, io) +def default_glob(path): + """Return the initial "In files:" pattern for a file path (gh-80504). + + Always include a full directory so that grep output shows which + directory was searched. + """ + dir, base = os.path.split(path) + dir = os.path.abspath(dir) # An empty dir becomes the current directory. + head, tail = os.path.splitext(base) + if not tail: + tail = ".py" + return os.path.join(dir, "*" + tail) + + def walk_error(msg): "Handle os.walk error." print(msg) @@ -103,11 +117,7 @@ def open(self, text, searchphrase, io=None): path = io.filename or "" else: path = "" - dir, base = os.path.split(path) - head, tail = os.path.splitext(base) - if not tail: - tail = ".py" - self.globvar.set(os.path.join(dir, "*" + tail)) + self.globvar.set(default_glob(path)) def create_entries(self): "Create base entry widgets and add widget for search path." diff --git a/Lib/idlelib/idle_test/test_grep.py b/Lib/idlelib/idle_test/test_grep.py index d67dba76911fcf..94f217effb656e 100644 --- a/Lib/idlelib/idle_test/test_grep.py +++ b/Lib/idlelib/idle_test/test_grep.py @@ -37,6 +37,25 @@ def close(self): # gui method _grep = Dummy_grep() +class DefaultGlobTest(unittest.TestCase): + + def test_no_path(self): + # gh-80504: an unsaved editor or the Shell has no path, so the + # pattern uses the current directory, not just '*.py'. + self.assertEqual(grep.default_glob(''), + os.path.join(os.getcwd(), '*.py')) + + def test_full_path(self): + path = os.path.join(os.path.abspath(os.sep), 'ab', 'foo.py') + self.assertEqual(grep.default_glob(path), + os.path.join(os.path.dirname(path), '*.py')) + + def test_other_extension(self): + path = os.path.join(os.path.abspath(os.sep), 'ab', 'foo.txt') + self.assertEqual(grep.default_glob(path), + os.path.join(os.path.dirname(path), '*.txt')) + + class FindfilesTest(unittest.TestCase): @classmethod diff --git a/Misc/NEWS.d/next/IDLE/2026-07-01-16-00-00.gh-issue-80504.gReP.rst b/Misc/NEWS.d/next/IDLE/2026-07-01-16-00-00.gh-issue-80504.gReP.rst new file mode 100644 index 00000000000000..93adb33202a41c --- /dev/null +++ b/Misc/NEWS.d/next/IDLE/2026-07-01-16-00-00.gh-issue-80504.gReP.rst @@ -0,0 +1,3 @@ +The "In files:" field of IDLE's Find in Files dialog now always contains a +full directory path, even for an unsaved editor or the Shell. This shows +in the grep output which directory was searched.