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/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0985b5cf4..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 @@ -179,18 +179,19 @@ jobs: name: "๐Ÿ ${{ matrix.python-version }} โ€ข ${{ matrix.runs-on }} โ€ข x64 inplace C++14" runs-on: ${{ matrix.runs-on }} + timeout-minutes: 90 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - 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 - name: Install uv - uses: astral-sh/setup-uv@v6 + uses: astral-sh/setup-uv@v7 with: enable-cache: true @@ -202,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. @@ -212,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 @@ -242,7 +245,7 @@ jobs: timeout-minutes: 40 container: quay.io/pypa/musllinux_1_2_x86_64:latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: fetch-depth: 0 @@ -277,9 +280,10 @@ 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@v4 + - uses: actions/checkout@v6 - name: Setup Python ${{ matrix.python-version }} (deadsnakes) uses: deadsnakes/action@v3.2.0 @@ -364,9 +368,10 @@ 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@v4 + - 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 @@ -401,9 +406,10 @@ 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@v4 + - uses: actions/checkout@v6 # tzdata will try to ask for the timezone, so set the DEBIAN_FRONTEND - name: Install ๐Ÿ 3 @@ -427,7 +433,7 @@ jobs: # container: centos:8 # # steps: -# - uses: actions/checkout@v4 +# - 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 @@ -468,12 +474,13 @@ 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 DEBIAN_FRONTEND: 'noninteractive' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Add NVHPC Repo run: | @@ -532,9 +539,10 @@ 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@v4 + - 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 @@ -593,11 +601,12 @@ jobs: icc: if: github.event.pull_request.draft == false runs-on: ubuntu-22.04 + timeout-minutes: 90 name: "๐Ÿ 3 โ€ข ICC latest โ€ข x64" steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Add apt repo run: | @@ -707,10 +716,11 @@ jobs: name: "๐Ÿ 3 โ€ข ${{ matrix.container }} โ€ข x64" container: "${{ matrix.container }}" + timeout-minutes: 90 steps: - name: Latest actions/checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Add Python 3.8 if: matrix.container == 'almalinux:8' @@ -765,6 +775,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,11 +820,12 @@ jobs: if: github.event.pull_request.draft == false name: "Documentation build test" runs-on: ubuntu-latest + timeout-minutes: 90 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: python-version: "3.x" @@ -855,12 +867,13 @@ jobs: name: "๐Ÿ ${{ matrix.python }} โ€ข MSVC 2022 โ€ข x86 ${{ matrix.args }}" runs-on: windows-2022 + timeout-minutes: 90 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup Python ${{ matrix.python }} - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python }} architecture: x86 @@ -907,12 +920,13 @@ jobs: name: "๐Ÿ ${{ matrix.python }} โ€ข MSVC 2022 (Debug) โ€ข x86 ${{ matrix.args }}" runs-on: windows-2022 + timeout-minutes: 90 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup Python ${{ matrix.python }} - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python }} architecture: x86 @@ -955,12 +969,13 @@ jobs: name: "๐Ÿ ${{ matrix.python }} โ€ข MSVC 2022 C++20 โ€ข x64" runs-on: windows-2022 + timeout-minutes: 90 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup Python ${{ matrix.python }} - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python }} @@ -1012,6 +1027,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} @@ -1019,8 +1035,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,17 +1057,9 @@ jobs: mingw-w64-${{matrix.env}}-python-pytest mingw-w64-${{matrix.env}}-boost mingw-w64-${{matrix.env}}-catch + ${{ matrix.extra_install }} - - 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 - - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Configure C++11 # LTO leads to many undefined reference like @@ -1126,6 +1141,7 @@ jobs: python: ['3.10'] runs-on: "${{ matrix.os }}" + timeout-minutes: 90 name: "๐Ÿ ${{ matrix.python }} โ€ข ${{ matrix.os }} โ€ข clang-latest" @@ -1134,13 +1150,13 @@ jobs: run: env - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up Clang 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 }} @@ -1189,91 +1205,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/configure.yml b/.github/workflows/configure.yml index 6a3b365de..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 @@ -48,15 +48,15 @@ jobs: runs-on: ${{ matrix.runs-on }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup Python 3.11 - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: 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 d1f1a1726..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@v4 + - 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 9258e2792..6bf77324a 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -25,8 +25,8 @@ jobs: name: Format runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - uses: actions/checkout@v6 + - uses: actions/setup-python@v6 with: python-version: "3.x" - name: Add matchers @@ -40,7 +40,7 @@ jobs: runs-on: ubuntu-latest container: silkeh/clang:20 steps: - - uses: actions/checkout@v4 + - 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/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/nightlies.yml b/.github/workflows/nightlies.yml index 788f0cdc3..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@v4 + - 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 9b6a9dc82..8df91a00f 100644 --- a/.github/workflows/pip.yml +++ b/.github/workflows/pip.yml @@ -23,15 +23,15 @@ jobs: runs-on: windows-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup ๐Ÿ 3.8 - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: 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,15 +47,15 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup ๐Ÿ 3.8 - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: 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,10 +100,10 @@ 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@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..96b14bdfb 100644 --- a/.github/workflows/reusable-standard.yml +++ b/.github/workflows/reusable-standard.yml @@ -28,12 +28,13 @@ jobs: standard: name: ๐Ÿงช runs-on: ${{ inputs.runs-on }} + timeout-minutes: 90 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - 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 @@ -50,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 19fdf85fd..ef495a851 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@v4 + - 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: @@ -35,16 +35,17 @@ jobs: strategy: fail-fast: false matrix: - runs-on: [macos-14, macos-13] + runs-on: [macos-14, macos-15-intel] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: 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 + - uses: pypa/cibuildwheel@v3.3 env: CIBW_PLATFORM: ios with: @@ -56,9 +57,9 @@ 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@v4 + - uses: actions/checkout@v6 with: submodules: true fetch-depth: 0 @@ -68,6 +69,14 @@ jobs: if: contains(matrix.runs-on, 'macos') run: echo "CIBW_TEST_COMMAND=" >> "$GITHUB_ENV" + # 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 if: contains(matrix.runs-on, 'ubuntu') @@ -78,7 +87,7 @@ jobs: - run: pipx install patchelf - - uses: pypa/cibuildwheel@v3.1 + - uses: pypa/cibuildwheel@v3.3 env: CIBW_PLATFORM: android with: diff --git a/.github/workflows/upstream.yml b/.github/workflows/upstream.yml index 389260038..15ede7a85 100644 --- a/.github/workflows/upstream.yml +++ b/.github/workflows/upstream.yml @@ -24,10 +24,10 @@ jobs: if: "contains(github.event.pull_request.labels.*.name, 'python dev')" steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup Python 3.13 - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: "3.13" allow-prereleases: true 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 *~ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8435fbac9..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: "v20.1.8" + 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.12.7 + 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.17.1" + rev: "v1.19.0" 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: "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.33.2 + rev: 0.35.0 hooks: - id: check-readthedocs - id: check-github-workflows diff --git a/CMakeLists.txt b/CMakeLists.txt index ba5f665a2..806330393 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 @@ -187,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/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 ----- 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 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 | 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; } 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/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 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/cast.h b/include/pybind11/cast.h index c635791fe..5ecded36f 100644 --- a/include/pybind11/cast.h +++ b/include/pybind11/cast.h @@ -10,8 +10,10 @@ #pragma once +#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" @@ -92,37 +94,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 @@ -345,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 @@ -906,6 +911,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(); @@ -976,7 +985,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)."); } @@ -984,14 +993,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, @@ -1005,15 +1014,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 @@ -1040,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; @@ -1077,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); @@ -1183,21 +1195,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 @@ -1223,6 +1226,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; @@ -1281,6 +1290,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__)); @@ -1400,7 +1410,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 { @@ -1412,7 +1422,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 { @@ -1458,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 { @@ -1533,6 +1546,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 @@ -1883,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))...}}; @@ -1908,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 @@ -2037,6 +2077,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 +2089,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). @@ -2118,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; @@ -2129,6 +2178,7 @@ private: } } #endif + PYBIND11_WARNING_POP return true; } 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/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/detail/class.h b/include/pybind11/detail/class.h index cd7e87f84..7fe692856 100644 --- a/include/pybind11/detail/class.h +++ b/include/pybind11/detail/class.h @@ -221,10 +221,19 @@ 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); + 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); @@ -314,8 +323,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,10 +340,12 @@ inline void enable_try_inc_ref(PyObject *obj) { return; } } +# endif } #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 +353,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/common.h b/include/pybind11/detail/common.h index 20a659d26..05d675589 100644 --- a/include/pybind11/detail/common.h +++ b/include/pybind11/detail/common.h @@ -19,7 +19,7 @@ /* -- 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. @@ -27,7 +27,7 @@ #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 1a0 +#define PYBIND11_VERSION_PATCH 2a0 /* -- end version constants -- */ #if !defined(Py_PACK_FULL_VERSION) @@ -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 @@ -322,6 +326,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)) @@ -1293,8 +1304,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/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/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/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/detail/internals.h b/include/pybind11/detail/internals.h index 4ea374861..2600d4356 100644 --- a/include/pybind11/detail/internals.h +++ b/include/pybind11/detail/internals.h @@ -176,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; @@ -236,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) @@ -260,7 +275,9 @@ 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; +#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; @@ -269,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); @@ -301,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; }; @@ -336,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. @@ -347,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 "__" @@ -501,8 +554,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. @@ -513,15 +569,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_) { @@ -535,8 +591,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 @@ -548,8 +604,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; } @@ -563,6 +619,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(); @@ -588,12 +647,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_; }; @@ -623,10 +690,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 @@ -654,9 +719,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. 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/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 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 1b23c5c68..c6b80734b 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 @@ -189,35 +205,73 @@ 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_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()) { return it->second; } return nullptr; } -inline detail::type_info *get_global_type_info(const std::type_index &tp) { - return with_internals([&](internals &internals) { - detail::type_info *type_info = nullptr; - auto &types = internals.registered_types_cpp; - auto it = types.find(tp); - if (it != types.end()) { - type_info = it->second; - } - return type_info; - }); +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. + detail::type_info *type_info = nullptr; + auto &internals = get_internals(); +#if PYBIND11_INTERNALS_VERSION >= 12 + auto &fast_types = internals.registered_types_cpp_fast; +#endif + auto &types = internals.registered_types_cpp; +#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 + // 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; + } + 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_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)) { + 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; } @@ -237,9 +291,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); @@ -491,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 + explicit cast_sources(const itype *ptr); + + // 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); @@ -553,16 +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(); } - void *src_raw_void_ptr = const_cast(st.first); - assert(st.second != nullptr); - const detail::type_info *tinfo = st.second; + // 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(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) { @@ -609,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: @@ -641,10 +765,11 @@ handle smart_holder_from_shared_ptr(const std::shared_ptr &src, return none().release(); } - auto src_raw_ptr = src.get(); - assert(st.second != nullptr); - void *src_raw_void_ptr = static_cast(src_raw_ptr); - const detail::type_info *tinfo = st.second; + // 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(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. @@ -657,8 +782,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) { @@ -672,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 { @@ -869,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; @@ -1018,6 +1160,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); @@ -1056,14 +1199,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()); @@ -1129,13 +1272,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. @@ -1149,31 +1292,12 @@ public: } if (convert && cpptype && this_.try_cpp_conduit(src)) { - return true; + return this_.set_foreign_holder(src); } 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; @@ -1468,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 { @@ -1479,6 +1617,13 @@ 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 { + explicit 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) { @@ -1491,50 +1636,25 @@ 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) { + return cast(cast_sources{src}, policy, parent); } - 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); + 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); } template 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[") 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/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/include/pybind11/pybind11.h b/include/pybind11/pybind11.h index 117fecabf..60db0a087 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; } @@ -248,6 +249,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 +525,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. @@ -1048,13 +1097,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. @@ -1636,10 +1686,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 @@ -2137,10 +2191,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); @@ -2368,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) { 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))); } 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) 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_; } 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/CMakeLists.txt b/tests/CMakeLists.txt index 34b22a57a..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") @@ -647,8 +649,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/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 95cc1ac61..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") @@ -18,20 +16,14 @@ _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(): - """ - 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() +TYPES_ARE_IMMORTAL = ( + PYPY + or GRAALPY + or (CPYTHON and PY_GIL_DISABLED and (3, 13) <= sys.version_info < (3, 14)) +) diff --git a/tests/extra_python_package/test_files.py b/tests/extra_python_package/test_files.py index 63e59f65a..1539b171a 100644 --- a/tests/extra_python_package/test_files.py +++ b/tests/extra_python_package/test_files.py @@ -76,12 +76,14 @@ 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", "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..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"; @@ -26,7 +35,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"); @@ -146,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/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 diff --git a/tests/test_buffers.cpp b/tests/test_buffers.cpp index a090c8745..525be80bc 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) + "(*" @@ -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; 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..23c191cec 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): @@ -301,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 @@ -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(): @@ -354,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 @@ -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.cpp b/tests/test_class.cpp index 3d567fc1f..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, ""); @@ -521,6 +528,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"); } }; @@ -577,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 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_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..58c8f72bb --- /dev/null +++ b/tests/test_class_cross_module_use_after_one_module_dealloc.py @@ -0,0 +1,43 @@ +from __future__ import annotations + +import types + +import pytest + +import env +from pybind11_tests import class_cross_module_use_after_one_module_dealloc as m + + +@pytest.mark.skipif( + env.TYPES_ARE_IMMORTAL, reason="can't GC type objects on this platform" +) +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 + pytest.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) 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_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) 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_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_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_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_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_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_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.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_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_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: 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!" diff --git a/tests/test_kwargs_and_defaults.py b/tests/test_kwargs_and_defaults.py index b62e4b741..d41e50558 100644 --- a/tests/test_kwargs_and_defaults.py +++ b/tests/test_kwargs_and_defaults.py @@ -9,44 +9,45 @@ 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 ( - 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) - == "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 +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, arg1: typing.SupportsFloat, *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, arg1: typing.SupportsFloat, *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 = 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[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 = 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[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, 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[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, 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[int, float, tuple, int, dict[str, typing.Any]]\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[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, {}) @@ -294,11 +295,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 +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 = 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[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, {}) @@ -387,30 +388,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[int, int]\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[int, int]\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[int, int]\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[int, int]\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[int, int, int]\n" ) 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""" 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_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 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_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_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_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..09fc5f37e 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,21 +937,21 @@ 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" ) 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" ) @@ -962,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" ) @@ -982,14 +989,15 @@ 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" ) 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]" ) @@ -1000,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" ) @@ -1037,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" ) @@ -1160,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" @@ -1183,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]" @@ -1197,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 @@ -1212,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 @@ -1229,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_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_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_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_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, 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) 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)) 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 50% rename from tests/test_embed/external_module.cpp rename to tests/test_with_catch/external_module.cpp index 3465e8b37..933ee3a6f 100644 --- a/tests/test_embed/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} {}; 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 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():