From f5fbe867d2d26e4a0a9177a51f6e568868ad3dc8 Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Fri, 22 Aug 2025 15:57:09 -0400 Subject: [PATCH 01/47] chore: bump to 3.0.1 (#5810) * docs: prepare for 3.0.1 Signed-off-by: Henry Schreiner * chore: bump for 3.0.1 Signed-off-by: Henry Schreiner * Update docs/changelog.md --------- Signed-off-by: Henry Schreiner --- docs/changelog.md | 86 ++++++++++++++++++++++++++++++++ include/pybind11/detail/common.h | 4 +- tools/make_changelog.py | 6 +++ 3 files changed, 94 insertions(+), 2 deletions(-) diff --git a/docs/changelog.md b/docs/changelog.md index 722085b6a..c8d631879 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -12,6 +12,92 @@ versioning](http://semver.org) policy. Changes will be added here periodically from the "Suggested changelog entry" block in pull request descriptions. + +## Version 3.0.1 (August 22, 2025) + +Bug fixes: + +- Fixed compilation error in `type_caster_enum_type` when casting + pointer-to-enum types. Added pointer overload to handle dereferencing before + enum conversion. + [#5776](https://github.com/pybind/pybind11/pull/5776) + +- Implement binary version of `make_index_sequence` to reduce template depth + requirements for functions with many parameters. + [#5751](https://github.com/pybind/pybind11/pull/5751) + +- Subinterpreter-specific exception handling code was removed to resolve segfaults. + [#5795](https://github.com/pybind/pybind11/pull/5795) + +- Fixed issue that caused ``PYBIND11_MODULE`` code to run again if the module + was re-imported after being deleted from ``sys.modules``. + [#5782](https://github.com/pybind/pybind11/pull/5782) + +- Prevent concurrent creation of sub-interpreters as a workaround for stdlib + concurrency issues in Python 3.12. + [#5779](https://github.com/pybind/pybind11/pull/5779) + +- Fixed potential crash when using `cpp_function` objects with sub-interpreters. + [#5771](https://github.com/pybind/pybind11/pull/5771) + +- Fixed non-entrant check in `implicitly_convertible()`. + [#5777](https://github.com/pybind/pybind11/pull/5777) + +- Support C++20 on platforms that have older c++ runtimes. + [#5761](https://github.com/pybind/pybind11/pull/5761) + +- Fix compilation with clang on msys2. + [#5757](https://github.com/pybind/pybind11/pull/5757) + +- Avoid `nullptr` dereference warning with GCC 13.3.0 and python 3.11.13. + [#5756](https://github.com/pybind/pybind11/pull/5756) + +- Fix potential warning about number of threads being too large. + [#5807](https://github.com/pybind/pybind11/pull/5807) + + + + +- Fix gcc 11.4+ warning about serial compilation using CMake. + [#5791](https://github.com/pybind/pybind11/pull/5791) + + +Documentation: + +- Improve `buffer_info` type checking in numpy docs. + [#5805](https://github.com/pybind/pybind11/pull/5805) + +- Replace `robotpy-build` with `semiwrap` in the binding tool list. + [#5804](https://github.com/pybind/pybind11/pull/5804) + +- Show nogil in most examples. + [#5770](https://github.com/pybind/pybind11/pull/5770) + +- Fix `py::trampoline_self_life_support` visibility in docs. + [#5766](https://github.com/pybind/pybind11/pull/5766) + + +Tests: + +- Avoid a spurious warning about `DOWNLOAD_CATCH` being manually specified. + [#5803](https://github.com/pybind/pybind11/pull/5803) + +- Fix an IsolatedConfig test. + [#5768](https://github.com/pybind/pybind11/pull/5768) + + +CI: + +- Add CI testing for Android. + [#5714](https://github.com/pybind/pybind11/pull/5714) + + +Internal: + +- Rename internal variables to avoid the word `slots` (reads better). + [#5793](https://github.com/pybind/pybind11/pull/5793) + + ## Version 3.0.0 (July 10, 2025) Pybind11 3.0 includes an ABI bump, the first required bump in many years diff --git a/include/pybind11/detail/common.h b/include/pybind11/detail/common.h index 20a659d26..314dba409 100644 --- a/include/pybind11/detail/common.h +++ b/include/pybind11/detail/common.h @@ -24,10 +24,10 @@ // - The release level is set to "alpha" for development versions. // Use 0xA0 (LEVEL=0xA, SERIAL=0) for development versions. // - For stable releases, set the serial to 0. -#define PYBIND11_VERSION_RELEASE_LEVEL PY_RELEASE_LEVEL_ALPHA +#define PYBIND11_VERSION_RELEASE_LEVEL PY_RELEASE_LEVEL_FINAL #define PYBIND11_VERSION_RELEASE_SERIAL 0 // String version of (micro, release level, release serial), e.g.: 0a0, 0b1, 0rc1, 0 -#define PYBIND11_VERSION_PATCH 1a0 +#define PYBIND11_VERSION_PATCH 1 /* -- end version constants -- */ #if !defined(Py_PACK_FULL_VERSION) diff --git a/tools/make_changelog.py b/tools/make_changelog.py index a2e4c79cd..bb49de64d 100755 --- a/tools/make_changelog.py +++ b/tools/make_changelog.py @@ -65,12 +65,18 @@ for issue in issues: continue msg = changelog.group("content").strip() + if not msg: + missing.append(issue) + continue if msg.startswith("* "): msg = msg[2:] if not msg.startswith("- "): msg = "- " + msg if not msg.endswith("."): msg += "." + if msg == "- Placeholder.": + missing.append(issue) + continue msg += f"\n [#{issue.number}]({issue.html_url})" for cat, cat_list in cats.items(): From 6e0d1c2400a712f80acefdf66c45684930e20055 Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Fri, 22 Aug 2025 16:08:51 -0400 Subject: [PATCH 02/47] chore: back to work Signed-off-by: Henry Schreiner --- include/pybind11/detail/common.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/include/pybind11/detail/common.h b/include/pybind11/detail/common.h index 314dba409..22dfc62b4 100644 --- a/include/pybind11/detail/common.h +++ b/include/pybind11/detail/common.h @@ -19,15 +19,15 @@ /* -- start version constants -- */ #define PYBIND11_VERSION_MAJOR 3 #define PYBIND11_VERSION_MINOR 0 -#define PYBIND11_VERSION_MICRO 1 +#define PYBIND11_VERSION_MICRO 2 // ALPHA = 0xA, BETA = 0xB, GAMMA = 0xC (release candidate), FINAL = 0xF (stable release) // - The release level is set to "alpha" for development versions. // Use 0xA0 (LEVEL=0xA, SERIAL=0) for development versions. // - For stable releases, set the serial to 0. -#define PYBIND11_VERSION_RELEASE_LEVEL PY_RELEASE_LEVEL_FINAL +#define PYBIND11_VERSION_RELEASE_LEVEL PY_RELEASE_LEVEL_ALPHA #define PYBIND11_VERSION_RELEASE_SERIAL 0 // String version of (micro, release level, release serial), e.g.: 0a0, 0b1, 0rc1, 0 -#define PYBIND11_VERSION_PATCH 1 +#define PYBIND11_VERSION_PATCH 2a0 /* -- end version constants -- */ #if !defined(Py_PACK_FULL_VERSION) From 3c0ee89716e6248620d63583b35c2a1c75c2dafd Mon Sep 17 00:00:00 2001 From: Tobias Leibner <7058290+tobiasleibner@users.noreply.github.com> Date: Wed, 27 Aug 2025 20:06:29 +0200 Subject: [PATCH 03/47] Fix compiler detection with clang-cl (#5816) * Fix compiler detection with clang-cl * Follow review suggestion --- include/pybind11/detail/pybind11_namespace_macros.h | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/include/pybind11/detail/pybind11_namespace_macros.h b/include/pybind11/detail/pybind11_namespace_macros.h index bd18c8c0a..6f74bf85c 100644 --- a/include/pybind11/detail/pybind11_namespace_macros.h +++ b/include/pybind11/detail/pybind11_namespace_macros.h @@ -16,12 +16,7 @@ // possible using these macros. Please also be sure to push/pop with the pybind11 macros. Please // only use compiler specifics if you need to check specific versions, e.g. Apple Clang vs. vanilla // Clang. -#if defined(_MSC_VER) -# define PYBIND11_COMPILER_MSVC -# define PYBIND11_PRAGMA(...) __pragma(__VA_ARGS__) -# define PYBIND11_WARNING_PUSH PYBIND11_PRAGMA(warning(push)) -# define PYBIND11_WARNING_POP PYBIND11_PRAGMA(warning(pop)) -#elif defined(__INTEL_COMPILER) +#if defined(__INTEL_COMPILER) # define PYBIND11_COMPILER_INTEL # define PYBIND11_PRAGMA(...) _Pragma(#__VA_ARGS__) # define PYBIND11_WARNING_PUSH PYBIND11_PRAGMA(warning push) @@ -36,6 +31,11 @@ # define PYBIND11_PRAGMA(...) _Pragma(#__VA_ARGS__) # define PYBIND11_WARNING_PUSH PYBIND11_PRAGMA(GCC diagnostic push) # define PYBIND11_WARNING_POP PYBIND11_PRAGMA(GCC diagnostic pop) +#elif defined(_MSC_VER) // Must be after the clang branch because clang-cl also defines _MSC_VER +# define PYBIND11_COMPILER_MSVC +# define PYBIND11_PRAGMA(...) __pragma(__VA_ARGS__) +# define PYBIND11_WARNING_PUSH PYBIND11_PRAGMA(warning(push)) +# define PYBIND11_WARNING_POP PYBIND11_PRAGMA(warning(pop)) #endif #ifdef PYBIND11_COMPILER_MSVC From 3878c23f8d04a96b647255081a321ae9e32ee7c6 Mon Sep 17 00:00:00 2001 From: MoonE Date: Sun, 31 Aug 2025 08:07:03 +0200 Subject: [PATCH 04/47] Fix typo in error message (#5817) --- include/pybind11/stl/filesystem.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/pybind11/stl/filesystem.h b/include/pybind11/stl/filesystem.h index 1c0b18c1e..52d296210 100644 --- a/include/pybind11/stl/filesystem.h +++ b/include/pybind11/stl/filesystem.h @@ -17,7 +17,7 @@ #elif defined(PYBIND11_HAS_EXPERIMENTAL_FILESYSTEM) # include #else -# error "Neither #include nor #include nor #include is available." #endif PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE) From cd56888c893f5a266b2eecd2589bedf632258d5e Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Wed, 3 Sep 2025 09:06:41 -0700 Subject: [PATCH 05/47] Bring CI back to all-working condition (#5822) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix "🐍 3 β€’ windows-latest β€’ mingw64" job (apparently msys2/setup-msys2@v2 cannot be run twice anymore): https://github.com/pybind/pybind11/actions/runs/17394902023/job/49417376616?pr=5796 ``` Run msys2/setup-msys2@v2 with: msystem: mingw64 install: mingw-w64-x86_64-python-numpy mingw-w64-x86_64-python-scipy mingw-w64-x86_64-eigen3 path-type: minimal update: false pacboy: false release: true location: RUNNER_TEMP platform-check-severity: fatal cache: true env: PYTHONDEVMODE: 1 PIP_BREAK_SYSTEM_PACKAGES: 1 PIP_ONLY_BINARY: numpy FORCE_COLOR: 3 PYTEST_TIMEOUT: 300 VERBOSE: 1 CMAKE_COLOR_DIAGNOSTICS: 1 MSYSTEM: MINGW64 Error: Trying to install MSYS2 to D:\a\_temp\msys64 but that already exists, cannot continue. ``` * Add `pytest.xfail("[TEST-GIL-SCOPED] macOS free-threading...)` * Change env.SYS_IS_GIL_ENABLED constant to env.sys_is_gil_enabled function * Change install_mingw64_only β†’ extra_install * Also xfail if macOS and PY_GIL_DISABLED, show SOABI * build-ios: brew upgrade|install cmake * Revert "build-ios: brew upgrade|install cmake" This reverts commit bd3900ee794a01bb1ef91a9355ce72be619a7196. See also: https://github.com/pybind/pybind11/pull/5822#issuecomment-3247827317 * Disable build-ios job in tests-cibw.yml * Remove macos_brew_install_llvm job because it started failing, to reduce our maintenance overhead: Failures tracked here: https://github.com/pybind/pybind11/pull/5822#issuecomment-3247998220 * Fix iOS build step for cmake installation Replaced brew upgrade with brew install for cmake. * Update cmake installation steps in CI workflow Uninstall cmake before installing the latest version due to GitHub's local tap changes. * Update .github/workflows/tests-cibw.yml --------- Co-authored-by: Henry Schreiner --- .github/workflows/ci.yml | 109 +++---------------------------- .github/workflows/tests-cibw.yml | 3 +- tests/env.py | 4 ++ tests/test_cpp_conduit.py | 2 +- tests/test_gil_scoped.py | 10 ++- 5 files changed, 25 insertions(+), 103 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0985b5cf4..b0f1aee9d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1019,8 +1019,15 @@ jobs: fail-fast: false matrix: include: - - { sys: mingw64, env: x86_64 } - - { sys: mingw32, env: i686 } + - sys: mingw32 + env: i686 + extra_install: "" + - sys: mingw64 + env: x86_64 + extra_install: | + mingw-w64-x86_64-python-numpy + mingw-w64-x86_64-python-scipy + mingw-w64-x86_64-eigen3 steps: - uses: msys2/setup-msys2@v2 with: @@ -1034,15 +1041,7 @@ jobs: mingw-w64-${{matrix.env}}-python-pytest mingw-w64-${{matrix.env}}-boost mingw-w64-${{matrix.env}}-catch - - - uses: msys2/setup-msys2@v2 - if: matrix.sys == 'mingw64' - with: - msystem: ${{matrix.sys}} - install: >- - mingw-w64-${{matrix.env}}-python-numpy - mingw-w64-${{matrix.env}}-python-scipy - mingw-w64-${{matrix.env}}-eigen3 + ${{ matrix.extra_install }} - uses: actions/checkout@v4 @@ -1189,91 +1188,3 @@ jobs: - name: Clean directory run: git clean -fdx - - macos_brew_install_llvm: - if: github.event.pull_request.draft == false - name: "macos-13 β€’ brew install llvm" - runs-on: macos-13 - - env: - # https://apple.stackexchange.com/questions/227026/how-to-install-recent-clang-with-homebrew - LDFLAGS: '-L/usr/local/opt/llvm/lib -Wl,-rpath,/usr/local/opt/llvm/lib' - - steps: - - name: Update PATH - run: echo "/usr/local/opt/llvm/bin" >> $GITHUB_PATH - - - name: Show env - run: env - - - name: Checkout - uses: actions/checkout@v4 - - - name: Show Clang++ version before brew install llvm - run: clang++ --version - - - name: brew install llvm - run: brew install llvm - - - name: Show Clang++ version after brew install llvm - run: clang++ --version - - - name: Update CMake - uses: jwlawson/actions-setup-cmake@v2.0 - - - name: Run pip installs - run: | - python3 -m pip install --upgrade pip - python3 -m pip install -r tests/requirements.txt - python3 -m pip install numpy - python3 -m pip install scipy - - - name: Show CMake version - run: cmake --version - - - name: CMake Configure - run: > - cmake -S . -B . - -DPYBIND11_WERROR=ON - -DPYBIND11_SIMPLE_GIL_MANAGEMENT=OFF - -DDOWNLOAD_CATCH=ON - -DDOWNLOAD_EIGEN=ON - -DCMAKE_CXX_COMPILER=clang++ - -DCMAKE_CXX_STANDARD=17 - -DPYTHON_EXECUTABLE=$(python3 -c "import sys; print(sys.executable)") - - - name: Build - run: cmake --build . -j 2 - - - name: Python tests - run: cmake --build . --target pytest -j 2 - - - name: C++ tests - run: cmake --build . --target cpptest -j 2 - - - name: Interface test - run: cmake --build . --target test_cmake_build -j 2 - - - name: Visibility test - run: cmake --build . --target test_cross_module_rtti -j 2 - - - name: CMake Configure - Exercise cmake -DPYBIND11_TEST_OVERRIDE - run: > - cmake -S . -B build_partial - -DPYBIND11_WERROR=ON - -DPYBIND11_SIMPLE_GIL_MANAGEMENT=OFF - -DDOWNLOAD_CATCH=ON - -DDOWNLOAD_EIGEN=ON - -DCMAKE_CXX_COMPILER=clang++ - -DCMAKE_CXX_STANDARD=17 - -DPYTHON_EXECUTABLE=$(python3 -c "import sys; print(sys.executable)") - "-DPYBIND11_TEST_OVERRIDE=test_call_policies.cpp;test_gil_scoped.cpp;test_thread.cpp" - - - name: Build - Exercise cmake -DPYBIND11_TEST_OVERRIDE - run: cmake --build build_partial -j 2 - - - name: Python tests - Exercise cmake -DPYBIND11_TEST_OVERRIDE - run: cmake --build build_partial --target pytest -j 2 - - - name: Clean directory - run: git clean -fdx diff --git a/.github/workflows/tests-cibw.yml b/.github/workflows/tests-cibw.yml index 0abddd0be..71d07a764 100644 --- a/.github/workflows/tests-cibw.yml +++ b/.github/workflows/tests-cibw.yml @@ -42,7 +42,8 @@ jobs: submodules: true fetch-depth: 0 - - run: brew upgrade cmake + # We have to uninstall first because GH is now using a local tap to build cmake<4, iOS needs cmake>=4 + - run: brew uninstall cmake && brew install cmake - uses: pypa/cibuildwheel@v3.1 env: diff --git a/tests/env.py b/tests/env.py index 95cc1ac61..ae239a741 100644 --- a/tests/env.py +++ b/tests/env.py @@ -18,7 +18,11 @@ _graalpy_version = ( sys.modules["__graalpython__"].get_graalvm_version() if GRAALPY else "0.0.0" ) GRAALPY_VERSION = tuple(int(t) for t in _graalpy_version.split("-")[0].split(".")[:3]) + +# Compile-time config (what the binary was built for) PY_GIL_DISABLED = bool(sysconfig.get_config_var("Py_GIL_DISABLED")) +# Runtime state (what's actually happening now) +sys_is_gil_enabled = getattr(sys, "_is_gil_enabled", lambda: True) def deprecated_call(): diff --git a/tests/test_cpp_conduit.py b/tests/test_cpp_conduit.py index 5650a6536..652b9b9c5 100644 --- a/tests/test_cpp_conduit.py +++ b/tests/test_cpp_conduit.py @@ -12,7 +12,7 @@ from pybind11_tests import cpp_conduit as home_planet def import_warns_freethreaded(name): - if name not in sys.modules and not getattr(sys, "_is_gil_enabled", lambda: True)(): + if name not in sys.modules and not env.sys_is_gil_enabled(): with pytest.warns( RuntimeWarning, match=f"has been enabled to load module '{name}'" ): diff --git a/tests/test_gil_scoped.py b/tests/test_gil_scoped.py index 04f7b09c7..84a7a999a 100644 --- a/tests/test_gil_scoped.py +++ b/tests/test_gil_scoped.py @@ -199,8 +199,14 @@ def _run_in_process(target, *args, **kwargs): if process.exitcode is None: assert t_delta > 0.9 * timeout msg = "DEADLOCK, most likely, exactly what this test is meant to detect." - if env.PYPY and env.WIN: - pytest.skip(msg) + soabi = sysconfig.get_config_var("SOABI") + if env.WIN and env.PYPY: + pytest.xfail(f"[TEST-GIL-SCOPED] {soabi} PyPy: " + msg) + if env.MACOS: + if not env.sys_is_gil_enabled(): + pytest.xfail(f"[TEST-GIL-SCOPED] {soabi} with GIL disabled: " + msg) + if env.PY_GIL_DISABLED: + pytest.xfail(f"[TEST-GIL-SCOPED] {soabi}: " + msg) raise RuntimeError(msg) return process.exitcode finally: From bf2d56e8acb0c0d83b7ab6ec0009e903530f8b9b Mon Sep 17 00:00:00 2001 From: Plamen Totev Date: Wed, 3 Sep 2025 19:07:22 +0300 Subject: [PATCH 06/47] Fix the first example in the first steps guide not compiling (#5823) The alias of the pybind11 namespace is missing --- docs/basics.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/basics.rst b/docs/basics.rst index 1e68869d4..074d98850 100644 --- a/docs/basics.rst +++ b/docs/basics.rst @@ -108,6 +108,8 @@ a file named :file:`example.cpp` with the following contents: #include + namespace py = pybind11; + int add(int i, int j) { return i + j; } From ef0f1ff5f1828727d05d6216629edf49162890c8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 3 Sep 2025 18:25:27 -0700 Subject: [PATCH 07/47] chore(deps): bump the actions group across 1 directory with 2 updates (#5818) * chore(deps): bump the actions group across 1 directory with 2 updates Bumps the actions group with 2 updates in the / directory: [actions/checkout](https://github.com/actions/checkout) and [actions/attest-build-provenance](https://github.com/actions/attest-build-provenance). Updates `actions/checkout` from 1 to 5 - [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/v1...v5) Updates `actions/attest-build-provenance` from 2 to 3 - [Release notes](https://github.com/actions/attest-build-provenance/releases) - [Changelog](https://github.com/actions/attest-build-provenance/blob/main/RELEASE.md) - [Commits](https://github.com/actions/attest-build-provenance/compare/v2...v3) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '5' dependency-type: direct:production update-type: version-update:semver-major dependency-group: actions - dependency-name: actions/attest-build-provenance dependency-version: '3' dependency-type: direct:production update-type: version-update:semver-major dependency-group: actions ... Signed-off-by: dependabot[bot] * Update .github/workflows/ci.yml --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Henry Schreiner --- .github/workflows/ci.yml | 32 ++++++++++++------------- .github/workflows/configure.yml | 2 +- .github/workflows/docs-link.yml | 2 +- .github/workflows/format.yml | 4 ++-- .github/workflows/nightlies.yml | 2 +- .github/workflows/pip.yml | 6 ++--- .github/workflows/reusable-standard.yml | 2 +- .github/workflows/tests-cibw.yml | 6 ++--- .github/workflows/upstream.yml | 2 +- 9 files changed, 29 insertions(+), 29 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b0f1aee9d..fc673643b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -181,7 +181,7 @@ jobs: runs-on: ${{ matrix.runs-on }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Setup Python ${{ matrix.python-version }} uses: actions/setup-python@v5 @@ -242,7 +242,7 @@ jobs: timeout-minutes: 40 container: quay.io/pypa/musllinux_1_2_x86_64:latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: fetch-depth: 0 @@ -279,7 +279,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Setup Python ${{ matrix.python-version }} (deadsnakes) uses: deadsnakes/action@v3.2.0 @@ -366,7 +366,7 @@ jobs: container: "silkeh/clang:${{ matrix.clang }}${{ matrix.container_suffix }}" steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Add wget and python3 run: apt-get update && apt-get install -y python3-dev python3-numpy python3-pytest libeigen3-dev @@ -403,7 +403,7 @@ jobs: container: nvidia/cuda:12.2.0-devel-ubuntu22.04 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 # tzdata will try to ask for the timezone, so set the DEBIAN_FRONTEND - name: Install 🐍 3 @@ -427,7 +427,7 @@ jobs: # container: centos:8 # # steps: -# - uses: actions/checkout@v4 +# - uses: actions/checkout@v5 # # - name: Add Python 3 and a few requirements # run: yum update -y && yum install -y git python3-devel python3-numpy python3-pytest make environment-modules @@ -473,7 +473,7 @@ jobs: # tzdata will try to ask for the timezone, so set the DEBIAN_FRONTEND DEBIAN_FRONTEND: 'noninteractive' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Add NVHPC Repo run: | @@ -534,7 +534,7 @@ jobs: container: "gcc:${{ matrix.gcc }}" steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Add Python 3 run: apt-get update; apt-get install -y python3-dev python3-numpy python3-pytest python3-pip libeigen3-dev @@ -597,7 +597,7 @@ jobs: name: "🐍 3 β€’ ICC latest β€’ x64" steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Add apt repo run: | @@ -710,7 +710,7 @@ jobs: steps: - name: Latest actions/checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Add Python 3.8 if: matrix.container == 'almalinux:8' @@ -811,7 +811,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/setup-python@v5 with: @@ -857,7 +857,7 @@ jobs: runs-on: windows-2022 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Setup Python ${{ matrix.python }} uses: actions/setup-python@v5 @@ -909,7 +909,7 @@ jobs: runs-on: windows-2022 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Setup Python ${{ matrix.python }} uses: actions/setup-python@v5 @@ -957,7 +957,7 @@ jobs: runs-on: windows-2022 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Setup Python ${{ matrix.python }} uses: actions/setup-python@v5 @@ -1043,7 +1043,7 @@ jobs: mingw-w64-${{matrix.env}}-catch ${{ matrix.extra_install }} - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Configure C++11 # LTO leads to many undefined reference like @@ -1133,7 +1133,7 @@ jobs: run: env - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Set up Clang uses: egor-tensin/setup-clang@v1 diff --git a/.github/workflows/configure.yml b/.github/workflows/configure.yml index 6a3b365de..b4a904a2e 100644 --- a/.github/workflows/configure.yml +++ b/.github/workflows/configure.yml @@ -48,7 +48,7 @@ jobs: runs-on: ${{ matrix.runs-on }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Setup Python 3.11 uses: actions/setup-python@v5 diff --git a/.github/workflows/docs-link.yml b/.github/workflows/docs-link.yml index d1f1a1726..2f397aff9 100644 --- a/.github/workflows/docs-link.yml +++ b/.github/workflows/docs-link.yml @@ -19,7 +19,7 @@ jobs: runs-on: ubuntu-latest if: github.event.repository.fork == false steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Check for docs changes id: docs_changes diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index 9258e2792..b1711daf7 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -25,7 +25,7 @@ jobs: name: Format runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/setup-python@v5 with: python-version: "3.x" @@ -40,7 +40,7 @@ jobs: runs-on: ubuntu-latest container: silkeh/clang:20 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Install requirements run: apt-get update && apt-get install -y git python3-dev python3-pytest ninja-build diff --git a/.github/workflows/nightlies.yml b/.github/workflows/nightlies.yml index 788f0cdc3..3197f5aba 100644 --- a/.github/workflows/nightlies.yml +++ b/.github/workflows/nightlies.yml @@ -20,7 +20,7 @@ jobs: if: github.repository_owner == 'pybind' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: fetch-depth: 0 diff --git a/.github/workflows/pip.yml b/.github/workflows/pip.yml index 9b6a9dc82..e2ad0471e 100644 --- a/.github/workflows/pip.yml +++ b/.github/workflows/pip.yml @@ -23,7 +23,7 @@ jobs: runs-on: windows-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Setup 🐍 3.8 uses: actions/setup-python@v5 @@ -47,7 +47,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Setup 🐍 3.8 uses: actions/setup-python@v5 @@ -103,7 +103,7 @@ jobs: - uses: actions/download-artifact@v5 - name: Generate artifact attestation for sdist and wheel - uses: actions/attest-build-provenance@v2 + uses: actions/attest-build-provenance@v3 with: subject-path: "*/pybind11*" diff --git a/.github/workflows/reusable-standard.yml b/.github/workflows/reusable-standard.yml index 5e258e727..1723668e0 100644 --- a/.github/workflows/reusable-standard.yml +++ b/.github/workflows/reusable-standard.yml @@ -30,7 +30,7 @@ jobs: runs-on: ${{ inputs.runs-on }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Setup Python ${{ inputs.python-version }} uses: actions/setup-python@v5 diff --git a/.github/workflows/tests-cibw.yml b/.github/workflows/tests-cibw.yml index 71d07a764..f232544bd 100644 --- a/.github/workflows/tests-cibw.yml +++ b/.github/workflows/tests-cibw.yml @@ -17,7 +17,7 @@ jobs: name: Pyodide wheel runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: submodules: true fetch-depth: 0 @@ -37,7 +37,7 @@ jobs: matrix: runs-on: [macos-14, macos-13] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: submodules: true fetch-depth: 0 @@ -60,7 +60,7 @@ jobs: matrix: runs-on: [macos-latest, macos-13, ubuntu-latest] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: submodules: true fetch-depth: 0 diff --git a/.github/workflows/upstream.yml b/.github/workflows/upstream.yml index 389260038..4fb9a744e 100644 --- a/.github/workflows/upstream.yml +++ b/.github/workflows/upstream.yml @@ -24,7 +24,7 @@ jobs: if: "contains(github.event.pull_request.labels.*.name, 'python dev')" steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Setup Python 3.13 uses: actions/setup-python@v5 From 852a4b5010e339930e974e03eafdaaa3f6f32c36 Mon Sep 17 00:00:00 2001 From: Scott Wolchok Date: Thu, 4 Sep 2025 20:39:33 -0700 Subject: [PATCH 08/47] s/windows-2022/windows-latest/ in .github/workflows/{ci,pip}.yml (#5826) Per request from @rwgk: https://github.com/pybind/pybind11/pull/5825#issuecomment-3256438901 --- .github/workflows/ci.yml | 20 ++++++++++---------- .github/workflows/pip.yml | 4 ++-- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fc673643b..b95de6bc6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -119,7 +119,7 @@ jobs: - runs-on: macos-latest python-version: 'graalpy-24.2' - - runs-on: windows-latest + - runs-on: windows-2022 python-version: '3.9' cmake-args: -DPYBIND11_TEST_SMART_HOLDER=ON - runs-on: windows-2022 @@ -138,19 +138,19 @@ jobs: - runs-on: windows-2022 python-version: '3.13' cmake-args: -DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreadedDebugDLL - - runs-on: windows-latest + - runs-on: windows-2022 python-version: '3.13t' cmake-args: -DCMAKE_CXX_STANDARD=17 - - runs-on: windows-latest + - runs-on: windows-2022 python-version: '3.14' cmake-args: -DCMAKE_CXX_STANDARD=20 - - runs-on: windows-latest + - runs-on: windows-2022 python-version: '3.14t' cmake-args: -DCMAKE_CXX_STANDARD=23 - - runs-on: windows-latest + - runs-on: windows-2022 python-version: 'pypy-3.10' cmake-args: -DCMAKE_CXX_STANDARD=17 - - runs-on: windows-latest + - runs-on: windows-2022 python-version: 'pypy3.11' cmake-args: -DCMAKE_CXX_STANDARD=20 # The setup-python action currently doesn't have graalpy for windows @@ -174,7 +174,7 @@ jobs: python-version: '3.9' - runs-on: macos-latest python-version: '3.12' - - runs-on: windows-latest + - runs-on: windows-2022 python-version: '3.11' name: "🐍 ${{ matrix.python-version }} β€’ ${{ matrix.runs-on }} β€’ x64 inplace C++14" @@ -1010,8 +1010,8 @@ jobs: mingw: if: github.event.pull_request.draft == false - name: "🐍 3 β€’ windows-latest β€’ ${{ matrix.sys }}" - runs-on: windows-latest + name: "🐍 3 β€’ windows-2022 β€’ ${{ matrix.sys }}" + runs-on: windows-2022 defaults: run: shell: msys2 {0} @@ -1121,7 +1121,7 @@ jobs: strategy: matrix: - os: [windows-latest] + os: [windows-2022] python: ['3.10'] runs-on: "${{ matrix.os }}" diff --git a/.github/workflows/pip.yml b/.github/workflows/pip.yml index e2ad0471e..496f84723 100644 --- a/.github/workflows/pip.yml +++ b/.github/workflows/pip.yml @@ -19,8 +19,8 @@ jobs: # This builds the sdists and wheels and makes sure the files are exactly as # expected. test-packaging: - name: 🐍 3.8 β€’ πŸ“¦ tests β€’ windows-latest - runs-on: windows-latest + name: 🐍 3.8 β€’ πŸ“¦ tests β€’ windows-2022 + runs-on: windows-2022 steps: - uses: actions/checkout@v5 From 7fb54e306516b51bf0760ab210a1c2730271fed4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 7 Sep 2025 20:22:29 -0700 Subject: [PATCH 09/47] chore(deps): bump the actions group with 3 updates (#5831) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore(deps): bump the actions group with 3 updates Bumps the actions group with 3 updates: [actions/checkout](https://github.com/actions/checkout), [actions/setup-python](https://github.com/actions/setup-python) and [actions/labeler](https://github.com/actions/labeler). Updates `actions/checkout` from 1 to 5 - [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/v1...v5) Updates `actions/setup-python` from 5 to 6 - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v5...v6) Updates `actions/labeler` from 5 to 6 - [Release notes](https://github.com/actions/labeler/releases) - [Commits](https://github.com/actions/labeler/compare/v5...v6) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '5' dependency-type: direct:production update-type: version-update:semver-major dependency-group: actions - dependency-name: actions/setup-python dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major dependency-group: actions - dependency-name: actions/labeler dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major dependency-group: actions ... Signed-off-by: dependabot[bot] * Manually reset "🐍 3.9 β€’ Debian β€’ x86 β€’ Install" back to `actions/checkout@v1` --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Ralf W. Grosse-Kunstleve --- .github/workflows/ci.yml | 12 ++++++------ .github/workflows/configure.yml | 2 +- .github/workflows/format.yml | 2 +- .github/workflows/labeler.yml | 2 +- .github/workflows/pip.yml | 4 ++-- .github/workflows/reusable-standard.yml | 2 +- .github/workflows/upstream.yml | 2 +- 7 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b95de6bc6..010d5ddc9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -184,7 +184,7 @@ jobs: - uses: actions/checkout@v5 - name: Setup Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} allow-prereleases: true @@ -813,7 +813,7 @@ jobs: steps: - uses: actions/checkout@v5 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: python-version: "3.x" @@ -860,7 +860,7 @@ jobs: - uses: actions/checkout@v5 - name: Setup Python ${{ matrix.python }} - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python }} architecture: x86 @@ -912,7 +912,7 @@ jobs: - uses: actions/checkout@v5 - name: Setup Python ${{ matrix.python }} - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python }} architecture: x86 @@ -960,7 +960,7 @@ jobs: - uses: actions/checkout@v5 - name: Setup Python ${{ matrix.python }} - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python }} @@ -1139,7 +1139,7 @@ jobs: uses: egor-tensin/setup-clang@v1 - name: Setup Python ${{ matrix.python }} - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python }} diff --git a/.github/workflows/configure.yml b/.github/workflows/configure.yml index b4a904a2e..c498d39b7 100644 --- a/.github/workflows/configure.yml +++ b/.github/workflows/configure.yml @@ -51,7 +51,7 @@ jobs: - uses: actions/checkout@v5 - name: Setup Python 3.11 - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: 3.11 diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index b1711daf7..a01e60ba4 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -26,7 +26,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: python-version: "3.x" - name: Add matchers diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index 2152abbcf..f5b618ba8 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -14,7 +14,7 @@ jobs: pull-requests: write steps: - - uses: actions/labeler@v5 + - uses: actions/labeler@v6 if: > github.event.pull_request.merged == true && !startsWith(github.event.pull_request.title, 'chore(deps):') && diff --git a/.github/workflows/pip.yml b/.github/workflows/pip.yml index 496f84723..0347936de 100644 --- a/.github/workflows/pip.yml +++ b/.github/workflows/pip.yml @@ -26,7 +26,7 @@ jobs: - uses: actions/checkout@v5 - name: Setup 🐍 3.8 - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: 3.8 @@ -50,7 +50,7 @@ jobs: - uses: actions/checkout@v5 - name: Setup 🐍 3.8 - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: 3.8 diff --git a/.github/workflows/reusable-standard.yml b/.github/workflows/reusable-standard.yml index 1723668e0..36cad8d5a 100644 --- a/.github/workflows/reusable-standard.yml +++ b/.github/workflows/reusable-standard.yml @@ -33,7 +33,7 @@ jobs: - uses: actions/checkout@v5 - name: Setup Python ${{ inputs.python-version }} - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ inputs.python-version }} allow-prereleases: true diff --git a/.github/workflows/upstream.yml b/.github/workflows/upstream.yml index 4fb9a744e..edc09f51e 100644 --- a/.github/workflows/upstream.yml +++ b/.github/workflows/upstream.yml @@ -27,7 +27,7 @@ jobs: - uses: actions/checkout@v5 - name: Setup Python 3.13 - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: "3.13" allow-prereleases: true From a6581eee89bcff3eb9e2e1d25d47da827a58aa81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20K=C3=B6ppe?= Date: Mon, 8 Sep 2025 19:52:41 +0100 Subject: [PATCH 10/47] pytypes.h: constrain accessor::operator= templates so that they do not obscure special members (#5832) * pytypes.h: constrain accessor::operator= templates so that they do not match calls that should use the special member functions. Found by an experimental, new clang-tidy check. While we may not know the exact design decisions now, it seems unlikely that the special members were deliberately meant to not be selected (for otherwise they could have been defined differently to make this clear). Rather, it seems like an oversight that the operator templates win in overload resolution, and we should restore the intended resolution. * Use C++11-compatible facilities * Use C++11-compatible facilities * style: pre-commit fixes --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- include/pybind11/pytypes.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/include/pybind11/pytypes.h b/include/pybind11/pytypes.h index 9c60c94c0..cee4ab562 100644 --- a/include/pybind11/pytypes.h +++ b/include/pybind11/pytypes.h @@ -1039,11 +1039,11 @@ public: void operator=(const accessor &a) & { operator=(handle(a)); } template - void operator=(T &&value) && { + enable_if_t>::value> operator=(T &&value) && { Policy::set(obj, key, object_or_cast(std::forward(value))); } template - void operator=(T &&value) & { + enable_if_t>::value> operator=(T &&value) & { get_cache() = ensure_object(object_or_cast(std::forward(value))); } From 68cbae6641faceeecf39335135206fb92e4b4e3a Mon Sep 17 00:00:00 2001 From: Joshua Oreman Date: Mon, 8 Sep 2025 15:47:24 -0700 Subject: [PATCH 11/47] tests: add or delete copy/move ctors where needed to make type traits match reality (#5833) --- tests/test_buffers.cpp | 2 +- tests/test_class.cpp | 1 + tests/test_class_release_gil_before_calling_cpp_dtor.cpp | 1 + tests/test_class_sh_property_non_owning.cpp | 2 ++ tests/test_cross_module_rtti/lib.h | 1 + tests/test_eigen_matrix.cpp | 1 + tests/test_numpy_array.cpp | 1 + tests/test_potentially_slicing_weak_ptr.cpp | 2 ++ tests/test_smart_ptr.cpp | 6 ++++++ tests/test_stl.cpp | 1 + tests/test_tagbased_polymorphic.cpp | 2 ++ 11 files changed, 19 insertions(+), 1 deletion(-) diff --git a/tests/test_buffers.cpp b/tests/test_buffers.cpp index a090c8745..b11d1bb31 100644 --- a/tests/test_buffers.cpp +++ b/tests/test_buffers.cpp @@ -227,7 +227,7 @@ TEST_SUBMODULE(buffers, m) { + std::to_string(cols) + "(*" + std::to_string(col_factor) + ") matrix"); } - + DiscontiguousMatrix(const DiscontiguousMatrix &) = delete; ~DiscontiguousMatrix() { print_destroyed(this, std::to_string(rows() / m_row_factor) + "(*" diff --git a/tests/test_class.cpp b/tests/test_class.cpp index 3d567fc1f..0c05614e6 100644 --- a/tests/test_class.cpp +++ b/tests/test_class.cpp @@ -521,6 +521,7 @@ TEST_SUBMODULE(class_, m) { // test_exception_rvalue_abort struct PyPrintDestructor { PyPrintDestructor() = default; + PyPrintDestructor(const PyPrintDestructor &) = default; ~PyPrintDestructor() { py::print("Print from destructor"); } void throw_something() { throw std::runtime_error("error"); } }; diff --git a/tests/test_class_release_gil_before_calling_cpp_dtor.cpp b/tests/test_class_release_gil_before_calling_cpp_dtor.cpp index a4869a846..7349c416b 100644 --- a/tests/test_class_release_gil_before_calling_cpp_dtor.cpp +++ b/tests/test_class_release_gil_before_calling_cpp_dtor.cpp @@ -22,6 +22,7 @@ private: public: explicit ProbeType(const std::string &unique_key) : unique_key{unique_key} {} + ProbeType(const ProbeType &) = default; ~ProbeType() { RegistryType ® = PyGILState_Check_Results(); diff --git a/tests/test_class_sh_property_non_owning.cpp b/tests/test_class_sh_property_non_owning.cpp index 45fe7c7be..5a5877e4e 100644 --- a/tests/test_class_sh_property_non_owning.cpp +++ b/tests/test_class_sh_property_non_owning.cpp @@ -33,6 +33,8 @@ public: } } + DataFieldsHolder(DataFieldsHolder &&) noexcept = default; + DataField *vec_at(std::size_t index) { if (index >= vec.size()) { return nullptr; diff --git a/tests/test_cross_module_rtti/lib.h b/tests/test_cross_module_rtti/lib.h index 0925b084c..2f76043c3 100644 --- a/tests/test_cross_module_rtti/lib.h +++ b/tests/test_cross_module_rtti/lib.h @@ -12,6 +12,7 @@ __pragma(warning(disable : 4251)) class TEST_CROSS_MODULE_RTTI_LIB_EXPORT Base : public std::enable_shared_from_this { public: Base(int a, int b); + Base(const Base &) = default; virtual ~Base() = default; virtual int get() const; diff --git a/tests/test_eigen_matrix.cpp b/tests/test_eigen_matrix.cpp index 4e6689a79..96a96c2b1 100644 --- a/tests/test_eigen_matrix.cpp +++ b/tests/test_eigen_matrix.cpp @@ -237,6 +237,7 @@ TEST_SUBMODULE(eigen_matrix, m) { public: ReturnTester() { print_created(this); } + ReturnTester(const ReturnTester &) = default; ~ReturnTester() { print_destroyed(this); } static Eigen::MatrixXd create() { return Eigen::MatrixXd::Ones(10, 10); } // NOLINTNEXTLINE(readability-const-return-type) diff --git a/tests/test_numpy_array.cpp b/tests/test_numpy_array.cpp index 1bfca33bb..28359c46d 100644 --- a/tests/test_numpy_array.cpp +++ b/tests/test_numpy_array.cpp @@ -282,6 +282,7 @@ TEST_SUBMODULE(numpy_array, sm) { struct ArrayClass { int data[2] = {1, 2}; ArrayClass() { py::print("ArrayClass()"); } + ArrayClass(const ArrayClass &) = default; ~ArrayClass() { py::print("~ArrayClass()"); } }; py::class_(sm, "ArrayClass") diff --git a/tests/test_potentially_slicing_weak_ptr.cpp b/tests/test_potentially_slicing_weak_ptr.cpp index 01b147faf..c1bf36f19 100644 --- a/tests/test_potentially_slicing_weak_ptr.cpp +++ b/tests/test_potentially_slicing_weak_ptr.cpp @@ -11,7 +11,9 @@ namespace potentially_slicing_weak_ptr { template // Using int as a trick to easily generate multiple types. struct VirtBase { + VirtBase() = default; virtual ~VirtBase() = default; + VirtBase(const VirtBase &) = delete; virtual int get_code() { return 100; } }; diff --git a/tests/test_smart_ptr.cpp b/tests/test_smart_ptr.cpp index 5fdd69db3..2e98d469f 100644 --- a/tests/test_smart_ptr.cpp +++ b/tests/test_smart_ptr.cpp @@ -161,6 +161,8 @@ public: print_created(this); pointer_set().insert(this); }; + MyObject4a(const MyObject4a &) = delete; + int value; static void cleanupAllInstances() { @@ -182,6 +184,7 @@ protected: class MyObject4b : public MyObject4a { public: explicit MyObject4b(int i) : MyObject4a(i) { print_created(this); } + MyObject4b(const MyObject4b &) = delete; ~MyObject4b() override { print_destroyed(this); } }; @@ -189,6 +192,7 @@ public: class MyObject5 { // managed by huge_unique_ptr public: explicit MyObject5(int value) : value{value} { print_created(this); } + MyObject5(const MyObject5 &) = delete; ~MyObject5() { print_destroyed(this); } int value; }; @@ -245,6 +249,7 @@ struct SharedFromThisVirt : virtual SharedFromThisVBase {}; // test_move_only_holder struct C { C() { print_created(this); } + C(const C &) = delete; ~C() { print_destroyed(this); } }; @@ -265,6 +270,7 @@ struct TypeForHolderWithAddressOf { // test_move_only_holder_with_addressof_operator struct TypeForMoveOnlyHolderWithAddressOf { explicit TypeForMoveOnlyHolderWithAddressOf(int value) : value{value} { print_created(this); } + TypeForMoveOnlyHolderWithAddressOf(const TypeForMoveOnlyHolderWithAddressOf &) = delete; ~TypeForMoveOnlyHolderWithAddressOf() { print_destroyed(this); } std::string toString() const { return "MoveOnlyHolderWithAddressOf[" + std::to_string(value) + "]"; diff --git a/tests/test_stl.cpp b/tests/test_stl.cpp index 5e6d6a333..6084d517d 100644 --- a/tests/test_stl.cpp +++ b/tests/test_stl.cpp @@ -99,6 +99,7 @@ public: using OptionalEnumValue = OptionalImpl; OptionalProperties() : value(EnumType::kSet) {} + OptionalProperties(const OptionalProperties &) = default; ~OptionalProperties() { // Reset value to detect use-after-destruction. // This is set to a specific value rather than nullopt to ensure that diff --git a/tests/test_tagbased_polymorphic.cpp b/tests/test_tagbased_polymorphic.cpp index 13e5ed319..8a8a3280c 100644 --- a/tests/test_tagbased_polymorphic.cpp +++ b/tests/test_tagbased_polymorphic.cpp @@ -17,6 +17,8 @@ struct Animal { // (https://github.com/pybind/pybind11/pull/2016/). virtual ~Animal() = default; + Animal(const Animal &) = delete; + // Enum for tag-based polymorphism. enum class Kind { Unknown = 0, From 937552f0ad8eeab1265007fa3d7508ede6c97313 Mon Sep 17 00:00:00 2001 From: Scott Wolchok Date: Fri, 12 Sep 2025 14:37:01 -0700 Subject: [PATCH 12/47] Use thread_local for loader_life_support to improve performance (#5830) * Use thread_local for loader_life_support to improve performance As explained in a new code comment, `loader_life_support` needs to be `thread_local` but does not need to be isolated to a particular interpreter because any given function call is already going to only happen on a single interpreter by definiton. Performance before: - on M4 Max using pybind/pybind11_benchmark unmodified repo: ``` > python -m timeit --setup 'from pybind11_benchmark import collatz' 'collatz(4)' 5000000 loops, best of 5: 63.8 nsec per loop ``` - Linux server: ``` python -m timeit --setup 'from pybind11_benchmark import collatz' 'collatz(4)' (pytorch) 2000000 loops, best of 5: 120 nsec per loop ``` After: - M4 Max: ``` python -m timeit --setup 'from pybind11_benchmark import collatz' 'collatz(4)' 5000000 loops, best of 5: 53.1 nsec per loop ``` - Linux server: ``` > python -m timeit --setup 'from pybind11_benchmark import collatz' 'collatz(4)' (pytorch) 2000000 loops, best of 5: 101 nsec per loop ``` A quick profile with perf shows that pthread_setspecific and pthread_getspecific are gone. Open questions: - How do we determine whether we can safely use `thread_local`? I see concerns about old iOS versions on https://github.com/pybind/pybind11/pull/5705#issuecomment-2922858880 and https://github.com/pybind/pybind11/pull/5709; is there anything else? - Do we have a test that covers "function called in one interpreter calls a C++ function that causes a function call in another interpreter"? I think it's fine, but can it happen? - Are we happy with what we think will happen in the case where multiple extensions compiled with and without this PR interoperate? I think it's fine -- each dispatch pushes and cleans up its own state -- but a second opinion is certainly welcome. * Remove PYBIND11_CAN_USE_THREAD_LOCAL * clarify comment * Simplify loader_life_support TLS storage Replace the `fake_thread_specific_storage` struct with a direct thread-local pointer managed via a function-local static: static loader_life_support *& tls_current_frame() This retains the "stack of frames" behavior via the `parent` link. It also reduces indirection and clarifies intent. Note: this form is C++11-compatible; once pybind11 requires C++17, the helper can be simplified to: inline static thread_local loader_life_support *tls_current_frame = nullptr; * loader_life_support: avoid duplicate tls_current_frame() calls Replace repeated calls with a single local reference: auto &frame = tls_current_frame(); This ensures the thread_local initialization guard is checked only once per constructor/destructor call site, avoids potential clang-tidy complaints, and makes the code more readable. Functional behavior is unchanged. * Add REMINDER for next version bump in internals.h --------- Co-authored-by: Ralf W. Grosse-Kunstleve --- include/pybind11/detail/internals.h | 3 ++- include/pybind11/detail/type_caster_base.h | 30 +++++++++++++++++----- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/include/pybind11/detail/internals.h b/include/pybind11/detail/internals.h index 4ea374861..cd3afdfe3 100644 --- a/include/pybind11/detail/internals.h +++ b/include/pybind11/detail/internals.h @@ -39,6 +39,7 @@ /// further ABI-incompatible changes may be made before the ABI is officially /// changed to the new version. #ifndef PYBIND11_INTERNALS_VERSION +// REMINDER for next version bump: remove loader_life_support_tls # define PYBIND11_INTERNALS_VERSION 11 #endif @@ -260,7 +261,7 @@ struct internals { PyObject *instance_base = nullptr; // Unused if PYBIND11_SIMPLE_GIL_MANAGEMENT is defined: thread_specific_storage tstate; - thread_specific_storage loader_life_support_tls; + thread_specific_storage loader_life_support_tls; // OBSOLETE (PR #5830) // Unused if PYBIND11_SIMPLE_GIL_MANAGEMENT is defined: PyInterpreterState *istate = nullptr; diff --git a/include/pybind11/detail/type_caster_base.h b/include/pybind11/detail/type_caster_base.h index 1b23c5c68..23d940735 100644 --- a/include/pybind11/detail/type_caster_base.h +++ b/include/pybind11/detail/type_caster_base.h @@ -42,24 +42,40 @@ PYBIND11_NAMESPACE_BEGIN(detail) /// Adding a patient will keep it alive up until the enclosing function returns. class loader_life_support { private: + // Thread-local top-of-stack for loader_life_support frames (linked via parent). + // Observation: loader_life_support needs to be thread-local, + // but we don't need to go to extra effort to keep it + // per-interpreter (i.e., by putting it in internals) since + // individual function calls are already isolated to a single + // interpreter, even though they could potentially call into a + // different interpreter later in the same call chain. This + // saves a significant cost per function call spent in + // loader_life_support destruction. + // Note for future C++17 simplification: + // inline static thread_local loader_life_support *tls_current_frame = nullptr; + static loader_life_support *&tls_current_frame() { + static thread_local loader_life_support *frame_ptr = nullptr; + return frame_ptr; + } + loader_life_support *parent = nullptr; std::unordered_set keep_alive; public: /// A new patient frame is created when a function is entered loader_life_support() { - auto &stack_top = get_internals().loader_life_support_tls; - parent = stack_top.get(); - stack_top = this; + auto &frame = tls_current_frame(); + parent = frame; + frame = this; } /// ... and destroyed after it returns ~loader_life_support() { - auto &stack_top = get_internals().loader_life_support_tls; - if (stack_top.get() != this) { + auto &frame = tls_current_frame(); + if (frame != this) { pybind11_fail("loader_life_support: internal error"); } - stack_top = parent; + frame = parent; for (auto *item : keep_alive) { Py_DECREF(item); } @@ -68,7 +84,7 @@ public: /// This can only be used inside a pybind11-bound function, either by `argument_loader` /// at argument preparation time or by `py::cast()` at execution time. PYBIND11_NOINLINE static void add_patient(handle h) { - loader_life_support *frame = get_internals().loader_life_support_tls.get(); + loader_life_support *frame = tls_current_frame(); if (!frame) { // NOTE: It would be nice to include the stack frames here, as this indicates // use of pybind11::cast<> outside the normal call framework, finding such From d4d555d9e05ad77c1f7ed1e900cb987e691445d6 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Fri, 12 Sep 2025 21:52:44 -0700 Subject: [PATCH 13/47] Restore `runs-on: windows-latest` (#5835) * Revert "s/windows-2022/windows-latest/ in .github/workflows/{ci,pip}.yml (#5826)" This reverts commit 852a4b5010e339930e974e03eafdaaa3f6f32c36. * Add module-level skip for Windows build >= 26100 in test_iostream.py * Changes suggested by at-henryiii --- .github/workflows/ci.yml | 20 ++++++++++---------- .github/workflows/pip.yml | 4 ++-- tests/test_iostream.py | 11 +++++++++++ 3 files changed, 23 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 010d5ddc9..9798f3aee 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -119,7 +119,7 @@ jobs: - runs-on: macos-latest python-version: 'graalpy-24.2' - - runs-on: windows-2022 + - runs-on: windows-latest python-version: '3.9' cmake-args: -DPYBIND11_TEST_SMART_HOLDER=ON - runs-on: windows-2022 @@ -138,19 +138,19 @@ jobs: - runs-on: windows-2022 python-version: '3.13' cmake-args: -DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreadedDebugDLL - - runs-on: windows-2022 + - runs-on: windows-latest python-version: '3.13t' cmake-args: -DCMAKE_CXX_STANDARD=17 - - runs-on: windows-2022 + - runs-on: windows-latest python-version: '3.14' cmake-args: -DCMAKE_CXX_STANDARD=20 - - runs-on: windows-2022 + - runs-on: windows-latest python-version: '3.14t' cmake-args: -DCMAKE_CXX_STANDARD=23 - - runs-on: windows-2022 + - runs-on: windows-latest python-version: 'pypy-3.10' cmake-args: -DCMAKE_CXX_STANDARD=17 - - runs-on: windows-2022 + - runs-on: windows-latest python-version: 'pypy3.11' cmake-args: -DCMAKE_CXX_STANDARD=20 # The setup-python action currently doesn't have graalpy for windows @@ -174,7 +174,7 @@ jobs: python-version: '3.9' - runs-on: macos-latest python-version: '3.12' - - runs-on: windows-2022 + - runs-on: windows-latest python-version: '3.11' name: "🐍 ${{ matrix.python-version }} β€’ ${{ matrix.runs-on }} β€’ x64 inplace C++14" @@ -1010,8 +1010,8 @@ jobs: mingw: if: github.event.pull_request.draft == false - name: "🐍 3 β€’ windows-2022 β€’ ${{ matrix.sys }}" - runs-on: windows-2022 + name: "🐍 3 β€’ windows-latest β€’ ${{ matrix.sys }}" + runs-on: windows-latest defaults: run: shell: msys2 {0} @@ -1121,7 +1121,7 @@ jobs: strategy: matrix: - os: [windows-2022] + os: [windows-latest] python: ['3.10'] runs-on: "${{ matrix.os }}" diff --git a/.github/workflows/pip.yml b/.github/workflows/pip.yml index 0347936de..a5e7ffb51 100644 --- a/.github/workflows/pip.yml +++ b/.github/workflows/pip.yml @@ -19,8 +19,8 @@ jobs: # This builds the sdists and wheels and makes sure the files are exactly as # expected. test-packaging: - name: 🐍 3.8 β€’ πŸ“¦ tests β€’ windows-2022 - runs-on: windows-2022 + name: 🐍 3.8 β€’ πŸ“¦ tests β€’ windows-latest + runs-on: windows-latest steps: - uses: actions/checkout@v5 diff --git a/tests/test_iostream.py b/tests/test_iostream.py index c3d987787..00b24ab70 100644 --- a/tests/test_iostream.py +++ b/tests/test_iostream.py @@ -6,8 +6,19 @@ from io import StringIO import pytest +import env from pybind11_tests import iostream as m +if env.WIN: + wv_build = sys.getwindowsversion().build + skip_if_ge = 26100 + if wv_build >= skip_if_ge: + pytest.skip( + f"Windows build {wv_build} >= {skip_if_ge}:" + " Skipping iostream capture (redirection regression needs investigation)", + allow_module_level=True, + ) + def test_captured(capsys): msg = "I've been redirected to Python, I hope!" From 326b10637a9fe2d1c9f290b6e052097ffc2413c3 Mon Sep 17 00:00:00 2001 From: b-pass Date: Sun, 14 Sep 2025 12:07:08 -0400 Subject: [PATCH 14/47] Use thread_local instead of thread_specific_storage for internals (#5834) * Use thread_local instead of thread_specific_storage for internals mangement thread_local is faster. * Make the pp manager a singleton. Strictly speaking, since the members are static, the instances must also be singletons or this wouldn't work. They already are, but we can make the class enforce it to be more 'self-documenting'. --- include/pybind11/detail/internals.h | 50 +++++++++++++++++------------ 1 file changed, 30 insertions(+), 20 deletions(-) diff --git a/include/pybind11/detail/internals.h b/include/pybind11/detail/internals.h index cd3afdfe3..d23ee6ec9 100644 --- a/include/pybind11/detail/internals.h +++ b/include/pybind11/detail/internals.h @@ -502,8 +502,11 @@ template class internals_pp_manager { public: using on_fetch_function = void(InternalsType *); - internals_pp_manager(char const *id, on_fetch_function *on_fetch) - : holder_id_(id), on_fetch_(on_fetch) {} + + inline static internals_pp_manager &get_instance(char const *id, on_fetch_function *on_fetch) { + static internals_pp_manager instance(id, on_fetch); + return instance; + } /// Get the current pointer-to-pointer, allocating it if it does not already exist. May /// acquire the GIL. Will never return nullptr. @@ -514,15 +517,15 @@ public: // internals_pp so that it can be pulled from the interpreter's state dict. That is // slow, so we use the current PyThreadState to check if it is necessary. auto *tstate = get_thread_state_unchecked(); - if (!tstate || tstate->interp != last_istate_.get()) { + if (!tstate || tstate->interp != last_istate_tls()) { gil_scoped_acquire_simple gil; if (!tstate) { tstate = get_thread_state_unchecked(); } - last_istate_ = tstate->interp; - internals_tls_p_ = get_or_create_pp_in_state_dict(); + last_istate_tls() = tstate->interp; + internals_p_tls() = get_or_create_pp_in_state_dict(); } - return internals_tls_p_.get(); + return internals_p_tls(); } #endif if (!internals_singleton_pp_) { @@ -536,8 +539,8 @@ public: void unref() { #ifdef PYBIND11_HAS_SUBINTERPRETER_SUPPORT if (get_num_interpreters_seen() > 1) { - last_istate_.reset(); - internals_tls_p_.reset(); + last_istate_tls() = nullptr; + internals_p_tls() = nullptr; return; } #endif @@ -549,8 +552,8 @@ public: if (get_num_interpreters_seen() > 1) { auto *tstate = get_thread_state_unchecked(); // this could be called without an active interpreter, just use what was cached - if (!tstate || tstate->interp == last_istate_.get()) { - auto tpp = internals_tls_p_.get(); + if (!tstate || tstate->interp == last_istate_tls()) { + auto tpp = internals_p_tls(); if (tpp) { delete tpp; } @@ -564,6 +567,9 @@ public: } private: + internals_pp_manager(char const *id, on_fetch_function *on_fetch) + : holder_id_(id), on_fetch_(on_fetch) {} + std::unique_ptr *get_or_create_pp_in_state_dict() { error_scope err_scope; dict state_dict = get_python_state_dict(); @@ -589,12 +595,20 @@ private: return pp; } +#ifdef PYBIND11_HAS_SUBINTERPRETER_SUPPORT + static PyInterpreterState *&last_istate_tls() { + static thread_local PyInterpreterState *last_istate = nullptr; + return last_istate; + } + + static std::unique_ptr *&internals_p_tls() { + static thread_local std::unique_ptr *internals_p = nullptr; + return internals_p; + } +#endif + char const *holder_id_ = nullptr; on_fetch_function *on_fetch_ = nullptr; -#ifdef PYBIND11_HAS_SUBINTERPRETER_SUPPORT - thread_specific_storage last_istate_; - thread_specific_storage> internals_tls_p_; -#endif std::unique_ptr *internals_singleton_pp_; }; @@ -624,10 +638,8 @@ inline internals_pp_manager &get_internals_pp_manager() { #else # define ON_FETCH_FN &check_internals_local_exception_translator #endif - static internals_pp_manager internals_pp_manager(PYBIND11_INTERNALS_ID, - ON_FETCH_FN); + return internals_pp_manager::get_instance(PYBIND11_INTERNALS_ID, ON_FETCH_FN); #undef ON_FETCH_FN - return internals_pp_manager; } /// Return a reference to the current `internals` data @@ -655,9 +667,7 @@ inline internals_pp_manager &get_local_internals_pp_manager() { static const std::string this_module_idstr = PYBIND11_MODULE_LOCAL_ID + std::to_string(reinterpret_cast(&this_module_idstr)); - static internals_pp_manager local_internals_pp_manager( - this_module_idstr.c_str(), nullptr); - return local_internals_pp_manager; + return internals_pp_manager::get_instance(this_module_idstr.c_str(), nullptr); } /// Works like `get_internals`, but for things which are locally registered. From 30748f863f18c2e0d48e9dcb3be1e77542904da8 Mon Sep 17 00:00:00 2001 From: Scott Wolchok Date: Fri, 19 Sep 2025 13:44:40 -0700 Subject: [PATCH 15/47] Avoid heap allocation for function calls with a small number of args (#5824) * Avoid heap allocation for function calls with a small number of arguments We don't have access to llvm::SmallVector or similar, but given the limited subset of the `std::vector` API that `function_call::args{,_convert}` need and the "reserve-then-fill" usage pattern, it is relatively straightforward to implement custom containers that get the job done. Seems to improves time to call the collatz function in pybind/pybind11_benchmark significantly; numbers are a little noisy but there's a clear improvement from "about 60 ns per call" to "about 45 ns per call" on my machine (M4 Max Mac), as measured with `timeit.repeat('collatz(4)', 'from pybind11_benchmark import collatz')`. * clang-tidy * more clang-tidy * clang-tidy NOLINTBEGIN/END instead of NOLINTNEXTLINE * forgot to increase inline size after removing std::variant * constexpr arg_vector_small_size, use move instead of swap to hopefully clarify second_pass_convert * rename test_embed to test_low_level * rename test_low_level to test_with_catch * Be careful to NOINLINE slow paths * rename array/vector members to iarray/hvector. Move comment per request. Add static_asserts for our untagged union implementation per request. * drop is_standard_layout assertions; see https://github.com/pybind/pybind11/pull/5824#issuecomment-3308616072 --- CMakeLists.txt | 1 + include/pybind11/cast.h | 9 +- include/pybind11/detail/argument_vector.h | 330 ++++++++++++++++++ include/pybind11/pybind11.h | 7 +- tests/CMakeLists.txt | 4 +- tests/extra_python_package/test_files.py | 1 + .../subdirectory_embed/CMakeLists.txt | 6 +- .../CMakeLists.txt | 11 +- .../{test_embed => test_with_catch}/catch.cpp | 2 +- .../external_module.cpp | 0 .../test_args_convert_vector.cpp | 80 +++++ .../test_with_catch/test_argument_vector.cpp | 94 +++++ .../test_interpreter.cpp | 5 +- .../test_interpreter.py | 0 .../test_subinterpreter.cpp | 0 .../test_trampoline.py | 0 16 files changed, 532 insertions(+), 18 deletions(-) create mode 100644 include/pybind11/detail/argument_vector.h rename tests/{test_embed => test_with_catch}/CMakeLists.txt (84%) rename tests/{test_embed => test_with_catch}/catch.cpp (93%) rename tests/{test_embed => test_with_catch}/external_module.cpp (100%) create mode 100644 tests/test_with_catch/test_args_convert_vector.cpp create mode 100644 tests/test_with_catch/test_argument_vector.cpp rename tests/{test_embed => test_with_catch}/test_interpreter.cpp (99%) rename tests/{test_embed => test_with_catch}/test_interpreter.py (100%) rename tests/{test_embed => test_with_catch}/test_subinterpreter.cpp (100%) rename tests/{test_embed => test_with_catch}/test_trampoline.py (100%) diff --git a/CMakeLists.txt b/CMakeLists.txt index ba5f665a2..a6d619bbd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -180,6 +180,7 @@ if(PYBIND11_MASTER_PROJECT) endif() set(PYBIND11_HEADERS + include/pybind11/detail/argument_vector.h include/pybind11/detail/class.h include/pybind11/detail/common.h include/pybind11/detail/cpp_conduit.h diff --git a/include/pybind11/cast.h b/include/pybind11/cast.h index c635791fe..4dcbf2623 100644 --- a/include/pybind11/cast.h +++ b/include/pybind11/cast.h @@ -10,6 +10,7 @@ #pragma once +#include "detail/argument_vector.h" #include "detail/common.h" #include "detail/descr.h" #include "detail/native_enum_data.h" @@ -2037,6 +2038,10 @@ using is_pos_only = std::is_same, pos_only>; // forward declaration (definition in attr.h) struct function_record; +/// (Inline size chosen mostly arbitrarily; 6 should pad function_call out to two cache lines +/// (16 pointers) in size.) +constexpr std::size_t arg_vector_small_size = 6; + /// Internal data associated with a single function call struct function_call { function_call(const function_record &f, handle p); // Implementation in attr.h @@ -2045,10 +2050,10 @@ struct function_call { const function_record &func; /// Arguments passed to the function: - std::vector args; + argument_vector args; /// The `convert` value the arguments should be loaded with - std::vector args_convert; + args_convert_vector args_convert; /// Extra references for the optional `py::args` and/or `py::kwargs` arguments (which, if /// present, are also in `args` but without a reference). diff --git a/include/pybind11/detail/argument_vector.h b/include/pybind11/detail/argument_vector.h new file mode 100644 index 000000000..e9bfe064d --- /dev/null +++ b/include/pybind11/detail/argument_vector.h @@ -0,0 +1,330 @@ +/* + pybind11/detail/argument_vector.h: small_vector-like containers to + avoid heap allocation of arguments during function call dispatch. + + Copyright (c) Meta Platforms, Inc. and affiliates. + + All rights reserved. Use of this source code is governed by a + BSD-style license that can be found in the LICENSE file. +*/ + +#pragma once + +#include + +#include "common.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE) + +PYBIND11_WARNING_DISABLE_MSVC(4127) + +PYBIND11_NAMESPACE_BEGIN(detail) + +// Shared implementation utility for our small_vector-like containers. +// We support C++11 and C++14, so we cannot use +// std::variant. Union with the tag packed next to the inline +// array's size is smaller anyway, allowing 1 extra handle of +// inline storage for free. Compare the layouts (1 line per +// size_t/void*, assuming a 64-bit machine): +// With variant, total is N + 2 for N >= 2: +// - variant tag (cannot be packed with the array size) +// - array size (or first pointer of 3 in std::vector) +// - N pointers of inline storage (or 2 remaining pointers of std::vector) +// Custom union, total is N + 1 for N >= 3: +// - variant tag & array size if applicable +// - N pointers of inline storage (or 3 pointers of std::vector) +// +// NOTE: this is a low-level representational convenience; the two +// use cases of this union are materially different and in particular +// have different semantics for inline_array::size. All that is being +// shared is the memory management behavior. +template +union inline_array_or_vector { + struct inline_array { + bool is_inline = true; + std::uint32_t size = 0; + std::array arr; + }; + struct heap_vector { + bool is_inline = false; + std::vector vec; + + heap_vector() = default; + heap_vector(std::size_t count, VectorT value) : vec(count, value) {} + }; + + inline_array iarray; + heap_vector hvector; + + static_assert(std::is_trivially_move_constructible::value, + "ArrayT must be trivially move constructible"); + static_assert(std::is_trivially_destructible::value, + "ArrayT must be trivially destructible"); + + inline_array_or_vector() : iarray() {} + ~inline_array_or_vector() { + if (!is_inline()) { + hvector.~heap_vector(); + } + } + // Disable copy ctor and assignment. + inline_array_or_vector(const inline_array_or_vector &) = delete; + inline_array_or_vector &operator=(const inline_array_or_vector &) = delete; + + inline_array_or_vector(inline_array_or_vector &&rhs) noexcept { + if (rhs.is_inline()) { + std::memcpy(&iarray, &rhs.iarray, sizeof(iarray)); + } else { + new (&hvector) heap_vector(std::move(rhs.hvector)); + } + assert(is_inline() == rhs.is_inline()); + } + + inline_array_or_vector &operator=(inline_array_or_vector &&rhs) noexcept { + if (this == &rhs) { + return *this; + } + + if (rhs.is_inline()) { + if (!is_inline()) { + hvector.~heap_vector(); + } + std::memcpy(&iarray, &rhs.iarray, sizeof(iarray)); + } else { + if (is_inline()) { + new (&hvector) heap_vector(std::move(rhs.hvector)); + } else { + hvector = std::move(rhs.hvector); + } + } + return *this; + } + + bool is_inline() const { + // It is undefined behavior to access the inactive member of a + // union directly. However, it is well-defined to reinterpret_cast any + // pointer into a pointer to char and examine it as an array + // of bytes. See + // https://dev-discuss.pytorch.org/t/unionizing-for-profit-how-to-exploit-the-power-of-unions-in-c/444#the-memcpy-loophole-4 + bool result = false; + static_assert(offsetof(inline_array, is_inline) == 0, + "untagged union implementation relies on this"); + static_assert(offsetof(heap_vector, is_inline) == 0, + "untagged union implementation relies on this"); + std::memcpy(&result, reinterpret_cast(this), sizeof(bool)); + return result; + } +}; + +// small_vector-like container to avoid heap allocation for N or fewer +// arguments. +template +struct argument_vector { +public: + argument_vector() = default; + + // Disable copy ctor and assignment. + argument_vector(const argument_vector &) = delete; + argument_vector &operator=(const argument_vector &) = delete; + argument_vector(argument_vector &&) noexcept = default; + argument_vector &operator=(argument_vector &&) noexcept = default; + + std::size_t size() const { + if (is_inline()) { + return m_repr.iarray.size; + } + return m_repr.hvector.vec.size(); + } + + handle &operator[](std::size_t idx) { + assert(idx < size()); + if (is_inline()) { + return m_repr.iarray.arr[idx]; + } + return m_repr.hvector.vec[idx]; + } + + handle operator[](std::size_t idx) const { + assert(idx < size()); + if (is_inline()) { + return m_repr.iarray.arr[idx]; + } + return m_repr.hvector.vec[idx]; + } + + void push_back(handle x) { + if (is_inline()) { + auto &ha = m_repr.iarray; + if (ha.size == N) { + move_to_heap_vector_with_reserved_size(N + 1); + push_back_slow_path(x); + } else { + ha.arr[ha.size++] = x; + } + } else { + push_back_slow_path(x); + } + } + + template + void emplace_back(Arg &&x) { + push_back(handle(x)); + } + + void reserve(std::size_t sz) { + if (is_inline()) { + if (sz > N) { + move_to_heap_vector_with_reserved_size(sz); + } + } else { + reserve_slow_path(sz); + } + } + +private: + using repr_type = inline_array_or_vector; + repr_type m_repr; + + PYBIND11_NOINLINE void move_to_heap_vector_with_reserved_size(std::size_t reserved_size) { + assert(is_inline()); + auto &ha = m_repr.iarray; + using heap_vector = typename repr_type::heap_vector; + heap_vector hv; + hv.vec.reserve(reserved_size); + std::copy(ha.arr.begin(), ha.arr.begin() + ha.size, std::back_inserter(hv.vec)); + new (&m_repr.hvector) heap_vector(std::move(hv)); + } + + PYBIND11_NOINLINE void push_back_slow_path(handle x) { m_repr.hvector.vec.push_back(x); } + + PYBIND11_NOINLINE void reserve_slow_path(std::size_t sz) { m_repr.hvector.vec.reserve(sz); } + + bool is_inline() const { return m_repr.is_inline(); } +}; + +// small_vector-like container to avoid heap allocation for N or fewer +// arguments. +template +struct args_convert_vector { +private: +public: + args_convert_vector() = default; + + // Disable copy ctor and assignment. + args_convert_vector(const args_convert_vector &) = delete; + args_convert_vector &operator=(const args_convert_vector &) = delete; + args_convert_vector(args_convert_vector &&) noexcept = default; + args_convert_vector &operator=(args_convert_vector &&) noexcept = default; + + args_convert_vector(std::size_t count, bool value) { + if (count > kInlineSize) { + new (&m_repr.hvector) typename repr_type::heap_vector(count, value); + } else { + auto &inline_arr = m_repr.iarray; + inline_arr.arr.fill(value ? std::size_t(-1) : 0); + inline_arr.size = static_cast(count); + } + } + + std::size_t size() const { + if (is_inline()) { + return m_repr.iarray.size; + } + return m_repr.hvector.vec.size(); + } + + void reserve(std::size_t sz) { + if (is_inline()) { + if (sz > kInlineSize) { + move_to_heap_vector_with_reserved_size(sz); + } + } else { + m_repr.hvector.vec.reserve(sz); + } + } + + bool operator[](std::size_t idx) const { + if (is_inline()) { + return inline_index(idx); + } + assert(idx < m_repr.hvector.vec.size()); + return m_repr.hvector.vec[idx]; + } + + void push_back(bool b) { + if (is_inline()) { + auto &ha = m_repr.iarray; + if (ha.size == kInlineSize) { + move_to_heap_vector_with_reserved_size(kInlineSize + 1); + push_back_slow_path(b); + } else { + assert(ha.size < kInlineSize); + const auto wbi = word_and_bit_index(ha.size++); + assert(wbi.word < kWords); + assert(wbi.bit < kBitsPerWord); + if (b) { + ha.arr[wbi.word] |= (std::size_t(1) << wbi.bit); + } else { + ha.arr[wbi.word] &= ~(std::size_t(1) << wbi.bit); + } + assert(operator[](ha.size - 1) == b); + } + } else { + push_back_slow_path(b); + } + } + + void swap(args_convert_vector &rhs) noexcept { std::swap(m_repr, rhs.m_repr); } + +private: + struct WordAndBitIndex { + std::size_t word; + std::size_t bit; + }; + + static WordAndBitIndex word_and_bit_index(std::size_t idx) { + return WordAndBitIndex{idx / kBitsPerWord, idx % kBitsPerWord}; + } + + bool inline_index(std::size_t idx) const { + const auto wbi = word_and_bit_index(idx); + assert(wbi.word < kWords); + assert(wbi.bit < kBitsPerWord); + return m_repr.iarray.arr[wbi.word] & (std::size_t(1) << wbi.bit); + } + + PYBIND11_NOINLINE void move_to_heap_vector_with_reserved_size(std::size_t reserved_size) { + auto &inline_arr = m_repr.iarray; + using heap_vector = typename repr_type::heap_vector; + heap_vector hv; + hv.vec.reserve(reserved_size); + for (std::size_t ii = 0; ii < inline_arr.size; ++ii) { + hv.vec.push_back(inline_index(ii)); + } + new (&m_repr.hvector) heap_vector(std::move(hv)); + } + + PYBIND11_NOINLINE void push_back_slow_path(bool b) { m_repr.hvector.vec.push_back(b); } + + static constexpr auto kBitsPerWord = 8 * sizeof(std::size_t); + static constexpr auto kWords = (kRequestedInlineSize + kBitsPerWord - 1) / kBitsPerWord; + static constexpr auto kInlineSize = kWords * kBitsPerWord; + + using repr_type = inline_array_or_vector; + repr_type m_repr; + + bool is_inline() const { return m_repr.is_inline(); } +}; + +PYBIND11_NAMESPACE_END(detail) +PYBIND11_NAMESPACE_END(PYBIND11_NAMESPACE) diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h index 117fecabf..515aab141 100644 --- a/include/pybind11/pybind11.h +++ b/include/pybind11/pybind11.h @@ -1048,13 +1048,14 @@ protected: } #endif - std::vector second_pass_convert; + args_convert_vector second_pass_convert; if (overloaded) { // We're in the first no-convert pass, so swap out the conversion flags for a // set of all-false flags. If the call fails, we'll swap the flags back in for // the conversion-allowed call below. - second_pass_convert.resize(func.nargs, false); - call.args_convert.swap(second_pass_convert); + second_pass_convert = std::move(call.args_convert); + call.args_convert + = args_convert_vector(func.nargs, false); } // 6. Call the function. diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 34b22a57a..8ac236d40 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -647,8 +647,8 @@ if(NOT PYBIND11_CUDA_TESTS) # Test pure C++ code (not depending on Python). Provides the `test_pure_cpp` target. add_subdirectory(pure_cpp) - # Test embedding the interpreter. Provides the `cpptest` target. - add_subdirectory(test_embed) + # Test C++ code that depends on Python, such as embedding the interpreter. Provides the `cpptest` target. + add_subdirectory(test_with_catch) # Test CMake build using functions and targets from subdirectory or installed location add_subdirectory(test_cmake_build) diff --git a/tests/extra_python_package/test_files.py b/tests/extra_python_package/test_files.py index 63e59f65a..d3452aa38 100644 --- a/tests/extra_python_package/test_files.py +++ b/tests/extra_python_package/test_files.py @@ -76,6 +76,7 @@ conduit_headers = { } detail_headers = { + "include/pybind11/detail/argument_vector.h", "include/pybind11/detail/class.h", "include/pybind11/detail/common.h", "include/pybind11/detail/cpp_conduit.h", diff --git a/tests/test_cmake_build/subdirectory_embed/CMakeLists.txt b/tests/test_cmake_build/subdirectory_embed/CMakeLists.txt index d84916ea7..bb83a6e6e 100644 --- a/tests/test_cmake_build/subdirectory_embed/CMakeLists.txt +++ b/tests/test_cmake_build/subdirectory_embed/CMakeLists.txt @@ -26,11 +26,11 @@ add_custom_target( DEPENDS test_subdirectory_embed) # Test custom export group -- PYBIND11_EXPORT_NAME -add_library(test_embed_lib ../embed.cpp) -target_link_libraries(test_embed_lib PRIVATE pybind11::embed) +add_library(test_with_catch_lib ../embed.cpp) +target_link_libraries(test_with_catch_lib PRIVATE pybind11::embed) install( - TARGETS test_embed_lib + TARGETS test_with_catch_lib EXPORT test_export ARCHIVE DESTINATION bin LIBRARY DESTINATION lib diff --git a/tests/test_embed/CMakeLists.txt b/tests/test_with_catch/CMakeLists.txt similarity index 84% rename from tests/test_embed/CMakeLists.txt rename to tests/test_with_catch/CMakeLists.txt index 23c333643..136537e67 100644 --- a/tests/test_embed/CMakeLists.txt +++ b/tests/test_with_catch/CMakeLists.txt @@ -33,10 +33,11 @@ if(PYBIND11_TEST_SMART_HOLDER) -DPYBIND11_RUN_TESTING_WITH_SMART_HOLDER_AS_DEFAULT_BUT_NEVER_USE_IN_PRODUCTION_PLEASE) endif() -add_executable(test_embed catch.cpp test_interpreter.cpp test_subinterpreter.cpp) -pybind11_enable_warnings(test_embed) +add_executable(test_with_catch catch.cpp test_args_convert_vector.cpp test_argument_vector.cpp + test_interpreter.cpp test_subinterpreter.cpp) +pybind11_enable_warnings(test_with_catch) -target_link_libraries(test_embed PRIVATE pybind11::embed Catch2::Catch2 Threads::Threads) +target_link_libraries(test_with_catch PRIVATE pybind11::embed Catch2::Catch2 Threads::Threads) if(NOT CMAKE_CURRENT_SOURCE_DIR STREQUAL CMAKE_CURRENT_BINARY_DIR) file(COPY test_interpreter.py test_trampoline.py DESTINATION "${CMAKE_CURRENT_BINARY_DIR}") @@ -44,8 +45,8 @@ endif() add_custom_target( cpptest - COMMAND "$" - DEPENDS test_embed + COMMAND "$" + DEPENDS test_with_catch WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}") pybind11_add_module(external_module THIN_LTO external_module.cpp) diff --git a/tests/test_embed/catch.cpp b/tests/test_with_catch/catch.cpp similarity index 93% rename from tests/test_embed/catch.cpp rename to tests/test_with_catch/catch.cpp index 558a7a35e..5bd8b3880 100644 --- a/tests/test_embed/catch.cpp +++ b/tests/test_with_catch/catch.cpp @@ -19,7 +19,7 @@ namespace py = pybind11; int main(int argc, char *argv[]) { // Setup for TEST_CASE in test_interpreter.cpp, tagging on a large random number: - std::string updated_pythonpath("pybind11_test_embed_PYTHONPATH_2099743835476552"); + std::string updated_pythonpath("pybind11_test_with_catch_PYTHONPATH_2099743835476552"); const char *preexisting_pythonpath = getenv("PYTHONPATH"); if (preexisting_pythonpath != nullptr) { #if defined(_WIN32) diff --git a/tests/test_embed/external_module.cpp b/tests/test_with_catch/external_module.cpp similarity index 100% rename from tests/test_embed/external_module.cpp rename to tests/test_with_catch/external_module.cpp diff --git a/tests/test_with_catch/test_args_convert_vector.cpp b/tests/test_with_catch/test_args_convert_vector.cpp new file mode 100644 index 000000000..7ce2d713f --- /dev/null +++ b/tests/test_with_catch/test_args_convert_vector.cpp @@ -0,0 +1,80 @@ +#include "pybind11/pybind11.h" +#include "catch.hpp" + +namespace py = pybind11; + +using args_convert_vector = py::detail::args_convert_vector; + +namespace { +template +std::vector get_sample_vectors() { + std::vector result; + result.emplace_back(); + for (const auto sz : {0, 4, 5, 6, 31, 32, 33, 63, 64, 65}) { + for (const bool b : {false, true}) { + result.emplace_back(static_cast(sz), b); + } + } + return result; +} + +void require_vector_matches_sample(const args_convert_vector &actual, + const std::vector &expected) { + REQUIRE(actual.size() == expected.size()); + for (size_t ii = 0; ii < actual.size(); ++ii) { + REQUIRE(actual[ii] == expected[ii]); + } +} + +template +void mutation_test_with_samples(ActualMutationFunc actual_mutation_func, + ExpectedMutationFunc expected_mutation_func) { + auto sample_contents = get_sample_vectors>(); + auto samples = get_sample_vectors(); + for (size_t ii = 0; ii < samples.size(); ++ii) { + auto &actual = samples[ii]; + auto &expected = sample_contents[ii]; + + actual_mutation_func(actual); + expected_mutation_func(expected); + require_vector_matches_sample(actual, expected); + } +} +} // namespace + +// I would like to write [capture](auto& vec) block inline, but we +// have to work with C++11, which doesn't have generic lambdas. +// NOLINTBEGIN(bugprone-macro-parentheses) +#define MUTATION_LAMBDA(capture, block) \ + [capture](args_convert_vector & vec) block, [capture](std::vector & vec) block +// NOLINTEND(bugprone-macro-parentheses) + +// For readability, rather than having ugly empty arguments. +#define NO_CAPTURE + +TEST_CASE("check sample args_convert_vector contents") { + mutation_test_with_samples(MUTATION_LAMBDA(NO_CAPTURE, { (void) vec; })); +} + +TEST_CASE("args_convert_vector push_back") { + for (const bool b : {false, true}) { + mutation_test_with_samples(MUTATION_LAMBDA(b, { vec.push_back(b); })); + } +} + +TEST_CASE("args_convert_vector reserve") { + for (std::size_t ii = 0; ii < 4; ++ii) { + mutation_test_with_samples(MUTATION_LAMBDA(ii, { vec.reserve(ii); })); + } +} + +TEST_CASE("args_convert_vector reserve then push_back") { + for (std::size_t ii = 0; ii < 4; ++ii) { + for (const bool b : {false, true}) { + mutation_test_with_samples(MUTATION_LAMBDA(=, { + vec.reserve(ii); + vec.push_back(b); + })); + } + } +} diff --git a/tests/test_with_catch/test_argument_vector.cpp b/tests/test_with_catch/test_argument_vector.cpp new file mode 100644 index 000000000..9cf302a9b --- /dev/null +++ b/tests/test_with_catch/test_argument_vector.cpp @@ -0,0 +1,94 @@ +#include "pybind11/pybind11.h" +#include "catch.hpp" + +namespace py = pybind11; + +// 2 is chosen because it is the smallest number (keeping tests short) +// where we can create non-empty vectors whose size is the inline size +// plus or minus 1. +using argument_vector = py::detail::argument_vector<2>; + +namespace { +argument_vector to_argument_vector(const std::vector &v) { + argument_vector result; + result.reserve(v.size()); + for (const auto x : v) { + result.push_back(x); + } + return result; +} + +std::vector> get_sample_argument_vector_contents() { + return std::vector>{ + {}, + {py::handle(Py_None)}, + {py::handle(Py_None), py::handle(Py_False)}, + {py::handle(Py_None), py::handle(Py_False), py::handle(Py_True)}, + }; +} + +std::vector get_sample_argument_vectors() { + std::vector result; + for (const auto &vec : get_sample_argument_vector_contents()) { + result.push_back(to_argument_vector(vec)); + } + return result; +} + +void require_vector_matches_sample(const argument_vector &actual, + const std::vector &expected) { + REQUIRE(actual.size() == expected.size()); + for (size_t ii = 0; ii < actual.size(); ++ii) { + REQUIRE(actual[ii].ptr() == expected[ii].ptr()); + } +} + +template +void mutation_test_with_samples(ActualMutationFunc actual_mutation_func, + ExpectedMutationFunc expected_mutation_func) { + auto sample_contents = get_sample_argument_vector_contents(); + auto samples = get_sample_argument_vectors(); + for (size_t ii = 0; ii < samples.size(); ++ii) { + auto &actual = samples[ii]; + auto &expected = sample_contents[ii]; + + actual_mutation_func(actual); + expected_mutation_func(expected); + require_vector_matches_sample(actual, expected); + } +} + +} // namespace + +// I would like to write [capture](auto& vec) block inline, but we +// have to work with C++11, which doesn't have generic lambdas. +// NOLINTBEGIN(bugprone-macro-parentheses) +#define MUTATION_LAMBDA(capture, block) \ + [capture](argument_vector & vec) block, [capture](std::vector & vec) block +// NOLINTEND(bugprone-macro-parentheses) + +// For readability, rather than having ugly empty arguments. +#define NO_CAPTURE + +TEST_CASE("check sample argument_vector contents") { + mutation_test_with_samples(MUTATION_LAMBDA(NO_CAPTURE, { (void) vec; })); +} + +TEST_CASE("argument_vector push_back") { + mutation_test_with_samples(MUTATION_LAMBDA(NO_CAPTURE, { vec.emplace_back(Py_None); })); +} + +TEST_CASE("argument_vector reserve") { + for (std::size_t ii = 0; ii < 4; ++ii) { + mutation_test_with_samples(MUTATION_LAMBDA(ii, { vec.reserve(ii); })); + } +} + +TEST_CASE("argument_vector reserve then push_back") { + for (std::size_t ii = 0; ii < 4; ++ii) { + mutation_test_with_samples(MUTATION_LAMBDA(ii, { + vec.reserve(ii); + vec.emplace_back(Py_True); + })); + } +} diff --git a/tests/test_embed/test_interpreter.cpp b/tests/test_with_catch/test_interpreter.cpp similarity index 99% rename from tests/test_embed/test_interpreter.cpp rename to tests/test_with_catch/test_interpreter.cpp index 0e6c17a77..d227eecdd 100644 --- a/tests/test_embed/test_interpreter.cpp +++ b/tests/test_with_catch/test_interpreter.cpp @@ -94,8 +94,9 @@ PYBIND11_EMBEDDED_MODULE(throw_error_already_set, ) { TEST_CASE("PYTHONPATH is used to update sys.path") { // The setup for this TEST_CASE is in catch.cpp! auto sys_path = py::str(py::module_::import("sys").attr("path")).cast(); - REQUIRE_THAT(sys_path, - Catch::Matchers::Contains("pybind11_test_embed_PYTHONPATH_2099743835476552")); + REQUIRE_THAT( + sys_path, + Catch::Matchers::Contains("pybind11_test_with_catch_PYTHONPATH_2099743835476552")); } TEST_CASE("Pass classes and data between modules defined in C++ and Python") { diff --git a/tests/test_embed/test_interpreter.py b/tests/test_with_catch/test_interpreter.py similarity index 100% rename from tests/test_embed/test_interpreter.py rename to tests/test_with_catch/test_interpreter.py diff --git a/tests/test_embed/test_subinterpreter.cpp b/tests/test_with_catch/test_subinterpreter.cpp similarity index 100% rename from tests/test_embed/test_subinterpreter.cpp rename to tests/test_with_catch/test_subinterpreter.cpp diff --git a/tests/test_embed/test_trampoline.py b/tests/test_with_catch/test_trampoline.py similarity index 100% rename from tests/test_embed/test_trampoline.py rename to tests/test_with_catch/test_trampoline.py From 8ed0dab67f1f26530297ebc9885b56ac69fd0d09 Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Sat, 27 Sep 2025 17:13:21 +0100 Subject: [PATCH 16/47] Add float type caster and revert type hint changes to int_ and float_ (#5839) * Revert type hint changes to int_ and float_ These two types do not support casting from int-like and float-like types. * Fix tests * Add a custom py::float_ caster The default py::object caster only works if the object is an instance of the type. py::float_ should accept python int objects as well as float. This caster will pass through float as usual and cast int to float. The caster handles the type name so the custom one is not required. * style: pre-commit fixes * Fix name * Fix variable * Try satisfying the formatter * Rename test function * Simplify type caster * Fix reference counting issue --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- include/pybind11/cast.h | 19 +++++++++++++++++-- tests/test_pytypes.cpp | 1 + tests/test_pytypes.py | 13 ++++++++++--- 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/include/pybind11/cast.h b/include/pybind11/cast.h index 4dcbf2623..7b014fed9 100644 --- a/include/pybind11/cast.h +++ b/include/pybind11/cast.h @@ -1401,7 +1401,7 @@ struct handle_type_name { }; template <> struct handle_type_name { - static constexpr auto name = io_name("typing.SupportsInt", "int"); + static constexpr auto name = const_name("int"); }; template <> struct handle_type_name { @@ -1413,7 +1413,7 @@ struct handle_type_name { }; template <> struct handle_type_name { - static constexpr auto name = io_name("typing.SupportsFloat", "float"); + static constexpr auto name = const_name("float"); }; template <> struct handle_type_name { @@ -1534,6 +1534,21 @@ struct pyobject_caster { template class type_caster::value>> : public pyobject_caster {}; +template <> +class type_caster : public pyobject_caster { +public: + bool load(handle src, bool /* convert */) { + if (isinstance(src)) { + value = reinterpret_borrow(src); + } else if (isinstance(src)) { + value = float_(reinterpret_borrow(src)); + } else { + return false; + } + return true; + } +}; + // Our conditions for enabling moving are quite restrictive: // At compile time: // - T needs to be a non-const, non-pointer, non-reference type diff --git a/tests/test_pytypes.cpp b/tests/test_pytypes.cpp index 1c136cf0f..7d5423e54 100644 --- a/tests/test_pytypes.cpp +++ b/tests/test_pytypes.cpp @@ -209,6 +209,7 @@ TEST_SUBMODULE(pytypes, m) { m.def("get_tuple_from_iterable", [](const py::iterable &iter) { return py::tuple(iter); }); // test_float m.def("get_float", [] { return py::float_(0.0f); }); + m.def("float_roundtrip", [](py::float_ f) { return f; }); // test_list m.def("list_no_args", []() { return py::list{}; }); m.def("list_ssize_t", []() { return py::list{(py::ssize_t) 0}; }); diff --git a/tests/test_pytypes.py b/tests/test_pytypes.py index a199d72f0..c1798f924 100644 --- a/tests/test_pytypes.py +++ b/tests/test_pytypes.py @@ -61,6 +61,13 @@ def test_iterable(doc): def test_float(doc): assert doc(m.get_float) == "get_float() -> float" + assert doc(m.float_roundtrip) == "float_roundtrip(arg0: float) -> float" + f1 = m.float_roundtrip(5.5) + assert isinstance(f1, float) + assert f1 == 5.5 + f2 = m.float_roundtrip(5) + assert isinstance(f2, float) + assert f2 == 5.0 def test_list(capture, doc): @@ -917,7 +924,7 @@ def test_inplace_rshift(a, b): def test_tuple_nonempty_annotations(doc): assert ( doc(m.annotate_tuple_float_str) - == "annotate_tuple_float_str(arg0: tuple[typing.SupportsFloat, str]) -> None" + == "annotate_tuple_float_str(arg0: tuple[float, str]) -> None" ) @@ -930,7 +937,7 @@ def test_tuple_empty_annotations(doc): def test_tuple_variable_length_annotations(doc): assert ( doc(m.annotate_tuple_variable_length) - == "annotate_tuple_variable_length(arg0: tuple[typing.SupportsFloat, ...]) -> None" + == "annotate_tuple_variable_length(arg0: tuple[float, ...]) -> None" ) @@ -989,7 +996,7 @@ def test_type_annotation(doc): def test_union_annotations(doc): assert ( doc(m.annotate_union) - == "annotate_union(arg0: list[str | typing.SupportsInt | object], arg1: str, arg2: typing.SupportsInt, arg3: object) -> list[str | int | object]" + == "annotate_union(arg0: list[str | int | object], arg1: str, arg2: int, arg3: object) -> list[str | int | object]" ) From 81ffb1d5cc72f242daa9c410d5ebdbd7d45dbd96 Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Sat, 27 Sep 2025 20:53:47 +0100 Subject: [PATCH 17/47] Add 90 minute limit for tests (#5851) Occasionally a test will get stuck and run for 6 hours until Github cancels the workflow. This reduces the timeout to 90 minutes to not waste resources. Pybind11's tests seem to run in 30 minutes so this should be plenty of time. --- .github/workflows/ci.yml | 15 +++++++++++++++ .github/workflows/reusable-standard.yml | 1 + 2 files changed, 16 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9798f3aee..460cd82e6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -179,6 +179,7 @@ jobs: name: "🐍 ${{ matrix.python-version }} β€’ ${{ matrix.runs-on }} β€’ x64 inplace C++14" runs-on: ${{ matrix.runs-on }} + timeout-minutes: 90 steps: - uses: actions/checkout@v5 @@ -277,6 +278,7 @@ jobs: name: "🐍 ${{ matrix.python-version }}${{ matrix.python-debug && '-dbg' || '' }} (deadsnakes)${{ matrix.valgrind && ' β€’ Valgrind' || '' }} β€’ x64" runs-on: ubuntu-latest + timeout-minutes: 90 steps: - uses: actions/checkout@v5 @@ -364,6 +366,7 @@ jobs: name: "🐍 3 β€’ Clang ${{ matrix.clang }} β€’ C++${{ matrix.std }} β€’ x64${{ matrix.cxx_flags && ' β€’ cxx_flags' || '' }}" container: "silkeh/clang:${{ matrix.clang }}${{ matrix.container_suffix }}" + timeout-minutes: 90 steps: - uses: actions/checkout@v5 @@ -401,6 +404,7 @@ jobs: runs-on: ubuntu-latest name: "🐍 3.10 β€’ CUDA 12.2 β€’ Ubuntu 22.04" container: nvidia/cuda:12.2.0-devel-ubuntu22.04 + timeout-minutes: 90 steps: - uses: actions/checkout@v5 @@ -468,6 +472,7 @@ jobs: if: github.event.pull_request.draft == false runs-on: ubuntu-22.04 name: "🐍 3 β€’ NVHPC 23.5 β€’ C++17 β€’ x64" + timeout-minutes: 90 env: # tzdata will try to ask for the timezone, so set the DEBIAN_FRONTEND @@ -532,6 +537,7 @@ jobs: name: "🐍 3 β€’ GCC ${{ matrix.gcc }} β€’ C++${{ matrix.std }} β€’ x64${{ matrix.cxx_flags && ' β€’ cxx_flags' || '' }}" container: "gcc:${{ matrix.gcc }}" + timeout-minutes: 90 steps: - uses: actions/checkout@v5 @@ -593,6 +599,7 @@ jobs: icc: if: github.event.pull_request.draft == false runs-on: ubuntu-22.04 + timeout-minutes: 90 name: "🐍 3 β€’ ICC latest β€’ x64" @@ -707,6 +714,7 @@ jobs: name: "🐍 3 β€’ ${{ matrix.container }} β€’ x64" container: "${{ matrix.container }}" + timeout-minutes: 90 steps: - name: Latest actions/checkout @@ -765,6 +773,7 @@ jobs: name: "🐍 3.9 β€’ Debian β€’ x86 β€’ Install" runs-on: ubuntu-latest container: i386/debian:bullseye + timeout-minutes: 90 steps: - uses: actions/checkout@v1 # v1 is required to run inside docker @@ -809,6 +818,7 @@ jobs: if: github.event.pull_request.draft == false name: "Documentation build test" runs-on: ubuntu-latest + timeout-minutes: 90 steps: - uses: actions/checkout@v5 @@ -855,6 +865,7 @@ jobs: name: "🐍 ${{ matrix.python }} β€’ MSVC 2022 β€’ x86 ${{ matrix.args }}" runs-on: windows-2022 + timeout-minutes: 90 steps: - uses: actions/checkout@v5 @@ -907,6 +918,7 @@ jobs: name: "🐍 ${{ matrix.python }} β€’ MSVC 2022 (Debug) β€’ x86 ${{ matrix.args }}" runs-on: windows-2022 + timeout-minutes: 90 steps: - uses: actions/checkout@v5 @@ -955,6 +967,7 @@ jobs: name: "🐍 ${{ matrix.python }} β€’ MSVC 2022 C++20 β€’ x64" runs-on: windows-2022 + timeout-minutes: 90 steps: - uses: actions/checkout@v5 @@ -1012,6 +1025,7 @@ jobs: if: github.event.pull_request.draft == false name: "🐍 3 β€’ windows-latest β€’ ${{ matrix.sys }}" runs-on: windows-latest + timeout-minutes: 90 defaults: run: shell: msys2 {0} @@ -1125,6 +1139,7 @@ jobs: python: ['3.10'] runs-on: "${{ matrix.os }}" + timeout-minutes: 90 name: "🐍 ${{ matrix.python }} β€’ ${{ matrix.os }} β€’ clang-latest" diff --git a/.github/workflows/reusable-standard.yml b/.github/workflows/reusable-standard.yml index 36cad8d5a..8bd0d340e 100644 --- a/.github/workflows/reusable-standard.yml +++ b/.github/workflows/reusable-standard.yml @@ -28,6 +28,7 @@ jobs: standard: name: πŸ§ͺ runs-on: ${{ inputs.runs-on }} + timeout-minutes: 90 steps: - uses: actions/checkout@v5 From 0161da9d6d87febef30d74905c78e04edccb50b1 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sat, 27 Sep 2025 13:12:56 -0700 Subject: [PATCH 18/47] [skip ci] .gitignore: exclude __pycache__ directories (#5838) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Python may leave behind temporary `.pyc.*` files inside `__pycache__` on some filesystems (e.g. WSL2 mounts). Adding `__pycache__/` ensures these directories and any leftover files are consistently ignored. Background: Python writes bytecode to a temp file with an extra suffix before renaming it to `.pyc`. If the process is interrupted or the filesystem rename isn’t fully atomic, those temp files may remain. See: https://docs.python.org/3/library/py_compile.html#py_compile.compile --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 5875be36a..b007bb19a 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,7 @@ MANIFEST /.ninja_* /*.ninja /docs/.build +__pycache__/ *.py[co] *.egg-info *~ From 4dc33d652478e11a1e9b8ba6558d2fbd04a1bee0 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Wed, 1 Oct 2025 11:21:47 -0700 Subject: [PATCH 19/47] Fix `smart_holder` multiple/virtual inheritance bugs in `shared_ptr` and `unique_ptr` to-Python conversions (#5836) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ChatGPT-generated diamond virtual-inheritance test case. * Report "virtual base at offset 0" but don't skip test. * Remove Left/Right virtual default dtors, to resolve clang-tidy errors: ``` /__w/pybind11/pybind11/tests/test_class_sh_mi_thunks.cpp:44:13: error: prefer using 'override' or (rarely) 'final' instead of 'virtual' [modernize-use-override,-warnings-as-errors] 44 | virtual ~Left() = default; | ~~~~~~~ ^ | override /__w/pybind11/pybind11/tests/test_class_sh_mi_thunks.cpp:48:13: error: prefer using 'override' or (rarely) 'final' instead of 'virtual' [modernize-use-override,-warnings-as-errors] 48 | virtual ~Right() = default; | ~~~~~~~ ^ | override ``` * Add assert(ptr) in register_instance_impl, deregister_instance_impl * Proper bug fix * Also exercise smart_holder_from_unique_ptr * [skip ci] ChatGPT-generated bug fix: smart_holder::from_unique_ptr() * Exception-safe ownership transfer from unique_ptr to shared_ptr ChatGPT: * shared_ptr’s ctor can throw (control-block alloc). Using get() keeps unique_ptr owning the memory if that happens, so no leak. * Only after the shared_ptr is successfully constructed do you release(), transferring ownership exactly once. * [skip ci] Rename alias_ptr to mi_subobject_ptr to distinguish from trampoline code (which often uses the term "alias", too) * [skip ci] Also exercise smart_holder::from_raw_ptr_take_ownership * [skip ci] Add st.first comments (generated by ChatGPT) * [skip ci] Copy and extend (raw_ptr, unique_ptr) reproducer from PR #5796 * Some polishing: comments, add back Left/Right dtors for consistency within test_class_sh_mi_thunks.cpp * explicitly default copy/move for VBase to silence -Wdeprecated-copy-with-dtor * Resolve clang-tidy error: ``` /__w/pybind11/pybind11/tests/test_class_sh_mi_thunks.cpp:67:5: error: 'auto ptr' can be declared as 'auto *ptr' [readability-qualified-auto,-warnings-as-errors] 67 | auto ptr = new Diamond; | ^~~~ | auto * ``` * Expand comment in `smart_holder::from_unique_ptr()` * Better Left/Right padding to make it more likely that we avoid "all at offset 0". Clarify comment. * Give up on `alignas(16)` to resolve MSVC warning: ``` "D:\a\pybind11\pybind11\build\ALL_BUILD.vcxproj" (default target) (1) -> "D:\a\pybind11\pybind11\build\tests\pybind11_tests.vcxproj" (default target) (13) -> (ClCompile target) -> D:\a\pybind11\pybind11\tests\test_class_sh_mi_thunks.cpp(70,17): warning C4316: 'test_class_sh_mi_thunks::Diamond': object allocated on the heap may not be aligned 16 [D:\a\pybind11\pybind11\build\tests\pybind11_tests.vcxproj] D:\a\pybind11\pybind11\tests\test_class_sh_mi_thunks.cpp(80,43): warning C4316: 'test_class_sh_mi_thunks::Diamond': object allocated on the heap may not be aligned 16 [D:\a\pybind11\pybind11\build\tests\pybind11_tests.vcxproj] C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Tools\MSVC\14.44.35207\include\memory(2913,46): warning C4316: 'std::_Ref_count_obj2<_Ty>': object allocated on the heap may not be aligned 16 [D:\a\pybind11\pybind11\build\tests\pybind11_tests.vcxproj] C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Tools\MSVC\14.44.35207\include\memory(2913,46): warning C4316: with [D:\a\pybind11\pybind11\build\tests\pybind11_tests.vcxproj] C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Tools\MSVC\14.44.35207\include\memory(2913,46): warning C4316: [ [D:\a\pybind11\pybind11\build\tests\pybind11_tests.vcxproj] C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Tools\MSVC\14.44.35207\include\memory(2913,46): warning C4316: _Ty=test_class_sh_mi_thunks::Diamond [D:\a\pybind11\pybind11\build\tests\pybind11_tests.vcxproj] C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Tools\MSVC\14.44.35207\include\memory(2913,46): warning C4316: ] [D:\a\pybind11\pybind11\build\tests\pybind11_tests.vcxproj] D:\a\pybind11\pybind11\include\pybind11\detail\init.h(77,21): warning C4316: 'test_class_sh_mi_thunks::Diamond': object allocated on the heap may not be aligned 16 [D:\a\pybind11\pybind11\build\tests\pybind11_tests.vcxproj] ``` The warning came from alignas(16) making Diamond over-aligned, while regular new/make_shared aren’t guaranteed to return 16-byte aligned memory on MSVC (hence C4316). I’ve removed the explicit alignment and switched to asymmetric payload sizes (char[4] vs char[24]), which still nudges MI layout without relying on over-alignment. This keeps the test goal and eliminates the warning across all MSVC builds. If we ever want to stress over-alignment explicitly, we can add aligned operator new/delete under __cpp_aligned_new, but that’s more than we need here. * Rename test_virtual_base_at_offset_0() β†’ test_virtual_base_not_at_offset_0() and replace pytest.skip() with assert. Add helpful comment for future maintainers. --- include/pybind11/detail/class.h | 2 + include/pybind11/detail/struct_smart_holder.h | 44 ++++-- include/pybind11/detail/type_caster_base.h | 10 +- tests/test_class_sh_mi_thunks.cpp | 137 ++++++++++++++++++ tests/test_class_sh_mi_thunks.py | 51 +++++++ 5 files changed, 228 insertions(+), 16 deletions(-) diff --git a/include/pybind11/detail/class.h b/include/pybind11/detail/class.h index cd7e87f84..64e371db4 100644 --- a/include/pybind11/detail/class.h +++ b/include/pybind11/detail/class.h @@ -334,6 +334,7 @@ inline void enable_try_inc_ref(PyObject *obj) { #endif inline bool register_instance_impl(void *ptr, instance *self) { + assert(ptr); #ifdef Py_GIL_DISABLED enable_try_inc_ref(reinterpret_cast(self)); #endif @@ -341,6 +342,7 @@ inline bool register_instance_impl(void *ptr, instance *self) { return true; // unused, but gives the same signature as the deregister func } inline bool deregister_instance_impl(void *ptr, instance *self) { + assert(ptr); return with_instance_map(ptr, [&](instance_map &instances) { auto range = instances.equal_range(ptr); for (auto it = range.first; it != range.second; ++it) { diff --git a/include/pybind11/detail/struct_smart_holder.h b/include/pybind11/detail/struct_smart_holder.h index 5b65b4a9b..5d7c31cda 100644 --- a/include/pybind11/detail/struct_smart_holder.h +++ b/include/pybind11/detail/struct_smart_holder.h @@ -339,22 +339,42 @@ struct smart_holder { template static smart_holder from_unique_ptr(std::unique_ptr &&unq_ptr, - void *void_ptr = nullptr) { + void *mi_subobject_ptr = nullptr) { smart_holder hld; hld.rtti_uqp_del = &typeid(D); hld.vptr_is_using_std_default_delete = uqp_del_is_std_default_delete(); - guarded_delete gd{nullptr, false}; - if (hld.vptr_is_using_std_default_delete) { - gd = make_guarded_std_default_delete(true); - } else { - gd = make_guarded_custom_deleter(std::move(unq_ptr.get_deleter()), true); - } - if (void_ptr != nullptr) { - hld.vptr.reset(void_ptr, std::move(gd)); - } else { - hld.vptr.reset(unq_ptr.get(), std::move(gd)); - } + + // Build the owning control block on the *real object start* (T*). + guarded_delete gd + = hld.vptr_is_using_std_default_delete + ? make_guarded_std_default_delete(true) + : make_guarded_custom_deleter(std::move(unq_ptr.get_deleter()), true); + // Critical: construct owner with pointer we intend to delete + std::shared_ptr owner(unq_ptr.get(), std::move(gd)); + // Relinquish ownership only after successful construction of owner (void) unq_ptr.release(); + + // Publish either the MI/VI subobject pointer (if provided) or the full object. + // Why this is needed: + // * The `owner` shared_ptr must always manage the true object start (T*). + // That ensures the deleter is invoked on a valid object header, so the + // virtual destructor can dispatch safely (critical on MSVC with virtual + // inheritance, where base subobjects are not at offset 0). + // * However, pybind11 needs to *register* and expose the subobject pointer + // appropriate for the type being bound. + // This pointer may differ from the T* object start under multiple/virtual + // inheritance. + // This is achieved by using an aliasing shared_ptr: + // - `owner` retains lifetime of the actual T* object start for deletion. + // - `vptr` points at the adjusted subobject (mi_subobject_ptr), giving + // Python the correct identity/registration address. + // If no subobject pointer is passed, we simply publish the full object. + if (mi_subobject_ptr) { + hld.vptr = std::shared_ptr(owner, mi_subobject_ptr); + } else { + hld.vptr = std::static_pointer_cast(owner); + } + hld.is_populated = true; return hld; } diff --git a/include/pybind11/detail/type_caster_base.h b/include/pybind11/detail/type_caster_base.h index 23d940735..79346cf6a 100644 --- a/include/pybind11/detail/type_caster_base.h +++ b/include/pybind11/detail/type_caster_base.h @@ -576,6 +576,8 @@ handle smart_holder_from_unique_ptr(std::unique_ptr &&src, if (!src) { return none().release(); } + // st.first is the subobject pointer appropriate for tinfo (may differ from src.get() + // under MI/VI). Use this for Python identity/registration, but keep ownership on T*. void *src_raw_void_ptr = const_cast(st.first); assert(st.second != nullptr); const detail::type_info *tinfo = st.second; @@ -657,9 +659,10 @@ handle smart_holder_from_shared_ptr(const std::shared_ptr &src, return none().release(); } - auto src_raw_ptr = src.get(); + // st.first is the subobject pointer appropriate for tinfo (may differ from src.get() + // under MI/VI). Use this for Python identity/registration, but keep ownership on T*. + void *src_raw_void_ptr = const_cast(st.first); assert(st.second != nullptr); - void *src_raw_void_ptr = static_cast(src_raw_ptr); const detail::type_info *tinfo = st.second; if (handle existing_inst = find_registered_python_instance(src_raw_void_ptr, tinfo)) { // PYBIND11:REMINDER: MISSING: Enforcement of consistency with existing smart_holder. @@ -673,8 +676,7 @@ handle smart_holder_from_shared_ptr(const std::shared_ptr &src, void *&valueptr = values_and_holders(inst_raw_ptr).begin()->value_ptr(); valueptr = src_raw_void_ptr; - auto smhldr - = smart_holder::from_shared_ptr(std::shared_ptr(src, const_cast(st.first))); + auto smhldr = smart_holder::from_shared_ptr(std::shared_ptr(src, src_raw_void_ptr)); tinfo->init_instance(inst_raw_ptr, static_cast(&smhldr)); if (policy == return_value_policy::reference_internal) { diff --git a/tests/test_class_sh_mi_thunks.cpp b/tests/test_class_sh_mi_thunks.cpp index d8548ec5c..0d1672cfb 100644 --- a/tests/test_class_sh_mi_thunks.cpp +++ b/tests/test_class_sh_mi_thunks.cpp @@ -10,6 +10,8 @@ namespace test_class_sh_mi_thunks { // C++ vtables - Part 2 - Multiple Inheritance // ... the compiler creates a 'thunk' method that corrects `this` ... +// This test was added under PR #4380 + struct Base0 { virtual ~Base0() = default; Base0() = default; @@ -30,6 +32,110 @@ struct Derived : Base1, Base0 { Derived(const Derived &) = delete; }; +// ChatGPT-generated Diamond added under PR #5836 + +struct VBase { + VBase() = default; + VBase(const VBase &) = default; // silence -Wdeprecated-copy-with-dtor + VBase &operator=(const VBase &) = default; + VBase(VBase &&) = default; + VBase &operator=(VBase &&) = default; + virtual ~VBase() = default; + virtual int ping() const { return 1; } + int vbase_tag = 42; // ensure it's not empty +}; + +// Make the virtual bases non-empty and (likely) differently sized. +// The test does *not* require different sizes; we only want to avoid "all at offset 0". +// If a compiler/ABI still places the virtual base at offset 0, our test logs that via +// test_virtual_base_at_offset_0() and continues. +struct Left : virtual VBase { + char pad_l[4]; // small, typically 4 + padding + ~Left() override = default; +}; +struct Right : virtual VBase { + char pad_r[24]; // larger, to differ from Left + ~Right() override = default; +}; + +struct Diamond : Left, Right { + Diamond() = default; + Diamond(const Diamond &) = default; + ~Diamond() override = default; + int ping() const override { return 7; } + int self_tag = 99; +}; + +VBase *make_diamond_as_vbase_raw_ptr() { + auto *ptr = new Diamond; + return ptr; // upcast +} + +std::shared_ptr make_diamond_as_vbase_shared_ptr() { + auto shptr = std::make_shared(); + return shptr; // upcast +} + +std::unique_ptr make_diamond_as_vbase_unique_ptr() { + auto uqptr = std::unique_ptr(new Diamond); + return uqptr; // upcast +} + +// For diagnostics +struct DiamondAddrs { + uintptr_t as_self; + uintptr_t as_vbase; + uintptr_t as_left; + uintptr_t as_right; +}; + +DiamondAddrs diamond_addrs() { + auto sp = std::make_shared(); + return DiamondAddrs{reinterpret_cast(sp.get()), + reinterpret_cast(static_cast(sp.get())), + reinterpret_cast(static_cast(sp.get())), + reinterpret_cast(static_cast(sp.get()))}; +} + +// Animal-Cat-Tiger reproducer copied from PR #5796 +// clone_raw_ptr, clone_unique_ptr added under PR #5836 + +class Animal { +public: + Animal() = default; + Animal(const Animal &) = default; + Animal &operator=(const Animal &) = default; + virtual Animal *clone_raw_ptr() const = 0; + virtual std::shared_ptr clone_shared_ptr() const = 0; + virtual std::unique_ptr clone_unique_ptr() const = 0; + virtual ~Animal() = default; +}; + +class Cat : virtual public Animal { +public: + Cat() = default; + Cat(const Cat &) = default; + Cat &operator=(const Cat &) = default; + ~Cat() override = default; +}; + +class Tiger : virtual public Cat { +public: + Tiger() = default; + Tiger(const Tiger &) = default; + Tiger &operator=(const Tiger &) = default; + ~Tiger() override = default; + Animal *clone_raw_ptr() const override { + return new Tiger(*this); // upcast + } + std::shared_ptr clone_shared_ptr() const override { + return std::make_shared(*this); // upcast + } + std::unique_ptr clone_unique_ptr() const override { + return std::unique_ptr(new Tiger(*this)); // upcast + } +}; + } // namespace test_class_sh_mi_thunks TEST_SUBMODULE(class_sh_mi_thunks, m) { @@ -90,4 +196,35 @@ TEST_SUBMODULE(class_sh_mi_thunks, m) { } return obj_der->vec.size(); }); + + py::class_(m, "VBase").def("ping", &VBase::ping); + + py::class_(m, "Left"); + py::class_(m, "Right"); + + py::class_(m, "Diamond", py::multiple_inheritance()) + .def(py::init<>()) + .def("ping", &Diamond::ping); + + m.def("make_diamond_as_vbase_raw_ptr", + &make_diamond_as_vbase_raw_ptr, + py::return_value_policy::take_ownership); + m.def("make_diamond_as_vbase_shared_ptr", &make_diamond_as_vbase_shared_ptr); + m.def("make_diamond_as_vbase_unique_ptr", &make_diamond_as_vbase_unique_ptr); + + py::class_(m, "DiamondAddrs") + .def_readonly("as_self", &DiamondAddrs::as_self) + .def_readonly("as_vbase", &DiamondAddrs::as_vbase) + .def_readonly("as_left", &DiamondAddrs::as_left) + .def_readonly("as_right", &DiamondAddrs::as_right); + + m.def("diamond_addrs", &diamond_addrs); + + py::classh(m, "Animal"); + py::classh(m, "Cat"); + py::classh(m, "Tiger", py::multiple_inheritance()) + .def(py::init<>()) + .def("clone_raw_ptr", &Tiger::clone_raw_ptr) + .def("clone_shared_ptr", &Tiger::clone_shared_ptr) + .def("clone_unique_ptr", &Tiger::clone_unique_ptr); } diff --git a/tests/test_class_sh_mi_thunks.py b/tests/test_class_sh_mi_thunks.py index 32bf47554..7fa164d48 100644 --- a/tests/test_class_sh_mi_thunks.py +++ b/tests/test_class_sh_mi_thunks.py @@ -51,3 +51,54 @@ def test_get_shared_vec_size_unique(): assert ( str(exc_info.value) == "Cannot disown external shared_ptr (load_as_unique_ptr)." ) + + +def test_virtual_base_not_at_offset_0(): + # This test ensures that the Diamond fixture actually exercises a non-zero + # virtual-base subobject offset on our supported platforms/ABIs. + # + # If this assert ever fails on some platform/toolchain, please adjust the + # C++ fixture so the virtual base is *not* at offset 0: + # - Keep VBase non-empty. + # - Make Left and Right non-empty and asymmetrically sized and, if + # needed, nudge with a modest alignment. + # - The goal is to achieve a non-zero address delta between `Diamond*` + # and `static_cast(Diamond*)`. + # + # Rationale: certain smart_holder features are exercised only when the + # registered subobject address differs from the most-derived object start, + # so this check guards test efficacy across compilers. + addrs = m.diamond_addrs() + assert addrs.as_vbase - addrs.as_self != 0, ( + "Diamond VBase at offset 0 on this platform; to ensure test efficacy, " + "tweak fixtures (VBase/Left/Right) to ensure non-zero subobject offset." + ) + + +@pytest.mark.parametrize( + "make_fn", + [ + m.make_diamond_as_vbase_raw_ptr, # exercises smart_holder::from_raw_ptr_take_ownership + m.make_diamond_as_vbase_shared_ptr, # exercises smart_holder_from_shared_ptr + m.make_diamond_as_vbase_unique_ptr, # exercises smart_holder_from_unique_ptr + ], +) +def test_make_diamond_as_vbase(make_fn): + # Added under PR #5836 + vb = make_fn() + assert vb.ping() == 7 + + +@pytest.mark.parametrize( + "clone_fn", + [ + m.Tiger.clone_raw_ptr, + m.Tiger.clone_shared_ptr, + m.Tiger.clone_unique_ptr, + ], +) +def test_animal_cat_tiger(clone_fn): + # Based on Animal-Cat-Tiger reproducer under PR #5796 + tiger = m.Tiger() + cloned = clone_fn(tiger) + assert isinstance(cloned, m.Tiger) From 9ea197627d225f0109589e9fb392b4c6648564fe Mon Sep 17 00:00:00 2001 From: Sam Gross Date: Sun, 5 Oct 2025 12:58:13 -0400 Subject: [PATCH 20/47] Use new 3.14 C APIs when available (#5854) * Use new 3.14 C APIs when available Use the new "unstable" C APIs for the functions added in #5494. * style: pre-commit fixes --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- include/pybind11/detail/class.h | 6 ++++-- include/pybind11/detail/type_caster_base.h | 6 +++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/include/pybind11/detail/class.h b/include/pybind11/detail/class.h index 64e371db4..74b70489c 100644 --- a/include/pybind11/detail/class.h +++ b/include/pybind11/detail/class.h @@ -314,8 +314,9 @@ inline void traverse_offset_bases(void *valueptr, #ifdef Py_GIL_DISABLED inline void enable_try_inc_ref(PyObject *obj) { - // TODO: Replace with PyUnstable_Object_EnableTryIncRef when available. - // See https://github.com/python/cpython/issues/128844 +# if PY_VERSION_HEX >= 0x030E00A4 + PyUnstable_EnableTryIncRef(obj); +# else if (_Py_IsImmortal(obj)) { return; } @@ -330,6 +331,7 @@ inline void enable_try_inc_ref(PyObject *obj) { return; } } +# endif } #endif diff --git a/include/pybind11/detail/type_caster_base.h b/include/pybind11/detail/type_caster_base.h index 79346cf6a..3564bb242 100644 --- a/include/pybind11/detail/type_caster_base.h +++ b/include/pybind11/detail/type_caster_base.h @@ -253,9 +253,9 @@ PYBIND11_NOINLINE handle get_type_handle(const std::type_info &tp, bool throw_if inline bool try_incref(PyObject *obj) { // Tries to increment the reference count of an object if it's not zero. - // TODO: Use PyUnstable_TryIncref when available. - // See https://github.com/python/cpython/issues/128844 -#ifdef Py_GIL_DISABLED +#if defined(Py_GIL_DISABLED) && PY_VERSION_HEX >= 0x030E00A4 + return PyUnstable_TryIncRef(obj); +#elif defined(Py_GIL_DISABLED) // See // https://github.com/python/cpython/blob/d05140f9f77d7dfc753dd1e5ac3a5962aaa03eff/Include/internal/pycore_object.h#L761 uint32_t local = _Py_atomic_load_uint32_relaxed(&obj->ob_ref_local); From 3262000195616afedaf95185f1fe965c7eb82be6 Mon Sep 17 00:00:00 2001 From: Scott Wolchok Date: Sun, 5 Oct 2025 11:07:25 -0700 Subject: [PATCH 21/47] Add fast_type_map, use it authoritatively for local types and as a hint for global types (ABI breaking) (#5842) * Add fast_type_map, use it authoritatively for local types and as a hint for global types nanobind has a similar two-level lookup strategy, added and explained by https://github.com/wjakob/nanobind/commit/b515b1f7f2f4ecc0357818e6201c94a9f4cbfdc2 In this PR I've ported this approach to pybind11. To avoid an ABI break, I've kept the fast maps to the `local_internals`. I think this should be safe because any particular module should see its `local_internals` reset at least as often as the global `internals`, and misses in the fast "hint" map for global types fall back to the global `internals`. Performance seems to have improved. Using my patched fork of pybind11_benchmark (https://github.com/swolchok/pybind11_benchmark/tree/benchmark-updates, specifically commit hash b6613d12607104d547b1c10a8145d1b3e9937266), I run bench.py and observe the MyInt case. Each time, I do 3 runs and just report all 3. master, Mac: 75.9, 76.9, 75.3 nsec/loop this PR, Mac: 73.8, 73.8, 73.6 nsec/loop master, Linux box: 188, 187, 188 nsec/loop this PR, Linux box: 164, 165, 164 nsec/loop Note that the "real" percentage improvement is larger than implied by the above because master does not yet include #5824. * simplify unsafe_reset_local_internals in test * pre-implement PYBIND11_INTERNALS_VERSION 12 * use PYBIND11_INTERNALS_VERSION 12 on Python 3.14 per suggestion * Implement reviewer comments: revert PY_VERSION_HEX change, fix REVIEW comment, add two-level lookup comments. ci.yml coming separately * Use the inplace build to smoke test ABI bump? * [skip ci] Remove "smoke" from comment. This is full testing, just only on a few platforms. --------- Co-authored-by: Ralf W. Grosse-Kunstleve --- .github/workflows/ci.yml | 4 ++- include/pybind11/detail/class.h | 6 +++- include/pybind11/detail/internals.h | 24 +++++++++++++-- include/pybind11/detail/type_caster_base.h | 34 ++++++++++++++++++---- include/pybind11/pybind11.h | 22 ++++++++++---- tests/test_with_catch/external_module.cpp | 17 ++++++++++- 6 files changed, 91 insertions(+), 16 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 460cd82e6..15f031ad6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -203,7 +203,8 @@ jobs: if: runner.os != 'Windows' run: echo "CMAKE_GENERATOR=Ninja" >> "$GITHUB_ENV" - # More-or-less randomly adding a few extra flags here + # More-or-less randomly adding a few extra flags here. + # In particular, use this one to test the next ABI bump (internals version). - name: Configure run: > cmake -S. -B. @@ -213,6 +214,7 @@ jobs: -DDOWNLOAD_CATCH=ON -DDOWNLOAD_EIGEN=ON -DCMAKE_CXX_STANDARD=14 + -DPYBIND11_INTERNALS_VERSION=10000000 # Checks to makes sure defining `_` is allowed # Triggers EHsc missing error on Windows diff --git a/include/pybind11/detail/class.h b/include/pybind11/detail/class.h index 74b70489c..b2ee3cc69 100644 --- a/include/pybind11/detail/class.h +++ b/include/pybind11/detail/class.h @@ -221,10 +221,14 @@ extern "C" inline void pybind11_meta_dealloc(PyObject *obj) { auto tindex = std::type_index(*tinfo->cpptype); internals.direct_conversions.erase(tindex); + auto &local_internals = get_local_internals(); if (tinfo->module_local) { - get_local_internals().registered_types_cpp.erase(tindex); + local_internals.registered_types_cpp.erase(tinfo->cpptype); } else { internals.registered_types_cpp.erase(tindex); +#if PYBIND11_INTERNALS_VERSION >= 12 + internals.registered_types_cpp_fast.erase(tinfo->cpptype); +#endif } internals.registered_types_py.erase(tinfo->type); diff --git a/include/pybind11/detail/internals.h b/include/pybind11/detail/internals.h index d23ee6ec9..ead330d28 100644 --- a/include/pybind11/detail/internals.h +++ b/include/pybind11/detail/internals.h @@ -39,7 +39,6 @@ /// further ABI-incompatible changes may be made before the ABI is officially /// changed to the new version. #ifndef PYBIND11_INTERNALS_VERSION -// REMINDER for next version bump: remove loader_life_support_tls # define PYBIND11_INTERNALS_VERSION 11 #endif @@ -177,6 +176,12 @@ struct type_equal_to { }; #endif +// For now, we don't bother adding a fancy hash for pointers and just +// let the standard library use the identity hash function if that's +// what it wants to do (e.g., as in libstdc++). +template +using fast_type_map = std::unordered_map; + template using type_map = std::unordered_map; @@ -237,6 +242,15 @@ struct internals { pymutex mutex; pymutex exception_translator_mutex; #endif +#if PYBIND11_INTERNALS_VERSION >= 12 + // non-normative but fast "hint" for registered_types_cpp. Meant + // to be used as the first level of a two-level lookup: successful + // lookups are correct, but unsuccessful lookups need to try + // registered_types_cpp and then backfill this map if they find + // anything. + fast_type_map registered_types_cpp_fast; +#endif + // std::type_index -> pybind11's type information type_map registered_types_cpp; // PyTypeObject* -> base type_info(s) @@ -261,7 +275,9 @@ struct internals { PyObject *instance_base = nullptr; // Unused if PYBIND11_SIMPLE_GIL_MANAGEMENT is defined: thread_specific_storage tstate; +#if PYBIND11_INTERNALS_VERSION <= 11 thread_specific_storage loader_life_support_tls; // OBSOLETE (PR #5830) +#endif // Unused if PYBIND11_SIMPLE_GIL_MANAGEMENT is defined: PyInterpreterState *istate = nullptr; @@ -302,7 +318,11 @@ struct internals { // impact any other modules, because the only things accessing the local internals is the // module that contains them. struct local_internals { - type_map registered_types_cpp; + // It should be safe to use fast_type_map here because this entire + // data structure is scoped to our single module, and thus a single + // DSO and single instance of type_info for any particular type. + fast_type_map registered_types_cpp; + std::forward_list registered_exception_translators; PyTypeObject *function_record_py_type = nullptr; }; diff --git a/include/pybind11/detail/type_caster_base.h b/include/pybind11/detail/type_caster_base.h index 3564bb242..a2512a552 100644 --- a/include/pybind11/detail/type_caster_base.h +++ b/include/pybind11/detail/type_caster_base.h @@ -205,21 +205,43 @@ PYBIND11_NOINLINE detail::type_info *get_type_info(PyTypeObject *type) { return bases.front(); } -inline detail::type_info *get_local_type_info(const std::type_index &tp) { - auto &locals = get_local_internals().registered_types_cpp; - auto it = locals.find(tp); +inline detail::type_info *get_local_type_info(const std::type_info &tp) { + const auto &locals = get_local_internals().registered_types_cpp; + auto it = locals.find(&tp); if (it != locals.end()) { return it->second; } return nullptr; } -inline detail::type_info *get_global_type_info(const std::type_index &tp) { +inline detail::type_info *get_global_type_info(const std::type_info &tp) { + // This is a two-level lookup. Hopefully we find the type info in + // registered_types_cpp_fast, but if not we try + // registered_types_cpp and fill registered_types_cpp_fast for + // next time. return with_internals([&](internals &internals) { detail::type_info *type_info = nullptr; +#if PYBIND11_INTERNALS_VERSION >= 12 + auto &fast_types = internals.registered_types_cpp_fast; +#endif auto &types = internals.registered_types_cpp; - auto it = types.find(tp); +#if PYBIND11_INTERNALS_VERSION >= 12 + auto fast_it = fast_types.find(&tp); + if (fast_it != fast_types.end()) { +# ifndef NDEBUG + auto types_it = types.find(std::type_index(tp)); + assert(types_it != types.end()); + assert(types_it->second == fast_it->second); +# endif + return fast_it->second; + } +#endif // PYBIND11_INTERNALS_VERSION >= 12 + + auto it = types.find(std::type_index(tp)); if (it != types.end()) { +#if PYBIND11_INTERNALS_VERSION >= 12 + fast_types.emplace(&tp, it->second); +#endif type_info = it->second; } return type_info; @@ -228,7 +250,7 @@ inline detail::type_info *get_global_type_info(const std::type_index &tp) { /// Return the type info for a given C++ type; on lookup failure can either throw or return /// nullptr. -PYBIND11_NOINLINE detail::type_info *get_type_info(const std::type_index &tp, +PYBIND11_NOINLINE detail::type_info *get_type_info(const std::type_info &tp, bool throw_if_missing = false) { if (auto *ltype = get_local_type_info(tp)) { return ltype; diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h index 515aab141..fcf10f199 100644 --- a/include/pybind11/pybind11.h +++ b/include/pybind11/pybind11.h @@ -1637,10 +1637,14 @@ protected: with_internals([&](internals &internals) { auto tindex = std::type_index(*rec.type); tinfo->direct_conversions = &internals.direct_conversions[tindex]; + auto &local_internals = get_local_internals(); if (rec.module_local) { - get_local_internals().registered_types_cpp[tindex] = tinfo; + local_internals.registered_types_cpp[rec.type] = tinfo; } else { internals.registered_types_cpp[tindex] = tinfo; +#if PYBIND11_INTERNALS_VERSION >= 12 + internals.registered_types_cpp_fast[rec.type] = tinfo; +#endif } PYBIND11_WARNING_PUSH @@ -2138,10 +2142,18 @@ public: if (has_alias) { with_internals([&](internals &internals) { - auto &instances = record.module_local ? get_local_internals().registered_types_cpp - : internals.registered_types_cpp; - instances[std::type_index(typeid(type_alias))] - = instances[std::type_index(typeid(type))]; + auto &local_internals = get_local_internals(); + if (record.module_local) { + local_internals.registered_types_cpp[&typeid(type_alias)] + = local_internals.registered_types_cpp[&typeid(type)]; + } else { + type_info *const val + = internals.registered_types_cpp[std::type_index(typeid(type))]; + internals.registered_types_cpp[std::type_index(typeid(type_alias))] = val; +#if PYBIND11_INTERNALS_VERSION >= 12 + internals.registered_types_cpp_fast[&typeid(type_alias)] = val; +#endif + } }); } def("_pybind11_conduit_v1_", cpp_conduit_method); diff --git a/tests/test_with_catch/external_module.cpp b/tests/test_with_catch/external_module.cpp index 3465e8b37..933ee3a6f 100644 --- a/tests/test_with_catch/external_module.cpp +++ b/tests/test_with_catch/external_module.cpp @@ -6,11 +6,26 @@ namespace py = pybind11; * modules aren't preserved over a finalize/initialize. */ +namespace { +// Compare unsafe_reset_internals_for_single_interpreter in +// test_subinterpreter.cpp. +void unsafe_reset_local_internals() { + // NOTE: This code is NOT SAFE unless the caller guarantees no other threads are alive + // NOTE: This code is tied to the precise implementation of the internals holder + + py::detail::get_local_internals_pp_manager().unref(); + py::detail::get_local_internals(); +} +} // namespace + PYBIND11_MODULE(external_module, m, py::mod_gil_not_used(), py::multiple_interpreters::per_interpreter_gil()) { - + // At least one test ("Single Subinterpreter") wants to reset + // internals. We have separate local internals because we are a + // separate DSO, so ours need to be reset too! + unsafe_reset_local_internals(); class A { public: explicit A(int value) : v{value} {}; From cae4ae083e2ed4acbd28f16e1dbe275fe19a700f Mon Sep 17 00:00:00 2001 From: Joshua Oreman Date: Sat, 11 Oct 2025 12:09:14 -0500 Subject: [PATCH 22/47] docs: clarify to what extent bindings are actually global (#5859) --- docs/advanced/classes.rst | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/docs/advanced/classes.rst b/docs/advanced/classes.rst index 14bfc0bcd..faaba38b8 100644 --- a/docs/advanced/classes.rst +++ b/docs/advanced/classes.rst @@ -972,9 +972,14 @@ Module-local class bindings =========================== When creating a binding for a class, pybind11 by default makes that binding -"global" across modules. What this means is that a type defined in one module -can be returned from any module resulting in the same Python type. For -example, this allows the following: +"global" across modules. What this means is that instances whose type is +defined with a ``py::class_`` statement in one module can be passed to or +returned from a function defined in any other module that is "ABI compatible" +with the first, i.e., that was built with sufficiently similar versions of +pybind11 and of the C++ compiler and C++ standard library. The internal data +structures that pybind11 uses to keep track of its types and instances are +shared just as they would be if everything were in the same module. +For example, this allows the following: .. code-block:: cpp From 1cf0948d345f5339e3f0ceb81f47dec103995d1f Mon Sep 17 00:00:00 2001 From: Joshua Oreman Date: Sat, 11 Oct 2025 12:19:52 -0500 Subject: [PATCH 23/47] Avoid a heap allocation on every legacy py::enum_ load (#5860) --- include/pybind11/cast.h | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/include/pybind11/cast.h b/include/pybind11/cast.h index 7b014fed9..785e27fc9 100644 --- a/include/pybind11/cast.h +++ b/include/pybind11/cast.h @@ -93,37 +93,37 @@ public: if (!underlying_caster.load(src.attr("value"), convert)) { pybind11_fail("native_enum internal consistency failure."); } - value = static_cast(static_cast(underlying_caster)); + native_value = static_cast(static_cast(underlying_caster)); + native_loaded = true; return true; } - if (!pybind11_enum_) { - pybind11_enum_.reset(new type_caster_base()); + + type_caster_base legacy_caster; + if (legacy_caster.load(src, convert)) { + legacy_ptr = static_cast(legacy_caster); + return true; } - return pybind11_enum_->load(src, convert); + return false; } template using cast_op_type = detail::cast_op_type; // NOLINTNEXTLINE(google-explicit-constructor) - operator EnumType *() { - if (!pybind11_enum_) { - return &value; - } - return pybind11_enum_->operator EnumType *(); - } + operator EnumType *() { return native_loaded ? &native_value : legacy_ptr; } // NOLINTNEXTLINE(google-explicit-constructor) operator EnumType &() { - if (!pybind11_enum_) { - return value; + if (!native_loaded && !legacy_ptr) { + throw reference_cast_error(); } - return pybind11_enum_->operator EnumType &(); + return native_loaded ? native_value : *legacy_ptr; } private: - std::unique_ptr> pybind11_enum_; - EnumType value; + EnumType native_value; // if loading a py::native_enum + bool native_loaded = false; + EnumType *legacy_ptr = nullptr; // if loading a py::enum_ }; template From aa4259b4f835177cdd1da81ca1221f77cd84d5c8 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 11 Oct 2025 10:23:50 -0700 Subject: [PATCH 24/47] chore(deps): update pre-commit hooks (#5820) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore(deps): update pre-commit hooks updates: - [github.com/pre-commit/mirrors-clang-format: v20.1.8 β†’ v21.1.2](https://github.com/pre-commit/mirrors-clang-format/compare/v20.1.8...v21.1.2) - [github.com/astral-sh/ruff-pre-commit: v0.12.7 β†’ v0.13.3](https://github.com/astral-sh/ruff-pre-commit/compare/v0.12.7...v0.13.3) - [github.com/pre-commit/mirrors-mypy: v1.17.1 β†’ v1.18.2](https://github.com/pre-commit/mirrors-mypy/compare/v1.17.1...v1.18.2) - [github.com/pre-commit/pre-commit-hooks: v5.0.0 β†’ v6.0.0](https://github.com/pre-commit/pre-commit-hooks/compare/v5.0.0...v6.0.0) - [github.com/adamchainz/blacken-docs: 1.19.1 β†’ 1.20.0](https://github.com/adamchainz/blacken-docs/compare/1.19.1...1.20.0) - [github.com/shellcheck-py/shellcheck-py: v0.10.0.1 β†’ v0.11.0.1](https://github.com/shellcheck-py/shellcheck-py/compare/v0.10.0.1...v0.11.0.1) - [github.com/PyCQA/pylint: v3.3.7 β†’ v3.3.9](https://github.com/PyCQA/pylint/compare/v3.3.7...v3.3.9) - [github.com/python-jsonschema/check-jsonschema: 0.33.2 β†’ 0.34.0](https://github.com/python-jsonschema/check-jsonschema/compare/0.33.2...0.34.0) * style: pre-commit fixes --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 16 ++++++++-------- include/pybind11/detail/common.h | 3 +-- include/pybind11/eigen/tensor.h | 7 +++---- 3 files changed, 12 insertions(+), 14 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8435fbac9..8ffc34ea3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,14 +25,14 @@ repos: # Clang format the codebase automatically - repo: https://github.com/pre-commit/mirrors-clang-format - rev: "v20.1.8" + rev: "v21.1.2" hooks: - id: clang-format types_or: [c++, c, cuda] # Ruff, the Python auto-correcting linter/formatter written in Rust - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.12.7 + rev: v0.13.3 hooks: - id: ruff-check args: ["--fix", "--show-fixes"] @@ -40,7 +40,7 @@ repos: # Check static types with mypy - repo: https://github.com/pre-commit/mirrors-mypy - rev: "v1.17.1" + rev: "v1.18.2" hooks: - id: mypy args: [] @@ -62,7 +62,7 @@ repos: # Standard hooks - repo: https://github.com/pre-commit/pre-commit-hooks - rev: "v5.0.0" + rev: "v6.0.0" hooks: - id: check-added-large-files - id: check-case-conflict @@ -80,7 +80,7 @@ repos: # Also code format the docs - repo: https://github.com/adamchainz/blacken-docs - rev: "1.19.1" + rev: "1.20.0" hooks: - id: blacken-docs additional_dependencies: @@ -120,7 +120,7 @@ repos: # Check for common shell mistakes - repo: https://github.com/shellcheck-py/shellcheck-py - rev: "v0.10.0.1" + rev: "v0.11.0.1" hooks: - id: shellcheck @@ -135,14 +135,14 @@ repos: # PyLint has native support - not always usable, but works for us - repo: https://github.com/PyCQA/pylint - rev: "v3.3.7" + rev: "v3.3.9" hooks: - id: pylint files: ^pybind11 # Check schemas on some of our YAML files - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.33.2 + rev: 0.34.0 hooks: - id: check-readthedocs - id: check-github-workflows diff --git a/include/pybind11/detail/common.h b/include/pybind11/detail/common.h index 22dfc62b4..16952c582 100644 --- a/include/pybind11/detail/common.h +++ b/include/pybind11/detail/common.h @@ -1293,8 +1293,7 @@ template #if defined(_MSC_VER) && _MSC_VER < 1920 // MSVC 2017 constexpr #endif - inline void - silence_unused_warnings(Args &&...) { + inline void silence_unused_warnings(Args &&...) { } // MSVC warning C4100: Unreferenced formal parameter diff --git a/include/pybind11/eigen/tensor.h b/include/pybind11/eigen/tensor.h index 50e8b50b1..e5c844103 100644 --- a/include/pybind11/eigen/tensor.h +++ b/include/pybind11/eigen/tensor.h @@ -124,10 +124,9 @@ struct eigen_tensor_helper< template struct get_tensor_descriptor { static constexpr auto details - = const_name(", \"flags.writeable\"", "") + const_name - < static_cast(Type::Layout) - == static_cast(Eigen::RowMajor) - > (", \"flags.c_contiguous\"", ", \"flags.f_contiguous\""); + = const_name(", \"flags.writeable\"", "") + + const_name(Type::Layout) == static_cast(Eigen::RowMajor)>( + ", \"flags.c_contiguous\"", ", \"flags.f_contiguous\""); static constexpr auto value = const_name("typing.Annotated[") + io_name("numpy.typing.ArrayLike, ", "numpy.typing.NDArray[") From 9f75202191c0b8ed55a14935cd5b7a667926f8cf Mon Sep 17 00:00:00 2001 From: Sam Gross Date: Sun, 12 Oct 2025 17:37:48 -0400 Subject: [PATCH 25/47] Fix thread-safety in `get_local_type_info()` (#5856) Fixes potential thread-safety issues if types are concurrently registered while `get_local_type_info()` is called in free threaded Python. Use the `internals` mutex to also protect `local_internals`. This keeps the locking strategy simpler, and we already follow this pattern in some places, such as `pybind11_meta_dealloc`. --- .../detail/function_record_pyobject.h | 1 + include/pybind11/detail/type_caster_base.h | 55 +++++++++++-------- 2 files changed, 34 insertions(+), 22 deletions(-) diff --git a/include/pybind11/detail/function_record_pyobject.h b/include/pybind11/detail/function_record_pyobject.h index d9c5bad93..694625f89 100644 --- a/include/pybind11/detail/function_record_pyobject.h +++ b/include/pybind11/detail/function_record_pyobject.h @@ -91,6 +91,7 @@ static PyType_Spec function_record_PyType_Spec function_record_PyType_Slots}; inline PyTypeObject *get_function_record_PyTypeObject() { + PYBIND11_LOCK_INTERNALS(get_internals()); PyTypeObject *&py_type_obj = detail::get_local_internals().function_record_py_type; if (!py_type_obj) { PyObject *py_obj = PyType_FromSpec(&function_record_PyType_Spec); diff --git a/include/pybind11/detail/type_caster_base.h b/include/pybind11/detail/type_caster_base.h index a2512a552..a07ef1a89 100644 --- a/include/pybind11/detail/type_caster_base.h +++ b/include/pybind11/detail/type_caster_base.h @@ -205,7 +205,7 @@ PYBIND11_NOINLINE detail::type_info *get_type_info(PyTypeObject *type) { return bases.front(); } -inline detail::type_info *get_local_type_info(const std::type_info &tp) { +inline detail::type_info *get_local_type_info_lock_held(const std::type_info &tp) { const auto &locals = get_local_internals().registered_types_cpp; auto it = locals.find(&tp); if (it != locals.end()) { @@ -214,48 +214,59 @@ inline detail::type_info *get_local_type_info(const std::type_info &tp) { return nullptr; } -inline detail::type_info *get_global_type_info(const std::type_info &tp) { +inline detail::type_info *get_local_type_info(const std::type_info &tp) { + // NB: internals and local_internals share a single mutex + PYBIND11_LOCK_INTERNALS(get_internals()); + return get_local_type_info_lock_held(tp); +} + +inline detail::type_info *get_global_type_info_lock_held(const std::type_info &tp) { // This is a two-level lookup. Hopefully we find the type info in // registered_types_cpp_fast, but if not we try // registered_types_cpp and fill registered_types_cpp_fast for // next time. - return with_internals([&](internals &internals) { - detail::type_info *type_info = nullptr; + detail::type_info *type_info = nullptr; + auto &internals = get_internals(); #if PYBIND11_INTERNALS_VERSION >= 12 - auto &fast_types = internals.registered_types_cpp_fast; + auto &fast_types = internals.registered_types_cpp_fast; #endif - auto &types = internals.registered_types_cpp; + auto &types = internals.registered_types_cpp; #if PYBIND11_INTERNALS_VERSION >= 12 - auto fast_it = fast_types.find(&tp); - if (fast_it != fast_types.end()) { + auto fast_it = fast_types.find(&tp); + if (fast_it != fast_types.end()) { # ifndef NDEBUG - auto types_it = types.find(std::type_index(tp)); - assert(types_it != types.end()); - assert(types_it->second == fast_it->second); + auto types_it = types.find(std::type_index(tp)); + assert(types_it != types.end()); + assert(types_it->second == fast_it->second); # endif - return fast_it->second; - } + return fast_it->second; + } #endif // PYBIND11_INTERNALS_VERSION >= 12 - auto it = types.find(std::type_index(tp)); - if (it != types.end()) { + auto it = types.find(std::type_index(tp)); + if (it != types.end()) { #if PYBIND11_INTERNALS_VERSION >= 12 - fast_types.emplace(&tp, it->second); + fast_types.emplace(&tp, it->second); #endif - type_info = it->second; - } - return type_info; - }); + type_info = it->second; + } + return type_info; +} + +inline detail::type_info *get_global_type_info(const std::type_info &tp) { + PYBIND11_LOCK_INTERNALS(get_internals()); + return get_global_type_info_lock_held(tp); } /// Return the type info for a given C++ type; on lookup failure can either throw or return /// nullptr. PYBIND11_NOINLINE detail::type_info *get_type_info(const std::type_info &tp, bool throw_if_missing = false) { - if (auto *ltype = get_local_type_info(tp)) { + PYBIND11_LOCK_INTERNALS(get_internals()); + if (auto *ltype = get_local_type_info_lock_held(tp)) { return ltype; } - if (auto *gtype = get_global_type_info(tp)) { + if (auto *gtype = get_global_type_info_lock_held(tp)) { return gtype; } From c7b4f66a73d0f8b5c34271fd29913374a6a075eb Mon Sep 17 00:00:00 2001 From: Joshua Oreman Date: Mon, 13 Oct 2025 20:00:28 -0400 Subject: [PATCH 26/47] type_caster_generic: add set_foreign_holder method for subclasses to implement (#5862) * type_caster_generic: add set_foreign_holder method for subclasses to implement * style: pre-commit fixes * Rename try_shared_from_this -> set_via_shared_from_this to avoid confusion against try_get_shared_from_this * Add comment explaining the limits of the test * CI * style: pre-commit fixes * Fixes from code review * style: pre-commit fixes --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .codespell-ignore-lines | 1 + CMakeLists.txt | 1 + include/pybind11/cast.h | 24 +++++- .../detail/holder_caster_foreign_helpers.h | 86 +++++++++++++++++++ include/pybind11/detail/type_caster_base.h | 11 +-- tests/extra_python_package/test_files.py | 1 + tests/local_bindings.h | 15 ++-- tests/pybind11_cross_module_tests.cpp | 4 +- tests/test_local_bindings.cpp | 25 ++++++ tests/test_local_bindings.py | 34 ++++++++ 10 files changed, 186 insertions(+), 16 deletions(-) create mode 100644 include/pybind11/detail/holder_caster_foreign_helpers.h diff --git a/.codespell-ignore-lines b/.codespell-ignore-lines index e8cbf3144..c03cd2f72 100644 --- a/.codespell-ignore-lines +++ b/.codespell-ignore-lines @@ -2,6 +2,7 @@ template auto &this_ = static_cast(*this); if (load_impl(temp, false)) { + return load_impl(src, false); ssize_t nd = 0; auto trivial = broadcast(buffers, nd, shape); auto ndim = (size_t) nd; diff --git a/CMakeLists.txt b/CMakeLists.txt index a6d619bbd..806330393 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -188,6 +188,7 @@ set(PYBIND11_HEADERS include/pybind11/detail/dynamic_raw_ptr_cast_if_possible.h include/pybind11/detail/exception_translation.h include/pybind11/detail/function_record_pyobject.h + include/pybind11/detail/holder_caster_foreign_helpers.h include/pybind11/detail/init.h include/pybind11/detail/internals.h include/pybind11/detail/native_enum_data.h diff --git a/include/pybind11/cast.h b/include/pybind11/cast.h index 785e27fc9..f6e4aed1b 100644 --- a/include/pybind11/cast.h +++ b/include/pybind11/cast.h @@ -13,6 +13,7 @@ #include "detail/argument_vector.h" #include "detail/common.h" #include "detail/descr.h" +#include "detail/holder_caster_foreign_helpers.h" #include "detail/native_enum_data.h" #include "detail/type_caster_base.h" #include "detail/typeid.h" @@ -907,6 +908,10 @@ protected: } } + bool set_foreign_holder(handle src) { + return holder_caster_foreign_helpers::set_foreign_holder(src, (type *) value, &holder); + } + void load_value(value_and_holder &&v_h) { if (v_h.holder_constructed()) { value = v_h.value_ptr(); @@ -977,7 +982,7 @@ public: } explicit operator std::shared_ptr *() { - if (typeinfo->holder_enum_v == detail::holder_enum_t::smart_holder) { + if (sh_load_helper.was_populated) { pybind11_fail("Passing `std::shared_ptr *` from Python to C++ is not supported " "(inherently unsafe)."); } @@ -985,14 +990,14 @@ public: } explicit operator std::shared_ptr &() { - if (typeinfo->holder_enum_v == detail::holder_enum_t::smart_holder) { + if (sh_load_helper.was_populated) { shared_ptr_storage = sh_load_helper.load_as_shared_ptr(typeinfo, value); } return shared_ptr_storage; } std::weak_ptr potentially_slicing_weak_ptr() { - if (typeinfo->holder_enum_v == detail::holder_enum_t::smart_holder) { + if (sh_load_helper.was_populated) { // Reusing shared_ptr code to minimize code complexity. shared_ptr_storage = sh_load_helper.load_as_shared_ptr(typeinfo, @@ -1041,6 +1046,11 @@ protected: } } + bool set_foreign_holder(handle src) { + return holder_caster_foreign_helpers::set_foreign_holder( + src, (type *) value, &shared_ptr_storage); + } + void load_value(value_and_holder &&v_h) { if (typeinfo->holder_enum_v == detail::holder_enum_t::smart_holder) { sh_load_helper.loaded_v_h = v_h; @@ -1078,6 +1088,7 @@ protected: value = cast.second(sub_caster.value); if (typeinfo->holder_enum_v == detail::holder_enum_t::smart_holder) { sh_load_helper.loaded_v_h = sub_caster.sh_load_helper.loaded_v_h; + sh_load_helper.was_populated = true; } else { shared_ptr_storage = std::shared_ptr(sub_caster.shared_ptr_storage, (type *) value); @@ -1224,6 +1235,12 @@ public: return false; } + bool set_foreign_holder(handle) { + throw cast_error("Foreign instance cannot be converted to std::unique_ptr " + "because we don't know how to make it relinquish " + "ownership"); + } + void load_value(value_and_holder &&v_h) { if (typeinfo->holder_enum_v == detail::holder_enum_t::smart_holder) { sh_load_helper.loaded_v_h = v_h; @@ -1282,6 +1299,7 @@ public: value = cast.second(sub_caster.value); if (typeinfo->holder_enum_v == detail::holder_enum_t::smart_holder) { sh_load_helper.loaded_v_h = sub_caster.sh_load_helper.loaded_v_h; + sh_load_helper.was_populated = true; } else { pybind11_fail("Expected to be UNREACHABLE: " __FILE__ ":" PYBIND11_TOSTRING(__LINE__)); diff --git a/include/pybind11/detail/holder_caster_foreign_helpers.h b/include/pybind11/detail/holder_caster_foreign_helpers.h new file mode 100644 index 000000000..f636618e9 --- /dev/null +++ b/include/pybind11/detail/holder_caster_foreign_helpers.h @@ -0,0 +1,86 @@ +/* + pybind11/detail/holder_caster_foreign_helpers.h: Logic to implement + set_foreign_holder() in copyable_ and movable_holder_caster. + + Copyright (c) 2025 Hudson River Trading LLC + + All rights reserved. Use of this source code is governed by a + BSD-style license that can be found in the LICENSE file. +*/ + +#pragma once + +#include + +#include "common.h" + +PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE) +PYBIND11_NAMESPACE_BEGIN(detail) + +struct holder_caster_foreign_helpers { + struct py_deleter { + void operator()(const void *) const noexcept { + // Don't run the deleter if the interpreter has been shut down + if (Py_IsInitialized() == 0) { + return; + } + gil_scoped_acquire guard; + Py_DECREF(o); + } + + PyObject *o; + }; + + template + static auto set_via_shared_from_this(type *value, std::shared_ptr *holder_out) + -> decltype(value->shared_from_this(), bool()) { + // object derives from enable_shared_from_this; + // try to reuse an existing shared_ptr if one is known + if (auto existing = try_get_shared_from_this(value)) { + *holder_out = std::static_pointer_cast(existing); + return true; + } + return false; + } + + template + static bool set_via_shared_from_this(void *, std::shared_ptr *) { + return false; + } + + template + static bool set_foreign_holder(handle src, type *value, std::shared_ptr *holder_out) { + // We only support using std::shared_ptr for foreign T, and + // it's done by creating a new shared_ptr control block that + // owns a reference to the original Python object. + if (value == nullptr) { + *holder_out = {}; + return true; + } + if (set_via_shared_from_this(value, holder_out)) { + return true; + } + *holder_out = std::shared_ptr(value, py_deleter{src.inc_ref().ptr()}); + return true; + } + + template + static bool + set_foreign_holder(handle src, const type *value, std::shared_ptr *holder_out) { + std::shared_ptr holder_mut; + if (set_foreign_holder(src, const_cast(value), &holder_mut)) { + *holder_out = holder_mut; + return true; + } + return false; + } + + template + static bool set_foreign_holder(handle, type *, ...) { + throw cast_error("Unable to cast foreign type to held instance -- " + "only std::shared_ptr is supported in this case"); + } +}; + +PYBIND11_NAMESPACE_END(detail) +PYBIND11_NAMESPACE_END(PYBIND11_NAMESPACE) diff --git a/include/pybind11/detail/type_caster_base.h b/include/pybind11/detail/type_caster_base.h index a07ef1a89..14235ab82 100644 --- a/include/pybind11/detail/type_caster_base.h +++ b/include/pybind11/detail/type_caster_base.h @@ -1069,6 +1069,7 @@ public: return false; } void check_holder_compat() {} + bool set_foreign_holder(handle) { return true; } PYBIND11_NOINLINE static void *local_load(PyObject *src, const type_info *ti) { auto caster = type_caster_generic(ti); @@ -1107,14 +1108,14 @@ public: // logic (without having to resort to virtual inheritance). template PYBIND11_NOINLINE bool load_impl(handle src, bool convert) { + auto &this_ = static_cast(*this); if (!src) { return false; } if (!typeinfo) { - return try_load_foreign_module_local(src); + return try_load_foreign_module_local(src) && this_.set_foreign_holder(src); } - auto &this_ = static_cast(*this); this_.check_holder_compat(); PyTypeObject *srctype = Py_TYPE(src.ptr()); @@ -1180,13 +1181,13 @@ public: if (typeinfo->module_local) { if (auto *gtype = get_global_type_info(*typeinfo->cpptype)) { typeinfo = gtype; - return load(src, false); + return load_impl(src, false); } } // Global typeinfo has precedence over foreign module_local if (try_load_foreign_module_local(src)) { - return true; + return this_.set_foreign_holder(src); } // Custom converters didn't take None, now we convert None to nullptr. @@ -1200,7 +1201,7 @@ public: } if (convert && cpptype && this_.try_cpp_conduit(src)) { - return true; + return this_.set_foreign_holder(src); } return false; diff --git a/tests/extra_python_package/test_files.py b/tests/extra_python_package/test_files.py index d3452aa38..1539b171a 100644 --- a/tests/extra_python_package/test_files.py +++ b/tests/extra_python_package/test_files.py @@ -83,6 +83,7 @@ detail_headers = { "include/pybind11/detail/descr.h", "include/pybind11/detail/dynamic_raw_ptr_cast_if_possible.h", "include/pybind11/detail/function_record_pyobject.h", + "include/pybind11/detail/holder_caster_foreign_helpers.h", "include/pybind11/detail/init.h", "include/pybind11/detail/internals.h", "include/pybind11/detail/native_enum_data.h", diff --git a/tests/local_bindings.h b/tests/local_bindings.h index dea181310..23d46eb26 100644 --- a/tests/local_bindings.h +++ b/tests/local_bindings.h @@ -25,8 +25,9 @@ using MixedLocalGlobal = LocalBase<4>; using MixedGlobalLocal = LocalBase<5>; /// Registered with py::module_local only in the secondary module: -using ExternalType1 = LocalBase<6>; -using ExternalType2 = LocalBase<7>; +using ExternalType1 = LocalBase<6>; // default holder +using ExternalType2 = LocalBase<7>; // held by std::shared_ptr +using ExternalType3 = LocalBase<8>; // held by smart_holder using LocalVec = std::vector; using LocalVec2 = std::vector; @@ -65,11 +66,11 @@ PYBIND11_MAKE_OPAQUE(NonLocalMap) PYBIND11_MAKE_OPAQUE(NonLocalMap2) // Simple bindings (used with the above): -template -py::class_ bind_local(Args &&...args) { - return py::class_(std::forward(args)...).def(py::init()).def("get", [](T &i) { - return i.i + Adjust; - }); +template , typename... Args> +py::class_ bind_local(Args &&...args) { + return py::class_(std::forward(args)...) + .def(py::init()) + .def("get", [](T &i) { return i.i + Adjust; }); } // Simulate a foreign library base class (to match the example in the docs): diff --git a/tests/pybind11_cross_module_tests.cpp b/tests/pybind11_cross_module_tests.cpp index 76f40bfa9..30210194b 100644 --- a/tests/pybind11_cross_module_tests.cpp +++ b/tests/pybind11_cross_module_tests.cpp @@ -26,7 +26,9 @@ PYBIND11_MODULE(pybind11_cross_module_tests, m, py::mod_gil_not_used()) { // test_load_external bind_local(m, "ExternalType1", py::module_local()); - bind_local(m, "ExternalType2", py::module_local()); + bind_local>( + m, "ExternalType2", py::module_local()); + bind_local(m, "ExternalType3", py::module_local()); // test_exceptions.py py::register_local_exception(m, "LocalSimpleException"); diff --git a/tests/test_local_bindings.cpp b/tests/test_local_bindings.cpp index 137367744..5118b25da 100644 --- a/tests/test_local_bindings.cpp +++ b/tests/test_local_bindings.cpp @@ -21,6 +21,31 @@ TEST_SUBMODULE(local_bindings, m) { // test_load_external m.def("load_external1", [](ExternalType1 &e) { return e.i; }); m.def("load_external2", [](ExternalType2 &e) { return e.i; }); + m.def("load_external3", [](ExternalType3 &e) { return e.i; }); + + struct SharedKeepAlive { + std::shared_ptr contents; + int value() const { return contents ? *contents : -20251012; } + long use_count() const { return contents.use_count(); } + }; + py::class_(m, "SharedKeepAlive") + .def_property_readonly("value", &SharedKeepAlive::value) + .def_property_readonly("use_count", &SharedKeepAlive::use_count); + m.def("load_external2_shared", [](const std::shared_ptr &p) { + return SharedKeepAlive{std::shared_ptr(p, &p->i)}; + }); + m.def("load_external3_shared", [](const std::shared_ptr &p) { + return SharedKeepAlive{std::shared_ptr(p, &p->i)}; + }); + m.def("load_external1_unique", [](std::unique_ptr p) { return p->i; }); + m.def("load_external3_unique", [](std::unique_ptr p) { return p->i; }); + + // Aspects of set_foreign_holder that are not covered: + // - loading a foreign instance into a custom holder should fail + // - we're only covering the case where the local module doesn't know + // about the type; the paths where it does (e.g., if both global and + // foreign-module-local bindings exist for the same type) should work + // the same way (they use the same code so they very likely do) // test_local_bindings // Register a class with py::module_local: diff --git a/tests/test_local_bindings.py b/tests/test_local_bindings.py index 57552f7ec..cac89d0da 100644 --- a/tests/test_local_bindings.py +++ b/tests/test_local_bindings.py @@ -1,5 +1,8 @@ from __future__ import annotations +import sys +from contextlib import suppress + import pytest from pybind11_tests import local_bindings as m @@ -11,6 +14,7 @@ def test_load_external(): assert m.load_external1(cm.ExternalType1(11)) == 11 assert m.load_external2(cm.ExternalType2(22)) == 22 + assert m.load_external3(cm.ExternalType3(33)) == 33 with pytest.raises(TypeError) as excinfo: assert m.load_external2(cm.ExternalType1(21)) == 21 @@ -20,6 +24,36 @@ def test_load_external(): assert m.load_external1(cm.ExternalType2(12)) == 12 assert "incompatible function arguments" in str(excinfo.value) + def test_shared(val, ctor, loader): + obj = ctor(val) + with suppress(AttributeError): # non-cpython VMs don't have getrefcount + rc_before = sys.getrefcount(obj) + wrapper = loader(obj) + # wrapper holds a shared_ptr that keeps obj alive + assert wrapper.use_count == 1 + assert wrapper.value == val + with suppress(AttributeError): + rc_after = sys.getrefcount(obj) + assert rc_after > rc_before + + test_shared(220, cm.ExternalType2, m.load_external2_shared) + test_shared(330, cm.ExternalType3, m.load_external3_shared) + + with pytest.raises(TypeError, match="incompatible function arguments"): + test_shared(320, cm.ExternalType2, m.load_external3_shared) + with pytest.raises(TypeError, match="incompatible function arguments"): + test_shared(230, cm.ExternalType3, m.load_external2_shared) + + with pytest.raises( + RuntimeError, match="Foreign instance cannot be converted to std::unique_ptr" + ): + m.load_external1_unique(cm.ExternalType1(2200)) + + with pytest.raises( + RuntimeError, match="Foreign instance cannot be converted to std::unique_ptr" + ): + m.load_external3_unique(cm.ExternalType3(3300)) + def test_local_bindings(): """Tests that duplicate `py::module_local` class bindings work across modules""" From fc423c948a29a9b2b52a65f0e5f406c5aeac64f3 Mon Sep 17 00:00:00 2001 From: Scott Wolchok Date: Tue, 14 Oct 2025 15:52:03 -0700 Subject: [PATCH 27/47] Fix dangling pointer in internals::registered_types_cpp_fast from #5842 (#5867) * Fix dangling pointer in internals::registered_types_cpp_fast from #5842 @oremanj pointed out in a comment on #5842 that I missed part of the nanobind PR I was porting in such a way that we could have dangling pointers in internals::registered_types_cpp_fast. This PR adds a test that reproed the bug and then fixes the test. * review feedback, attempt to fix -Werror in CI * use const ref, skip test on python 3.13 free-threaded * Skip test on 3.13t more robustly * style: pre-commit fixes * CI fix --------- Co-authored-by: Joshua Oreman Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- include/pybind11/detail/class.h | 5 ++ include/pybind11/detail/internals.h | 14 ++++ include/pybind11/detail/type_caster_base.h | 5 ++ tests/CMakeLists.txt | 6 +- tests/pybind11_cross_module_tests.cpp | 12 ++++ ...ss_module_use_after_one_module_dealloc.cpp | 23 +++++++ ...oss_module_use_after_one_module_dealloc.py | 67 +++++++++++++++++++ 7 files changed, 130 insertions(+), 2 deletions(-) create mode 100644 tests/test_class_cross_module_use_after_one_module_dealloc.cpp create mode 100644 tests/test_class_cross_module_use_after_one_module_dealloc.py diff --git a/include/pybind11/detail/class.h b/include/pybind11/detail/class.h index b2ee3cc69..7fe692856 100644 --- a/include/pybind11/detail/class.h +++ b/include/pybind11/detail/class.h @@ -228,6 +228,11 @@ extern "C" inline void pybind11_meta_dealloc(PyObject *obj) { internals.registered_types_cpp.erase(tindex); #if PYBIND11_INTERNALS_VERSION >= 12 internals.registered_types_cpp_fast.erase(tinfo->cpptype); + for (const std::type_info *alias : tinfo->alias_chain) { + auto num_erased = internals.registered_types_cpp_fast.erase(alias); + (void) num_erased; + assert(num_erased > 0); + } #endif } internals.registered_types_py.erase(tinfo->type); diff --git a/include/pybind11/detail/internals.h b/include/pybind11/detail/internals.h index ead330d28..4f8de120f 100644 --- a/include/pybind11/detail/internals.h +++ b/include/pybind11/detail/internals.h @@ -357,6 +357,20 @@ struct type_info { void *get_buffer_data = nullptr; void *(*module_local_load)(PyObject *, const type_info *) = nullptr; holder_enum_t holder_enum_v = holder_enum_t::undefined; + +#if PYBIND11_INTERNALS_VERSION >= 12 + // When a type appears in multiple DSOs, + // internals::registered_types_cpp_fast will have multiple distinct + // keys (the std::type_info from each DSO) mapped to the same + // detail::type_info*. We need to keep track of these aliases so that we clean + // them up when our type is deallocated. A linked list is appropriate + // because it is expected to be 1) usually empty and 2) + // when it's not empty, usually very small. See also `struct + // nb_alias_chain` added in + // https://github.com/wjakob/nanobind/commit/b515b1f7f2f4ecc0357818e6201c94a9f4cbfdc2 + std::forward_list alias_chain; +#endif + /* A simple type never occurs as a (direct or indirect) parent * of a class that makes use of multiple inheritance. * A type can be simple even if it has non-simple ancestors as long as it has no descendants. diff --git a/include/pybind11/detail/type_caster_base.h b/include/pybind11/detail/type_caster_base.h index 14235ab82..c3f1f564e 100644 --- a/include/pybind11/detail/type_caster_base.h +++ b/include/pybind11/detail/type_caster_base.h @@ -246,6 +246,11 @@ inline detail::type_info *get_global_type_info_lock_held(const std::type_info &t auto it = types.find(std::type_index(tp)); if (it != types.end()) { #if PYBIND11_INTERNALS_VERSION >= 12 + // We found the type in the slow map but not the fast one, so + // some other DSO added it (otherwise it would be in the fast + // map under &tp) and therefore we must be an alias. Record + // that. + it->second->alias_chain.push_front(&tp); fast_types.emplace(&tp, it->second); #endif type_info = it->second; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 8ac236d40..47ba4aa86 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -117,6 +117,7 @@ set(PYBIND11_TEST_FILES test_callbacks test_chrono test_class + test_class_cross_module_use_after_one_module_dealloc test_class_release_gil_before_calling_cpp_dtor test_class_sh_basic test_class_sh_disowning @@ -239,8 +240,9 @@ list(SORT PYBIND11_PYTEST_FILES) # Contains the set of test files that require pybind11_cross_module_tests to be # built; if none of these are built (i.e. because TEST_OVERRIDE is used and # doesn't include them) the second module doesn't get built. -tests_extra_targets("test_exceptions.py;test_local_bindings.py;test_stl.py;test_stl_binders.py" - "pybind11_cross_module_tests") +tests_extra_targets( + "test_class_cross_module_use_after_one_module_dealloc.py;test_exceptions.py;test_local_bindings.py;test_stl.py;test_stl_binders.py" + "pybind11_cross_module_tests") # And add additional targets for other tests. tests_extra_targets("test_exceptions.py" "cross_module_interleaved_error_already_set") diff --git a/tests/pybind11_cross_module_tests.cpp b/tests/pybind11_cross_module_tests.cpp index 30210194b..9a00c00dd 100644 --- a/tests/pybind11_cross_module_tests.cpp +++ b/tests/pybind11_cross_module_tests.cpp @@ -16,6 +16,15 @@ #include #include +class CrossDSOClass { +public: + CrossDSOClass() = default; + virtual ~CrossDSOClass(); + CrossDSOClass(const CrossDSOClass &) = default; +}; + +CrossDSOClass::~CrossDSOClass() = default; + PYBIND11_MODULE(pybind11_cross_module_tests, m, py::mod_gil_not_used()) { m.doc() = "pybind11 cross-module test module"; @@ -148,4 +157,7 @@ PYBIND11_MODULE(pybind11_cross_module_tests, m, py::mod_gil_not_used()) { // which appears when this header is missing. m.def("missing_header_arg", [](const std::vector &) {}); m.def("missing_header_return", []() { return std::vector(); }); + + // test_class_cross_module_use_after_one_module_dealloc + m.def("consume_cross_dso_class", [](const CrossDSOClass &) {}); } diff --git a/tests/test_class_cross_module_use_after_one_module_dealloc.cpp b/tests/test_class_cross_module_use_after_one_module_dealloc.cpp new file mode 100644 index 000000000..6f6e62deb --- /dev/null +++ b/tests/test_class_cross_module_use_after_one_module_dealloc.cpp @@ -0,0 +1,23 @@ +#include "pybind11_tests.h" + +#include + +class CrossDSOClass { +public: + CrossDSOClass() = default; + virtual ~CrossDSOClass(); + CrossDSOClass(const CrossDSOClass &) = default; +}; + +CrossDSOClass::~CrossDSOClass() = default; + +struct UnrelatedClass {}; + +TEST_SUBMODULE(class_cross_module_use_after_one_module_dealloc, m) { + m.def("register_and_instantiate_cross_dso_class", [](const py::module_ &m) { + py::class_(m, "CrossDSOClass").def(py::init<>()); + return CrossDSOClass(); + }); + m.def("register_unrelated_class", + [](const py::module_ &m) { py::class_(m, "UnrelatedClass"); }); +} diff --git a/tests/test_class_cross_module_use_after_one_module_dealloc.py b/tests/test_class_cross_module_use_after_one_module_dealloc.py new file mode 100644 index 000000000..ac8a47a8f --- /dev/null +++ b/tests/test_class_cross_module_use_after_one_module_dealloc.py @@ -0,0 +1,67 @@ +from __future__ import annotations + +import gc +import sys +import sysconfig +import types +import weakref + +import pytest + +import env +from pybind11_tests import class_cross_module_use_after_one_module_dealloc as m + +is_python_3_13_free_threaded = ( + env.CPYTHON + and sysconfig.get_config_var("Py_GIL_DISABLED") + and (3, 13) <= sys.version_info < (3, 14) +) + + +def delattr_and_ensure_destroyed(*specs): + wrs = [] + for mod, name in specs: + wrs.append(weakref.ref(getattr(mod, name))) + delattr(mod, name) + + for _ in range(5): + gc.collect() + if all(wr() is None for wr in wrs): + break + else: + pytest.fail( + f"Could not delete bindings such as {next(wr for wr in wrs if wr() is not None)!r}" + ) + + +@pytest.mark.skipif("env.PYPY or env.GRAALPY or is_python_3_13_free_threaded") +def test_cross_module_use_after_one_module_dealloc(): + # This is a regression test for a bug that occurred during development of + # internals::registered_types_cpp_fast (see #5842). registered_types_cpp_fast maps + # &typeid(T) to a raw non-owning pointer to a Python type object. If two DSOs both + # look up the same global type, they will create two separate entries in + # registered_types_cpp_fast, which will look like: + # +=========================================+ + # |&typeid(T) from DSO 1|type object pointer| + # |&typeid(T) from DSO 2|type object pointer| + # +=========================================+ + # + # Then, if the type object is destroyed and we don't take extra steps to clean up + # the table thoroughly, the first row of the table will be cleaned up but the second + # one will contain a dangling pointer to the old type object. Further lookups from + # DSO 2 will then return that dangling pointer, which will cause use-after-frees. + + import pybind11_cross_module_tests as cm + + module_scope = types.ModuleType("module_scope") + instance = m.register_and_instantiate_cross_dso_class(module_scope) + cm.consume_cross_dso_class(instance) + + del instance + delattr_and_ensure_destroyed((module_scope, "CrossDSOClass")) + + # Make sure that CrossDSOClass gets allocated at a different address. + m.register_unrelated_class(module_scope) + + instance = m.register_and_instantiate_cross_dso_class(module_scope) + cm.consume_cross_dso_class(instance) From a2c59711b20707d64b7ad61d488e002611369780 Mon Sep 17 00:00:00 2001 From: Joshua Oreman Date: Tue, 14 Oct 2025 20:42:04 -0400 Subject: [PATCH 28/47] type_caster_generic: add cast_sources abstraction (#5866) * type_caster_generic: add cast_sources abstraction * Respond to code review comments * style: pre-commit fixes --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- include/pybind11/cast.h | 28 +-- include/pybind11/detail/type_caster_base.h | 210 +++++++++++++-------- 2 files changed, 141 insertions(+), 97 deletions(-) diff --git a/include/pybind11/cast.h b/include/pybind11/cast.h index f6e4aed1b..4708101d8 100644 --- a/include/pybind11/cast.h +++ b/include/pybind11/cast.h @@ -1011,15 +1011,12 @@ public: static handle cast(const std::shared_ptr &src, return_value_policy policy, handle parent) { const auto *ptr = src.get(); - auto st = type_caster_base::src_and_type(ptr); - if (st.second == nullptr) { - return handle(); // no type info: error will be set already - } - if (st.second->holder_enum_v == detail::holder_enum_t::smart_holder) { + typename type_caster_base::cast_sources srcs{ptr}; + if (srcs.creates_smart_holder()) { return smart_holder_type_caster_support::smart_holder_from_shared_ptr( - src, policy, parent, st); + src, policy, parent, srcs.result); } - return type_caster_base::cast_holder(ptr, &src); + return type_caster_base::cast_holder(srcs, &src); } // This function will succeed even if the `responsible_parent` does not own the @@ -1195,21 +1192,12 @@ public: static handle cast(std::unique_ptr &&src, return_value_policy policy, handle parent) { auto *ptr = src.get(); - auto st = type_caster_base::src_and_type(ptr); - if (st.second == nullptr) { - return handle(); // no type info: error will be set already - } - if (st.second->holder_enum_v == detail::holder_enum_t::smart_holder) { + typename type_caster_base::cast_sources srcs{ptr}; + if (srcs.creates_smart_holder()) { return smart_holder_type_caster_support::smart_holder_from_unique_ptr( - std::move(src), policy, parent, st); + std::move(src), policy, parent, srcs.result); } - return type_caster_generic::cast(st.first, - return_value_policy::take_ownership, - {}, - st.second, - nullptr, - nullptr, - std::addressof(src)); + return type_caster_base::cast_holder(srcs, &src); } static handle diff --git a/include/pybind11/detail/type_caster_base.h b/include/pybind11/detail/type_caster_base.h index c3f1f564e..cf32401b0 100644 --- a/include/pybind11/detail/type_caster_base.h +++ b/include/pybind11/detail/type_caster_base.h @@ -545,6 +545,74 @@ PYBIND11_NOINLINE handle get_object_handle(const void *ptr, const detail::type_i }); } +// Information about how type_caster_generic::cast() can obtain its source object +struct cast_sources { + // A type-erased pointer and the type it points to + struct raw_source { + const void *cppobj; + const std::type_info *cpptype; + }; + + // A C++ pointer and the Python type info we will convert it to; + // we expect that cppobj points to something of type tinfo->cpptype + struct resolved_source { + const void *cppobj; + const type_info *tinfo; + }; + + // Use the given pointer with its compile-time type, possibly downcast + // via polymorphic_type_hook() + template + cast_sources(const itype *ptr); // NOLINT(google-explicit-constructor) + + // Use the given pointer and type + // NOLINTNEXTLINE(google-explicit-constructor) + cast_sources(const raw_source &orig) : original(orig) { result = resolve(); } + + // Use the given object and pybind11 type info. NB: if tinfo is null, + // this does not provide enough information to use a foreign type or + // to render a useful error message + cast_sources(const void *obj, const detail::type_info *tinfo) + : original{obj, tinfo ? tinfo->cpptype : nullptr}, result{obj, tinfo} {} + + // The object passed to cast(), with its static type. + // original.type must not be null if resolve() will be called. + // original.obj may be null if we're converting nullptr to a Python None + raw_source original; + + // A more-derived version of `original` provided by a + // polymorphic_type_hook. downcast.type may be null if this is not + // a relevant concept for the current cast. + raw_source downcast{}; + + // The source to use for this cast, and the corresponding pybind11 + // type_info. If the type_info is null, then pybind11 doesn't know + // about this type. + resolved_source result; + + // Returns true if the cast will use a pybind11 type that uses + // a smart holder. + bool creates_smart_holder() const { + return result.tinfo != nullptr + && result.tinfo->holder_enum_v == detail::holder_enum_t::smart_holder; + } + +private: + resolved_source resolve() { + if (downcast.cpptype) { + if (same_type(*original.cpptype, *downcast.cpptype)) { + downcast.cpptype = nullptr; + } else if (const auto *tpi = get_type_info(*downcast.cpptype)) { + return {downcast.cppobj, tpi}; + } + } + if (const auto *tpi = get_type_info(*original.cpptype)) { + return {original.cppobj, tpi}; + } + return {nullptr, nullptr}; + } +}; + // Forward declarations void keep_alive_impl(handle nurse, handle patient); inline PyObject *make_new_instance(PyTypeObject *type); @@ -607,18 +675,18 @@ template handle smart_holder_from_unique_ptr(std::unique_ptr &&src, return_value_policy policy, handle parent, - const std::pair &st) { + const cast_sources::resolved_source &cs) { if (policy == return_value_policy::copy) { throw cast_error("return_value_policy::copy is invalid for unique_ptr."); } if (!src) { return none().release(); } - // st.first is the subobject pointer appropriate for tinfo (may differ from src.get() + // cs.cppobj is the subobject pointer appropriate for tinfo (may differ from src.get() // under MI/VI). Use this for Python identity/registration, but keep ownership on T*. - void *src_raw_void_ptr = const_cast(st.first); - assert(st.second != nullptr); - const detail::type_info *tinfo = st.second; + void *src_raw_void_ptr = const_cast(cs.cppobj); + assert(cs.tinfo != nullptr); + const detail::type_info *tinfo = cs.tinfo; if (handle existing_inst = find_registered_python_instance(src_raw_void_ptr, tinfo)) { auto *self_life_support = tinfo->get_trampoline_self_life_support(src.get()); if (self_life_support != nullptr) { @@ -665,20 +733,20 @@ template handle smart_holder_from_unique_ptr(std::unique_ptr &&src, return_value_policy policy, handle parent, - const std::pair &st) { + const cast_sources::resolved_source &cs) { return smart_holder_from_unique_ptr( std::unique_ptr(const_cast(src.release()), std::move(src.get_deleter())), // Const2Mutbl policy, parent, - st); + cs); } template handle smart_holder_from_shared_ptr(const std::shared_ptr &src, return_value_policy policy, handle parent, - const std::pair &st) { + const cast_sources::resolved_source &cs) { switch (policy) { case return_value_policy::automatic: case return_value_policy::automatic_reference: @@ -697,11 +765,11 @@ handle smart_holder_from_shared_ptr(const std::shared_ptr &src, return none().release(); } - // st.first is the subobject pointer appropriate for tinfo (may differ from src.get() + // cs.cppobj is the subobject pointer appropriate for tinfo (may differ from src.get() // under MI/VI). Use this for Python identity/registration, but keep ownership on T*. - void *src_raw_void_ptr = const_cast(st.first); - assert(st.second != nullptr); - const detail::type_info *tinfo = st.second; + void *src_raw_void_ptr = const_cast(cs.cppobj); + assert(cs.tinfo != nullptr); + const detail::type_info *tinfo = cs.tinfo; if (handle existing_inst = find_registered_python_instance(src_raw_void_ptr, tinfo)) { // PYBIND11:REMINDER: MISSING: Enforcement of consistency with existing smart_holder. // PYBIND11:REMINDER: MISSING: keep_alive. @@ -728,11 +796,11 @@ template handle smart_holder_from_shared_ptr(const std::shared_ptr &src, return_value_policy policy, handle parent, - const std::pair &st) { + const cast_sources::resolved_source &cs) { return smart_holder_from_shared_ptr(std::const_pointer_cast(src), // Const2Mutbl policy, parent, - st); + cs); } struct shared_ptr_parent_life_support { @@ -925,21 +993,39 @@ public: bool load(handle src, bool convert) { return load_impl(src, convert); } - PYBIND11_NOINLINE static handle cast(const void *_src, + static handle cast(const void *src, + return_value_policy policy, + handle parent, + const detail::type_info *tinfo, + void *(*copy_constructor)(const void *), + void *(*move_constructor)(const void *), + const void *existing_holder = nullptr) { + cast_sources srcs{src, tinfo}; + return cast(srcs, policy, parent, copy_constructor, move_constructor, existing_holder); + } + + PYBIND11_NOINLINE static handle cast(const cast_sources &srcs, return_value_policy policy, handle parent, - const detail::type_info *tinfo, void *(*copy_constructor)(const void *), void *(*move_constructor)(const void *), const void *existing_holder = nullptr) { - if (!tinfo) { // no type info: error will be set already + if (!srcs.result.tinfo) { + // No pybind11 type info. Raise an exception. + std::string tname = srcs.downcast.cpptype ? srcs.downcast.cpptype->name() + : srcs.original.cpptype ? srcs.original.cpptype->name() + : ""; + detail::clean_type_id(tname); + std::string msg = "Unregistered type : " + tname; + set_error(PyExc_TypeError, msg.c_str()); return handle(); } - void *src = const_cast(_src); + void *src = const_cast(srcs.result.cppobj); if (src == nullptr) { return none().release(); } + const type_info *tinfo = srcs.result.tinfo; if (handle registered_inst = find_registered_python_instance(src, tinfo)) { return registered_inst; @@ -1212,25 +1298,6 @@ public: return false; } - // Called to do type lookup and wrap the pointer and type in a pair when a dynamic_cast - // isn't needed or can't be used. If the type is unknown, sets the error and returns a pair - // with .second = nullptr. (p.first = nullptr is not an error: it becomes None). - PYBIND11_NOINLINE static std::pair - src_and_type(const void *src, - const std::type_info &cast_type, - const std::type_info *rtti_type = nullptr) { - if (auto *tpi = get_type_info(cast_type)) { - return {src, const_cast(tpi)}; - } - - // Not found, set error: - std::string tname = rtti_type ? rtti_type->name() : cast_type.name(); - detail::clean_type_id(tname); - std::string msg = "Unregistered type : " + tname; - set_error(PyExc_TypeError, msg.c_str()); - return {nullptr, nullptr}; - } - const type_info *typeinfo = nullptr; const std::type_info *cpptype = nullptr; void *value = nullptr; @@ -1525,6 +1592,20 @@ struct polymorphic_type_hook : public polymorphic_type_hook_base {}; PYBIND11_NAMESPACE_BEGIN(detail) +template +cast_sources::cast_sources(const itype *ptr) : original{ptr, &typeid(itype)} { + // If this is a base pointer to a derived type, and the derived type is + // registered with pybind11, we want to make the full derived object + // available. In the typical case where itype is polymorphic, we get the + // correct derived pointer (which may be != base pointer) by a dynamic_cast + // to most derived type. If itype is not polymorphic, a user-provided + // specialization of polymorphic_type_hook can do the same thing. + // If there is no downcast to perform, then the default hook will leave + // derived.type set to nullptr, which causes us to ignore derived.obj. + downcast.cppobj = polymorphic_type_hook::get(ptr, downcast.cpptype); + result = resolve(); +} + /// Generic type caster for objects stored on the heap template class type_caster_base : public type_caster_generic { @@ -1536,6 +1617,14 @@ public: type_caster_base() : type_caster_base(typeid(type)) {} explicit type_caster_base(const std::type_info &info) : type_caster_generic(info) {} + // Wrap the generic cast_sources to be only constructible from the type + // that's correct in this context, so you can't use type_caster_base + // to convert an unrelated B* to Python. + struct cast_sources : detail::cast_sources { + // NOLINTNEXTLINE(google-explicit-constructor) + cast_sources(const itype *ptr) : detail::cast_sources(ptr) {} + }; + static handle cast(const itype &src, return_value_policy policy, handle parent) { if (policy == return_value_policy::automatic || policy == return_value_policy::automatic_reference) { @@ -1548,50 +1637,17 @@ public: return cast(std::addressof(src), return_value_policy::move, parent); } - // Returns a (pointer, type_info) pair taking care of necessary type lookup for a - // polymorphic type (using RTTI by default, but can be overridden by specializing - // polymorphic_type_hook). If the instance isn't derived, returns the base version. - static std::pair src_and_type(const itype *src) { - const auto &cast_type = typeid(itype); - const std::type_info *instance_type = nullptr; - const void *vsrc = polymorphic_type_hook::get(src, instance_type); - if (instance_type && !same_type(cast_type, *instance_type)) { - // This is a base pointer to a derived type. If the derived type is registered - // with pybind11, we want to make the full derived object available. - // In the typical case where itype is polymorphic, we get the correct - // derived pointer (which may be != base pointer) by a dynamic_cast to - // most derived type. If itype is not polymorphic, we won't get here - // except via a user-provided specialization of polymorphic_type_hook, - // and the user has promised that no this-pointer adjustment is - // required in that case, so it's OK to use static_cast. - if (const auto *tpi = get_type_info(*instance_type)) { - return {vsrc, tpi}; - } - } - // Otherwise we have either a nullptr, an `itype` pointer, or an unknown derived pointer, - // so don't do a cast - return type_caster_generic::src_and_type(src, cast_type, instance_type); - } - - static handle cast(const itype *src, return_value_policy policy, handle parent) { - auto st = src_and_type(src); - return type_caster_generic::cast(st.first, + static handle cast(const cast_sources &srcs, return_value_policy policy, handle parent) { + return type_caster_generic::cast(srcs, policy, parent, - st.second, - make_copy_constructor(src), - make_move_constructor(src)); + make_copy_constructor((const itype *) nullptr), + make_move_constructor((const itype *) nullptr)); } - static handle cast_holder(const itype *src, const void *holder) { - auto st = src_and_type(src); - return type_caster_generic::cast(st.first, - return_value_policy::take_ownership, - {}, - st.second, - nullptr, - nullptr, - holder); + static handle cast_holder(const cast_sources &srcs, const void *holder) { + auto policy = return_value_policy::take_ownership; + return type_caster_generic::cast(srcs, policy, {}, nullptr, nullptr, holder); } template From cc36ac51a0e46486e5dc714d22a6c6f718283e56 Mon Sep 17 00:00:00 2001 From: Joshua Oreman Date: Wed, 15 Oct 2025 12:10:50 -0400 Subject: [PATCH 29/47] type_caster_generic: fix compiler error when casting a T that is implicitly convertible from T* (#5873) * type_caster_generic: fix compiler error when casting a T that is implicitly convertible from T* * style: pre-commit fixes * Placate clang-tidy * Expand NOLINT to specify Clang-Tidy check names --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Ralf W. Grosse-Kunstleve --- include/pybind11/detail/type_caster_base.h | 13 ++++++++++--- tests/test_class.cpp | 12 ++++++++++++ 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/include/pybind11/detail/type_caster_base.h b/include/pybind11/detail/type_caster_base.h index cf32401b0..c6b80734b 100644 --- a/include/pybind11/detail/type_caster_base.h +++ b/include/pybind11/detail/type_caster_base.h @@ -563,7 +563,7 @@ struct cast_sources { // Use the given pointer with its compile-time type, possibly downcast // via polymorphic_type_hook() template - cast_sources(const itype *ptr); // NOLINT(google-explicit-constructor) + explicit cast_sources(const itype *ptr); // Use the given pointer and type // NOLINTNEXTLINE(google-explicit-constructor) @@ -1621,8 +1621,7 @@ public: // that's correct in this context, so you can't use type_caster_base // to convert an unrelated B* to Python. struct cast_sources : detail::cast_sources { - // NOLINTNEXTLINE(google-explicit-constructor) - cast_sources(const itype *ptr) : detail::cast_sources(ptr) {} + explicit cast_sources(const itype *ptr) : detail::cast_sources(ptr) {} }; static handle cast(const itype &src, return_value_policy policy, handle parent) { @@ -1637,6 +1636,10 @@ public: return cast(std::addressof(src), return_value_policy::move, parent); } + static handle cast(const itype *src, return_value_policy policy, handle parent) { + return cast(cast_sources{src}, policy, parent); + } + static handle cast(const cast_sources &srcs, return_value_policy policy, handle parent) { return type_caster_generic::cast(srcs, policy, @@ -1645,6 +1648,10 @@ public: make_move_constructor((const itype *) nullptr)); } + static handle cast_holder(const itype *src, const void *holder) { + return cast_holder(cast_sources{src}, holder); + } + static handle cast_holder(const cast_sources &srcs, const void *holder) { auto policy = return_value_policy::take_ownership; return type_caster_generic::cast(srcs, policy, {}, nullptr, nullptr, holder); diff --git a/tests/test_class.cpp b/tests/test_class.cpp index 0c05614e6..2030cd671 100644 --- a/tests/test_class.cpp +++ b/tests/test_class.cpp @@ -58,6 +58,13 @@ class ForwardClass; class Args : public py::args {}; } // namespace pr5396_forward_declared_class +struct ConvertibleFromAnything { + ConvertibleFromAnything() = default; + template + // NOLINTNEXTLINE(bugprone-forwarding-reference-overload,google-explicit-constructor) + ConvertibleFromAnything(T &&) {} +}; + } // namespace test_class static_assert(py::detail::is_same_or_base_of::value, ""); @@ -578,6 +585,11 @@ TEST_SUBMODULE(class_, m) { }); test_class::pr4220_tripped_over_this::bind_empty0(m); + + // Regression test for compiler error that showed up in #5866 + m.def("return_universal_recipient", []() -> test_class::ConvertibleFromAnything { + return test_class::ConvertibleFromAnything{}; + }); } template From 1e5bc66e3835749d0ec942acfd261d5139692b20 Mon Sep 17 00:00:00 2001 From: Scott Wolchok Date: Wed, 15 Oct 2025 21:12:44 -0700 Subject: [PATCH 30/47] Factor out readable function signatures to avoid duplication (#5857) * Centralize readable function signatures to avoid duplication This seems to reduce size costs of adding enum_-specific implementations of dunder methods, but also should provide a nice to have size optimization for programs that use pybind11 in general. * gate disabling of -Wdeprecated-redundant-constexpr-static-def to clang 17+ * fix gating to include Apple Clang 15 * Make GCC happy with types * fix apple clang gating again. suppress -Wdeprecated for GCC * Gate warning suppressions to C++17. Suppress -Wdeprecated for clang as well. * hopefully fix last straggler CI job * attempt to address readability review feedback from @rwgk * drop warning suppressions and instead just gate compilation the pre-C++17 compat code --- include/pybind11/pybind11.h | 54 ++++++++++++++++++++++++++++++++++--- 1 file changed, 51 insertions(+), 3 deletions(-) diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h index fcf10f199..8ab4681c7 100644 --- a/include/pybind11/pybind11.h +++ b/include/pybind11/pybind11.h @@ -248,6 +248,49 @@ inline std::string generate_type_signature() { # define PYBIND11_COMPAT_STRDUP strdup #endif +#define PYBIND11_READABLE_FUNCTION_SIGNATURE_EXPR \ + detail::const_name("(") + cast_in::arg_names + detail::const_name(") -> ") + cast_out::name + +// We factor out readable function signatures to a specific template +// so that they don't get duplicated across different instantiations of +// cpp_function::initialize (which is templated on more types). +template +class ReadableFunctionSignature { +public: + using sig_type = decltype(PYBIND11_READABLE_FUNCTION_SIGNATURE_EXPR); + +private: + // We have to repeat PYBIND11_READABLE_FUNCTION_SIGNATURE_EXPR in decltype() + // because C++11 doesn't allow functions to return `auto`. (We don't + // know the type because it's some variant of detail::descr with + // unknown N.) + static constexpr sig_type sig() { return PYBIND11_READABLE_FUNCTION_SIGNATURE_EXPR; } + +public: + static constexpr sig_type kSig = sig(); + // We can only stash the result of detail::descr::types() in a + // constexpr variable if we aren't on MSVC (see + // PYBIND11_DESCR_CONSTEXPR). +#if !defined(_MSC_VER) + using types_type = decltype(sig_type::types()); + static constexpr types_type kTypes = sig_type::types(); +#endif +}; +#undef PYBIND11_READABLE_FUNCTION_SIGNATURE_EXPR + +// Prior to C++17, we don't have inline variables, so we have to +// provide an out-of-line definition of the class member. +#if !defined(PYBIND11_CPP17) +template +constexpr typename ReadableFunctionSignature::sig_type + ReadableFunctionSignature::kSig; +# if !defined(_MSC_VER) +template +constexpr typename ReadableFunctionSignature::types_type + ReadableFunctionSignature::kTypes; +# endif +#endif + PYBIND11_NAMESPACE_END(detail) /// Wraps an arbitrary C++ function/method/lambda function/.. into a callable Python object @@ -481,9 +524,14 @@ protected: /* Generate a readable signature describing the function's arguments and return value types */ - static constexpr auto signature - = const_name("(") + cast_in::arg_names + const_name(") -> ") + cast_out::name; - PYBIND11_DESCR_CONSTEXPR auto types = decltype(signature)::types(); + static constexpr const auto &signature + = detail::ReadableFunctionSignature::kSig; +#if !defined(_MSC_VER) + static constexpr const auto &types + = detail::ReadableFunctionSignature::kTypes; +#else + PYBIND11_DESCR_CONSTEXPR auto types = std::decay::type::types(); +#endif /* Register the function with Python from generic (non-templated) code */ // Pass on the ownership over the `unique_rec` to `initialize_generic`. `rec` stays valid. From 15943963b3c3294cfe8073a9b589676f3117be02 Mon Sep 17 00:00:00 2001 From: daltairwalter <31971208+daltairwalter@users.noreply.github.com> Date: Thu, 16 Oct 2025 10:40:50 -0500 Subject: [PATCH 31/47] fix dangling thread state issue (#5870) * fix dangling thread state issue * formatting rules * use tstate .set(nullptr) to pass clang-tidy check * fix spelling mistake * improve comments for maintainability --- include/pybind11/detail/internals.h | 2 +- include/pybind11/subinterpreter.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/include/pybind11/detail/internals.h b/include/pybind11/detail/internals.h index 4f8de120f..003ee6e15 100644 --- a/include/pybind11/detail/internals.h +++ b/include/pybind11/detail/internals.h @@ -286,8 +286,8 @@ struct internals { internals() : static_property_type(make_static_property_type()), default_metaclass(make_default_metaclass()) { + tstate.set(nullptr); // See PR #5870 PyThreadState *cur_tstate = PyThreadState_Get(); - tstate = cur_tstate; istate = cur_tstate->interp; registered_exception_translators.push_front(&translate_exception); diff --git a/include/pybind11/subinterpreter.h b/include/pybind11/subinterpreter.h index 4b208ed9b..5d2f0a839 100644 --- a/include/pybind11/subinterpreter.h +++ b/include/pybind11/subinterpreter.h @@ -271,7 +271,7 @@ inline subinterpreter_scoped_activate::subinterpreter_scoped_activate(subinterpr // make the interpreter active and acquire the GIL old_tstate_ = PyThreadState_Swap(tstate_); - // save this in internals for scoped_gil calls + // save this in internals for scoped_gil calls (see also: PR #5870) detail::get_internals().tstate = tstate_; } From e6984c805ec09c0e5f826e3081a32f322a6bfe63 Mon Sep 17 00:00:00 2001 From: Joshua Oreman Date: Sat, 18 Oct 2025 13:07:00 -0400 Subject: [PATCH 32/47] native_enum: add capsule containing enum information and cleanup logic (#5871) * native_enum: add capsule containing enum information and cleanup logic * style: pre-commit fixes * Updates from code review --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- include/pybind11/detail/internals.h | 18 ++++++ include/pybind11/detail/native_enum_data.h | 40 +++++++++---- include/pybind11/native_enum.h | 13 ++++- tests/conftest.py | 48 ++++++++++++++-- tests/env.py | 6 ++ ...oss_module_use_after_one_module_dealloc.py | 32 ++--------- tests/test_native_enum.cpp | 12 ++-- tests/test_native_enum.py | 57 ++++++++++++++++++- 8 files changed, 174 insertions(+), 52 deletions(-) diff --git a/include/pybind11/detail/internals.h b/include/pybind11/detail/internals.h index 003ee6e15..2600d4356 100644 --- a/include/pybind11/detail/internals.h +++ b/include/pybind11/detail/internals.h @@ -382,6 +382,24 @@ struct type_info { bool module_local : 1; }; +/// Information stored in a capsule on py::native_enum() types. Since we don't +/// create a type_info record for native enums, we must store here any +/// information we will need about the enum at runtime. +/// +/// If you make backward-incompatible changes to this structure, you must +/// change the `attribute_name()` so that native enums from older version of +/// pybind11 don't have their records reinterpreted. Better would be to keep +/// the changes backward-compatible (i.e., only add new fields at the end) +/// and detect/indicate their presence using the currently-unused `version`. +struct native_enum_record { + const std::type_info *cpptype; + uint32_t size_bytes; + bool is_signed; + const uint8_t version = 1; + + static const char *attribute_name() { return "__pybind11_native_enum__"; } +}; + #define PYBIND11_INTERNALS_ID \ "__pybind11_internals_v" PYBIND11_TOSTRING(PYBIND11_INTERNALS_VERSION) \ PYBIND11_COMPILER_TYPE_LEADING_UNDERSCORE PYBIND11_PLATFORM_ABI_ID "__" diff --git a/include/pybind11/detail/native_enum_data.h b/include/pybind11/detail/native_enum_data.h index a8f7675ba..6770378fc 100644 --- a/include/pybind11/detail/native_enum_data.h +++ b/include/pybind11/detail/native_enum_data.h @@ -22,17 +22,36 @@ native_enum_missing_finalize_error_message(const std::string &enum_name_encoded) return "pybind11::native_enum<...>(\"" + enum_name_encoded + "\", ...): MISSING .finalize()"; } +// Internals for pybind11::native_enum; one native_enum_data object exists +// inside each pybind11::native_enum and lives only for the duration of the +// native_enum binding statement. class native_enum_data { public: - native_enum_data(const object &parent_scope, + native_enum_data(handle parent_scope_, const char *enum_name, const char *native_type_name, const char *class_doc, - const std::type_index &enum_type_index) + const native_enum_record &enum_record_) : enum_name_encoded{enum_name}, native_type_name_encoded{native_type_name}, - enum_type_index{enum_type_index}, parent_scope(parent_scope), enum_name{enum_name}, + enum_type_index{*enum_record_.cpptype}, + parent_scope(reinterpret_borrow(parent_scope_)), enum_name{enum_name}, native_type_name{native_type_name}, class_doc(class_doc), export_values_flag{false}, - finalize_needed{false} {} + finalize_needed{false} { + // Create the enum record capsule. It will be installed on the enum + // type object during finalize(). Its destructor removes the enum + // mapping from our internals, so that we won't try to convert to an + // enum type that's been destroyed. + enum_record = capsule( + new native_enum_record{enum_record_}, + native_enum_record::attribute_name(), + +[](void *record_) { + auto *record = static_cast(record_); + with_internals([&](internals &internals) { + internals.native_enum_type_map.erase(*record->cpptype); + }); + delete record; + }); + } void finalize(); @@ -71,6 +90,7 @@ private: str enum_name; str native_type_name; std::string class_doc; + capsule enum_record; protected: list members; @@ -81,12 +101,6 @@ private: bool finalize_needed : 1; }; -inline void global_internals_native_enum_type_map_set_item(const std::type_index &enum_type_index, - PyObject *py_enum) { - with_internals( - [&](internals &internals) { internals.native_enum_type_map[enum_type_index] = py_enum; }); -} - inline handle global_internals_native_enum_type_map_get_item(const std::type_index &enum_type_index) { return with_internals([&](internals &internals) { @@ -202,7 +216,11 @@ inline void native_enum_data::finalize() { for (auto doc : member_docs) { py_enum[doc[int_(0)]].attr("__doc__") = doc[int_(1)]; } - global_internals_native_enum_type_map_set_item(enum_type_index, py_enum.release().ptr()); + + py_enum.attr(native_enum_record::attribute_name()) = enum_record; + with_internals([&](internals &internals) { + internals.native_enum_type_map[enum_type_index] = py_enum.ptr(); + }); } PYBIND11_NAMESPACE_END(detail) diff --git a/include/pybind11/native_enum.h b/include/pybind11/native_enum.h index 5537218f2..af166d0c8 100644 --- a/include/pybind11/native_enum.h +++ b/include/pybind11/native_enum.h @@ -22,12 +22,12 @@ class native_enum : public detail::native_enum_data { public: using Underlying = typename std::underlying_type::type; - native_enum(const object &parent_scope, + native_enum(handle parent_scope, const char *name, const char *native_type_name, const char *class_doc = "") : detail::native_enum_data( - parent_scope, name, native_type_name, class_doc, std::type_index(typeid(EnumType))) { + parent_scope, name, native_type_name, class_doc, make_record()) { if (detail::get_local_type_info(typeid(EnumType)) != nullptr || detail::get_global_type_info(typeid(EnumType)) != nullptr) { pybind11_fail( @@ -62,6 +62,15 @@ public: native_enum(const native_enum &) = delete; native_enum &operator=(const native_enum &) = delete; + +private: + static detail::native_enum_record make_record() { + detail::native_enum_record ret; + ret.cpptype = &typeid(EnumType); + ret.size_bytes = sizeof(EnumType); + ret.is_signed = std::is_signed::value; + return ret; + } }; PYBIND11_NAMESPACE_END(PYBIND11_NAMESPACE) diff --git a/tests/conftest.py b/tests/conftest.py index a2574f166..39de4e138 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -16,6 +16,7 @@ import sys import sysconfig import textwrap import traceback +import weakref from typing import Callable import pytest @@ -208,19 +209,54 @@ def pytest_assertrepr_compare(op, left, right): # noqa: ARG001 return None +# Number of times we think repeatedly collecting garbage might do anything. +# The only reason to do more than once is because finalizers executed during +# one GC pass could create garbage that can't be collected until a future one. +# This quickly produces diminishing returns, and GC passes can be slow, so this +# value is a tradeoff between non-flakiness and fast tests. (It errs on the +# side of non-flakiness; many uses of this idiom only do 3 passes.) +num_gc_collect = 5 + + def gc_collect(): - """Run the garbage collector three times (needed when running + """Run the garbage collector several times (needed when running reference counting tests with PyPy)""" - gc.collect() - gc.collect() - gc.collect() - gc.collect() - gc.collect() + for _ in range(num_gc_collect): + gc.collect() + + +def delattr_and_ensure_destroyed(*specs): + """For each of the given *specs* (a tuple of the form ``(scope, name)``), + perform ``delattr(scope, name)``, then do enough GC collections that the + deleted reference has actually caused the target to be destroyed. This is + typically used to test what happens when a type object is destroyed; if you + use it for that, you should be aware that extension types, or all types, + are immortal on some Python versions. See ``env.TYPES_ARE_IMMORTAL``. + """ + wrs = [] + for mod, name in specs: + wrs.append(weakref.ref(getattr(mod, name))) + delattr(mod, name) + + for _ in range(num_gc_collect): + gc.collect() + if all(wr() is None for wr in wrs): + break + else: + # If this fires, most likely something is still holding a reference + # to the object you tried to destroy - for example, it's a type that + # still has some instances alive. Try setting a breakpoint here and + # examining `gc.get_referrers(wrs[0]())`. It's vaguely possible that + # num_gc_collect needs to be increased also. + pytest.fail( + f"Could not delete bindings such as {next(wr for wr in wrs if wr() is not None)!r}" + ) def pytest_configure(): pytest.suppress = contextlib.suppress pytest.gc_collect = gc_collect + pytest.delattr_and_ensure_destroyed = delattr_and_ensure_destroyed def pytest_report_header(): diff --git a/tests/env.py b/tests/env.py index ae239a741..177392591 100644 --- a/tests/env.py +++ b/tests/env.py @@ -24,6 +24,12 @@ PY_GIL_DISABLED = bool(sysconfig.get_config_var("Py_GIL_DISABLED")) # Runtime state (what's actually happening now) sys_is_gil_enabled = getattr(sys, "_is_gil_enabled", lambda: True) +TYPES_ARE_IMMORTAL = ( + PYPY + or GRAALPY + or (CPYTHON and PY_GIL_DISABLED and (3, 13) <= sys.version_info < (3, 14)) +) + def deprecated_call(): """ diff --git a/tests/test_class_cross_module_use_after_one_module_dealloc.py b/tests/test_class_cross_module_use_after_one_module_dealloc.py index ac8a47a8f..58c8f72bb 100644 --- a/tests/test_class_cross_module_use_after_one_module_dealloc.py +++ b/tests/test_class_cross_module_use_after_one_module_dealloc.py @@ -1,40 +1,16 @@ from __future__ import annotations -import gc -import sys -import sysconfig import types -import weakref import pytest import env from pybind11_tests import class_cross_module_use_after_one_module_dealloc as m -is_python_3_13_free_threaded = ( - env.CPYTHON - and sysconfig.get_config_var("Py_GIL_DISABLED") - and (3, 13) <= sys.version_info < (3, 14) + +@pytest.mark.skipif( + env.TYPES_ARE_IMMORTAL, reason="can't GC type objects on this platform" ) - - -def delattr_and_ensure_destroyed(*specs): - wrs = [] - for mod, name in specs: - wrs.append(weakref.ref(getattr(mod, name))) - delattr(mod, name) - - for _ in range(5): - gc.collect() - if all(wr() is None for wr in wrs): - break - else: - pytest.fail( - f"Could not delete bindings such as {next(wr for wr in wrs if wr() is not None)!r}" - ) - - -@pytest.mark.skipif("env.PYPY or env.GRAALPY or is_python_3_13_free_threaded") def test_cross_module_use_after_one_module_dealloc(): # This is a regression test for a bug that occurred during development of # internals::registered_types_cpp_fast (see #5842). registered_types_cpp_fast maps @@ -58,7 +34,7 @@ def test_cross_module_use_after_one_module_dealloc(): cm.consume_cross_dso_class(instance) del instance - delattr_and_ensure_destroyed((module_scope, "CrossDSOClass")) + pytest.delattr_and_ensure_destroyed((module_scope, "CrossDSOClass")) # Make sure that CrossDSOClass gets allocated at a different address. m.register_unrelated_class(module_scope) diff --git a/tests/test_native_enum.cpp b/tests/test_native_enum.cpp index 0ec6d8554..816ace01d 100644 --- a/tests/test_native_enum.cpp +++ b/tests/test_native_enum.cpp @@ -93,10 +93,14 @@ TEST_SUBMODULE(native_enum, m) { .value("blue", color::blue) .finalize(); - py::native_enum(m, "altitude", "enum.Enum") - .value("high", altitude::high) - .value("low", altitude::low) - .finalize(); + m.def("bind_altitude", [](const py::module_ &mod) { + py::native_enum(mod, "altitude", "enum.Enum") + .value("high", altitude::high) + .value("low", altitude::low) + .finalize(); + }); + m.def("is_high_altitude", [](altitude alt) { return alt == altitude::high; }); + m.def("get_altitude", []() -> altitude { return altitude::high; }); py::native_enum(m, "flags_uchar", "enum.Flag") .value("bit0", flags_uchar::bit0) diff --git a/tests/test_native_enum.py b/tests/test_native_enum.py index b039747ee..426220013 100644 --- a/tests/test_native_enum.py +++ b/tests/test_native_enum.py @@ -59,7 +59,6 @@ FUNC_SIG_RENDERING_MEMBERS = () ENUM_TYPES_AND_MEMBERS = ( (m.smallenum, SMALLENUM_MEMBERS), (m.color, COLOR_MEMBERS), - (m.altitude, ALTITUDE_MEMBERS), (m.flags_uchar, FLAGS_UCHAR_MEMBERS), (m.flags_uint, FLAGS_UINT_MEMBERS), (m.export_values, EXPORT_VALUES_MEMBERS), @@ -320,3 +319,59 @@ def test_native_enum_missing_finalize_failure(): if not isinstance(m.native_enum_missing_finalize_failure, str): m.native_enum_missing_finalize_failure() pytest.fail("Process termination expected.") + + +def test_unregister_native_enum_when_destroyed(): + # For stability when running tests in parallel, this test should be the + # only one that touches `m.altitude` or calls `m.bind_altitude`. + + def test_altitude_enum(): + # Logic copied from test_enum_type / test_enum_members. + # We don't test altitude there to avoid possible clashes if + # parallelizing against other tests in this file, and we also + # don't want to hold any references to the enumerators that + # would prevent GCing the enum type below. + assert isinstance(m.altitude, enum.EnumMeta) + assert m.altitude.__module__ == m.__name__ + for name, value in ALTITUDE_MEMBERS: + assert m.altitude[name].value == value + + def test_altitude_binding(): + assert m.is_high_altitude(m.altitude.high) + assert not m.is_high_altitude(m.altitude.low) + assert m.get_altitude() is m.altitude.high + with pytest.raises(TypeError, match="incompatible function arguments"): + m.is_high_altitude("oops") + + m.bind_altitude(m) + test_altitude_enum() + test_altitude_binding() + + if env.TYPES_ARE_IMMORTAL: + pytest.skip("can't GC type objects on this platform") + + # Delete the enum type. Returning an instance from Python should fail + # rather than accessing a deleted object. + pytest.delattr_and_ensure_destroyed((m, "altitude")) + with pytest.raises(TypeError, match="Unable to convert function return"): + m.get_altitude() + with pytest.raises(TypeError, match="incompatible function arguments"): + m.is_high_altitude("oops") + + # Recreate the enum type; should not have any duplicate-binding error + m.bind_altitude(m) + test_altitude_enum() + test_altitude_binding() + + # Remove the pybind11 capsule without removing the type; enum is still + # usable but can't be passed to/from bound functions + del m.altitude.__pybind11_native_enum__ + pytest.gc_collect() + test_altitude_enum() # enum itself still works + + with pytest.raises(TypeError, match="Unable to convert function return"): + m.get_altitude() + with pytest.raises(TypeError, match="incompatible function arguments"): + m.is_high_altitude(m.altitude.high) + + del m.altitude From 73da78c3e4f925c3c641ebcc34ec4ab424d55fd1 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 10 Nov 2025 16:52:13 -0500 Subject: [PATCH 33/47] chore(deps): update pre-commit hooks (#5888) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.13.3 β†’ v0.14.3](https://github.com/astral-sh/ruff-pre-commit/compare/v0.13.3...v0.14.3) - [github.com/PyCQA/pylint: v3.3.9 β†’ v4.0.2](https://github.com/PyCQA/pylint/compare/v3.3.9...v4.0.2) - [github.com/python-jsonschema/check-jsonschema: 0.34.0 β†’ 0.34.1](https://github.com/python-jsonschema/check-jsonschema/compare/0.34.0...0.34.1) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8ffc34ea3..7f2c5a697 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -32,7 +32,7 @@ repos: # Ruff, the Python auto-correcting linter/formatter written in Rust - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.13.3 + rev: v0.14.3 hooks: - id: ruff-check args: ["--fix", "--show-fixes"] @@ -135,14 +135,14 @@ repos: # PyLint has native support - not always usable, but works for us - repo: https://github.com/PyCQA/pylint - rev: "v3.3.9" + rev: "v4.0.2" hooks: - id: pylint files: ^pybind11 # Check schemas on some of our YAML files - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.34.0 + rev: 0.34.1 hooks: - id: check-readthedocs - id: check-github-workflows From 9f1187f97c987b4336119fb7b6dd9c1350de48d6 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Mon, 10 Nov 2025 20:26:50 -0800 Subject: [PATCH 34/47] Add `typing.SupportsIndex` to `int`/`float`/`complex` type hints (#5891) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add typing.SupportsIndex to int/float/complex type hints This corrects a mistake where these types were supported but the type hint was not updated to reflect that SupportsIndex objects are accepted. To track the resulting test failures: The output of "$(cat PYROOT)"/bin/python3 $HOME/clone/pybind11_scons/run_tests.py $HOME/forked/pybind11 -v is in ~/logs/pybind11_pr5879_scons_run_tests_v_log_2025-11-10+122217.txt * Cursor auto-fixes (partial) plus pre-commit cleanup. 7 test failures left to do. * Fix remaining test failures, partially done by cursor, partially manually. * Cursor-generated commit: Added the Index() tests from PR 5879. Summary: Changes Made 1. **C++ Bindings** (`tests/test_builtin_casters.cpp`) β€’ Added complex_convert and complex_noconvert functions needed for the tests 2. **Python Tests** (`tests/test_builtin_casters.py`) `test_float_convert`: β€’ Added Index class with __index__ returning -7 β€’ Added Int class with __int__ returning -5 β€’ Added test showing Index() works with convert mode: assert pytest.approx(convert(Index())) == -7.0 β€’ Added test showing Index() doesn't work with noconvert mode: requires_conversion(Index()) β€’ Added additional assertions for int literals and Int() class `test_complex_cast`: β€’ Expanded the test to include convert and noconvert functionality β€’ Added Index, Complex, Float, and Int classes β€’ Added test showing Index() works with convert mode: assert convert(Index()) == 1 and assert isinstance(convert(Index()), complex) β€’ Added test showing Index() doesn't work with noconvert mode: requires_conversion(Index()) β€’ Added type hint assertions matching the SupportsIndex additions These tests demonstrate that custom __index__ objects work with float and complex in convert mode, matching the typing.SupportsIndex type hint added in PR 5891. * Reflect behavior changes going back from PR 5879 to master. This diff will have to be reapplied under PR 5879. * Add PyPy-specific __index__ handling for complex caster Extract PyPy-specific __index__ backporting from PR 5879 to fix PyPy 3.10 test failures in PR 5891. This adds: 1. PYBIND11_INDEX_CHECK macro in detail/common.h: - Uses PyIndex_Check on CPython - Uses hasattr check on PyPy (workaround for PyPy 7.3.3 behavior) 2. PyPy-specific __index__ handling in complex.h: - Handles __index__ objects on PyPy 7.3.7's 3.8 which doesn't implement PyLong_*'s __index__ calls - Mirrors the logic used in numeric_caster for ints and floats This backports __index__ handling for PyPy, matching the approach used in PR 5879's expand-float-strict branch. --- include/pybind11/cast.h | 9 ++- include/pybind11/complex.h | 23 +++++++- include/pybind11/detail/common.h | 7 +++ tests/test_builtin_casters.cpp | 2 + tests/test_builtin_casters.py | 81 ++++++++++++++++++++++++-- tests/test_callbacks.py | 4 +- tests/test_class.py | 4 +- tests/test_custom_type_casters.py | 2 +- tests/test_docstring_options.py | 6 +- tests/test_enum.py | 2 +- tests/test_factory_constructors.py | 8 +-- tests/test_kwargs_and_defaults.py | 48 +++++++-------- tests/test_methods_and_attributes.py | 20 ++++--- tests/test_numpy_dtypes.py | 2 +- tests/test_numpy_vectorize.py | 4 +- tests/test_pytypes.py | 37 ++++++++---- tests/test_stl.py | 22 +++---- tests/test_type_caster_pyobject_ptr.py | 3 +- 18 files changed, 204 insertions(+), 80 deletions(-) diff --git a/include/pybind11/cast.h b/include/pybind11/cast.h index 4708101d8..556bdb7e3 100644 --- a/include/pybind11/cast.h +++ b/include/pybind11/cast.h @@ -347,9 +347,12 @@ public: return PyLong_FromUnsignedLongLong((unsigned long long) src); } - PYBIND11_TYPE_CASTER(T, - io_name::value>( - "typing.SupportsInt", "int", "typing.SupportsFloat", "float")); + PYBIND11_TYPE_CASTER( + T, + io_name::value>("typing.SupportsInt | typing.SupportsIndex", + "int", + "typing.SupportsFloat | typing.SupportsIndex", + "float")); }; template diff --git a/include/pybind11/complex.h b/include/pybind11/complex.h index 8a831c12c..0b6f49365 100644 --- a/include/pybind11/complex.h +++ b/include/pybind11/complex.h @@ -54,7 +54,23 @@ public: if (!convert && !PyComplex_Check(src.ptr())) { return false; } - Py_complex result = PyComplex_AsCComplex(src.ptr()); + handle src_or_index = src; + // PyPy: 7.3.7's 3.8 does not implement PyLong_*'s __index__ calls. + // The same logic is used in numeric_caster for ints and floats +#if defined(PYPY_VERSION) + object index; + if (PYBIND11_INDEX_CHECK(src.ptr())) { + index = reinterpret_steal(PyNumber_Index(src.ptr())); + if (!index) { + PyErr_Clear(); + if (!convert) + return false; + } else { + src_or_index = index; + } + } +#endif + Py_complex result = PyComplex_AsCComplex(src_or_index.ptr()); if (result.real == -1.0 && PyErr_Occurred()) { PyErr_Clear(); return false; @@ -68,7 +84,10 @@ public: return PyComplex_FromDoubles((double) src.real(), (double) src.imag()); } - PYBIND11_TYPE_CASTER(std::complex, const_name("complex")); + PYBIND11_TYPE_CASTER( + std::complex, + io_name("typing.SupportsComplex | typing.SupportsFloat | typing.SupportsIndex", + "complex")); }; PYBIND11_NAMESPACE_END(detail) PYBIND11_NAMESPACE_END(PYBIND11_NAMESPACE) diff --git a/include/pybind11/detail/common.h b/include/pybind11/detail/common.h index 16952c582..07c094300 100644 --- a/include/pybind11/detail/common.h +++ b/include/pybind11/detail/common.h @@ -322,6 +322,13 @@ #define PYBIND11_BYTES_AS_STRING PyBytes_AsString #define PYBIND11_BYTES_SIZE PyBytes_Size #define PYBIND11_LONG_CHECK(o) PyLong_Check(o) +// In PyPy 7.3.3, `PyIndex_Check` is implemented by calling `__index__`, +// while CPython only considers the existence of `nb_index`/`__index__`. +#if !defined(PYPY_VERSION) +# define PYBIND11_INDEX_CHECK(o) PyIndex_Check(o) +#else +# define PYBIND11_INDEX_CHECK(o) hasattr(o, "__index__") +#endif #define PYBIND11_LONG_AS_LONGLONG(o) PyLong_AsLongLong(o) #define PYBIND11_LONG_FROM_SIGNED(o) PyLong_FromSsize_t((ssize_t) (o)) #define PYBIND11_LONG_FROM_UNSIGNED(o) PyLong_FromSize_t((size_t) (o)) diff --git a/tests/test_builtin_casters.cpp b/tests/test_builtin_casters.cpp index c516f8de7..1aa9f89b4 100644 --- a/tests/test_builtin_casters.cpp +++ b/tests/test_builtin_casters.cpp @@ -363,6 +363,8 @@ TEST_SUBMODULE(builtin_casters, m) { m.def("complex_cast", [](float x) { return "{}"_s.format(x); }); m.def("complex_cast", [](std::complex x) { return "({}, {})"_s.format(x.real(), x.imag()); }); + m.def("complex_convert", [](std::complex x) { return x; }); + m.def("complex_noconvert", [](std::complex x) { return x; }, py::arg{}.noconvert()); // test int vs. long (Python 2) m.def("int_cast", []() { return (int) 42; }); diff --git a/tests/test_builtin_casters.py b/tests/test_builtin_casters.py index 7a2c6a4d8..ae1b1bd17 100644 --- a/tests/test_builtin_casters.py +++ b/tests/test_builtin_casters.py @@ -286,7 +286,10 @@ def test_int_convert(doc): convert, noconvert = m.int_passthrough, m.int_passthrough_noconvert - assert doc(convert) == "int_passthrough(arg0: typing.SupportsInt) -> int" + assert ( + doc(convert) + == "int_passthrough(arg0: typing.SupportsInt | typing.SupportsIndex) -> int" + ) assert doc(noconvert) == "int_passthrough_noconvert(arg0: int) -> int" def requires_conversion(v): @@ -322,19 +325,39 @@ def test_int_convert(doc): def test_float_convert(doc): + class Int: + def __int__(self): + return -5 + + class Index: + def __index__(self) -> int: + return -7 + class Float: def __float__(self): return 41.45 convert, noconvert = m.float_passthrough, m.float_passthrough_noconvert - assert doc(convert) == "float_passthrough(arg0: typing.SupportsFloat) -> float" + assert ( + doc(convert) + == "float_passthrough(arg0: typing.SupportsFloat | typing.SupportsIndex) -> float" + ) assert doc(noconvert) == "float_passthrough_noconvert(arg0: float) -> float" def requires_conversion(v): pytest.raises(TypeError, noconvert, v) + def cant_convert(v): + pytest.raises(TypeError, convert, v) + requires_conversion(Float()) + requires_conversion(Index()) assert pytest.approx(convert(Float())) == 41.45 + assert pytest.approx(convert(Index())) == -7.0 + assert isinstance(convert(Float()), float) + assert pytest.approx(convert(3)) == 3.0 + requires_conversion(3) + cant_convert(Int()) def test_numpy_int_convert(): @@ -381,7 +404,7 @@ def test_tuple(doc): assert ( doc(m.tuple_passthrough) == """ - tuple_passthrough(arg0: tuple[bool, str, typing.SupportsInt]) -> tuple[int, str, bool] + tuple_passthrough(arg0: tuple[bool, str, typing.SupportsInt | typing.SupportsIndex]) -> tuple[int, str, bool] Return a triple in reversed order """ @@ -458,11 +481,61 @@ def test_reference_wrapper(): assert m.refwrap_call_iiw(IncType(10), m.refwrap_iiw) == [10, 10, 10, 10] -def test_complex_cast(): +def test_complex_cast(doc): """std::complex casts""" + + class Complex: + def __complex__(self) -> complex: + return complex(5, 4) + + class Float: + def __float__(self) -> float: + return 5.0 + + class Int: + def __int__(self) -> int: + return 3 + + class Index: + def __index__(self) -> int: + return 1 + assert m.complex_cast(1) == "1.0" + assert m.complex_cast(1.0) == "1.0" + assert m.complex_cast(Complex()) == "(5.0, 4.0)" assert m.complex_cast(2j) == "(0.0, 2.0)" + convert, noconvert = m.complex_convert, m.complex_noconvert + + def requires_conversion(v): + pytest.raises(TypeError, noconvert, v) + + def cant_convert(v): + pytest.raises(TypeError, convert, v) + + assert ( + doc(convert) + == "complex_convert(arg0: typing.SupportsComplex | typing.SupportsFloat | typing.SupportsIndex) -> complex" + ) + assert doc(noconvert) == "complex_noconvert(arg0: complex) -> complex" + + assert convert(1) == 1.0 + assert convert(2.0) == 2.0 + assert convert(1 + 5j) == 1.0 + 5.0j + assert convert(Complex()) == 5.0 + 4j + assert convert(Float()) == 5.0 + assert isinstance(convert(Float()), complex) + cant_convert(Int()) + assert convert(Index()) == 1 + assert isinstance(convert(Index()), complex) + + requires_conversion(1) + requires_conversion(2.0) + assert noconvert(1 + 5j) == 1.0 + 5.0j + requires_conversion(Complex()) + requires_conversion(Float()) + requires_conversion(Index()) + def test_bool_caster(): """Test bool caster implicit conversions.""" diff --git a/tests/test_callbacks.py b/tests/test_callbacks.py index 09dadd94c..c0a57a7b8 100644 --- a/tests/test_callbacks.py +++ b/tests/test_callbacks.py @@ -140,11 +140,11 @@ def test_cpp_function_roundtrip(): def test_function_signatures(doc): assert ( doc(m.test_callback3) - == "test_callback3(arg0: collections.abc.Callable[[typing.SupportsInt], int]) -> str" + == "test_callback3(arg0: collections.abc.Callable[[typing.SupportsInt | typing.SupportsIndex], int]) -> str" ) assert ( doc(m.test_callback4) - == "test_callback4() -> collections.abc.Callable[[typing.SupportsInt], int]" + == "test_callback4() -> collections.abc.Callable[[typing.SupportsInt | typing.SupportsIndex], int]" ) diff --git a/tests/test_class.py b/tests/test_class.py index 1e8293036..fae6a3189 100644 --- a/tests/test_class.py +++ b/tests/test_class.py @@ -163,13 +163,13 @@ def test_qualname(doc): assert ( doc(m.NestBase.Nested.fn) == """ - fn(self: m.class_.NestBase.Nested, arg0: typing.SupportsInt, arg1: m.class_.NestBase, arg2: m.class_.NestBase.Nested) -> None + fn(self: m.class_.NestBase.Nested, arg0: typing.SupportsInt | typing.SupportsIndex, arg1: m.class_.NestBase, arg2: m.class_.NestBase.Nested) -> None """ ) assert ( doc(m.NestBase.Nested.fa) == """ - fa(self: m.class_.NestBase.Nested, a: typing.SupportsInt, b: m.class_.NestBase, c: m.class_.NestBase.Nested) -> None + fa(self: m.class_.NestBase.Nested, a: typing.SupportsInt | typing.SupportsIndex, b: m.class_.NestBase, c: m.class_.NestBase.Nested) -> None """ ) assert m.NestBase.__module__ == "pybind11_tests.class_" diff --git a/tests/test_custom_type_casters.py b/tests/test_custom_type_casters.py index bf31d3f37..6ed1c564f 100644 --- a/tests/test_custom_type_casters.py +++ b/tests/test_custom_type_casters.py @@ -75,7 +75,7 @@ def test_noconvert_args(msg): msg(excinfo.value) == """ ints_preferred(): incompatible function arguments. The following argument types are supported: - 1. (i: typing.SupportsInt) -> int + 1. (i: typing.SupportsInt | typing.SupportsIndex) -> int Invoked with: 4.0 """ diff --git a/tests/test_docstring_options.py b/tests/test_docstring_options.py index f2a10480c..802a1ec9e 100644 --- a/tests/test_docstring_options.py +++ b/tests/test_docstring_options.py @@ -20,11 +20,11 @@ def test_docstring_options(): # options.enable_function_signatures() assert m.test_function3.__doc__.startswith( - "test_function3(a: typing.SupportsInt, b: typing.SupportsInt) -> None" + "test_function3(a: typing.SupportsInt | typing.SupportsIndex, b: typing.SupportsInt | typing.SupportsIndex) -> None" ) assert m.test_function4.__doc__.startswith( - "test_function4(a: typing.SupportsInt, b: typing.SupportsInt) -> None" + "test_function4(a: typing.SupportsInt | typing.SupportsIndex, b: typing.SupportsInt | typing.SupportsIndex) -> None" ) assert m.test_function4.__doc__.endswith("A custom docstring\n") @@ -37,7 +37,7 @@ def test_docstring_options(): # RAII destructor assert m.test_function7.__doc__.startswith( - "test_function7(a: typing.SupportsInt, b: typing.SupportsInt) -> None" + "test_function7(a: typing.SupportsInt | typing.SupportsIndex, b: typing.SupportsInt | typing.SupportsIndex) -> None" ) assert m.test_function7.__doc__.endswith("A custom docstring\n") diff --git a/tests/test_enum.py b/tests/test_enum.py index 99d4a88c8..f295b0145 100644 --- a/tests/test_enum.py +++ b/tests/test_enum.py @@ -328,7 +328,7 @@ def test_generated_dunder_methods_pos_only(): ) assert ( re.match( - r"^__setstate__\(self: [\w\.]+, state: [\w\.]+, /\)", + r"^__setstate__\(self: [\w\.]+, state: [\w\. \|]+, /\)", enum_type.__setstate__.__doc__, ) is not None diff --git a/tests/test_factory_constructors.py b/tests/test_factory_constructors.py index 67f859b9a..c6ae98c7f 100644 --- a/tests/test_factory_constructors.py +++ b/tests/test_factory_constructors.py @@ -78,10 +78,10 @@ def test_init_factory_signature(msg): msg(excinfo.value) == """ __init__(): incompatible constructor arguments. The following argument types are supported: - 1. m.factory_constructors.TestFactory1(arg0: m.factory_constructors.tag.unique_ptr_tag, arg1: typing.SupportsInt) + 1. m.factory_constructors.TestFactory1(arg0: m.factory_constructors.tag.unique_ptr_tag, arg1: typing.SupportsInt | typing.SupportsIndex) 2. m.factory_constructors.TestFactory1(arg0: str) 3. m.factory_constructors.TestFactory1(arg0: m.factory_constructors.tag.pointer_tag) - 4. m.factory_constructors.TestFactory1(arg0: object, arg1: typing.SupportsInt, arg2: object) + 4. m.factory_constructors.TestFactory1(arg0: object, arg1: typing.SupportsInt | typing.SupportsIndex, arg2: object) Invoked with: 'invalid', 'constructor', 'arguments' """ @@ -93,13 +93,13 @@ def test_init_factory_signature(msg): __init__(*args, **kwargs) Overloaded function. - 1. __init__(self: m.factory_constructors.TestFactory1, arg0: m.factory_constructors.tag.unique_ptr_tag, arg1: typing.SupportsInt) -> None + 1. __init__(self: m.factory_constructors.TestFactory1, arg0: m.factory_constructors.tag.unique_ptr_tag, arg1: typing.SupportsInt | typing.SupportsIndex) -> None 2. __init__(self: m.factory_constructors.TestFactory1, arg0: str) -> None 3. __init__(self: m.factory_constructors.TestFactory1, arg0: m.factory_constructors.tag.pointer_tag) -> None - 4. __init__(self: m.factory_constructors.TestFactory1, arg0: object, arg1: typing.SupportsInt, arg2: object) -> None + 4. __init__(self: m.factory_constructors.TestFactory1, arg0: object, arg1: typing.SupportsInt | typing.SupportsIndex, arg2: object) -> None """ ) diff --git a/tests/test_kwargs_and_defaults.py b/tests/test_kwargs_and_defaults.py index b62e4b741..57345f128 100644 --- a/tests/test_kwargs_and_defaults.py +++ b/tests/test_kwargs_and_defaults.py @@ -9,28 +9,28 @@ from pybind11_tests import kwargs_and_defaults as m def test_function_signatures(doc): assert ( doc(m.kw_func0) - == "kw_func0(arg0: typing.SupportsInt, arg1: typing.SupportsInt) -> str" + == "kw_func0(arg0: typing.SupportsInt | typing.SupportsIndex, arg1: typing.SupportsInt | typing.SupportsIndex) -> str" ) assert ( doc(m.kw_func1) - == "kw_func1(x: typing.SupportsInt, y: typing.SupportsInt) -> str" + == "kw_func1(x: typing.SupportsInt | typing.SupportsIndex, y: typing.SupportsInt | typing.SupportsIndex) -> str" ) assert ( doc(m.kw_func2) - == "kw_func2(x: typing.SupportsInt = 100, y: typing.SupportsInt = 200) -> str" + == "kw_func2(x: typing.SupportsInt | typing.SupportsIndex = 100, y: typing.SupportsInt | typing.SupportsIndex = 200) -> str" ) assert doc(m.kw_func3) == "kw_func3(data: str = 'Hello world!') -> None" assert ( doc(m.kw_func4) - == "kw_func4(myList: collections.abc.Sequence[typing.SupportsInt] = [13, 17]) -> str" + == "kw_func4(myList: collections.abc.Sequence[typing.SupportsInt | typing.SupportsIndex] = [13, 17]) -> str" ) assert ( doc(m.kw_func_udl) - == "kw_func_udl(x: typing.SupportsInt, y: typing.SupportsInt = 300) -> str" + == "kw_func_udl(x: typing.SupportsInt | typing.SupportsIndex, y: typing.SupportsInt | typing.SupportsIndex = 300) -> str" ) assert ( doc(m.kw_func_udl_z) - == "kw_func_udl_z(x: typing.SupportsInt, y: typing.SupportsInt = 0) -> str" + == "kw_func_udl_z(x: typing.SupportsInt | typing.SupportsIndex, y: typing.SupportsInt | typing.SupportsIndex = 0) -> str" ) assert doc(m.args_function) == "args_function(*args) -> tuple" assert ( @@ -42,11 +42,11 @@ def test_function_signatures(doc): ) assert ( doc(m.KWClass.foo0) - == "foo0(self: m.kwargs_and_defaults.KWClass, arg0: typing.SupportsInt, arg1: typing.SupportsFloat) -> None" + == "foo0(self: m.kwargs_and_defaults.KWClass, arg0: typing.SupportsInt | typing.SupportsIndex, arg1: typing.SupportsFloat | typing.SupportsIndex) -> None" ) assert ( doc(m.KWClass.foo1) - == "foo1(self: m.kwargs_and_defaults.KWClass, x: typing.SupportsInt, y: typing.SupportsFloat) -> None" + == "foo1(self: m.kwargs_and_defaults.KWClass, x: typing.SupportsInt | typing.SupportsIndex, y: typing.SupportsFloat | typing.SupportsIndex) -> None" ) assert ( doc(m.kw_lb_func0) @@ -138,7 +138,7 @@ def test_mixed_args_and_kwargs(msg): msg(excinfo.value) == """ mixed_plus_args(): incompatible function arguments. The following argument types are supported: - 1. (arg0: typing.SupportsInt, arg1: typing.SupportsFloat, *args) -> tuple + 1. (arg0: typing.SupportsInt | typing.SupportsIndex, arg1: typing.SupportsFloat | typing.SupportsIndex, *args) -> tuple Invoked with: 1 """ @@ -149,7 +149,7 @@ def test_mixed_args_and_kwargs(msg): msg(excinfo.value) == """ mixed_plus_args(): incompatible function arguments. The following argument types are supported: - 1. (arg0: typing.SupportsInt, arg1: typing.SupportsFloat, *args) -> tuple + 1. (arg0: typing.SupportsInt | typing.SupportsIndex, arg1: typing.SupportsFloat | typing.SupportsIndex, *args) -> tuple Invoked with: """ @@ -183,7 +183,7 @@ def test_mixed_args_and_kwargs(msg): msg(excinfo.value) == """ mixed_plus_args_kwargs_defaults(): incompatible function arguments. The following argument types are supported: - 1. (i: typing.SupportsInt = 1, j: typing.SupportsFloat = 3.14159, *args, **kwargs) -> tuple + 1. (i: typing.SupportsInt | typing.SupportsIndex = 1, j: typing.SupportsFloat | typing.SupportsIndex = 3.14159, *args, **kwargs) -> tuple Invoked with: 1; kwargs: i=1 """ @@ -194,7 +194,7 @@ def test_mixed_args_and_kwargs(msg): msg(excinfo.value) == """ mixed_plus_args_kwargs_defaults(): incompatible function arguments. The following argument types are supported: - 1. (i: typing.SupportsInt = 1, j: typing.SupportsFloat = 3.14159, *args, **kwargs) -> tuple + 1. (i: typing.SupportsInt | typing.SupportsIndex = 1, j: typing.SupportsFloat | typing.SupportsIndex = 3.14159, *args, **kwargs) -> tuple Invoked with: 1, 2; kwargs: j=1 """ @@ -211,7 +211,7 @@ def test_mixed_args_and_kwargs(msg): msg(excinfo.value) == """ args_kwonly(): incompatible function arguments. The following argument types are supported: - 1. (i: typing.SupportsInt, j: typing.SupportsFloat, *args, z: typing.SupportsInt) -> tuple + 1. (i: typing.SupportsInt | typing.SupportsIndex, j: typing.SupportsFloat | typing.SupportsIndex, *args, z: typing.SupportsInt | typing.SupportsIndex) -> tuple Invoked with: 2, 2.5, 22 """ @@ -233,12 +233,12 @@ def test_mixed_args_and_kwargs(msg): ) assert ( m.args_kwonly_kwargs.__doc__ - == "args_kwonly_kwargs(i: typing.SupportsInt, j: typing.SupportsFloat, *args, z: typing.SupportsInt, **kwargs) -> tuple\n" + == "args_kwonly_kwargs(i: typing.SupportsInt | typing.SupportsIndex, j: typing.SupportsFloat | typing.SupportsIndex, *args, z: typing.SupportsInt | typing.SupportsIndex, **kwargs) -> tuple\n" ) assert ( m.args_kwonly_kwargs_defaults.__doc__ - == "args_kwonly_kwargs_defaults(i: typing.SupportsInt = 1, j: typing.SupportsFloat = 3.14159, *args, z: typing.SupportsInt = 42, **kwargs) -> tuple\n" + == "args_kwonly_kwargs_defaults(i: typing.SupportsInt | typing.SupportsIndex = 1, j: typing.SupportsFloat | typing.SupportsIndex = 3.14159, *args, z: typing.SupportsInt | typing.SupportsIndex = 42, **kwargs) -> tuple\n" ) assert m.args_kwonly_kwargs_defaults() == (1, 3.14159, (), 42, {}) assert m.args_kwonly_kwargs_defaults(2) == (2, 3.14159, (), 42, {}) @@ -294,11 +294,11 @@ def test_keyword_only_args(msg): x.method(i=1, j=2) assert ( m.first_arg_kw_only.__init__.__doc__ - == "__init__(self: pybind11_tests.kwargs_and_defaults.first_arg_kw_only, *, i: typing.SupportsInt = 0) -> None\n" + == "__init__(self: pybind11_tests.kwargs_and_defaults.first_arg_kw_only, *, i: typing.SupportsInt | typing.SupportsIndex = 0) -> None\n" ) assert ( m.first_arg_kw_only.method.__doc__ - == "method(self: pybind11_tests.kwargs_and_defaults.first_arg_kw_only, *, i: typing.SupportsInt = 1, j: typing.SupportsInt = 2) -> None\n" + == "method(self: pybind11_tests.kwargs_and_defaults.first_arg_kw_only, *, i: typing.SupportsInt | typing.SupportsIndex = 1, j: typing.SupportsInt | typing.SupportsIndex = 2) -> None\n" ) @@ -344,7 +344,7 @@ def test_positional_only_args(): # Mix it with args and kwargs: assert ( m.args_kwonly_full_monty.__doc__ - == "args_kwonly_full_monty(arg0: typing.SupportsInt = 1, arg1: typing.SupportsInt = 2, /, j: typing.SupportsFloat = 3.14159, *args, z: typing.SupportsInt = 42, **kwargs) -> tuple\n" + == "args_kwonly_full_monty(arg0: typing.SupportsInt | typing.SupportsIndex = 1, arg1: typing.SupportsInt | typing.SupportsIndex = 2, /, j: typing.SupportsFloat | typing.SupportsIndex = 3.14159, *args, z: typing.SupportsInt | typing.SupportsIndex = 42, **kwargs) -> tuple\n" ) assert m.args_kwonly_full_monty() == (1, 2, 3.14159, (), 42, {}) assert m.args_kwonly_full_monty(8) == (8, 2, 3.14159, (), 42, {}) @@ -387,30 +387,30 @@ def test_positional_only_args(): # https://github.com/pybind/pybind11/pull/3402#issuecomment-963341987 assert ( m.first_arg_kw_only.pos_only.__doc__ - == "pos_only(self: pybind11_tests.kwargs_and_defaults.first_arg_kw_only, /, i: typing.SupportsInt, j: typing.SupportsInt) -> None\n" + == "pos_only(self: pybind11_tests.kwargs_and_defaults.first_arg_kw_only, /, i: typing.SupportsInt | typing.SupportsIndex, j: typing.SupportsInt | typing.SupportsIndex) -> None\n" ) def test_signatures(): assert ( m.kw_only_all.__doc__ - == "kw_only_all(*, i: typing.SupportsInt, j: typing.SupportsInt) -> tuple\n" + == "kw_only_all(*, i: typing.SupportsInt | typing.SupportsIndex, j: typing.SupportsInt | typing.SupportsIndex) -> tuple\n" ) assert ( m.kw_only_mixed.__doc__ - == "kw_only_mixed(i: typing.SupportsInt, *, j: typing.SupportsInt) -> tuple\n" + == "kw_only_mixed(i: typing.SupportsInt | typing.SupportsIndex, *, j: typing.SupportsInt | typing.SupportsIndex) -> tuple\n" ) assert ( m.pos_only_all.__doc__ - == "pos_only_all(i: typing.SupportsInt, j: typing.SupportsInt, /) -> tuple\n" + == "pos_only_all(i: typing.SupportsInt | typing.SupportsIndex, j: typing.SupportsInt | typing.SupportsIndex, /) -> tuple\n" ) assert ( m.pos_only_mix.__doc__ - == "pos_only_mix(i: typing.SupportsInt, /, j: typing.SupportsInt) -> tuple\n" + == "pos_only_mix(i: typing.SupportsInt | typing.SupportsIndex, /, j: typing.SupportsInt | typing.SupportsIndex) -> tuple\n" ) assert ( m.pos_kw_only_mix.__doc__ - == "pos_kw_only_mix(i: typing.SupportsInt, /, j: typing.SupportsInt, *, k: typing.SupportsInt) -> tuple\n" + == "pos_kw_only_mix(i: typing.SupportsInt | typing.SupportsIndex, /, j: typing.SupportsInt | typing.SupportsIndex, *, k: typing.SupportsInt | typing.SupportsIndex) -> tuple\n" ) diff --git a/tests/test_methods_and_attributes.py b/tests/test_methods_and_attributes.py index 6a8d993cb..553d5bfc1 100644 --- a/tests/test_methods_and_attributes.py +++ b/tests/test_methods_and_attributes.py @@ -251,7 +251,7 @@ def test_no_mixed_overloads(): "#define PYBIND11_DETAILED_ERROR_MESSAGES or compile in debug mode for more details" if not detailed_error_messages_enabled else "error while attempting to bind static method ExampleMandA.overload_mixed1" - "(arg0: typing.SupportsFloat) -> str" + "(arg0: typing.SupportsFloat | typing.SupportsIndex) -> str" ) ) @@ -264,7 +264,7 @@ def test_no_mixed_overloads(): "#define PYBIND11_DETAILED_ERROR_MESSAGES or compile in debug mode for more details" if not detailed_error_messages_enabled else "error while attempting to bind instance method ExampleMandA.overload_mixed2" - "(self: pybind11_tests.methods_and_attributes.ExampleMandA, arg0: typing.SupportsInt, arg1: typing.SupportsInt)" + "(self: pybind11_tests.methods_and_attributes.ExampleMandA, arg0: typing.SupportsInt | typing.SupportsIndex, arg1: typing.SupportsInt | typing.SupportsIndex)" " -> str" ) ) @@ -491,7 +491,7 @@ def test_str_issue(msg): msg(excinfo.value) == """ __init__(): incompatible constructor arguments. The following argument types are supported: - 1. m.methods_and_attributes.StrIssue(arg0: typing.SupportsInt) + 1. m.methods_and_attributes.StrIssue(arg0: typing.SupportsInt | typing.SupportsIndex) 2. m.methods_and_attributes.StrIssue() Invoked with: 'no', 'such', 'constructor' @@ -534,21 +534,27 @@ def test_overload_ordering(): assert m.overload_order(0) == 4 assert ( - "1. overload_order(arg0: typing.SupportsInt) -> int" in m.overload_order.__doc__ + "1. overload_order(arg0: typing.SupportsInt | typing.SupportsIndex) -> int" + in m.overload_order.__doc__ ) assert "2. overload_order(arg0: str) -> int" in m.overload_order.__doc__ assert "3. overload_order(arg0: str) -> int" in m.overload_order.__doc__ assert ( - "4. overload_order(arg0: typing.SupportsInt) -> int" in m.overload_order.__doc__ + "4. overload_order(arg0: typing.SupportsInt | typing.SupportsIndex) -> int" + in m.overload_order.__doc__ ) with pytest.raises(TypeError) as err: m.overload_order(1.1) - assert "1. (arg0: typing.SupportsInt) -> int" in str(err.value) + assert "1. (arg0: typing.SupportsInt | typing.SupportsIndex) -> int" in str( + err.value + ) assert "2. (arg0: str) -> int" in str(err.value) assert "3. (arg0: str) -> int" in str(err.value) - assert "4. (arg0: typing.SupportsInt) -> int" in str(err.value) + assert "4. (arg0: typing.SupportsInt | typing.SupportsIndex) -> int" in str( + err.value + ) def test_rvalue_ref_param(): diff --git a/tests/test_numpy_dtypes.py b/tests/test_numpy_dtypes.py index 9f8574280..22814aba5 100644 --- a/tests/test_numpy_dtypes.py +++ b/tests/test_numpy_dtypes.py @@ -367,7 +367,7 @@ def test_complex_array(): def test_signature(doc): assert ( doc(m.create_rec_nested) - == "create_rec_nested(arg0: typing.SupportsInt) -> numpy.typing.NDArray[NestedStruct]" + == "create_rec_nested(arg0: typing.SupportsInt | typing.SupportsIndex) -> numpy.typing.NDArray[NestedStruct]" ) diff --git a/tests/test_numpy_vectorize.py b/tests/test_numpy_vectorize.py index d405e6800..05f7c704f 100644 --- a/tests/test_numpy_vectorize.py +++ b/tests/test_numpy_vectorize.py @@ -211,11 +211,11 @@ def test_passthrough_arguments(doc): "vec_passthrough(" + ", ".join( [ - "arg0: typing.SupportsFloat", + "arg0: typing.SupportsFloat | typing.SupportsIndex", "arg1: typing.Annotated[numpy.typing.ArrayLike, numpy.float64]", "arg2: typing.Annotated[numpy.typing.ArrayLike, numpy.float64]", "arg3: typing.Annotated[numpy.typing.ArrayLike, numpy.int32]", - "arg4: typing.SupportsInt", + "arg4: typing.SupportsInt | typing.SupportsIndex", "arg5: m.numpy_vectorize.NonPODClass", "arg6: typing.Annotated[numpy.typing.ArrayLike, numpy.float64]", ] diff --git a/tests/test_pytypes.py b/tests/test_pytypes.py index c1798f924..09fc5f37e 100644 --- a/tests/test_pytypes.py +++ b/tests/test_pytypes.py @@ -944,14 +944,14 @@ def test_tuple_variable_length_annotations(doc): def test_dict_annotations(doc): assert ( doc(m.annotate_dict_str_int) - == "annotate_dict_str_int(arg0: dict[str, typing.SupportsInt]) -> None" + == "annotate_dict_str_int(arg0: dict[str, typing.SupportsInt | typing.SupportsIndex]) -> None" ) def test_list_annotations(doc): assert ( doc(m.annotate_list_int) - == "annotate_list_int(arg0: list[typing.SupportsInt]) -> None" + == "annotate_list_int(arg0: list[typing.SupportsInt | typing.SupportsIndex]) -> None" ) @@ -969,7 +969,7 @@ def test_iterable_annotations(doc): def test_iterator_annotations(doc): assert ( doc(m.annotate_iterator_int) - == "annotate_iterator_int(arg0: collections.abc.Iterator[typing.SupportsInt]) -> None" + == "annotate_iterator_int(arg0: collections.abc.Iterator[typing.SupportsInt | typing.SupportsIndex]) -> None" ) @@ -989,7 +989,8 @@ def test_fn_return_only(doc): def test_type_annotation(doc): assert ( - doc(m.annotate_type) == "annotate_type(arg0: type[typing.SupportsInt]) -> type" + doc(m.annotate_type) + == "annotate_type(arg0: type[typing.SupportsInt | typing.SupportsIndex]) -> type" ) @@ -1007,7 +1008,7 @@ def test_union_typing_only(doc): def test_union_object_annotations(doc): assert ( doc(m.annotate_union_to_object) - == "annotate_union_to_object(arg0: typing.SupportsInt | str) -> object" + == "annotate_union_to_object(arg0: typing.SupportsInt | typing.SupportsIndex | str) -> object" ) @@ -1044,7 +1045,7 @@ def test_never_annotation(doc, backport_typehints): def test_optional_object_annotations(doc): assert ( doc(m.annotate_optional_to_object) - == "annotate_optional_to_object(arg0: typing.SupportsInt | None) -> object" + == "annotate_optional_to_object(arg0: typing.SupportsInt | typing.SupportsIndex | None) -> object" ) @@ -1167,7 +1168,10 @@ def get_annotations_helper(o): def test_module_attribute_types() -> None: module_annotations = get_annotations_helper(m) - assert module_annotations["list_int"] == "list[typing.SupportsInt]" + assert ( + module_annotations["list_int"] + == "list[typing.SupportsInt | typing.SupportsIndex]" + ) assert module_annotations["set_str"] == "set[str]" assert module_annotations["foo"] == "pybind11_tests.pytypes.foo" @@ -1190,7 +1194,10 @@ def test_get_annotations_compliance() -> None: module_annotations = get_annotations(m) - assert module_annotations["list_int"] == "list[typing.SupportsInt]" + assert ( + module_annotations["list_int"] + == "list[typing.SupportsInt | typing.SupportsIndex]" + ) assert module_annotations["set_str"] == "set[str]" @@ -1204,10 +1211,13 @@ def test_class_attribute_types() -> None: instance_annotations = get_annotations_helper(m.Instance) assert empty_annotations is None - assert static_annotations["x"] == "typing.ClassVar[typing.SupportsFloat]" + assert ( + static_annotations["x"] + == "typing.ClassVar[typing.SupportsFloat | typing.SupportsIndex]" + ) assert ( static_annotations["dict_str_int"] - == "typing.ClassVar[dict[str, typing.SupportsInt]]" + == "typing.ClassVar[dict[str, typing.SupportsInt | typing.SupportsIndex]]" ) assert m.Static.x == 1.0 @@ -1219,7 +1229,7 @@ def test_class_attribute_types() -> None: static.dict_str_int["hi"] = 3 assert m.Static().dict_str_int == {"hi": 3} - assert instance_annotations["y"] == "typing.SupportsFloat" + assert instance_annotations["y"] == "typing.SupportsFloat | typing.SupportsIndex" instance1 = m.Instance() instance1.y = 4.0 @@ -1236,7 +1246,10 @@ def test_class_attribute_types() -> None: def test_redeclaration_attr_with_type_hint() -> None: obj = m.Instance() m.attr_with_type_hint_float_x(obj) - assert get_annotations_helper(obj)["x"] == "typing.SupportsFloat" + assert ( + get_annotations_helper(obj)["x"] + == "typing.SupportsFloat | typing.SupportsIndex" + ) with pytest.raises( RuntimeError, match=r'^__annotations__\["x"\] was set already\.$' ): diff --git a/tests/test_stl.py b/tests/test_stl.py index 4a57635e2..b04f55c9f 100644 --- a/tests/test_stl.py +++ b/tests/test_stl.py @@ -22,7 +22,7 @@ def test_vector(doc): assert doc(m.cast_vector) == "cast_vector() -> list[int]" assert ( doc(m.load_vector) - == "load_vector(arg0: collections.abc.Sequence[typing.SupportsInt]) -> bool" + == "load_vector(arg0: collections.abc.Sequence[typing.SupportsInt | typing.SupportsIndex]) -> bool" ) # Test regression caused by 936: pointers to stl containers weren't castable @@ -51,7 +51,7 @@ def test_array(doc): ) assert ( doc(m.load_array) - == 'load_array(arg0: typing.Annotated[collections.abc.Sequence[typing.SupportsInt], "FixedSize(2)"]) -> bool' + == 'load_array(arg0: typing.Annotated[collections.abc.Sequence[typing.SupportsInt | typing.SupportsIndex], "FixedSize(2)"]) -> bool' ) @@ -72,7 +72,7 @@ def test_valarray(doc): assert doc(m.cast_valarray) == "cast_valarray() -> list[int]" assert ( doc(m.load_valarray) - == "load_valarray(arg0: collections.abc.Sequence[typing.SupportsInt]) -> bool" + == "load_valarray(arg0: collections.abc.Sequence[typing.SupportsInt | typing.SupportsIndex]) -> bool" ) @@ -234,7 +234,7 @@ def test_reference_sensitive_optional(doc): assert ( doc(m.double_or_zero_refsensitive) - == "double_or_zero_refsensitive(arg0: typing.SupportsInt | None) -> int" + == "double_or_zero_refsensitive(arg0: typing.SupportsInt | typing.SupportsIndex | None) -> int" ) assert m.half_or_none_refsensitive(0) is None @@ -352,7 +352,7 @@ def test_variant(doc): assert ( doc(m.load_variant) - == "load_variant(arg0: typing.SupportsInt | str | typing.SupportsFloat | None) -> str" + == "load_variant(arg0: typing.SupportsInt | typing.SupportsIndex | str | typing.SupportsFloat | typing.SupportsIndex | None) -> str" ) @@ -368,7 +368,7 @@ def test_variant_monostate(doc): assert ( doc(m.load_monostate_variant) - == "load_monostate_variant(arg0: None | typing.SupportsInt | str) -> str" + == "load_monostate_variant(arg0: None | typing.SupportsInt | typing.SupportsIndex | str) -> str" ) @@ -388,7 +388,7 @@ def test_stl_pass_by_pointer(msg): msg(excinfo.value) == """ stl_pass_by_pointer(): incompatible function arguments. The following argument types are supported: - 1. (v: collections.abc.Sequence[typing.SupportsInt] = None) -> list[int] + 1. (v: collections.abc.Sequence[typing.SupportsInt | typing.SupportsIndex] = None) -> list[int] Invoked with: """ @@ -400,7 +400,7 @@ def test_stl_pass_by_pointer(msg): msg(excinfo.value) == """ stl_pass_by_pointer(): incompatible function arguments. The following argument types are supported: - 1. (v: collections.abc.Sequence[typing.SupportsInt] = None) -> list[int] + 1. (v: collections.abc.Sequence[typing.SupportsInt | typing.SupportsIndex] = None) -> list[int] Invoked with: None """ @@ -615,7 +615,7 @@ def test_sequence_caster_protocol(doc): # convert mode assert ( doc(m.roundtrip_std_vector_int) - == "roundtrip_std_vector_int(arg0: collections.abc.Sequence[typing.SupportsInt]) -> list[int]" + == "roundtrip_std_vector_int(arg0: collections.abc.Sequence[typing.SupportsInt | typing.SupportsIndex]) -> list[int]" ) assert m.roundtrip_std_vector_int([1, 2, 3]) == [1, 2, 3] assert m.roundtrip_std_vector_int((1, 2, 3)) == [1, 2, 3] @@ -668,7 +668,7 @@ def test_mapping_caster_protocol(doc): # convert mode assert ( doc(m.roundtrip_std_map_str_int) - == "roundtrip_std_map_str_int(arg0: collections.abc.Mapping[str, typing.SupportsInt]) -> dict[str, int]" + == "roundtrip_std_map_str_int(arg0: collections.abc.Mapping[str, typing.SupportsInt | typing.SupportsIndex]) -> dict[str, int]" ) assert m.roundtrip_std_map_str_int(a1b2c3) == a1b2c3 assert m.roundtrip_std_map_str_int(FormalMappingLike(**a1b2c3)) == a1b2c3 @@ -714,7 +714,7 @@ def test_set_caster_protocol(doc): # convert mode assert ( doc(m.roundtrip_std_set_int) - == "roundtrip_std_set_int(arg0: collections.abc.Set[typing.SupportsInt]) -> set[int]" + == "roundtrip_std_set_int(arg0: collections.abc.Set[typing.SupportsInt | typing.SupportsIndex]) -> set[int]" ) assert m.roundtrip_std_set_int({1, 2, 3}) == {1, 2, 3} assert m.roundtrip_std_set_int(FormalSetLike(1, 2, 3)) == {1, 2, 3} diff --git a/tests/test_type_caster_pyobject_ptr.py b/tests/test_type_caster_pyobject_ptr.py index 5df8ca019..f9abd0063 100644 --- a/tests/test_type_caster_pyobject_ptr.py +++ b/tests/test_type_caster_pyobject_ptr.py @@ -103,7 +103,8 @@ def test_return_list_pyobject_ptr_reference(): def test_type_caster_name_via_incompatible_function_arguments_type_error(): with pytest.raises( - TypeError, match=r"1\. \(arg0: object, arg1: typing.SupportsInt\) -> None" + TypeError, + match=r"1\. \(arg0: object, arg1: typing.SupportsInt \| typing.SupportsIndex\) -> None", ): m.pass_pyobject_ptr_and_int(ValueHolder(101), ValueHolder(202)) From 1ccaad5b12e0c77221ea7d929bd74b280fcbc3b0 Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Mon, 10 Nov 2025 23:28:09 -0500 Subject: [PATCH 35/47] chore: log_level is better than log_cli_level (#5890) --- tests/pytest.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/pytest.ini b/tests/pytest.ini index 6ca7a9136..6147c6be2 100644 --- a/tests/pytest.ini +++ b/tests/pytest.ini @@ -9,7 +9,7 @@ addopts = --capture=sys # Show local info when a failure occurs --showlocals -log_cli_level = info +log_level = INFO filterwarnings = # make warnings into errors but ignore certain third-party extension issues error From 3370fe14b7079df143eec4781e6e5c1d293e7eec Mon Sep 17 00:00:00 2001 From: Rangsiman Ketkaew Date: Tue, 11 Nov 2025 05:28:23 +0100 Subject: [PATCH 36/47] Enhance: edit doc py::native_enum feature in upgrade.rst (#5885) * Enhance: edit doc py::native_enum feature in upgrade.rst Added information about the inclusion requirement for py::native_enum feature. * [skip ci] Polish wording --------- Co-authored-by: Ralf W. Grosse-Kunstleve --- docs/upgrade.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/upgrade.rst b/docs/upgrade.rst index 9b373fc26..966a319a8 100644 --- a/docs/upgrade.rst +++ b/docs/upgrade.rst @@ -48,6 +48,8 @@ C++ enumerations as native Python types β€” typically standard-library ``enum.Enum`` or related subclasses. This provides improved integration with Python's enum system, compared to the older (now deprecated) ``py::enum_``. See `#5555 `_ for details. +Note that ``#include `` is not included automatically +and must be added explicitly. Functions exposed with pybind11 are now pickleable. This removes a long-standing obstacle when using pybind11-bound functions with Python features From b30e72c6f6185e87d11bfe9b092d5271f14b7851 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Tue, 11 Nov 2025 19:27:53 -0800 Subject: [PATCH 37/47] Replace env.deprecated_call() with pytest.deprecated_call() (#5893) Since we already require pytest>=6 (see tests/requirements.txt), the old compatibility function is obsolete and pytest.deprecated_call() can be used directly. Extracted from PR #5879 Co-authored-by: Michael Carlstrom Co-authored-by: gentlegiantJGC --- tests/env.py | 18 ------------------ tests/test_builtin_casters.py | 4 ++-- 2 files changed, 2 insertions(+), 20 deletions(-) diff --git a/tests/env.py b/tests/env.py index 177392591..ccb1fd30b 100644 --- a/tests/env.py +++ b/tests/env.py @@ -4,8 +4,6 @@ import platform import sys import sysconfig -import pytest - ANDROID = sys.platform.startswith("android") LINUX = sys.platform.startswith("linux") MACOS = sys.platform.startswith("darwin") @@ -29,19 +27,3 @@ TYPES_ARE_IMMORTAL = ( or GRAALPY or (CPYTHON and PY_GIL_DISABLED and (3, 13) <= sys.version_info < (3, 14)) ) - - -def deprecated_call(): - """ - pytest.deprecated_call() seems broken in pytest<3.9.x; concretely, it - doesn't work on CPython 3.8.0 with pytest==3.3.2 on Ubuntu 18.04 (#2922). - - This is a narrowed reimplementation of the following PR :( - https://github.com/pytest-dev/pytest/pull/4104 - """ - # TODO: Remove this when testing requires pytest>=3.9. - pieces = pytest.__version__.split(".") - pytest_major_minor = (int(pieces[0]), int(pieces[1])) - if pytest_major_minor < (3, 9): - return pytest.warns((DeprecationWarning, PendingDeprecationWarning)) - return pytest.deprecated_call() diff --git a/tests/test_builtin_casters.py b/tests/test_builtin_casters.py index ae1b1bd17..23c191cec 100644 --- a/tests/test_builtin_casters.py +++ b/tests/test_builtin_casters.py @@ -304,7 +304,7 @@ def test_int_convert(doc): # TODO: Avoid DeprecationWarning in `PyLong_AsLong` (and similar) # TODO: PyPy 3.8 does not behave like CPython 3.8 here yet (7.3.7) if sys.version_info < (3, 10) and env.CPYTHON: - with env.deprecated_call(): + with pytest.deprecated_call(): assert convert(Int()) == 42 else: assert convert(Int()) == 42 @@ -377,7 +377,7 @@ def test_numpy_int_convert(): # TODO: PyPy 3.8 does not behave like CPython 3.8 here yet (7.3.7) # https://github.com/pybind/pybind11/issues/3408 if (3, 8) <= sys.version_info < (3, 10) and env.CPYTHON: - with env.deprecated_call(): + with pytest.deprecated_call(): assert convert(np.float32(3.14159)) == 3 else: assert convert(np.float32(3.14159)) == 3 From 8ecf10e8cc19f8d2f24e612e3f04386f07f3e6b3 Mon Sep 17 00:00:00 2001 From: Rostan Date: Fri, 14 Nov 2025 01:29:02 +0100 Subject: [PATCH 38/47] Fix crash in `gil_scoped_acquire` (#5828) * Add a test reproducing the #5827 crash Signed-off-by: Rostan Tabet * Fix #5827 Signed-off-by: Rostan Tabet * Rename PYBIND11_HAS_BARRIER and move it to common.h Signed-off-by: Rostan Tabet * In test_thread.{cpp,py}, rename has_barrier Signed-off-by: Rostan Tabet --------- Signed-off-by: Rostan Tabet --- include/pybind11/detail/common.h | 4 +++ include/pybind11/gil.h | 4 +++ tests/test_scoped_critical_section.cpp | 7 ++--- tests/test_thread.cpp | 38 +++++++++++++++++++++++++- tests/test_thread.py | 12 ++++++++ 5 files changed, 60 insertions(+), 5 deletions(-) diff --git a/include/pybind11/detail/common.h b/include/pybind11/detail/common.h index 07c094300..05d675589 100644 --- a/include/pybind11/detail/common.h +++ b/include/pybind11/detail/common.h @@ -103,6 +103,10 @@ # define PYBIND11_DTOR_CONSTEXPR #endif +#if defined(PYBIND11_CPP20) && defined(__has_include) && __has_include() +# define PYBIND11_HAS_STD_BARRIER 1 +#endif + // Compiler version assertions #if defined(__INTEL_COMPILER) # if __INTEL_COMPILER < 1800 diff --git a/include/pybind11/gil.h b/include/pybind11/gil.h index 4222a035f..9e799b3cf 100644 --- a/include/pybind11/gil.h +++ b/include/pybind11/gil.h @@ -120,7 +120,11 @@ public: pybind11_fail("scoped_acquire::dec_ref(): internal error!"); } # endif + // Make sure that PyThreadState_Clear is not recursively called by finalizers. + // See issue #5827 + ++tstate->gilstate_counter; PyThreadState_Clear(tstate); + --tstate->gilstate_counter; if (active) { PyThreadState_DeleteCurrent(); } diff --git a/tests/test_scoped_critical_section.cpp b/tests/test_scoped_critical_section.cpp index dc9a69e03..7401eb09f 100644 --- a/tests/test_scoped_critical_section.cpp +++ b/tests/test_scoped_critical_section.cpp @@ -7,8 +7,7 @@ #include #include -#if defined(PYBIND11_CPP20) && defined(__has_include) && __has_include() -# define PYBIND11_HAS_BARRIER 1 +#if defined(PYBIND11_HAS_STD_BARRIER) # include #endif @@ -39,7 +38,7 @@ private: std::atomic_bool value_{false}; }; -#if defined(PYBIND11_HAS_BARRIER) +#if defined(PYBIND11_HAS_STD_BARRIER) // Modifying the C/C++ members of a Python object from multiple threads requires a critical section // to ensure thread safety and data integrity. @@ -259,7 +258,7 @@ TEST_SUBMODULE(scoped_critical_section, m) { (void) BoolWrapperHandle.ptr(); // suppress unused variable warning m.attr("has_barrier") = -#ifdef PYBIND11_HAS_BARRIER +#ifdef PYBIND11_HAS_STD_BARRIER true; #else false; diff --git a/tests/test_thread.cpp b/tests/test_thread.cpp index eabf39afa..131bd8771 100644 --- a/tests/test_thread.cpp +++ b/tests/test_thread.cpp @@ -15,6 +15,10 @@ #include #include +#if defined(PYBIND11_HAS_STD_BARRIER) +# include +#endif + namespace py = pybind11; namespace { @@ -34,7 +38,6 @@ EmptyStruct SharedInstance; } // namespace TEST_SUBMODULE(thread, m) { - py::class_(m, "IntStruct").def(py::init([](const int i) { return IntStruct(i); })); // implicitly_convertible uses loader_life_support when an implicit @@ -67,6 +70,39 @@ TEST_SUBMODULE(thread, m) { py::class_(m, "EmptyStruct") .def_readonly_static("SharedInstance", &SharedInstance); +#if defined(PYBIND11_HAS_STD_BARRIER) + // In the free-threaded build, during PyThreadState_Clear, removing the thread from the biased + // reference counting table may call destructors. Make sure that it doesn't crash. + m.def("test_pythread_state_clear_destructor", [](py::type cls) { + py::handle obj; + + std::barrier barrier{2}; + std::thread thread1{[&]() { + py::gil_scoped_acquire gil; + obj = cls().release(); + barrier.arrive_and_wait(); + }}; + std::thread thread2{[&]() { + py::gil_scoped_acquire gil; + barrier.arrive_and_wait(); + // ob_ref_shared becomes negative; transition to the queued state + obj.dec_ref(); + }}; + + // jthread is not supported by Apple Clang + thread1.join(); + thread2.join(); + }); +#endif + + m.attr("defined_PYBIND11_HAS_STD_BARRIER") = +#ifdef PYBIND11_HAS_STD_BARRIER + true; +#else + false; +#endif + m.def("acquire_gil", []() { py::gil_scoped_acquire gil_acquired; }); + // NOTE: std::string_view also uses loader_life_support to ensure that // the string contents remain alive, but that's a C++ 17 feature. } diff --git a/tests/test_thread.py b/tests/test_thread.py index e9d7bafb2..d302c382c 100644 --- a/tests/test_thread.py +++ b/tests/test_thread.py @@ -5,6 +5,7 @@ import threading import pytest +import env from pybind11_tests import thread as m @@ -66,3 +67,14 @@ def test_bind_shared_instance(): thread.start() for thread in threads: thread.join() + + +@pytest.mark.skipif(sys.platform.startswith("emscripten"), reason="Requires threads") +@pytest.mark.skipif(not m.defined_PYBIND11_HAS_STD_BARRIER, reason="no ") +@pytest.mark.skipif(env.sys_is_gil_enabled(), reason="Deadlock with the GIL") +def test_pythread_state_clear_destructor(): + class Foo: + def __del__(self): + m.acquire_gil() + + m.test_pythread_state_clear_destructor(Foo) From 42cda7570e658beadc036be7848b60e64c374597 Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Thu, 13 Nov 2025 21:03:53 -0800 Subject: [PATCH 39/47] Fix `*args/**kwargs` return types. Add type hinting to `py::make_tuple` (#5881) * Type hint make_tuple / fix *args/**kwargs return type Signed-off-by: Michael Carlstrom * add back commented out panic * ignore return std move clang Signed-off-by: Michael Carlstrom * fix for mingmw Signed-off-by: Michael Carlstrom * added missing case Signed-off-by: Michael Carlstrom --------- Signed-off-by: Michael Carlstrom --- include/pybind11/cast.h | 27 +++++++++++++++++++------ include/pybind11/detail/init.h | 12 ++++++++--- include/pybind11/pybind11.h | 3 ++- include/pybind11/typing.h | 5 +---- tests/test_factory_constructors.cpp | 6 +++++- tests/test_kwargs_and_defaults.py | 31 +++++++++++++++-------------- 6 files changed, 54 insertions(+), 30 deletions(-) diff --git a/include/pybind11/cast.h b/include/pybind11/cast.h index 556bdb7e3..2f5247904 100644 --- a/include/pybind11/cast.h +++ b/include/pybind11/cast.h @@ -1468,21 +1468,24 @@ template <> struct handle_type_name { static constexpr auto name = const_name("weakref.ReferenceType"); }; +// args/Args/kwargs/KWArgs have name as well as typehint included template <> struct handle_type_name { - static constexpr auto name = const_name("*args"); + static constexpr auto name = io_name("*args", "tuple"); }; template struct handle_type_name> { - static constexpr auto name = const_name("*args: ") + make_caster::name; + static constexpr auto name + = io_name("*args: ", "tuple[") + make_caster::name + io_name("", ", ...]"); }; template <> struct handle_type_name { - static constexpr auto name = const_name("**kwargs"); + static constexpr auto name = io_name("**kwargs", "dict[str, typing.Any]"); }; template struct handle_type_name> { - static constexpr auto name = const_name("**kwargs: ") + make_caster::name; + static constexpr auto name + = io_name("**kwargs: ", "dict[str, ") + make_caster::name + io_name("", "]"); }; template <> struct handle_type_name { @@ -1908,13 +1911,20 @@ inline cast_error cast_error_unable_to_convert_call_arg(const std::string &name, } #endif +namespace typing { +template +class Tuple : public tuple { + using tuple::tuple; +}; +} // namespace typing + template -tuple make_tuple() { +typing::Tuple<> make_tuple() { return tuple(0); } template -tuple make_tuple(Args &&...args_) { +typing::Tuple make_tuple(Args &&...args_) { constexpr size_t size = sizeof...(Args); std::array args{{reinterpret_steal( detail::make_caster::cast(std::forward(args_), policy, nullptr))...}}; @@ -1933,7 +1943,12 @@ tuple make_tuple(Args &&...args_) { for (auto &arg_value : args) { PyTuple_SET_ITEM(result.ptr(), counter++, arg_value.release().ptr()); } + PYBIND11_WARNING_PUSH +#ifdef PYBIND11_DETECTED_CLANG_WITH_MISLEADING_CALL_STD_MOVE_EXPLICITLY_WARNING + PYBIND11_WARNING_DISABLE_CLANG("-Wreturn-std-move") +#endif return result; + PYBIND11_WARNING_POP } /// \ingroup annotations diff --git a/include/pybind11/detail/init.h b/include/pybind11/detail/init.h index 9589d74d2..d7c84cb84 100644 --- a/include/pybind11/detail/init.h +++ b/include/pybind11/detail/init.h @@ -501,9 +501,15 @@ template struct pickle_factory { - static_assert(std::is_same, intrinsic_t>::value, - "The type returned by `__getstate__` must be the same " - "as the argument accepted by `__setstate__`"); + using Ret = intrinsic_t; + using Arg = intrinsic_t; + + // Subclasses are now allowed for support between type hint and generic versions of types + // (e.g.) typing::List <--> list + static_assert(std::is_same::value || std::is_base_of::value + || std::is_base_of::value, + "The type returned by `__getstate__` must be the same or subclass of the " + "argument accepted by `__setstate__`"); remove_reference_t get; remove_reference_t set; diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h index 8ab4681c7..5cc9e9e1c 100644 --- a/include/pybind11/pybind11.h +++ b/include/pybind11/pybind11.h @@ -120,7 +120,8 @@ inline std::string generate_function_signature(const char *type_caster_name_fiel const auto c = *pc; if (c == '{') { // Write arg name for everything except *args and **kwargs. - is_starred = *(pc + 1) == '*'; + // Detect {@*args...} or {@**kwargs...} + is_starred = *(pc + 1) == '@' && *(pc + 2) == '*'; if (is_starred) { continue; } diff --git a/include/pybind11/typing.h b/include/pybind11/typing.h index 1715026ef..43e2187b9 100644 --- a/include/pybind11/typing.h +++ b/include/pybind11/typing.h @@ -34,10 +34,7 @@ PYBIND11_NAMESPACE_BEGIN(typing) There is no additional enforcement of types at runtime. */ -template -class Tuple : public tuple { - using tuple::tuple; -}; +// Tuple type hint defined in cast.h for use in py::make_tuple to avoid circular includes template class Dict : public dict { diff --git a/tests/test_factory_constructors.cpp b/tests/test_factory_constructors.cpp index a387cd2e7..e50494b33 100644 --- a/tests/test_factory_constructors.cpp +++ b/tests/test_factory_constructors.cpp @@ -376,10 +376,14 @@ TEST_SUBMODULE(factory_constructors, m) { py::print("noisy placement new"); return p; } - static void operator delete(void *p, size_t) { + static void operator delete(void *p) noexcept { py::print("noisy delete"); ::operator delete(p); } + static void operator delete(void *p, size_t) { + py::print("noisy delete size"); + ::operator delete(p); + } static void operator delete(void *, void *) { py::print("noisy placement delete"); } }; diff --git a/tests/test_kwargs_and_defaults.py b/tests/test_kwargs_and_defaults.py index 57345f128..d41e50558 100644 --- a/tests/test_kwargs_and_defaults.py +++ b/tests/test_kwargs_and_defaults.py @@ -34,11 +34,12 @@ def test_function_signatures(doc): ) assert doc(m.args_function) == "args_function(*args) -> tuple" assert ( - doc(m.args_kwargs_function) == "args_kwargs_function(*args, **kwargs) -> tuple" + doc(m.args_kwargs_function) + == "args_kwargs_function(*args, **kwargs) -> tuple[tuple, dict[str, typing.Any]]" ) assert ( doc(m.args_kwargs_subclass_function) - == "args_kwargs_subclass_function(*args: str, **kwargs: str) -> tuple" + == "args_kwargs_subclass_function(*args: str, **kwargs: str) -> tuple[tuple[str, ...], dict[str, str]]" ) assert ( doc(m.KWClass.foo0) @@ -138,7 +139,7 @@ def test_mixed_args_and_kwargs(msg): msg(excinfo.value) == """ mixed_plus_args(): incompatible function arguments. The following argument types are supported: - 1. (arg0: typing.SupportsInt | typing.SupportsIndex, arg1: typing.SupportsFloat | typing.SupportsIndex, *args) -> tuple + 1. (arg0: typing.SupportsInt | typing.SupportsIndex, arg1: typing.SupportsFloat | typing.SupportsIndex, *args) -> tuple[int, float, tuple] Invoked with: 1 """ @@ -149,7 +150,7 @@ def test_mixed_args_and_kwargs(msg): msg(excinfo.value) == """ mixed_plus_args(): incompatible function arguments. The following argument types are supported: - 1. (arg0: typing.SupportsInt | typing.SupportsIndex, arg1: typing.SupportsFloat | typing.SupportsIndex, *args) -> tuple + 1. (arg0: typing.SupportsInt | typing.SupportsIndex, arg1: typing.SupportsFloat | typing.SupportsIndex, *args) -> tuple[int, float, tuple] Invoked with: """ @@ -183,7 +184,7 @@ def test_mixed_args_and_kwargs(msg): msg(excinfo.value) == """ mixed_plus_args_kwargs_defaults(): incompatible function arguments. The following argument types are supported: - 1. (i: typing.SupportsInt | typing.SupportsIndex = 1, j: typing.SupportsFloat | typing.SupportsIndex = 3.14159, *args, **kwargs) -> tuple + 1. (i: typing.SupportsInt | typing.SupportsIndex = 1, j: typing.SupportsFloat | typing.SupportsIndex = 3.14159, *args, **kwargs) -> tuple[int, float, tuple, dict[str, typing.Any]] Invoked with: 1; kwargs: i=1 """ @@ -194,7 +195,7 @@ def test_mixed_args_and_kwargs(msg): msg(excinfo.value) == """ mixed_plus_args_kwargs_defaults(): incompatible function arguments. The following argument types are supported: - 1. (i: typing.SupportsInt | typing.SupportsIndex = 1, j: typing.SupportsFloat | typing.SupportsIndex = 3.14159, *args, **kwargs) -> tuple + 1. (i: typing.SupportsInt | typing.SupportsIndex = 1, j: typing.SupportsFloat | typing.SupportsIndex = 3.14159, *args, **kwargs) -> tuple[int, float, tuple, dict[str, typing.Any]] Invoked with: 1, 2; kwargs: j=1 """ @@ -211,7 +212,7 @@ def test_mixed_args_and_kwargs(msg): msg(excinfo.value) == """ args_kwonly(): incompatible function arguments. The following argument types are supported: - 1. (i: typing.SupportsInt | typing.SupportsIndex, j: typing.SupportsFloat | typing.SupportsIndex, *args, z: typing.SupportsInt | typing.SupportsIndex) -> tuple + 1. (i: typing.SupportsInt | typing.SupportsIndex, j: typing.SupportsFloat | typing.SupportsIndex, *args, z: typing.SupportsInt | typing.SupportsIndex) -> tuple[int, float, tuple, int] Invoked with: 2, 2.5, 22 """ @@ -233,12 +234,12 @@ def test_mixed_args_and_kwargs(msg): ) assert ( m.args_kwonly_kwargs.__doc__ - == "args_kwonly_kwargs(i: typing.SupportsInt | typing.SupportsIndex, j: typing.SupportsFloat | typing.SupportsIndex, *args, z: typing.SupportsInt | typing.SupportsIndex, **kwargs) -> tuple\n" + == "args_kwonly_kwargs(i: typing.SupportsInt | typing.SupportsIndex, j: typing.SupportsFloat | typing.SupportsIndex, *args, z: typing.SupportsInt | typing.SupportsIndex, **kwargs) -> tuple[int, float, tuple, int, dict[str, typing.Any]]\n" ) assert ( m.args_kwonly_kwargs_defaults.__doc__ - == "args_kwonly_kwargs_defaults(i: typing.SupportsInt | typing.SupportsIndex = 1, j: typing.SupportsFloat | typing.SupportsIndex = 3.14159, *args, z: typing.SupportsInt | typing.SupportsIndex = 42, **kwargs) -> tuple\n" + == "args_kwonly_kwargs_defaults(i: typing.SupportsInt | typing.SupportsIndex = 1, j: typing.SupportsFloat | typing.SupportsIndex = 3.14159, *args, z: typing.SupportsInt | typing.SupportsIndex = 42, **kwargs) -> tuple[int, float, tuple, int, dict[str, typing.Any]]\n" ) assert m.args_kwonly_kwargs_defaults() == (1, 3.14159, (), 42, {}) assert m.args_kwonly_kwargs_defaults(2) == (2, 3.14159, (), 42, {}) @@ -344,7 +345,7 @@ def test_positional_only_args(): # Mix it with args and kwargs: assert ( m.args_kwonly_full_monty.__doc__ - == "args_kwonly_full_monty(arg0: typing.SupportsInt | typing.SupportsIndex = 1, arg1: typing.SupportsInt | typing.SupportsIndex = 2, /, j: typing.SupportsFloat | typing.SupportsIndex = 3.14159, *args, z: typing.SupportsInt | typing.SupportsIndex = 42, **kwargs) -> tuple\n" + == "args_kwonly_full_monty(arg0: typing.SupportsInt | typing.SupportsIndex = 1, arg1: typing.SupportsInt | typing.SupportsIndex = 2, /, j: typing.SupportsFloat | typing.SupportsIndex = 3.14159, *args, z: typing.SupportsInt | typing.SupportsIndex = 42, **kwargs) -> tuple[int, int, float, tuple, int, dict[str, typing.Any]]\n" ) assert m.args_kwonly_full_monty() == (1, 2, 3.14159, (), 42, {}) assert m.args_kwonly_full_monty(8) == (8, 2, 3.14159, (), 42, {}) @@ -394,23 +395,23 @@ def test_positional_only_args(): def test_signatures(): assert ( m.kw_only_all.__doc__ - == "kw_only_all(*, i: typing.SupportsInt | typing.SupportsIndex, j: typing.SupportsInt | typing.SupportsIndex) -> tuple\n" + == "kw_only_all(*, i: typing.SupportsInt | typing.SupportsIndex, j: typing.SupportsInt | typing.SupportsIndex) -> tuple[int, int]\n" ) assert ( m.kw_only_mixed.__doc__ - == "kw_only_mixed(i: typing.SupportsInt | typing.SupportsIndex, *, j: typing.SupportsInt | typing.SupportsIndex) -> tuple\n" + == "kw_only_mixed(i: typing.SupportsInt | typing.SupportsIndex, *, j: typing.SupportsInt | typing.SupportsIndex) -> tuple[int, int]\n" ) assert ( m.pos_only_all.__doc__ - == "pos_only_all(i: typing.SupportsInt | typing.SupportsIndex, j: typing.SupportsInt | typing.SupportsIndex, /) -> tuple\n" + == "pos_only_all(i: typing.SupportsInt | typing.SupportsIndex, j: typing.SupportsInt | typing.SupportsIndex, /) -> tuple[int, int]\n" ) assert ( m.pos_only_mix.__doc__ - == "pos_only_mix(i: typing.SupportsInt | typing.SupportsIndex, /, j: typing.SupportsInt | typing.SupportsIndex) -> tuple\n" + == "pos_only_mix(i: typing.SupportsInt | typing.SupportsIndex, /, j: typing.SupportsInt | typing.SupportsIndex) -> tuple[int, int]\n" ) assert ( m.pos_kw_only_mix.__doc__ - == "pos_kw_only_mix(i: typing.SupportsInt | typing.SupportsIndex, /, j: typing.SupportsInt | typing.SupportsIndex, *, k: typing.SupportsInt | typing.SupportsIndex) -> tuple\n" + == "pos_kw_only_mix(i: typing.SupportsInt | typing.SupportsIndex, /, j: typing.SupportsInt | typing.SupportsIndex, *, k: typing.SupportsInt | typing.SupportsIndex) -> tuple[int, int, int]\n" ) From af796d0a99f0cbd9aebb10591257c41a56811cf6 Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Sat, 15 Nov 2025 16:53:15 +0000 Subject: [PATCH 40/47] Don't allow keep_alive or call_guard on properties (#5533) * Don't allow keep_alive or call_guard on properties The def_property family blindly ignore the keep_alive and call_guard arguments passed to them making them confusing to use. This adds a static_assert if either is passed to make it clear it doesn't work. I would prefer this to be a compiler warning but I can't find a way to do that. Is that even possible? * style: pre-commit fixes * Re-run tests --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- include/pybind11/attr.h | 6 ++++++ include/pybind11/pybind11.h | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/include/pybind11/attr.h b/include/pybind11/attr.h index 9b631fa48..f902c7c60 100644 --- a/include/pybind11/attr.h +++ b/include/pybind11/attr.h @@ -702,6 +702,12 @@ struct process_attributes { } }; +template +struct is_keep_alive : std::false_type {}; + +template +struct is_keep_alive> : std::true_type {}; + template using is_call_guard = is_instantiation; diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h index 5cc9e9e1c..60db0a087 100644 --- a/include/pybind11/pybind11.h +++ b/include/pybind11/pybind11.h @@ -2430,6 +2430,12 @@ public: const Extra &...extra) { static_assert(0 == detail::constexpr_sum(std::is_base_of::value...), "Argument annotations are not allowed for properties"); + static_assert(0 == detail::constexpr_sum(detail::is_call_guard::value...), + "def_property family does not currently support call_guard. Use a " + "py::cpp_function instead."); + static_assert(0 == detail::constexpr_sum(detail::is_keep_alive::value...), + "def_property family does not currently support keep_alive. Use a " + "py::cpp_function instead."); auto rec_fget = get_function_record(fget), rec_fset = get_function_record(fset); auto *rec_active = rec_fget; if (rec_fget) { From 665461d06305c034334ef74303de5aaf05d380a7 Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Sat, 29 Nov 2025 06:15:01 +0000 Subject: [PATCH 41/47] Remove enum from bold in doc (#5903) * Remove enum from bold in doc * [skip ci] Remove bold formatting around (see #5528) --------- Co-authored-by: Ralf W. Grosse-Kunstleve --- docs/advanced/functions.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/advanced/functions.rst b/docs/advanced/functions.rst index ff00c9c8a..c647ae0f0 100644 --- a/docs/advanced/functions.rst +++ b/docs/advanced/functions.rst @@ -84,8 +84,8 @@ The following table provides an overview of available policies: | :enum:`return_value_policy::reference_internal` | If the return value is an lvalue reference or a pointer, the parent object | | | (the implicit ``this``, or ``self`` argument of the called method or | | | property) is kept alive for at least the lifespan of the return value. | -| | **Otherwise this policy falls back to :enum:`return_value_policy::move` | -| | (see #5528).** Internally, this policy works just like | +| | **Otherwise this policy falls back to** :enum:`return_value_policy::move` | +| | (see #5528). Internally, this policy works just like | | | :enum:`return_value_policy::reference` but additionally applies a | | | ``keep_alive<0, 1>`` *call policy* (described in the next section) that | | | prevents the parent object from being garbage collected as long as the | From 1fa9fad6d1b3a953e468abbb4e86dcbb79dde171 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 29 Nov 2025 09:57:32 -0800 Subject: [PATCH 42/47] chore(deps): bump the actions group with 5 updates (#5912) * chore(deps): bump the actions group with 5 updates Bumps the actions group with 5 updates: | Package | From | To | | --- | --- | --- | | [actions/checkout](https://github.com/actions/checkout) | `1` | `6` | | [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) | `6` | `7` | | [actions/upload-artifact](https://github.com/actions/upload-artifact) | `4` | `5` | | [actions/download-artifact](https://github.com/actions/download-artifact) | `5` | `6` | | [pypa/cibuildwheel](https://github.com/pypa/cibuildwheel) | `3.1` | `3.3` | Updates `actions/checkout` from 1 to 6 - [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/v1...v6) Updates `astral-sh/setup-uv` from 6 to 7 - [Release notes](https://github.com/astral-sh/setup-uv/releases) - [Commits](https://github.com/astral-sh/setup-uv/compare/v6...v7) Updates `actions/upload-artifact` from 4 to 5 - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/v4...v5) Updates `actions/download-artifact` from 5 to 6 - [Release notes](https://github.com/actions/download-artifact/releases) - [Commits](https://github.com/actions/download-artifact/compare/v5...v6) Updates `pypa/cibuildwheel` from 3.1 to 3.3 - [Release notes](https://github.com/pypa/cibuildwheel/releases) - [Changelog](https://github.com/pypa/cibuildwheel/blob/main/docs/changelog.md) - [Commits](https://github.com/pypa/cibuildwheel/compare/v3.1...v3.3) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major dependency-group: actions - dependency-name: astral-sh/setup-uv dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major dependency-group: actions - dependency-name: actions/upload-artifact dependency-version: '5' dependency-type: direct:production update-type: version-update:semver-major dependency-group: actions - dependency-name: actions/download-artifact dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major dependency-group: actions - dependency-name: pypa/cibuildwheel dependency-version: '3.3' dependency-type: direct:production update-type: version-update:semver-minor dependency-group: actions ... Signed-off-by: dependabot[bot] * Revert to actions/checkout@v1 for Debian * Temporarily disable Android cibuildwheel tests with warning (see #5913) * Changes from quotes to double-quotes (attempt to resolve job failures). * double-quotes in single-quotes * single-quotes, remove hash in string --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Ralf W. Grosse-Kunstleve --- .github/workflows/ci.yml | 34 ++++++++++++------------- .github/workflows/configure.yml | 4 +-- .github/workflows/docs-link.yml | 2 +- .github/workflows/format.yml | 4 +-- .github/workflows/nightlies.yml | 8 +++--- .github/workflows/pip.yml | 14 +++++----- .github/workflows/reusable-standard.yml | 4 +-- .github/workflows/tests-cibw.yml | 18 ++++++++----- .github/workflows/upstream.yml | 2 +- 9 files changed, 48 insertions(+), 42 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 15f031ad6..a3aa638ea 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -182,7 +182,7 @@ jobs: timeout-minutes: 90 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Setup Python ${{ matrix.python-version }} uses: actions/setup-python@v6 @@ -191,7 +191,7 @@ jobs: allow-prereleases: true - name: Install uv - uses: astral-sh/setup-uv@v6 + uses: astral-sh/setup-uv@v7 with: enable-cache: true @@ -245,7 +245,7 @@ jobs: timeout-minutes: 40 container: quay.io/pypa/musllinux_1_2_x86_64:latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: fetch-depth: 0 @@ -283,7 +283,7 @@ jobs: timeout-minutes: 90 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Setup Python ${{ matrix.python-version }} (deadsnakes) uses: deadsnakes/action@v3.2.0 @@ -371,7 +371,7 @@ jobs: timeout-minutes: 90 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Add wget and python3 run: apt-get update && apt-get install -y python3-dev python3-numpy python3-pytest libeigen3-dev @@ -409,7 +409,7 @@ jobs: timeout-minutes: 90 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 # tzdata will try to ask for the timezone, so set the DEBIAN_FRONTEND - name: Install 🐍 3 @@ -433,7 +433,7 @@ jobs: # container: centos:8 # # steps: -# - uses: actions/checkout@v5 +# - uses: actions/checkout@v6 # # - name: Add Python 3 and a few requirements # run: yum update -y && yum install -y git python3-devel python3-numpy python3-pytest make environment-modules @@ -480,7 +480,7 @@ jobs: # tzdata will try to ask for the timezone, so set the DEBIAN_FRONTEND DEBIAN_FRONTEND: 'noninteractive' steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Add NVHPC Repo run: | @@ -542,7 +542,7 @@ jobs: timeout-minutes: 90 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Add Python 3 run: apt-get update; apt-get install -y python3-dev python3-numpy python3-pytest python3-pip libeigen3-dev @@ -606,7 +606,7 @@ jobs: name: "🐍 3 β€’ ICC latest β€’ x64" steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Add apt repo run: | @@ -720,7 +720,7 @@ jobs: steps: - name: Latest actions/checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Add Python 3.8 if: matrix.container == 'almalinux:8' @@ -823,7 +823,7 @@ jobs: timeout-minutes: 90 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - uses: actions/setup-python@v6 with: @@ -870,7 +870,7 @@ jobs: timeout-minutes: 90 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Setup Python ${{ matrix.python }} uses: actions/setup-python@v6 @@ -923,7 +923,7 @@ jobs: timeout-minutes: 90 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Setup Python ${{ matrix.python }} uses: actions/setup-python@v6 @@ -972,7 +972,7 @@ jobs: timeout-minutes: 90 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Setup Python ${{ matrix.python }} uses: actions/setup-python@v6 @@ -1059,7 +1059,7 @@ jobs: mingw-w64-${{matrix.env}}-catch ${{ matrix.extra_install }} - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Configure C++11 # LTO leads to many undefined reference like @@ -1150,7 +1150,7 @@ jobs: run: env - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Set up Clang uses: egor-tensin/setup-clang@v1 diff --git a/.github/workflows/configure.yml b/.github/workflows/configure.yml index c498d39b7..78214ecc5 100644 --- a/.github/workflows/configure.yml +++ b/.github/workflows/configure.yml @@ -48,7 +48,7 @@ jobs: runs-on: ${{ matrix.runs-on }} steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Setup Python 3.11 uses: actions/setup-python@v6 @@ -56,7 +56,7 @@ jobs: python-version: 3.11 - name: Install uv - uses: astral-sh/setup-uv@v6 + uses: astral-sh/setup-uv@v7 - name: Prepare env run: uv pip install --python=python --system -r tests/requirements.txt diff --git a/.github/workflows/docs-link.yml b/.github/workflows/docs-link.yml index 2f397aff9..ea25410cb 100644 --- a/.github/workflows/docs-link.yml +++ b/.github/workflows/docs-link.yml @@ -19,7 +19,7 @@ jobs: runs-on: ubuntu-latest if: github.event.repository.fork == false steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Check for docs changes id: docs_changes diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index a01e60ba4..6bf77324a 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -25,7 +25,7 @@ jobs: name: Format runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - uses: actions/setup-python@v6 with: python-version: "3.x" @@ -40,7 +40,7 @@ jobs: runs-on: ubuntu-latest container: silkeh/clang:20 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Install requirements run: apt-get update && apt-get install -y git python3-dev python3-pytest ninja-build diff --git a/.github/workflows/nightlies.yml b/.github/workflows/nightlies.yml index 3197f5aba..ad4a35152 100644 --- a/.github/workflows/nightlies.yml +++ b/.github/workflows/nightlies.yml @@ -20,12 +20,12 @@ jobs: if: github.repository_owner == 'pybind' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: fetch-depth: 0 - name: Install uv - uses: astral-sh/setup-uv@v6 + uses: astral-sh/setup-uv@v7 - name: Build SDist and wheels run: | @@ -33,7 +33,7 @@ jobs: nox -s build nox -s build_global - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v5 with: name: Packages path: dist/* @@ -44,7 +44,7 @@ jobs: needs: [build_wheel] runs-on: ubuntu-latest steps: - - uses: actions/download-artifact@v5 + - uses: actions/download-artifact@v6 with: name: Packages path: dist diff --git a/.github/workflows/pip.yml b/.github/workflows/pip.yml index a5e7ffb51..8df91a00f 100644 --- a/.github/workflows/pip.yml +++ b/.github/workflows/pip.yml @@ -23,7 +23,7 @@ jobs: runs-on: windows-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Setup 🐍 3.8 uses: actions/setup-python@v6 @@ -31,7 +31,7 @@ jobs: python-version: 3.8 - name: Install uv - uses: astral-sh/setup-uv@v6 + uses: astral-sh/setup-uv@v7 - name: Prepare env run: uv pip install --system -r tests/requirements.txt @@ -47,7 +47,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Setup 🐍 3.8 uses: actions/setup-python@v6 @@ -55,7 +55,7 @@ jobs: python-version: 3.8 - name: Install uv - uses: astral-sh/setup-uv@v6 + uses: astral-sh/setup-uv@v7 - name: Prepare env run: uv pip install --system -r tests/requirements.txt twine nox @@ -72,13 +72,13 @@ jobs: run: twine check dist/* - name: Save standard package - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: standard path: dist/pybind11-* - name: Save global package - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: global path: dist/*global-* @@ -100,7 +100,7 @@ jobs: steps: # Downloads all to directories matching the artifact names - - uses: actions/download-artifact@v5 + - uses: actions/download-artifact@v6 - name: Generate artifact attestation for sdist and wheel uses: actions/attest-build-provenance@v3 diff --git a/.github/workflows/reusable-standard.yml b/.github/workflows/reusable-standard.yml index 8bd0d340e..96b14bdfb 100644 --- a/.github/workflows/reusable-standard.yml +++ b/.github/workflows/reusable-standard.yml @@ -31,7 +31,7 @@ jobs: timeout-minutes: 90 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Setup Python ${{ inputs.python-version }} uses: actions/setup-python@v6 @@ -51,7 +51,7 @@ jobs: run: brew install boost - name: Install uv - uses: astral-sh/setup-uv@v6 + uses: astral-sh/setup-uv@v7 with: enable-cache: true diff --git a/.github/workflows/tests-cibw.yml b/.github/workflows/tests-cibw.yml index f232544bd..44ec48ebe 100644 --- a/.github/workflows/tests-cibw.yml +++ b/.github/workflows/tests-cibw.yml @@ -17,12 +17,12 @@ jobs: name: Pyodide wheel runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: submodules: true fetch-depth: 0 - - uses: pypa/cibuildwheel@v3.1 + - uses: pypa/cibuildwheel@v3.3 env: PYODIDE_BUILD_EXPORTS: whole_archive with: @@ -37,7 +37,7 @@ jobs: matrix: runs-on: [macos-14, macos-13] steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: submodules: true fetch-depth: 0 @@ -45,7 +45,7 @@ jobs: # We have to uninstall first because GH is now using a local tap to build cmake<4, iOS needs cmake>=4 - run: brew uninstall cmake && brew install cmake - - uses: pypa/cibuildwheel@v3.1 + - uses: pypa/cibuildwheel@v3.3 env: CIBW_PLATFORM: ios CIBW_SKIP: cp314-* # https://github.com/pypa/cibuildwheel/issues/2494 @@ -60,7 +60,7 @@ jobs: matrix: runs-on: [macos-latest, macos-13, ubuntu-latest] steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: submodules: true fetch-depth: 0 @@ -70,6 +70,11 @@ jobs: if: contains(matrix.runs-on, 'macos') run: echo "CIBW_TEST_COMMAND=" >> "$GITHUB_ENV" + # NOTE: Android cibuildwheel tests are currently disabled. + # See https://github.com/pybind/pybind11/issues/5913. + - name: "NOTE: Android tests are disabled" + run: echo '::warning::Android cibuildwheel tests are disabled (CIBW_TEST_COMMAND is empty). See issue 5913.' + # https://github.blog/changelog/2024-04-02-github-actions-hardware-accelerated-android-virtualization-now-available/ - name: Enable KVM for Android emulator if: contains(matrix.runs-on, 'ubuntu') @@ -80,8 +85,9 @@ jobs: - run: pipx install patchelf - - uses: pypa/cibuildwheel@v3.1 + - uses: pypa/cibuildwheel@v3.3 env: CIBW_PLATFORM: android + CIBW_TEST_COMMAND: "" # Temporarily disable Android tests; emulator setup is broken (see #5913). with: package-dir: tests diff --git a/.github/workflows/upstream.yml b/.github/workflows/upstream.yml index edc09f51e..15ede7a85 100644 --- a/.github/workflows/upstream.yml +++ b/.github/workflows/upstream.yml @@ -24,7 +24,7 @@ jobs: if: "contains(github.event.pull_request.labels.*.name, 'python dev')" steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Setup Python 3.13 uses: actions/setup-python@v6 From 28ecc9b6a08e82528020719f8ad7187a9791a4fe Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sat, 29 Nov 2025 12:00:15 -0800 Subject: [PATCH 43/47] Disable Android cibuildwheel tests only on `ubuntu-latest` (#5915) * [skip ci] Re-enable Android cibuildwheel tests (refs #5913) * Disable Android cibuildwheel tests only on ubuntu-latest (see #5913, #5914) * [skip ci] Refer to PR 5914 instead of issue 5913 --- .github/workflows/tests-cibw.yml | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/.github/workflows/tests-cibw.yml b/.github/workflows/tests-cibw.yml index 44ec48ebe..02cce9905 100644 --- a/.github/workflows/tests-cibw.yml +++ b/.github/workflows/tests-cibw.yml @@ -70,10 +70,13 @@ jobs: if: contains(matrix.runs-on, 'macos') run: echo "CIBW_TEST_COMMAND=" >> "$GITHUB_ENV" - # NOTE: Android cibuildwheel tests are currently disabled. - # See https://github.com/pybind/pybind11/issues/5913. - - name: "NOTE: Android tests are disabled" - run: echo '::warning::Android cibuildwheel tests are disabled (CIBW_TEST_COMMAND is empty). See issue 5913.' + # Temporarily disable Android tests on ubuntu-latest due to emulator issues. + # See https://github.com/pybind/pybind11/pull/5914. + - name: "NOTE: Android tests are disabled on ubuntu-latest" + if: contains(matrix.runs-on, 'ubuntu') + run: | + echo "CIBW_TEST_COMMAND=" >> "$GITHUB_ENV" + echo '::warning::Android cibuildwheel tests are disabled on ubuntu-latest (CIBW_TEST_COMMAND is empty). See PR 5914.' # https://github.blog/changelog/2024-04-02-github-actions-hardware-accelerated-android-virtualization-now-available/ - name: Enable KVM for Android emulator @@ -88,6 +91,5 @@ jobs: - uses: pypa/cibuildwheel@v3.3 env: CIBW_PLATFORM: android - CIBW_TEST_COMMAND: "" # Temporarily disable Android tests; emulator setup is broken (see #5913). with: package-dir: tests From ab9ac90fcd7a474791e8b197f39b0b93e90fb499 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sat, 29 Nov 2025 13:16:01 -0800 Subject: [PATCH 44/47] Replace deprecated macos-13 runners with macos-15-intel (#5916) --- .github/workflows/ci.yml | 8 ++++---- .github/workflows/configure.yml | 2 +- .github/workflows/tests-cibw.yml | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a3aa638ea..5f2b688a9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -96,22 +96,22 @@ jobs: python-version: 'graalpy-24.1' # No SciPy for macOS ARM - - runs-on: macos-13 + - runs-on: macos-15-intel python-version: '3.8' cmake-args: -DCMAKE_CXX_STANDARD=14 - - runs-on: macos-13 + - runs-on: macos-15-intel python-version: '3.11' cmake-args: -DPYBIND11_TEST_SMART_HOLDER=ON - runs-on: macos-latest python-version: '3.12' cmake-args: -DCMAKE_CXX_STANDARD=17 -DPYBIND11_DISABLE_HANDLE_TYPE_NAME_DEFAULT_IMPLEMENTATION=ON - - runs-on: macos-13 + - runs-on: macos-15-intel python-version: '3.13t' cmake-args: -DCMAKE_CXX_STANDARD=11 - runs-on: macos-latest python-version: '3.14t' cmake-args: -DCMAKE_CXX_STANDARD=20 - - runs-on: macos-13 + - runs-on: macos-15-intel python-version: 'pypy-3.10' cmake-args: -DCMAKE_CXX_STANDARD=17 - runs-on: macos-latest diff --git a/.github/workflows/configure.yml b/.github/workflows/configure.yml index 78214ecc5..cd034c883 100644 --- a/.github/workflows/configure.yml +++ b/.github/workflows/configure.yml @@ -35,7 +35,7 @@ jobs: - runs-on: ubuntu-24.04 cmake: "3.29" - - runs-on: macos-13 + - runs-on: macos-15-intel cmake: "3.15" - runs-on: macos-14 diff --git a/.github/workflows/tests-cibw.yml b/.github/workflows/tests-cibw.yml index 02cce9905..5dfb5dc94 100644 --- a/.github/workflows/tests-cibw.yml +++ b/.github/workflows/tests-cibw.yml @@ -35,7 +35,7 @@ jobs: strategy: fail-fast: false matrix: - runs-on: [macos-14, macos-13] + runs-on: [macos-14, macos-15-intel] steps: - uses: actions/checkout@v6 with: @@ -58,7 +58,7 @@ jobs: strategy: fail-fast: false matrix: - runs-on: [macos-latest, macos-13, ubuntu-latest] + runs-on: [macos-latest, macos-15-intel, ubuntu-latest] steps: - uses: actions/checkout@v6 with: From 734c29b25ea81db06bf8bb8b5e28e59785969154 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sat, 29 Nov 2025 13:16:39 -0800 Subject: [PATCH 45/47] Point main README.rst to CI for supported platforms and compilers (#5910) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [skip ci] Point main README.rst to CI for supported platforms and compilers - **Replace** the hard-coded β€œSupported compilers” and β€œSupported platforms” lists in `README.rst`. - **Point** readers to the current GitHub Actions matrix as the source of truth for tested platforms, compilers, and Python/C++ versions. - **Clarify** that the matrix evolves over time and that configurations users care about can be kept working via contributions. - **Avoid stale documentation**: Enumerating specific compiler and platform versions in the README is both burdensome and error-prone, and tends to drift out of sync with reality. - **Align β€œsupported” with β€œtested”**: In practice, the CI configuration is the only place where we can say with confidence which combinations are exercised. Nearby versions (e.g., adjacent compiler minor releases) will often work, but we cannot test every variant. - **Reflect actual maintenance capacity**: pybind11 is maintained by a small, volunteer-based community, so support is necessarily best-effort. Pointing to CI and inviting contributions better matches how support is provided in practice. - **No behavior change**: This PR updates documentation only. - **Living source of truth**: As CI jobs are added or removed, the linked Actions view will automatically reflect the set of configurations we actively test. Keeping a configuration in CI is the best way to keep it β€œsupported”. * [skip ci] Slight rewording: point out GitHub's limits on concurrent jobs under the free tier (rather than free minutes). --- README.rst | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/README.rst b/README.rst index f30fd6183..26d618f93 100644 --- a/README.rst +++ b/README.rst @@ -120,24 +120,23 @@ goodies: - With little extra effort, C++ types can be pickled and unpickled similar to regular Python objects. -Supported compilers -------------------- +Supported platforms & compilers +------------------------------- -1. Clang/LLVM 3.3 or newer (for Apple Xcode's clang, this is 5.0.0 or - newer) -2. GCC 4.8 or newer -3. Microsoft Visual Studio 2022 or newer (2019 probably works, but was dropped in CI) -4. Intel classic C++ compiler 18 or newer (ICC 20.2 tested in CI) -5. Cygwin/GCC (previously tested on 2.5.1) -6. NVCC (CUDA 11.0 tested in CI) -7. NVIDIA PGI (20.9 tested in CI) +pybind11 is exercised in continuous integration across a range of operating +systems, Python versions, C++ standards, and toolchains. For an up-to-date +view of the combinations we currently test, please see the +`pybind11 GitHub Actions `_ +logs. -Supported Platforms -------------------- - -* Windows, Linux, macOS, and iOS -* CPython 3.8+, Pyodide, PyPy, and GraalPy -* C++11, C++14, C++17, C++20, and C++23 +The test matrix naturally evolves over time as older platforms and compilers +fall out of use and new ones are added by the community. Closely related +versions of a tested compiler or platform will often work as well in practice, +but we cannot promise to validate every possible combination. If a +configuration you rely on is missing from the matrix or regresses, issues and +pull requests to extend coverage are very welcome. At the same time, we need +to balance the size of the test matrix with the available CI resources, +such as GitHub's limits on concurrent jobs under the free tier. About ----- From 55e4bb9135f9ab19cf9af5ae2178eebb9d9ef23e Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sun, 30 Nov 2025 10:01:36 -0800 Subject: [PATCH 46/47] Work around GCC -Warray-bounds false positive in argument_vector (#5908) --- include/pybind11/cast.h | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/include/pybind11/cast.h b/include/pybind11/cast.h index 2f5247904..5ecded36f 100644 --- a/include/pybind11/cast.h +++ b/include/pybind11/cast.h @@ -2162,6 +2162,11 @@ private: template bool load_impl_sequence(function_call &call, index_sequence) { + PYBIND11_WARNING_PUSH +#if !defined(__clang__) && defined(__GNUC__) && __GNUC__ >= 13 + // Work around a GCC -Warray-bounds false positive in argument_vector usage. + PYBIND11_WARNING_DISABLE_GCC("-Warray-bounds") +#endif #ifdef __cpp_fold_expressions if ((... || !std::get(argcasters).load(call.args[Is], call.args_convert[Is]))) { return false; @@ -2173,6 +2178,7 @@ private: } } #endif + PYBIND11_WARNING_POP return true; } From d810d4f039f418239f19c3482cd59f8d7311e227 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 3 Dec 2025 01:34:04 -0500 Subject: [PATCH 47/47] chore(deps): update pre-commit hooks (#5918) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore(deps): update pre-commit hooks updates: - [github.com/pre-commit/mirrors-clang-format: v21.1.2 β†’ v21.1.6](https://github.com/pre-commit/mirrors-clang-format/compare/v21.1.2...v21.1.6) - [github.com/astral-sh/ruff-pre-commit: v0.14.3 β†’ v0.14.7](https://github.com/astral-sh/ruff-pre-commit/compare/v0.14.3...v0.14.7) - [github.com/pre-commit/mirrors-mypy: v1.18.2 β†’ v1.19.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.18.2...v1.19.0) - [github.com/PyCQA/pylint: v4.0.2 β†’ v4.0.4](https://github.com/PyCQA/pylint/compare/v4.0.2...v4.0.4) - [github.com/python-jsonschema/check-jsonschema: 0.34.1 β†’ 0.35.0](https://github.com/python-jsonschema/check-jsonschema/compare/0.34.1...0.35.0) * style: pre-commit fixes --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 10 +++++----- tests/test_buffers.cpp | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7f2c5a697..ba6f3829a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,14 +25,14 @@ repos: # Clang format the codebase automatically - repo: https://github.com/pre-commit/mirrors-clang-format - rev: "v21.1.2" + rev: "v21.1.6" hooks: - id: clang-format types_or: [c++, c, cuda] # Ruff, the Python auto-correcting linter/formatter written in Rust - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.14.3 + rev: v0.14.7 hooks: - id: ruff-check args: ["--fix", "--show-fixes"] @@ -40,7 +40,7 @@ repos: # Check static types with mypy - repo: https://github.com/pre-commit/mirrors-mypy - rev: "v1.18.2" + rev: "v1.19.0" hooks: - id: mypy args: [] @@ -135,14 +135,14 @@ repos: # PyLint has native support - not always usable, but works for us - repo: https://github.com/PyCQA/pylint - rev: "v4.0.2" + rev: "v4.0.4" hooks: - id: pylint files: ^pybind11 # Check schemas on some of our YAML files - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.34.1 + rev: 0.35.0 hooks: - id: check-readthedocs - id: check-github-workflows diff --git a/tests/test_buffers.cpp b/tests/test_buffers.cpp index b11d1bb31..525be80bc 100644 --- a/tests/test_buffers.cpp +++ b/tests/test_buffers.cpp @@ -237,11 +237,11 @@ TEST_SUBMODULE(buffers, m) { } float operator()(py::ssize_t i, py::ssize_t j) const { - return Matrix::operator()(i *m_row_factor, j *m_col_factor); + return Matrix::operator()(i * m_row_factor, j * m_col_factor); } float &operator()(py::ssize_t i, py::ssize_t j) { - return Matrix::operator()(i *m_row_factor, j *m_col_factor); + return Matrix::operator()(i * m_row_factor, j * m_col_factor); } using Matrix::data;