diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 4d31afe411d..7919c91f0c6 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -5,7 +5,7 @@ contact_links: about: Ask and answer questions about PyO3 on Discussions - name: 🔧 Troubleshooting url: https://github.com/PyO3/pyo3/discussions - about: For troubleshooting help, see the Discussions + about: For troubleshooting help, see the Discussions - name: 👋 Chat - url: https://gitter.im/PyO3/Lobby - about: Engage with PyO3's users and developers on Gitter \ No newline at end of file + url: https://discord.gg/33kcChzH7f + about: Engage with PyO3's users and developers on Discord diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index b344525cabe..11375e966b3 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -8,6 +8,6 @@ Please consider adding the following to your pull request: - docs to all new functions and / or detail in the guide - tests for all new or changed functions -PyO3's CI pipeline will check your pull request. To run its tests +PyO3's CI pipeline will check your pull request, thus make sure you have checked the `Contributing.md` guidelines. To run most of its tests locally, you can run ```nox```. See ```nox --list-sessions``` for a list of supported actions. diff --git a/.github/workflows/benches.yml b/.github/workflows/benches.yml index 5c6e781dae1..572f77efd76 100644 --- a/.github/workflows/benches.yml +++ b/.github/workflows/benches.yml @@ -9,6 +9,10 @@ on: # performance analysis in order to generate initial data. workflow_dispatch: +concurrency: + group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.event.pull_request.number || github.sha }}-benches + cancel-in-progress: true + jobs: benchmarks: runs-on: ubuntu-latest @@ -19,14 +23,11 @@ jobs: with: components: rust-src - - uses: actions/cache@v3 + - uses: Swatinem/rust-cache@v2 with: - path: | - ~/.cargo/registry - ~/.cargo/git - pyo3-benches/target - target - key: cargo-${{ runner.os }}-bench-${{ hashFiles('**/Cargo.toml') }} + workspaces: | + . + pyo3-benches continue-on-error: true - name: Install cargo-codspeed diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index de82dd20c05..40d4a0012fd 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -24,6 +24,7 @@ jobs: build: continue-on-error: ${{ endsWith(inputs.python-version, '-dev') || contains(fromJSON('["3.7", "pypy3.7"]'), inputs.python-version) || inputs.rust == 'beta' || inputs.rust == 'nightly' }} runs-on: ${{ inputs.os }} + if: ${{ !(startsWith(inputs.python-version, 'graalpy') && startsWith(inputs.os, 'windows')) }} steps: - uses: actions/checkout@v4 @@ -32,6 +33,7 @@ jobs: with: python-version: ${{ inputs.python-version }} architecture: ${{ inputs.python-architecture }} + check-latest: ${{ startsWith(inputs.python-version, 'pypy') }} # PyPy can have FFI changes within Python versions, which creates pain in CI - name: Install nox run: python -m pip install --upgrade pip && pip install nox @@ -46,14 +48,13 @@ jobs: - uses: Swatinem/rust-cache@v2 with: - key: cargo-${{ inputs.python-architecture }} - continue-on-error: true + save-if: ${{ github.event_name != 'merge_group' }} - if: inputs.os == 'ubuntu-latest' name: Prepare LD_LIBRARY_PATH (Ubuntu only) run: echo LD_LIBRARY_PATH=${pythonLocation}/lib >> $GITHUB_ENV - - if: inputs.rust == '1.56.0' + - if: inputs.rust == '1.63.0' name: Prepare minimal package versions (MSRV only) run: nox -s set-minimal-package-versions @@ -65,6 +66,7 @@ jobs: run: nox -s docs - name: Build (no features) + if: ${{ !startsWith(inputs.python-version, 'graalpy') }} run: cargo build --lib --tests --no-default-features # --no-default-features when used with `cargo build/test -p` doesn't seem to work! @@ -74,7 +76,7 @@ jobs: cargo build --no-default-features # Run tests (except on PyPy, because no embedding API). - - if: ${{ !startsWith(inputs.python-version, 'pypy') }} + - if: ${{ !startsWith(inputs.python-version, 'pypy') && !startsWith(inputs.python-version, 'graalpy') }} name: Test (no features) run: cargo test --no-default-features --lib --tests @@ -85,6 +87,7 @@ jobs: cargo test --no-default-features - name: Build (all additive features) + if: ${{ !startsWith(inputs.python-version, 'graalpy') }} run: cargo build --lib --tests --no-default-features --features "full ${{ inputs.extra-features }}" - if: ${{ startsWith(inputs.python-version, 'pypy') }} @@ -92,17 +95,17 @@ jobs: run: cargo build --lib --tests --no-default-features --features "abi3-py37 full ${{ inputs.extra-features }}" # Run tests (except on PyPy, because no embedding API). - - if: ${{ !startsWith(inputs.python-version, 'pypy') }} + - if: ${{ !startsWith(inputs.python-version, 'pypy') && !startsWith(inputs.python-version, 'graalpy') }} name: Test run: cargo test --no-default-features --features "full ${{ inputs.extra-features }}" # Run tests again, but in abi3 mode - - if: ${{ !startsWith(inputs.python-version, 'pypy') }} + - if: ${{ !startsWith(inputs.python-version, 'pypy') && !startsWith(inputs.python-version, 'graalpy') }} name: Test (abi3) run: cargo test --no-default-features --features "abi3 full ${{ inputs.extra-features }}" # Run tests again, for abi3-py37 (the minimal Python version) - - if: ${{ (!startsWith(inputs.python-version, 'pypy')) && (inputs.python-version != '3.7') }} + - if: ${{ (!startsWith(inputs.python-version, 'pypy') && !startsWith(inputs.python-version, 'graalpy')) && (inputs.python-version != '3.7') }} name: Test (abi3-py37) run: cargo test --no-default-features --features "abi3-py37 full ${{ inputs.extra-features }}" @@ -118,9 +121,9 @@ jobs: env: CARGO_TARGET_DIR: ${{ github.workspace }}/target - - uses: dorny/paths-filter@v2 + - uses: dorny/paths-filter@v3 # pypy 3.7 and 3.8 are not PEP 3123 compliant so fail checks here - if: ${{ inputs.rust == 'stable' && inputs.python-version != 'pypy3.7' && inputs.python-version != 'pypy3.8' }} + if: ${{ inputs.rust == 'stable' && inputs.python-version != 'pypy3.7' && inputs.python-version != 'pypy3.8' && !startsWith(inputs.python-version, 'graalpy') }} id: ffi-changes with: base: ${{ github.event.pull_request.base.ref || github.event.merge_group.base_ref }} @@ -135,63 +138,9 @@ jobs: - name: Run pyo3-ffi-check # pypy 3.7 and 3.8 are not PEP 3123 compliant so fail checks here, nor # is pypy 3.9 on windows - if: ${{ endsWith(inputs.python-version, '-dev') || (steps.ffi-changes.outputs.changed == 'true' && inputs.rust == 'stable' && inputs.python-version != 'pypy3.7' && inputs.python-version != 'pypy3.8' && !(inputs.python-version == 'pypy3.9' && contains(inputs.os, 'windows'))) }} + if: ${{ endsWith(inputs.python-version, '-dev') || (steps.ffi-changes.outputs.changed == 'true' && inputs.rust == 'stable' && inputs.python-version != 'pypy3.7' && inputs.python-version != 'pypy3.8' && !startsWith(inputs.python-version, 'graalpy') && !(inputs.python-version == 'pypy3.9' && contains(inputs.os, 'windows'))) }} run: nox -s ffi-check - - - name: Test cross compilation - if: ${{ inputs.os == 'ubuntu-latest' && inputs.python-version == '3.9' }} - uses: PyO3/maturin-action@v1 - env: - PYO3_CROSS_LIB_DIR: /opt/python/cp39-cp39/lib - with: - target: aarch64-unknown-linux-gnu - manylinux: auto - args: --release -i python3.9 -m examples/maturin-starter/Cargo.toml - - - run: sudo rm -rf examples/maturin-starter/target - if: ${{ inputs.os == 'ubuntu-latest' && inputs.python-version == '3.9' }} - - name: Test cross compile to same architecture - if: ${{ inputs.os == 'ubuntu-latest' && inputs.python-version == '3.9' }} - uses: PyO3/maturin-action@v1 - env: - PYO3_CROSS_LIB_DIR: /opt/python/cp39-cp39/lib - with: - target: x86_64-unknown-linux-gnu - manylinux: auto - args: --release -i python3.9 -m examples/maturin-starter/Cargo.toml - - - name: Test cross compilation - if: ${{ inputs.os == 'macos-latest' && inputs.python-version == '3.9' }} - uses: PyO3/maturin-action@v1 - with: - target: aarch64-apple-darwin - args: --release -i python3.9 -m examples/maturin-starter/Cargo.toml - - - name: Test cross compile to Windows - if: ${{ inputs.os == 'ubuntu-latest' && inputs.python-version == '3.8' }} - env: - XWIN_ARCH: x86_64 - run: | - set -ex - sudo apt-get install -y mingw-w64 llvm - rustup target add x86_64-pc-windows-gnu x86_64-pc-windows-msvc - pip install cargo-xwin - # abi3 - cargo build --manifest-path examples/maturin-starter/Cargo.toml --features abi3 --target x86_64-pc-windows-gnu - cargo xwin build --manifest-path examples/maturin-starter/Cargo.toml --features abi3 --target x86_64-pc-windows-msvc - # non-abi3 - export PYO3_CROSS_PYTHON_VERSION=3.9 - cargo build --manifest-path examples/maturin-starter/Cargo.toml --features generate-import-lib --target x86_64-pc-windows-gnu - cargo xwin build --manifest-path examples/maturin-starter/Cargo.toml --features generate-import-lib --target x86_64-pc-windows-msvc - - - name: Test cross compile to Windows with maturin - if: ${{ inputs.os == 'ubuntu-latest' && inputs.python-version == '3.8' }} - uses: PyO3/maturin-action@v1 - with: - target: x86_64-pc-windows-gnu - args: -i python3.8 -m examples/maturin-starter/Cargo.toml --features abi3 - env: CARGO_TERM_VERBOSE: true CARGO_BUILD_TARGET: ${{ inputs.rust-target }} diff --git a/.github/workflows/cache-cleanup.yml b/.github/workflows/cache-cleanup.yml new file mode 100644 index 00000000000..02e8a6ab3cb --- /dev/null +++ b/.github/workflows/cache-cleanup.yml @@ -0,0 +1,31 @@ +name: CI Cache Cleanup +on: + pull_request_target: + types: + - closed + +jobs: + cleanup: + runs-on: ubuntu-latest + permissions: + actions: write + steps: + - name: Cleanup + run: | + gh extension install actions/gh-actions-cache + + echo "Fetching list of cache key" + cacheKeysForPR=$(gh actions-cache list -R $REPO -B $BRANCH -L 100 | cut -f 1 ) + + ## Setting this to not fail the workflow while deleting cache keys. + set +e + echo "Deleting caches..." + for cacheKey in $cacheKeysForPR + do + gh actions-cache delete -R $REPO -B $BRANCH --confirm -- $cacheKey + done + echo "Done" + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REPO: ${{ github.repository }} + BRANCH: refs/pull/${{ github.event.pull_request.number }}/merge diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f97cc37cf0f..5bf2b7ea1e8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,7 +18,6 @@ env: jobs: fmt: - if: github.ref != 'refs/heads/main' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -44,12 +43,11 @@ jobs: check-msrv: needs: [fmt] runs-on: ubuntu-latest - if: github.ref != 'refs/heads/main' steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@master with: - toolchain: 1.56.0 + toolchain: 1.63.0 targets: x86_64-unknown-linux-gnu components: rust-src - uses: actions/setup-python@v5 @@ -57,8 +55,7 @@ jobs: architecture: "x64" - uses: Swatinem/rust-cache@v2 with: - key: check-msrv-1.56.0 - continue-on-error: true + save-if: ${{ github.event_name != 'merge_group' }} - run: python -m pip install --upgrade pip && pip install nox - name: Prepare minimal package versions run: nox -s set-minimal-package-versions @@ -70,7 +67,6 @@ jobs: clippy: needs: [fmt] runs-on: ${{ matrix.platform.os }} - if: github.ref != 'refs/heads/main' strategy: # If one platform fails, allow the rest to keep testing if `CI-no-fail-fast` label is present fail-fast: ${{ !contains(github.event.pull_request.labels.*.name, 'CI-no-fail-fast') }} @@ -78,9 +74,9 @@ jobs: rust: [stable] platform: [ { - os: "macos-latest", - python-architecture: "x64", - rust-target: "x86_64-apple-darwin", + os: "macos-14", # first available arm macos runner + python-architecture: "arm64", + rust-target: "aarch64-apple-darwin", }, { os: "ubuntu-latest", @@ -123,7 +119,7 @@ jobs: rust-target: "x86_64-unknown-linux-gnu", } name: clippy/${{ matrix.platform.rust-target }}/${{ matrix.rust }} - continue-on-error: ${{ matrix.platform.rust != 'stable' }} + continue-on-error: ${{ matrix.rust != 'stable' }} steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@master @@ -136,8 +132,7 @@ jobs: architecture: ${{ matrix.platform.python-architecture }} - uses: Swatinem/rust-cache@v2 with: - key: clippy-${{ matrix.platform.rust-target }}-${{ matrix.platform.os }}-${{ matrix.rust }} - continue-on-error: true + save-if: ${{ github.event_name != 'merge_group' }} - run: python -m pip install --upgrade pip && pip install nox - run: nox -s clippy-all env: @@ -166,7 +161,12 @@ jobs: platform: [ { - os: "macos-latest", + os: "macos-14", # first available arm macos runner + python-architecture: "arm64", + rust-target: "aarch64-apple-darwin", + }, + { + os: "macos-13", # last available x86_64 macos runner python-architecture: "x64", rust-target: "x86_64-apple-darwin", }, @@ -199,7 +199,7 @@ jobs: } extra-features: "nightly multiple-pymethods" build-full: - if: ${{ contains(github.event.pull_request.labels.*.name, 'CI-build-full') || (github.event_name != 'pull_request' && github.ref != 'refs/heads/main') }} + if: ${{ contains(github.event.pull_request.labels.*.name, 'CI-build-full') || github.event_name != 'pull_request' }} name: python${{ matrix.python-version }}-${{ matrix.platform.python-architecture }} ${{ matrix.platform.os }} rust-${{ matrix.rust }} needs: [fmt] uses: ./.github/workflows/build.yml @@ -224,15 +224,22 @@ jobs: "3.10", "3.11", "3.12", + "3.13-dev", "pypy3.7", "pypy3.8", "pypy3.9", "pypy3.10", + "graalpy24.0", ] platform: [ + # for the full matrix, use x86_64 macos runners because not all Python versions + # PyO3 supports are available for arm on GitHub Actions. (Availability starts + # around Python 3.10, can switch the full matrix to arm once earlier versions + # are dropped.) + # NB: if this switches to arm, switch the arm job below in the `include` to x86_64 { - os: "macos-latest", + os: "macos-13", python-architecture: "x64", rust-target: "x86_64-apple-darwin", }, @@ -249,7 +256,7 @@ jobs: ] include: # Test minimal supported Rust version - - rust: 1.56.0 + - rust: 1.63.0 python-version: "3.12" platform: { @@ -292,8 +299,20 @@ jobs: } extra-features: "multiple-pymethods" + # test arm macos runner with the latest Python version + # NB: if the full matrix switchess to arm, switch to x86_64 here + - rust: stable + python-version: "3.12" + platform: + { + os: "macos-14", + python-architecture: "arm64", + rust-target: "aarch64-apple-darwin", + } + extra-features: "multiple-pymethods" + valgrind: - if: ${{ contains(github.event.pull_request.labels.*.name, 'CI-build-full') || (github.event_name != 'pull_request' && github.ref != 'refs/heads/main') }} + if: ${{ contains(github.event.pull_request.labels.*.name, 'CI-build-full') || github.event_name != 'pull_request' }} needs: [fmt] runs-on: ubuntu-latest steps: @@ -301,8 +320,7 @@ jobs: - uses: actions/setup-python@v5 - uses: Swatinem/rust-cache@v2 with: - key: cargo-valgrind - continue-on-error: true + save-if: ${{ github.event_name != 'merge_group' }} - uses: dtolnay/rust-toolchain@stable - uses: taiki-e/install-action@valgrind - run: python -m pip install --upgrade pip && pip install nox @@ -313,7 +331,7 @@ jobs: TRYBUILD: overwrite careful: - if: ${{ contains(github.event.pull_request.labels.*.name, 'CI-build-full') || (github.event_name != 'pull_request' && github.ref != 'refs/heads/main') }} + if: ${{ contains(github.event.pull_request.labels.*.name, 'CI-build-full') || github.event_name != 'pull_request' }} needs: [fmt] runs-on: ubuntu-latest steps: @@ -321,8 +339,7 @@ jobs: - uses: actions/setup-python@v5 - uses: Swatinem/rust-cache@v2 with: - key: cargo-careful - continue-on-error: true + save-if: ${{ github.event_name != 'merge_group' }} - uses: dtolnay/rust-toolchain@nightly with: components: rust-src @@ -333,15 +350,30 @@ jobs: RUST_BACKTRACE: 1 TRYBUILD: overwrite + docsrs: + if: ${{ contains(github.event.pull_request.labels.*.name, 'CI-build-full') || github.event_name != 'pull_request' }} + needs: [fmt] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + - uses: Swatinem/rust-cache@v2 + with: + save-if: ${{ github.event_name != 'merge_group' }} + - uses: dtolnay/rust-toolchain@nightly + with: + components: rust-src + - run: cargo rustdoc --lib --no-default-features --features full -Zunstable-options --config "build.rustdocflags=[\"--cfg\", \"docsrs\"]" + coverage: needs: [fmt] - name: coverage-${{ matrix.os }} + name: coverage ${{ matrix.os }} strategy: matrix: - os: ["windows", "macos", "ubuntu"] - runs-on: ${{ matrix.os }}-latest + os: ["windows-latest", "macos-14", "ubuntu-latest"] # first available arm macos runner + runs-on: ${{ matrix.os }} steps: - - if: ${{ github.event_name == 'pull_request' && matrix.os != 'ubuntu' }} + - if: ${{ github.event_name == 'pull_request' && matrix.os != 'ubuntu-latest' }} id: should-skip shell: bash run: echo 'skip=true' >> $GITHUB_OUTPUT @@ -352,8 +384,7 @@ jobs: - uses: Swatinem/rust-cache@v2 if: steps.should-skip.outputs.skip != 'true' with: - key: coverage-cargo-${{ matrix.os }} - continue-on-error: true + save-if: ${{ github.event_name != 'merge_group' }} - uses: dtolnay/rust-toolchain@stable if: steps.should-skip.outputs.skip != 'true' with: @@ -365,15 +396,17 @@ jobs: if: steps.should-skip.outputs.skip != 'true' - run: nox -s coverage if: steps.should-skip.outputs.skip != 'true' - - uses: codecov/codecov-action@v3 + - uses: codecov/codecov-action@v4 if: steps.should-skip.outputs.skip != 'true' with: file: coverage.json name: ${{ matrix.os }} + token: ${{ secrets.CODECOV_TOKEN }} emscripten: name: emscripten - if: ${{ contains(github.event.pull_request.labels.*.name, 'CI-build-full') || (github.event_name != 'pull_request' && github.ref != 'refs/heads/main') }} + if: ${{ contains(github.event.pull_request.labels.*.name, 'CI-build-full') || github.event_name != 'pull_request' }} + needs: [fmt] runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -391,7 +424,7 @@ jobs: with: node-version: 14 - run: python -m pip install --upgrade pip && pip install nox - - uses: actions/cache@v3 + - uses: actions/cache@v4 id: cache with: path: | @@ -399,7 +432,7 @@ jobs: key: ${{ hashFiles('emscripten/*') }} - ${{ hashFiles('noxfile.py') }} - ${{ steps.setup-python.outputs.python-path }} - uses: Swatinem/rust-cache@v2 with: - key: cargo-emscripten-wasm32 + save-if: ${{ github.event_name != 'merge_group' }} - name: Build if: steps.cache.outputs.cache-hit != 'true' run: nox -s build-emscripten @@ -407,13 +440,14 @@ jobs: run: nox -s test-emscripten test-debug: + if: ${{ contains(github.event.pull_request.labels.*.name, 'CI-build-full') || github.event_name != 'pull_request' }} + needs: [fmt] runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: Swatinem/rust-cache@v2 with: - key: cargo-test-debug - continue-on-error: true + save-if: ${{ github.event_name != 'merge_group' }} - uses: dtolnay/rust-toolchain@stable with: components: rust-src @@ -449,6 +483,121 @@ jobs: echo PYO3_CONFIG_FILE=$PYO3_CONFIG_FILE >> $GITHUB_ENV - run: python3 -m nox -s test + test-version-limits: + needs: [fmt] + if: ${{ contains(github.event.pull_request.labels.*.name, 'CI-build-full') || github.event_name != 'pull_request' }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: Swatinem/rust-cache@v2 + with: + save-if: ${{ github.event_name != 'merge_group' }} + - uses: dtolnay/rust-toolchain@stable + - run: python3 -m pip install --upgrade pip && pip install nox + - run: python3 -m nox -s test-version-limits + + check-feature-powerset: + needs: [fmt] + if: ${{ contains(github.event.pull_request.labels.*.name, 'CI-build-full') || github.event_name != 'pull_request' }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + - uses: Swatinem/rust-cache@v2 + with: + save-if: ${{ github.event_name != 'merge_group' }} + - uses: dtolnay/rust-toolchain@stable + with: + components: rust-src + - uses: taiki-e/install-action@cargo-hack + - run: python3 -m pip install --upgrade pip && pip install nox + - run: python3 -m nox -s check-feature-powerset + + test-cross-compilation: + needs: [fmt] + if: ${{ contains(github.event.pull_request.labels.*.name, 'CI-build-full') || github.event_name != 'pull_request' }} + runs-on: ${{ matrix.os }} + name: test-cross-compilation ${{ matrix.os }} -> ${{ matrix.target }} + strategy: + # If one platform fails, allow the rest to keep testing if `CI-no-fail-fast` label is present + fail-fast: ${{ !contains(github.event.pull_request.labels.*.name, 'CI-no-fail-fast') }} + matrix: + include: + # ubuntu "cross compile" to itself + - os: "ubuntu-latest" + target: "x86_64-unknown-linux-gnu" + flags: "-i python3.12" + manylinux: auto + # ubuntu x86_64 -> aarch64 + - os: "ubuntu-latest" + target: "aarch64-unknown-linux-gnu" + flags: "-i python3.12" + manylinux: auto + # ubuntu x86_64 -> windows x86_64 + - os: "ubuntu-latest" + target: "x86_64-pc-windows-gnu" + flags: "-i python3.12 --features abi3 --features generate-import-lib" + manylinux: off + # macos x86_64 -> aarch64 + - os: "macos-13" # last x86_64 macos runners + target: "aarch64-apple-darwin" + # macos aarch64 -> x86_64 + - os: "macos-14" # aarch64 macos runners + target: "x86_64-apple-darwin" + steps: + - uses: actions/checkout@v4 + - uses: Swatinem/rust-cache@v2 + with: + workspaces: + examples/maturin-starter + save-if: ${{ github.event_name != 'merge_group' }} + key: ${{ matrix.target }} + - name: Setup cross-compiler + if: ${{ matrix.target == 'x86_64-pc-windows-gnu' }} + run: sudo apt-get install -y mingw-w64 llvm + - uses: PyO3/maturin-action@v1 + with: + target: ${{ matrix.target }} + manylinux: ${{ matrix.manylinux }} + args: --release -m examples/maturin-starter/Cargo.toml ${{ matrix.flags }} + + test-cross-compilation-windows: + needs: [fmt] + if: ${{ contains(github.event.pull_request.labels.*.name, 'CI-build-full') || github.event_name != 'pull_request' }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + - uses: Swatinem/rust-cache@v2 + with: + workspaces: + examples/maturin-starter + save-if: ${{ github.event_name != 'merge_group' }} + - uses: actions/cache/restore@v4 + with: + # https://github.com/PyO3/maturin/discussions/1953 + path: ~/.cache/cargo-xwin + key: cargo-xwin-cache + - name: Test cross compile to Windows + env: + XWIN_ARCH: x86_64 + run: | + set -ex + sudo apt-get install -y mingw-w64 llvm + rustup target add x86_64-pc-windows-gnu x86_64-pc-windows-msvc + pip install cargo-xwin + # abi3 + cargo build --manifest-path examples/maturin-starter/Cargo.toml --features abi3 --target x86_64-pc-windows-gnu + cargo xwin build --manifest-path examples/maturin-starter/Cargo.toml --features abi3 --target x86_64-pc-windows-msvc + # non-abi3 + export PYO3_CROSS_PYTHON_VERSION=3.12 + cargo build --manifest-path examples/maturin-starter/Cargo.toml --features generate-import-lib --target x86_64-pc-windows-gnu + cargo xwin build --manifest-path examples/maturin-starter/Cargo.toml --features generate-import-lib --target x86_64-pc-windows-msvc + - if: ${{ github.ref == 'refs/heads/main' }} + uses: actions/cache/save@v4 + with: + path: ~/.cache/cargo-xwin + key: cargo-xwin-cache conclusion: needs: - fmt @@ -458,8 +607,14 @@ jobs: - build-full - valgrind - careful + - docsrs - coverage - emscripten + - test-debug + - test-version-limits + - check-feature-powerset + - test-cross-compilation + - test-cross-compilation-windows if: always() runs-on: ubuntu-latest steps: diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml index b151c6e18db..a101eb6ad25 100644 --- a/.github/workflows/gh-pages.yml +++ b/.github/workflows/gh-pages.yml @@ -26,9 +26,9 @@ jobs: - uses: dtolnay/rust-toolchain@nightly - name: Setup mdBook - uses: peaceiris/actions-mdbook@v1 + uses: taiki-e/install-action@v2 with: - mdbook-version: "0.4.19" + tool: mdbook,lychee - name: Prepare tag id: prepare_tag @@ -36,17 +36,17 @@ jobs: TAG_NAME="${GITHUB_REF##*/}" echo "::set-output name=tag_name::${TAG_NAME}" - # This builds the book in target/guide. + # This builds the book in target/guide/. - name: Build the guide run: | python -m pip install --upgrade pip && pip install nox - nox -s build-guide + nox -s check-guide env: PYO3_VERSION_TAG: ${{ steps.prepare_tag.outputs.tag_name }} - name: Deploy docs and the guide if: ${{ github.event_name == 'release' }} - uses: peaceiris/actions-gh-pages@v3 + uses: peaceiris/actions-gh-pages@v4 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: ./target/guide/ @@ -63,7 +63,7 @@ jobs: - uses: actions/setup-python@v5 - uses: dtolnay/rust-toolchain@stable - - uses: actions/cache@v3 + - uses: actions/cache@v4 with: path: | ~/.cargo/registry @@ -82,7 +82,7 @@ jobs: # Download previous benchmark result from cache (if exists) - name: Download previous benchmark data - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ./cache key: ${{ runner.os }}-benchmark @@ -110,7 +110,7 @@ jobs: - uses: actions/setup-python@v5 - uses: dtolnay/rust-toolchain@stable - - uses: actions/cache@v3 + - uses: actions/cache@v4 with: path: | ~/.cargo/registry @@ -120,7 +120,7 @@ jobs: continue-on-error: true - name: Download previous benchmark data - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ./cache key: ${{ runner.os }}-pytest-benchmark diff --git a/.gitignore b/.gitignore index 4240d326f71..3ad8328cd15 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,8 @@ dist/ .eggs/ venv* guide/book/ +guide/src/LICENSE-APACHE +guide/src/LICENSE-MIT *.so *.out *.egg-info @@ -22,5 +24,6 @@ pip-wheel-metadata valgrind-python.supp *.pyd lcov.info +coverage.json netlify_build/ .nox/ diff --git a/.netlify/build.sh b/.netlify/build.sh index 10ec241c1d7..a61180be59a 100755 --- a/.netlify/build.sh +++ b/.netlify/build.sh @@ -2,6 +2,7 @@ set -uex +rustup update nightly rustup default nightly PYO3_VERSION=$(cargo search pyo3 --limit 1 | head -1 | tr -s ' ' | cut -d ' ' -f 3 | tr -d '"') @@ -48,6 +49,15 @@ done # Add latest redirect echo "/latest/* /v${PYO3_VERSION}/:splat 302" >> netlify_build/_redirects +# some backwards compatbiility redirects +echo "/latest/building_and_distribution/* /latest/building-and-distribution/:splat 302" >> netlify_build/_redirects +echo "/latest/building-and-distribution/multiple_python_versions/* /latest/building-and-distribution/multiple-python-versions:splat 302" >> netlify_build/_redirects +echo "/latest/function/error_handling/* /latest/function/error-handling/:splat 302" >> netlify_build/_redirects +echo "/latest/getting_started/* /latest/getting-started/:splat 302" >> netlify_build/_redirects +echo "/latest/python_from_rust/* /latest/python-from-rust/:splat 302" >> netlify_build/_redirects +echo "/latest/python_typing_hints/* /latest/python-typing-hints/:splat 302" >> netlify_build/_redirects +echo "/latest/trait_bounds/* /latest/trait-bounds/:splat 302" >> netlify_build/_redirects + ## Add landing page redirect if [ "${CONTEXT}" == "deploy-preview" ]; then echo "/ /main/" >> netlify_build/_redirects @@ -71,9 +81,17 @@ if [ "${INSTALLED_MDBOOK_VERSION}" != "mdbook v${MDBOOK_VERSION}" ]; then cargo install mdbook@${MDBOOK_VERSION} --force fi +# Install latest mdbook-linkcheck. Netlify will cache the cargo bin dir, so this will +# only build mdbook-linkcheck if needed. +MDBOOK_LINKCHECK_VERSION=$(cargo search mdbook-linkcheck --limit 1 | head -1 | tr -s ' ' | cut -d ' ' -f 3 | tr -d '"') +INSTALLED_MDBOOK_LINKCHECK_VERSION=$(mdbook-linkcheck --version || echo "none") +if [ "${INSTALLED_MDBOOK_LINKCHECK_VERSION}" != "mdbook v${MDBOOK_LINKCHECK_VERSION}" ]; then + cargo install mdbook-linkcheck@${MDBOOK_LINKCHECK_VERSION} --force +fi + pip install nox nox -s build-guide -mv target/guide netlify_build/main/ +mv target/guide/ netlify_build/main/ ## Build public docs diff --git a/Architecture.md b/Architecture.md index 9ec20931c10..a4218a7f71f 100644 --- a/Architecture.md +++ b/Architecture.md @@ -48,7 +48,7 @@ In the [`pyo3-ffi`] crate, there is lots of conditional compilation such as `#[c `#[cfg(Py_3_7)]`, and `#[cfg(PyPy)]`. `Py_LIMITED_API` corresponds to `#define Py_LIMITED_API` macro in Python/C API. With `Py_LIMITED_API`, we can build a Python-version-agnostic binary called an -[abi3 wheel](https://pyo3.rs/latest/building_and_distribution.html#py_limited_apiabi3). +[abi3 wheel](https://pyo3.rs/latest/building-and-distribution.html#py_limited_apiabi3). `Py_3_7` means that the API is available from Python >= 3.7. There are also `Py_3_8`, `Py_3_9`, and so on. `PyPy` means that the API definition is for PyPy. @@ -59,6 +59,7 @@ Those flags are set in [`build.rs`](#6-buildrs-and-pyo3-build-config). [`src/types`] contains bindings to [built-in types](https://docs.python.org/3/library/stdtypes.html) of Python, such as `dict` and `list`. For historical reasons, Python's `object` is called `PyAny` in PyO3 and located in [`src/types/any.rs`]. + Currently, `PyAny` is a straightforward wrapper of `ffi::PyObject`, defined as: ```rust @@ -66,38 +67,16 @@ Currently, `PyAny` is a straightforward wrapper of `ffi::PyObject`, defined as: pub struct PyAny(UnsafeCell); ``` -All built-in types are defined as a C struct. -For example, `dict` is defined as: - -```c -typedef struct { - /* Base object */ - PyObject ob_base; - /* Number of items in the dictionary */ - Py_ssize_t ma_used; - /* Dictionary version */ - uint64_t ma_version_tag; - PyDictKeysObject *ma_keys; - PyObject **ma_values; -} PyDictObject; -``` - -However, we cannot access such a specific data structure with `#[cfg(Py_LIMITED_API)]` set. -Thus, all builtin objects are implemented as opaque types by wrapping `PyAny`, e.g.,: +Concrete Python objects are implemented by wrapping `PyAny`, e.g.,: ```rust #[repr(transparent)] pub struct PyDict(PyAny); ``` -Note that `PyAny` is not a pointer, and it is usually used as a pointer to the object in the -Python heap, as `&PyAny`. -This design choice can be changed -(see the discussion in [#1056](https://github.com/PyO3/pyo3/issues/1056)). +These types are not intended to be accessed directly, and instead are used through the `Py` and `Bound` smart pointers. -Since we need lots of boilerplate for implementing common traits for these types -(e.g., `AsPyPointer`, `AsRef`, and `Debug`), we have some macros in -[`src/types/mod.rs`]. +We have some macros in [`src/types/mod.rs`] which make it easier to implement APIs for concrete Python types. ## 3. `PyClass` and related functionalities diff --git a/CHANGELOG.md b/CHANGELOG.md index 97ce9a00537..86055a9a80c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,152 @@ To see unreleased changes, please see the [CHANGELOG on the main branch guide](h +## [0.21.2] - 2024-04-16 + +### Changed + +- Deprecate the `PySet::empty()` gil-ref constructor. [#4082](https://github.com/PyO3/pyo3/pull/4082) + +### Fixed + +- Fix compile error for `async fn` in `#[pymethods]` with a `&self` receiver and more than one additional argument. [#4035](https://github.com/PyO3/pyo3/pull/4035) +- Improve error message for wrong receiver type in `__traverse__`. [#4045](https://github.com/PyO3/pyo3/pull/4045) +- Fix compile error when exporting a `#[pyclass]` living in a different Rust module using the `experimental-declarative-modules` feature. [#4054](https://github.com/PyO3/pyo3/pull/4054) +- Fix `missing_docs` lint triggering on documented `#[pymodule]` functions. [#4067](https://github.com/PyO3/pyo3/pull/4067) +- Fix undefined symbol errors for extension modules on AIX (by linking `libpython`). [#4073](https://github.com/PyO3/pyo3/pull/4073) + +## [0.21.1] - 2024-04-01 + +### Added + +- Implement `Send` and `Sync` for `PyBackedStr` and `PyBackedBytes`. [#4007](https://github.com/PyO3/pyo3/pull/4007) +- Implement `Clone`, `Debug`, `PartialEq`, `Eq`, `PartialOrd`, `Ord` and `Hash` implementation for `PyBackedBytes` and `PyBackedStr`, and `Display` for `PyBackedStr`. [#4020](https://github.com/PyO3/pyo3/pull/4020) +- Add `import_exception_bound!` macro to import exception types without generating GIL Ref functionality for them. [#4027](https://github.com/PyO3/pyo3/pull/4027) + +### Changed + +- Emit deprecation warning for uses of GIL Refs as `#[setter]` function arguments. [#3998](https://github.com/PyO3/pyo3/pull/3998) +- Add `#[inline]` hints on many `Bound` and `Borrowed` methods. [#4024](https://github.com/PyO3/pyo3/pull/4024) + +### Fixed + +- Handle `#[pyo3(from_py_with = "")]` in `#[setter]` methods [#3995](https://github.com/PyO3/pyo3/pull/3995) +- Allow extraction of `&Bound` in `#[setter]` methods. [#3998](https://github.com/PyO3/pyo3/pull/3998) +- Fix some uncovered code blocks emitted by `#[pymodule]`, `#[pyfunction]` and `#[pyclass]` macros. [#4009](https://github.com/PyO3/pyo3/pull/4009) +- Fix typo in the panic message when a class referenced in `pyo3::import_exception!` does not exist. [#4012](https://github.com/PyO3/pyo3/pull/4012) +- Fix compile error when using an async `#[pymethod]` with a receiver and additional arguments. [#4015](https://github.com/PyO3/pyo3/pull/4015) + + +## [0.21.0] - 2024-03-25 + +### Added + +- Add support for GraalPy (24.0 and up). [#3247](https://github.com/PyO3/pyo3/pull/3247) +- Add `PyMemoryView` type. [#3514](https://github.com/PyO3/pyo3/pull/3514) +- Allow `async fn` in for `#[pyfunction]` and `#[pymethods]`, with the `experimental-async` feature. [#3540](https://github.com/PyO3/pyo3/pull/3540) [#3588](https://github.com/PyO3/pyo3/pull/3588) [#3599](https://github.com/PyO3/pyo3/pull/3599) [#3931](https://github.com/PyO3/pyo3/pull/3931) +- Implement `PyTypeInfo` for `PyEllipsis`, `PyNone` and `PyNotImplemented`. [#3577](https://github.com/PyO3/pyo3/pull/3577) +- Support `#[pyclass]` on enums that have non-unit variants. [#3582](https://github.com/PyO3/pyo3/pull/3582) +- Support `chrono` feature with `abi3` feature. [#3664](https://github.com/PyO3/pyo3/pull/3664) +- `FromPyObject`, `IntoPy` and `ToPyObject` are implemented on `std::duration::Duration` [#3670](https://github.com/PyO3/pyo3/pull/3670) +- Add `PyString::to_cow`. Add `Py::to_str`, `Py::to_cow`, and `Py::to_string_lossy`, as ways to access Python string data safely beyond the GIL lifetime. [#3677](https://github.com/PyO3/pyo3/pull/3677) +- Add `Bound` and `Borrowed` smart pointers as a new API for accessing Python objects. [#3686](https://github.com/PyO3/pyo3/pull/3686) +- Add `PyNativeType::as_borrowed` to convert "GIL refs" to the new `Bound` smart pointer. [#3692](https://github.com/PyO3/pyo3/pull/3692) +- Add `FromPyObject::extract_bound` method, to migrate `FromPyObject` implementations to the Bound API. [#3706](https://github.com/PyO3/pyo3/pull/3706) +- Add `gil-refs` feature to allow continued use of the deprecated GIL Refs APIs. [#3707](https://github.com/PyO3/pyo3/pull/3707) +- Add methods to `PyAnyMethods` for binary operators (`add`, `sub`, etc.) [#3712](https://github.com/PyO3/pyo3/pull/3712) +- Add `chrono-tz` feature allowing conversion between `chrono_tz::Tz` and `zoneinfo.ZoneInfo` [#3730](https://github.com/PyO3/pyo3/pull/3730) +- Add FFI definition `PyType_GetModuleByDef`. [#3734](https://github.com/PyO3/pyo3/pull/3734) +- Conversion between `std::time::SystemTime` and `datetime.datetime` [#3736](https://github.com/PyO3/pyo3/pull/3736) +- Add `Py::as_any` and `Py::into_any`. [#3785](https://github.com/PyO3/pyo3/pull/3785) +- Add `PyStringMethods::encode_utf8`. [#3801](https://github.com/PyO3/pyo3/pull/3801) +- Add `PyBackedStr` and `PyBackedBytes`, as alternatives to `&str` and `&bytes` where a Python object owns the data. [#3802](https://github.com/PyO3/pyo3/pull/3802) [#3991](https://github.com/PyO3/pyo3/pull/3991) +- Allow `#[pymodule]` macro on Rust `mod` blocks, with the `experimental-declarative-modules` feature. [#3815](https://github.com/PyO3/pyo3/pull/3815) +- Implement `ExactSizeIterator` for `set` and `frozenset` iterators on `abi3` feature. [#3849](https://github.com/PyO3/pyo3/pull/3849) +- Add `Py::drop_ref` to explicitly drop a `Py`` and immediately decrease the Python reference count if the GIL is already held. [#3871](https://github.com/PyO3/pyo3/pull/3871) +- Allow `#[pymodule]` macro on single argument functions that take `&Bound<'_, PyModule>`. [#3905](https://github.com/PyO3/pyo3/pull/3905) +- Implement `FromPyObject` for `Cow`. [#3928](https://github.com/PyO3/pyo3/pull/3928) +- Implement `Default` for `GILOnceCell`. [#3971](https://github.com/PyO3/pyo3/pull/3971) +- Add `PyDictMethods::into_mapping`, `PyListMethods::into_sequence` and `PyTupleMethods::into_sequence`. [#3982](https://github.com/PyO3/pyo3/pull/3982) + +### Changed + +- `PyDict::from_sequence` now takes a single argument of type `&PyAny` (previously took two arguments `Python` and `PyObject`). [#3532](https://github.com/PyO3/pyo3/pull/3532) +- Deprecate `Py::is_ellipsis` and `PyAny::is_ellipsis` in favour of `any.is(py.Ellipsis())`. [#3577](https://github.com/PyO3/pyo3/pull/3577) +- Split some `PyTypeInfo` functionality into new traits `HasPyGilRef` and `PyTypeCheck`. [#3600](https://github.com/PyO3/pyo3/pull/3600) +- Deprecate `PyTryFrom` and `PyTryInto` traits in favor of `any.downcast()` via the `PyTypeCheck` and `PyTypeInfo` traits. [#3601](https://github.com/PyO3/pyo3/pull/3601) +- Allow async methods to accept `&self`/`&mut self` [#3609](https://github.com/PyO3/pyo3/pull/3609) +- `FromPyObject` for set types now also accept `frozenset` objects as input. [#3632](https://github.com/PyO3/pyo3/pull/3632) +- `FromPyObject` for `bool` now also accepts NumPy's `bool_` as input. [#3638](https://github.com/PyO3/pyo3/pull/3638) +- Add `AsRefSource` associated type to `PyNativeType`. [#3653](https://github.com/PyO3/pyo3/pull/3653) +- Rename `.is_true` to `.is_truthy` on `PyAny` and `Py` to clarify that the test is not based on identity with or equality to the True singleton. [#3657](https://github.com/PyO3/pyo3/pull/3657) +- `PyType::name` is now `PyType::qualname` whereas `PyType::name` efficiently accesses the full name which includes the module name. [#3660](https://github.com/PyO3/pyo3/pull/3660) +- The `Iter(A)NextOutput` types are now deprecated and `__(a)next__` can directly return anything which can be converted into Python objects, i.e. awaitables do not need to be wrapped into `IterANextOutput` or `Option` any more. `Option` can still be used as well and returning `None` will trigger the fast path for `__next__`, stopping iteration without having to raise a `StopIteration` exception. [#3661](https://github.com/PyO3/pyo3/pull/3661) +- Implement `FromPyObject` on `chrono::DateTime` for all `Tz`, not just `FixedOffset` and `Utc`. [#3663](https://github.com/PyO3/pyo3/pull/3663) +- Add lifetime parameter to `PyTzInfoAccess` trait. For the deprecated gil-ref API, the trait is now implemented for `&'py PyTime` and `&'py PyDateTime` instead of `PyTime` and `PyDate`. [#3679](https://github.com/PyO3/pyo3/pull/3679) +- Calls to `__traverse__` become no-ops for unsendable pyclasses if on the wrong thread, thereby avoiding hard aborts at the cost of potential leakage. [#3689](https://github.com/PyO3/pyo3/pull/3689) +- Include `PyNativeType` in `pyo3::prelude`. [#3692](https://github.com/PyO3/pyo3/pull/3692) +- Improve performance of `extract::` (and other integer types) by avoiding call to `__index__()` converting the value to an integer for 3.10+. Gives performance improvement of around 30% for successful extraction. [#3742](https://github.com/PyO3/pyo3/pull/3742) +- Relax bound of `FromPyObject` for `Py` to just `T: PyTypeCheck`. [#3776](https://github.com/PyO3/pyo3/pull/3776) +- `PySet` and `PyFrozenSet` iterators now always iterate the equivalent of `iter(set)`. (A "fast path" with no noticeable performance benefit was removed.) [#3849](https://github.com/PyO3/pyo3/pull/3849) +- Move implementations of `FromPyObject` for `&str`, `Cow`, `&[u8]` and `Cow<[u8]>` onto a temporary trait `FromPyObjectBound` when `gil-refs` feature is deactivated. [#3928](https://github.com/PyO3/pyo3/pull/3928) +- Deprecate `GILPool`, `Python::with_pool`, and `Python::new_pool`. [#3947](https://github.com/PyO3/pyo3/pull/3947) + +### Removed + +- Remove all functionality deprecated in PyO3 0.19. [#3603](https://github.com/PyO3/pyo3/pull/3603) + +### Fixed + +- Match PyPy 7.3.14 in removing PyPy-only symbol `Py_MAX_NDIMS` in favour of `PyBUF_MAX_NDIM`. [#3757](https://github.com/PyO3/pyo3/pull/3757) +- Fix segmentation fault using `datetime` types when an invalid `datetime` module is on sys.path. [#3818](https://github.com/PyO3/pyo3/pull/3818) +- Fix `non_local_definitions` lint warning triggered by many PyO3 macros. [#3901](https://github.com/PyO3/pyo3/pull/3901) +- Disable `PyCode` and `PyCode_Type` on PyPy: `PyCode_Type` is not exposed by PyPy. [#3934](https://github.com/PyO3/pyo3/pull/3934) + +## [0.21.0-beta.0] - 2024-03-10 + +Prerelease of PyO3 0.21. See [the GitHub diff](https://github.com/pyo3/pyo3/compare/v0.21.0-beta.0...v0.21.0) for what changed between 0.21.0-beta.0 and the final release. + +## [0.20.3] - 2024-02-23 + +### Packaging + +- Add `portable-atomic` dependency. [#3619](https://github.com/PyO3/pyo3/pull/3619) +- Check maximum version of Python at build time and for versions not yet supported require opt-in to the `abi3` stable ABI by the environment variable `PYO3_USE_ABI3_FORWARD_COMPATIBILITY=1`. [#3821](https://github.com/PyO3/pyo3/pull/3821) + +### Fixed + +- Use `portable-atomic` to support platforms without 64-bit atomics. [#3619](https://github.com/PyO3/pyo3/pull/3619) +- Fix compilation failure with `either` feature enabled without `experimental-inspect` enabled. [#3834](https://github.com/PyO3/pyo3/pull/3834) + +## [0.20.2] - 2024-01-04 + +### Packaging + +- Pin `pyo3` and `pyo3-ffi` dependencies on `pyo3-build-config` to require the same patch version, i.e. `pyo3` 0.20.2 requires _exactly_ `pyo3-build-config` 0.20.2. [#3721](https://github.com/PyO3/pyo3/pull/3721) + +### Fixed + +- Fix compile failure when building `pyo3` 0.20.0 with latest `pyo3-build-config` 0.20.X. [#3724](https://github.com/PyO3/pyo3/pull/3724) +- Fix docs.rs build. [#3722](https://github.com/PyO3/pyo3/pull/3722) + +## [0.20.1] - 2023-12-30 + +### Added + +- Add optional `either` feature to add conversions for `either::Either` sum type. [#3456](https://github.com/PyO3/pyo3/pull/3456) +- Add optional `smallvec` feature to add conversions for `smallvec::SmallVec`. [#3507](https://github.com/PyO3/pyo3/pull/3507) +- Add `take` and `into_inner` methods to `GILOnceCell` [#3556](https://github.com/PyO3/pyo3/pull/3556) +- `#[classmethod]` methods can now also receive `Py` as their first argument. [#3587](https://github.com/PyO3/pyo3/pull/3587) +- `#[pyfunction(pass_module)]` can now also receive `Py` as their first argument. [#3587](https://github.com/PyO3/pyo3/pull/3587) +- Add `traverse` method to `GILProtected`. [#3616](https://github.com/PyO3/pyo3/pull/3616) +- Added `abi3-py312` feature [#3687](https://github.com/PyO3/pyo3/pull/3687) + +### Fixed + +- Fix minimum version specification for optional `chrono` dependency. [#3512](https://github.com/PyO3/pyo3/pull/3512) +- Silenced new `clippy::unnecessary_fallible_conversions` warning when using a `Py` `self` receiver. [#3564](https://github.com/PyO3/pyo3/pull/3564) + + ## [0.20.0] - 2023-10-11 ### Packaging @@ -571,7 +717,7 @@ To see unreleased changes, please see the [CHANGELOG on the main branch guide](h - Respect Rust privacy rules for items wrapped with `wrap_pyfunction` and `wrap_pymodule`. [#2081](https://github.com/PyO3/pyo3/pull/2081) - Add modulo argument to `__ipow__` magic method. [#2083](https://github.com/PyO3/pyo3/pull/2083) - Fix FFI definition for `_PyCFunctionFast`. [#2126](https://github.com/PyO3/pyo3/pull/2126) -- `PyDateTimeAPI` and `PyDateTime_TimeZone_UTC` are are now unsafe functions instead of statics. [#2126](https://github.com/PyO3/pyo3/pull/2126) +- `PyDateTimeAPI` and `PyDateTime_TimeZone_UTC` are now unsafe functions instead of statics. [#2126](https://github.com/PyO3/pyo3/pull/2126) - `PyDateTimeAPI` does not implicitly call `PyDateTime_IMPORT` anymore to reflect the original Python API more closely. Before the first call to `PyDateTime_IMPORT` a null pointer is returned. Therefore before calling any of the following FFI functions `PyDateTime_IMPORT` must be called to avoid undefined behavior: [#2126](https://github.com/PyO3/pyo3/pull/2126) - `PyDateTime_TimeZone_UTC` - `PyDate_Check` @@ -1599,7 +1745,14 @@ Yanked - Initial release -[Unreleased]: https://github.com/pyo3/pyo3/compare/v0.20.0...HEAD +[Unreleased]: https://github.com/pyo3/pyo3/compare/v0.21.2...HEAD +[0.21.2]: https://github.com/pyo3/pyo3/compare/v0.21.1...v0.21.2 +[0.21.1]: https://github.com/pyo3/pyo3/compare/v0.21.0...v0.21.1 +[0.21.0]: https://github.com/pyo3/pyo3/compare/v0.20.3...v0.21.0 +[0.21.0-beta.0]: https://github.com/pyo3/pyo3/compare/v0.20.3...v0.21.0-beta.0 +[0.20.3]: https://github.com/pyo3/pyo3/compare/v0.20.2...v0.20.3 +[0.20.2]: https://github.com/pyo3/pyo3/compare/v0.20.1...v0.20.2 +[0.20.1]: https://github.com/pyo3/pyo3/compare/v0.20.0...v0.20.1 [0.20.0]: https://github.com/pyo3/pyo3/compare/v0.19.2...v0.20.0 [0.19.2]: https://github.com/pyo3/pyo3/compare/v0.19.1...v0.19.2 [0.19.1]: https://github.com/pyo3/pyo3/compare/v0.19.0...v0.19.1 @@ -1643,7 +1796,7 @@ Yanked [0.9.2]: https://github.com/pyo3/pyo3/compare/v0.9.1...v0.9.2 [0.9.1]: https://github.com/pyo3/pyo3/compare/v0.9.0...v0.9.1 [0.9.0]: https://github.com/pyo3/pyo3/compare/v0.8.5...v0.9.0 -[0.8.4]: https://github.com/pyo3/pyo3/compare/v0.8.4...v0.8.5 +[0.8.5]: https://github.com/pyo3/pyo3/compare/v0.8.4...v0.8.5 [0.8.4]: https://github.com/pyo3/pyo3/compare/v0.8.3...v0.8.4 [0.8.3]: https://github.com/pyo3/pyo3/compare/v0.8.2...v0.8.3 [0.8.2]: https://github.com/pyo3/pyo3/compare/v0.8.1...v0.8.2 @@ -1652,7 +1805,8 @@ Yanked [0.7.0]: https://github.com/pyo3/pyo3/compare/v0.6.0...v0.7.0 [0.6.0]: https://github.com/pyo3/pyo3/compare/v0.5.3...v0.6.0 [0.5.3]: https://github.com/pyo3/pyo3/compare/v0.5.2...v0.5.3 -[0.5.2]: https://github.com/pyo3/pyo3/compare/v0.5.0...v0.5.2 +[0.5.2]: https://github.com/pyo3/pyo3/compare/v0.5.1...v0.5.2 +[0.5.1]: https://github.com/pyo3/pyo3/compare/v0.5.0...v0.5.1 [0.5.0]: https://github.com/pyo3/pyo3/compare/v0.4.1...v0.5.0 [0.4.1]: https://github.com/pyo3/pyo3/compare/v0.4.0...v0.4.1 [0.4.0]: https://github.com/pyo3/pyo3/compare/v0.3.2...v0.4.0 diff --git a/Cargo.toml b/Cargo.toml index b8baf41a92d..4de78df063a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3" -version = "0.21.0-dev" +version = "0.22.0-dev" description = "Bindings to Python interpreter" authors = ["PyO3 Project and Contributors "] readme = "README.md" @@ -12,20 +12,18 @@ categories = ["api-bindings", "development-tools::ffi"] license = "MIT OR Apache-2.0" exclude = ["/.gitignore", ".cargo/config", "/codecov.yml", "/Makefile", "/pyproject.toml", "/noxfile.py", "/.github", "/tests/test_compile_error.rs", "/tests/ui"] edition = "2021" -rust-version = "1.56" +rust-version = "1.63" [dependencies] cfg-if = "1.0" libc = "0.2.62" -parking_lot = ">= 0.11, < 0.13" memoffset = "0.9" -portable-atomic = "1.0" # ffi bindings to the python interpreter, split into a separate crate so they can be used independently -pyo3-ffi = { path = "pyo3-ffi", version = "=0.21.0-dev" } +pyo3-ffi = { path = "pyo3-ffi", version = "=0.22.0-dev" } # support crates for macros feature -pyo3-macros = { path = "pyo3-macros", version = "=0.21.0-dev", optional = true } +pyo3-macros = { path = "pyo3-macros", version = "=0.22.0-dev", optional = true } indoc = { version = "2.0.1", optional = true } unindent = { version = "0.2.1", optional = true } @@ -35,40 +33,51 @@ inventory = { version = "0.3.0", optional = true } # crate integrations that can be added using the eponymous features anyhow = { version = "1.0", optional = true } chrono = { version = "0.4.25", default-features = false, optional = true } +chrono-tz = { version = ">= 0.6, < 0.10", default-features = false, optional = true } either = { version = "1.9", optional = true } eyre = { version = ">= 0.4, < 0.7", optional = true } hashbrown = { version = ">= 0.9, < 0.15", optional = true } indexmap = { version = ">= 1.6, < 3", optional = true } num-bigint = { version = "0.4", optional = true } num-complex = { version = ">= 0.2, < 0.5", optional = true } +num-rational = {version = "0.4.1", optional = true } rust_decimal = { version = "1.0.0", default-features = false, optional = true } serde = { version = "1.0", optional = true } smallvec = { version = "1.0", optional = true } +[target.'cfg(not(target_has_atomic = "64"))'.dependencies] +portable-atomic = "1.0" + [dev-dependencies] assert_approx_eq = "1.1.0" -chrono = { version = "0.4.25" } +chrono = "0.4.25" +chrono-tz = ">= 0.6, < 0.10" # Required for "and $N others" normalization trybuild = ">=1.0.70" proptest = { version = "1.0", default-features = false, features = ["std"] } send_wrapper = "0.6" -scoped-tls = "1.0" +scoped-tls-hkt = "0.1" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0.61" rayon = "1.6.1" -widestring = "0.5.1" futures = "0.3.28" [build-dependencies] -pyo3-build-config = { path = "pyo3-build-config", version = "0.21.0-dev", features = ["resolve-config"] } +pyo3-build-config = { path = "pyo3-build-config", version = "=0.22.0-dev", features = ["resolve-config"] } [features] default = ["macros"] +# Enables support for `async fn` for `#[pyfunction]` and `#[pymethods]`. +experimental-async = ["macros", "pyo3-macros/experimental-async"] + # Enables pyo3::inspect module and additional type information on FromPyObject # and IntoPy traits experimental-inspect = [] +# Enables annotating Rust inline modules with #[pymodule] to build Python modules declaratively +experimental-declarative-modules = ["pyo3-macros/experimental-declarative-modules", "macros"] + # Enables macros: #[pyclass], #[pymodule], #[pyfunction] etc. macros = ["pyo3-macros", "indoc", "unindent"] @@ -81,7 +90,7 @@ multiple-pymethods = ["inventory", "pyo3-macros/multiple-pymethods"] extension-module = ["pyo3-ffi/extension-module"] # Use the Python limited API. See https://www.python.org/dev/peps/pep-0384/ for more. -abi3 = ["pyo3-build-config/abi3", "pyo3-ffi/abi3", "pyo3-macros/abi3"] +abi3 = ["pyo3-build-config/abi3", "pyo3-ffi/abi3"] # With abi3, we can manually set the minimum Python version. abi3-py37 = ["abi3-py38", "pyo3-build-config/abi3-py37", "pyo3-ffi/abi3-py37"] @@ -97,23 +106,37 @@ generate-import-lib = ["pyo3-ffi/generate-import-lib"] # Changes `Python::with_gil` to automatically initialize the Python interpreter if needed. auto-initialize = [] +# Allows use of the deprecated "GIL Refs" APIs. +gil-refs = ["pyo3-macros/gil-refs"] + +# Enables `Clone`ing references to Python objects `Py` which panics if the GIL is not held. +py-clone = [] + +# Optimizes PyObject to Vec conversion and so on. +nightly = [] + # Activates all additional features # This is mostly intended for testing purposes - activating *all* of these isn't particularly useful. full = [ "macros", # "multiple-pymethods", # TODO re-add this when MSRV is greater than 1.62 + "anyhow", "chrono", - "num-bigint", - "num-complex", - "hashbrown", - "smallvec", - "serde", - "indexmap", + "chrono-tz", "either", - "eyre", - "anyhow", + "experimental-async", + "experimental-declarative-modules", "experimental-inspect", + "eyre", + "hashbrown", + "indexmap", + "num-bigint", + "num-complex", + "num-rational", + "py-clone", "rust_decimal", + "serde", + "smallvec", ] [workspace] @@ -128,7 +151,7 @@ members = [ [package.metadata.docs.rs] no-default-features = true -features = ["macros", "num-bigint", "num-complex", "hashbrown", "serde", "multiple-pymethods", "indexmap", "eyre", "either", "chrono", "rust_decimal"] +features = ["full", "gil-refs"] rustdoc-args = ["--cfg", "docsrs"] [workspace.lints.clippy] diff --git a/Contributing.md b/Contributing.md index 3af6a1605db..1503f803e80 100644 --- a/Contributing.md +++ b/Contributing.md @@ -23,19 +23,15 @@ To work and develop PyO3, you need Python & Rust installed on your system. * [virtualenv](https://virtualenv.pypa.io/en/latest/) can also be used with or without Pyenv to use specific installed Python versions. * [`nox`][nox] is used to automate many of our CI tasks. -### Caveats - -* When using pyenv on macOS, installing a Python version using `--enable-shared` is required to make it work. i.e `env PYTHON_CONFIGURE_OPTS="--enable-shared" pyenv install 3.7.12` - ### Help users identify bugs -The [PyO3 Gitter channel](https://gitter.im/PyO3/Lobby) is very active with users who are new to PyO3, and often completely new to Rust. Helping them debug is a great way to get experience with the PyO3 codebase. +The [PyO3 Discord server](https://discord.gg/33kcChzH7f) is very active with users who are new to PyO3, and often completely new to Rust. Helping them debug is a great way to get experience with the PyO3 codebase. Helping others often reveals bugs, documentation weaknesses, and missing APIs. It's a good idea to open GitHub issues for these immediately so the resolution can be designed and implemented! ### Implement issues ready for development -Issues where the solution is clear and work is not in progress use the [needs-implementer](https://github.com/PyO3/pyo3/issues?q=is%3Aissue+is%3Aopen+label%3Aneeds-implemeter) label. +Issues where the solution is clear and work is not in progress use the [needs-implementer](https://github.com/PyO3/pyo3/issues?q=is%3Aissue+is%3Aopen+label%3Aneeds-implementer) label. Don't be afraid if the solution is not clear to you! The core PyO3 contributors will be happy to mentor you through any questions you have to help you write the solution. @@ -48,7 +44,7 @@ There are some specific areas of focus where help is currently needed for the do - Issues requesting documentation improvements are tracked with the [documentation](https://github.com/PyO3/pyo3/issues?q=is%3Aissue+is%3Aopen+label%3Adocumentation) label. - Not all APIs had docs or examples when they were made. The goal is to have documentation on all PyO3 APIs ([#306](https://github.com/PyO3/pyo3/issues/306)). If you see an API lacking a doc, please write one and open a PR! -You can build the docs (including all features) with +To build the docs (including all features), install [`nox`][nox] and then run ```shell nox -s docs -- open @@ -70,6 +66,12 @@ First, install [`mdbook`][mdbook] and [`nox`][nox]. Then, run nox -s build-guide -- --open ``` +To check all links in the guide are valid, also install [`lychee`][lychee] and use the `check-guide` session instead: + +```shell +nox -s check-guide +``` + ### Help design the next PyO3 Issues which don't yet have a clear solution use the [needs-design](https://github.com/PyO3/pyo3/issues?q=is%3Aissue+is%3Aopen+label%3Aneeds-design) label. @@ -86,19 +88,42 @@ Here are a few things to note when you are writing PRs. ### Continuous Integration -The PyO3 repo uses GitHub Actions. PRs are blocked from merging if CI is not successful. - -Formatting, linting and tests are checked for all Rust and Python code. In addition, all warnings in Rust code are disallowed (using `RUSTFLAGS="-D warnings"`). +The PyO3 repo uses GitHub Actions. PRs are blocked from merging if CI is not successful. Formatting, linting and tests are checked for all Rust and Python code. In addition, all warnings in Rust code are disallowed (using `RUSTFLAGS="-D warnings"`). Tests run with all supported Python versions with the latest stable Rust compiler, as well as for Python 3.9 with the minimum supported Rust version. If you are adding a new feature, you should add it to the `full` feature in our *Cargo.toml** so that it is tested in CI. You can run these tests yourself with -```nox``` -and -```nox -l``` -lists further commands you can run. +`nox`. Use `nox -l` to list the full set of subcommands you can run. + +#### Linting Python code +`nox -s ruff` + +#### Linting Rust code +`nox -s rustfmt` + +#### Semver checks +`cargo semver-checks check-release` + +#### Clippy +`nox -s clippy-all` + +#### Tests +`cargo test --features full` + +#### Check all conditional compilation +`nox -s check-feature-powerset` + +#### UI Tests + +PyO3 uses [`trybuild`][trybuild] to develop UI tests to capture error messages from the Rust compiler for some of the macro functionality. + +Because there are several feature combinations for these UI tests, when updating them all (e.g. for a new Rust compiler version) it may be helpful to use the `update-ui-tests` nox session: + +```bash +nox -s update-ui-tests +``` ### Documenting changes @@ -171,15 +196,20 @@ First, there are Rust-based benchmarks located in the `pyo3-benches` subdirector nox -s bench -Second, there is a Python-based benchmark contained in the `pytests` subdirectory. You can read more about it [here](pytests). +Second, there is a Python-based benchmark contained in the `pytests` subdirectory. You can read more about it [here](https://github.com/PyO3/pyo3/tree/main/pytests). ## Code coverage You can view what code is and isn't covered by PyO3's tests. We aim to have 100% coverage - please check coverage and add tests if you notice a lack of coverage! -- First, generate a `lcov.info` file with +- First, ensure the llvm-cov cargo plugin is installed. You may need to run the plugin through cargo once before using it with `nox`. +```shell +cargo install cargo-llvm-cov +cargo llvm-cov +``` +- Then, generate an `lcov.info` file with ```shell -nox -s coverage +nox -s coverage -- lcov ``` You can install an IDE plugin to view the coverage. For example, if you use VSCode: - Add the [coverage-gutters](https://marketplace.visualstudio.com/items?itemName=ryanluker.vscode-coverage-gutters) plugin. @@ -198,7 +228,7 @@ You can install an IDE plugin to view the coverage. For example, if you use VSCo ## Sponsor this project -At the moment there is no official organisation that accepts sponsorship on PyO3's behalf. If you're seeking to provide significant funding to the PyO3 ecosystem, please reach out to us on [GitHub](https://github.com/PyO3/pyo3/issues/new) or [Gitter](https://gitter.im/PyO3/Lobby) and we can discuss. +At the moment there is no official organisation that accepts sponsorship on PyO3's behalf. If you're seeking to provide significant funding to the PyO3 ecosystem, please reach out to us on [GitHub](https://github.com/PyO3/pyo3/issues/new) or [Discord](https://discord.gg/33kcChzH7f) and we can discuss. In the meanwhile, some of our maintainers have personal GitHub sponsorship pages and would be grateful for your support: @@ -206,4 +236,5 @@ In the meanwhile, some of our maintainers have personal GitHub sponsorship pages - [messense](https://github.com/sponsors/messense) [mdbook]: https://rust-lang.github.io/mdBook/cli/index.html +[lychee]: https://github.com/lycheeverse/lychee [nox]: https://github.com/theacodes/nox diff --git a/README.md b/README.md index 62b7f32f543..5e34f35489d 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,8 @@ [![benchmark](https://img.shields.io/badge/benchmark-✓-Green?logo=github)](https://pyo3.rs/dev/bench/) [![codecov](https://img.shields.io/codecov/c/gh/PyO3/pyo3?logo=codecov)](https://codecov.io/gh/PyO3/pyo3) [![crates.io](https://img.shields.io/crates/v/pyo3?logo=rust)](https://crates.io/crates/pyo3) -[![minimum rustc 1.56](https://img.shields.io/badge/rustc-1.56+-blue?logo=rust)](https://rust-lang.github.io/rfcs/2495-min-rust-version.html) -[![dev chat](https://img.shields.io/gitter/room/PyO3/Lobby?logo=gitter)](https://gitter.im/PyO3/Lobby) +[![minimum rustc 1.63](https://img.shields.io/badge/rustc-1.63+-blue?logo=rust)](https://rust-lang.github.io/rfcs/2495-min-rust-version.html) +[![discord server](https://img.shields.io/discord/1209263839632424990?logo=discord)](https://discord.gg/33kcChzH7f) [![contributing notes](https://img.shields.io/badge/contribute-on%20github-Green?logo=github)](https://github.com/PyO3/pyo3/blob/main/Contributing.md) [Rust](https://www.rust-lang.org/) bindings for [Python](https://www.python.org/), including tools for creating native Python extension modules. Running and interacting with Python code from a Rust binary is also supported. @@ -17,8 +17,8 @@ ## Usage PyO3 supports the following software versions: - - Python 3.7 and up (CPython and PyPy) - - Rust 1.56 and up + - Python 3.7 and up (CPython, PyPy, and GraalPy) + - Rust 1.63 and up You can use PyO3 to write a native Python module in Rust, or to embed Python in a Rust binary. The following sections explain each of these in turn. @@ -68,7 +68,7 @@ name = "string_sum" crate-type = ["cdylib"] [dependencies] -pyo3 = { version = "0.20.0", features = ["extension-module"] } +pyo3 = { version = "0.21.2", features = ["extension-module"] } ``` **`src/lib.rs`** @@ -86,7 +86,7 @@ fn sum_as_string(a: usize, b: usize) -> PyResult { /// the `lib.name` setting in the `Cargo.toml`, else Python will not be able to /// import the module. #[pymodule] -fn string_sum(_py: Python<'_>, m: &PyModule) -> PyResult<()> { +fn string_sum(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(sum_as_string, m)?)?; Ok(()) } @@ -118,7 +118,7 @@ maturin develop If you want to be able to run `cargo test` or use this project in a Cargo workspace and are running into linker issues, there are some workarounds in [the FAQ](https://pyo3.rs/latest/faq.html#i-cant-run-cargo-test-or-i-cant-build-in-a-cargo-workspace-im-having-linker-issues-like-symbol-not-found-or-undefined-reference-to-_pyexc_systemerror). -As well as with `maturin`, it is possible to build using [`setuptools-rust`](https://github.com/PyO3/setuptools-rust) or [manually](https://pyo3.rs/latest/building_and_distribution.html#manual-builds). Both offer more flexibility than `maturin` but require more configuration to get started. +As well as with `maturin`, it is possible to build using [`setuptools-rust`](https://github.com/PyO3/setuptools-rust) or [manually](https://pyo3.rs/latest/building-and-distribution.html#manual-builds). Both offer more flexibility than `maturin` but require more configuration to get started. ### Using Python from Rust @@ -137,7 +137,7 @@ Start a new project with `cargo new` and add `pyo3` to the `Cargo.toml` like th ```toml [dependencies.pyo3] -version = "0.20.0" +version = "0.21.2" features = ["auto-initialize"] ``` @@ -149,12 +149,12 @@ use pyo3::types::IntoPyDict; fn main() -> PyResult<()> { Python::with_gil(|py| { - let sys = py.import("sys")?; + let sys = py.import_bound("sys")?; let version: String = sys.getattr("version")?.extract()?; - let locals = [("os", py.import("os")?)].into_py_dict(py); + let locals = [("os", py.import_bound("os")?)].into_py_dict_bound(py); let code = "os.getenv('USER') or os.getenv('USERNAME') or 'Unknown'"; - let user: String = py.eval(code, None, Some(&locals))?.extract()?; + let user: String = py.eval_bound(code, None, Some(&locals))?.extract()?; println!("Hello {}, I'm Python {}", user, version); Ok(()) @@ -162,7 +162,7 @@ fn main() -> PyResult<()> { } ``` -The guide has [a section](https://pyo3.rs/latest/python_from_rust.html) with lots of examples +The guide has [a section](https://pyo3.rs/latest/python-from-rust.html) with lots of examples about this topic. ## Tools and libraries @@ -192,13 +192,15 @@ about this topic. - [fastuuid](https://github.com/thedrow/fastuuid/) _Python bindings to Rust's UUID library._ - [feos](https://github.com/feos-org/feos) _Lightning fast thermodynamic modeling in Rust with fully developed Python interface._ - [forust](https://github.com/jinlow/forust) _A lightweight gradient boosted decision tree library written in Rust._ +- [greptimedb](https://github.com/GreptimeTeam/greptimedb/tree/main/src/script) _Support [Python scripting](https://docs.greptime.com/user-guide/python-scripts/overview) in the database_ - [haem](https://github.com/BooleanCat/haem) _A Python library for working on Bioinformatics problems._ - [html-py-ever](https://github.com/PyO3/setuptools-rust/tree/main/examples/html-py-ever) _Using [html5ever](https://github.com/servo/html5ever) through [kuchiki](https://github.com/kuchiki-rs/kuchiki) to speed up html parsing and css-selecting._ - [hyperjson](https://github.com/mre/hyperjson) _A hyper-fast Python module for reading/writing JSON data using Rust's serde-json._ - [inline-python](https://github.com/fusion-engineering/inline-python) _Inline Python code directly in your Rust code._ +- [johnnycanencrypt](https://github.com/kushaldas/johnnycanencrypt) OpenPGP library with Yubikey support. - [jsonschema-rs](https://github.com/Stranger6667/jsonschema-rs/tree/master/bindings/python) _Fast JSON Schema validation library._ - [mocpy](https://github.com/cds-astro/mocpy) _Astronomical Python library offering data structures for describing any arbitrary coverage regions on the unit sphere._ -- [opendal](https://github.com/apache/incubator-opendal/tree/main/bindings/python) _A data access layer that allows users to easily and efficiently retrieve data from various storage services in a unified way._ +- [opendal](https://github.com/apache/opendal/tree/main/bindings/python) _A data access layer that allows users to easily and efficiently retrieve data from various storage services in a unified way._ - [orjson](https://github.com/ijl/orjson) _Fast Python JSON library._ - [ormsgpack](https://github.com/aviramha/ormsgpack) _Fast Python msgpack library._ - [point-process](https://github.com/ManifoldFR/point-process-rust/tree/master/pylib) _High level API for pointprocesses as a Python library._ @@ -208,6 +210,7 @@ about this topic. - [pyheck](https://github.com/kevinheavey/pyheck) _Fast case conversion library, built by wrapping [heck](https://github.com/withoutboats/heck)._ - Quite easy to follow as there's not much code. - [pyre](https://github.com/Project-Dream-Weaver/pyre-http) _Fast Python HTTP server written in Rust._ +- [pyreqwest_impersonate](https://github.com/deedy5/pyreqwest_impersonate) _The fastest python HTTP client that can impersonate web browsers by mimicking their headers and TLS/JA3/JA4/HTTP2 fingerprints._ - [ril-py](https://github.com/Cryptex-github/ril-py) _A performant and high-level image processing library for Python written in Rust._ - [river](https://github.com/online-ml/river) _Online machine learning in python, the computationally heavy statistics algorithms are implemented in Rust._ - [rust-python-coverage](https://github.com/cjermain/rust-python-coverage) _Example PyO3 project with automated test coverage for Rust and Python._ @@ -219,6 +222,7 @@ about this topic. ## Articles and other media +- [(Video) Extending Python with Rust using PyO3](https://www.youtube.com/watch?v=T45ZEmSR1-s) - Dec 16, 2023 - [A Week of PyO3 + rust-numpy (How to Speed Up Your Data Pipeline X Times)](https://terencezl.github.io/blog/2023/06/06/a-week-of-pyo3-rust-numpy/) - Jun 6, 2023 - [(Podcast) PyO3 with David Hewitt](https://rustacean-station.org/episode/david-hewitt/) - May 19, 2023 - [Making Python 100x faster with less than 100 lines of Rust](https://ohadravid.github.io/posts/2023-03-rusty-python/) - Mar 28, 2023 @@ -235,7 +239,7 @@ about this topic. Everyone is welcomed to contribute to PyO3! There are many ways to support the project, such as: -- help PyO3 users with issues on GitHub and Gitter +- help PyO3 users with issues on GitHub and [Discord](https://discord.gg/33kcChzH7f) - improve documentation - write features and bugfixes - publish blogs and examples of how to use PyO3 diff --git a/build.rs b/build.rs index e20206310db..5d638291f3b 100644 --- a/build.rs +++ b/build.rs @@ -16,7 +16,7 @@ fn ensure_auto_initialize_ok(interpreter_config: &InterpreterConfig) -> Result<( \n\ For more information, see \ https://pyo3.rs/v{pyo3_version}/\ - building_and_distribution.html#embedding-python-in-rust", + building-and-distribution.html#embedding-python-in-rust", pyo3_version = env::var("CARGO_PKG_VERSION").unwrap() ); } @@ -39,13 +39,14 @@ fn configure_pyo3() -> Result<()> { println!("{}", cfg) } - // Emit cfgs like `thread_local_const_init` + // Emit cfgs like `invalid_from_utf8_lint` print_feature_cfgs(); Ok(()) } fn main() { + pyo3_build_config::print_expected_cfgs(); if let Err(e) = configure_pyo3() { eprintln!("error: {}", e.report()); std::process::exit(1) diff --git a/examples/Cargo.toml b/examples/Cargo.toml index e54b3b5cde2..81557e7f534 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -10,5 +10,5 @@ pyo3 = { path = "..", features = ["auto-initialize", "extension-module"] } [[example]] name = "decorator" path = "decorator/src/lib.rs" -crate_type = ["cdylib"] +crate-type = ["cdylib"] doc-scrape-examples = true diff --git a/examples/decorator/.template/pre-script.rhai b/examples/decorator/.template/pre-script.rhai index 5ba02b12ffd..37372854cd8 100644 --- a/examples/decorator/.template/pre-script.rhai +++ b/examples/decorator/.template/pre-script.rhai @@ -1,4 +1,4 @@ -variable::set("PYO3_VERSION", "0.20.0"); +variable::set("PYO3_VERSION", "0.21.2"); file::rename(".template/Cargo.toml", "Cargo.toml"); file::rename(".template/pyproject.toml", "pyproject.toml"); file::delete(".template"); diff --git a/examples/decorator/src/lib.rs b/examples/decorator/src/lib.rs index bc369c62b1e..cfb09c112d5 100644 --- a/examples/decorator/src/lib.rs +++ b/examples/decorator/src/lib.rs @@ -40,8 +40,8 @@ impl PyCounter { fn __call__( &self, py: Python<'_>, - args: &PyTuple, - kwargs: Option<&PyDict>, + args: &Bound<'_, PyTuple>, + kwargs: Option<&Bound<'_, PyDict>>, ) -> PyResult> { let old_count = self.count.get(); let new_count = old_count + 1; @@ -51,7 +51,7 @@ impl PyCounter { println!("{} has been called {} time(s).", name, new_count); // After doing something, we finally forward the call to the wrapped function - let ret = self.wraps.call(py, args, kwargs)?; + let ret = self.wraps.call_bound(py, args, kwargs)?; // We could do something with the return value of // the function before returning it @@ -60,7 +60,7 @@ impl PyCounter { } #[pymodule] -pub fn decorator(_py: Python<'_>, module: &PyModule) -> PyResult<()> { +pub fn decorator(module: &Bound<'_, PyModule>) -> PyResult<()> { module.add_class::()?; Ok(()) } diff --git a/examples/getitem/src/lib.rs b/examples/getitem/src/lib.rs index 90a3e9fc52f..ce162a70bf9 100644 --- a/examples/getitem/src/lib.rs +++ b/examples/getitem/src/lib.rs @@ -2,12 +2,11 @@ use pyo3::exceptions::PyTypeError; use pyo3::prelude::*; use pyo3::types::PySlice; -use std::os::raw::c_long; #[derive(FromPyObject)] enum IntOrSlice<'py> { Int(i32), - Slice(&'py PySlice), + Slice(Bound<'py, PySlice>), } #[pyclass] @@ -23,13 +22,13 @@ impl ExampleContainer { ExampleContainer { max_length: 100 } } - fn __getitem__(&self, key: &PyAny) -> PyResult { + fn __getitem__(&self, key: &Bound<'_, PyAny>) -> PyResult { if let Ok(position) = key.extract::() { return Ok(position); } else if let Ok(slice) = key.downcast::() { // METHOD 1 - the use PySliceIndices to help with bounds checking and for cases when only start or end are provided // in this case the start/stop/step all filled in to give valid values based on the max_length given - let index = slice.indices(self.max_length as c_long).unwrap(); + let index = slice.indices(self.max_length as isize).unwrap(); let _delta = index.stop - index.start; // METHOD 2 - Do the getattr manually really only needed if you have some special cases for stop/_step not being present @@ -62,8 +61,11 @@ impl ExampleContainer { fn __setitem__(&self, idx: IntOrSlice, value: u32) -> PyResult<()> { match idx { IntOrSlice::Slice(slice) => { - let index = slice.indices(self.max_length as c_long).unwrap(); - println!("Got a slice! {}-{}, step: {}, value: {}", index.start, index.stop, index.step, value); + let index = slice.indices(self.max_length as isize).unwrap(); + println!( + "Got a slice! {}-{}, step: {}, value: {}", + index.start, index.stop, index.step, value + ); } IntOrSlice::Int(index) => { println!("Got an index! {} : value: {}", index, value); @@ -75,7 +77,7 @@ impl ExampleContainer { #[pymodule] #[pyo3(name = "getitem")] -fn example(_py: Python<'_>, m: &PyModule) -> PyResult<()> { +fn example(m: &Bound<'_, PyModule>) -> PyResult<()> { // ? -https://github.com/PyO3/maturin/issues/475 m.add_class::()?; Ok(()) diff --git a/examples/maturin-starter/.template/pre-script.rhai b/examples/maturin-starter/.template/pre-script.rhai index 5ba02b12ffd..37372854cd8 100644 --- a/examples/maturin-starter/.template/pre-script.rhai +++ b/examples/maturin-starter/.template/pre-script.rhai @@ -1,4 +1,4 @@ -variable::set("PYO3_VERSION", "0.20.0"); +variable::set("PYO3_VERSION", "0.21.2"); file::rename(".template/Cargo.toml", "Cargo.toml"); file::rename(".template/pyproject.toml", "pyproject.toml"); file::delete(".template"); diff --git a/examples/maturin-starter/src/lib.rs b/examples/maturin-starter/src/lib.rs index 96ace0f97d5..faa147b2a10 100644 --- a/examples/maturin-starter/src/lib.rs +++ b/examples/maturin-starter/src/lib.rs @@ -20,15 +20,15 @@ impl ExampleClass { /// An example module implemented in Rust using PyO3. #[pymodule] -fn maturin_starter(py: Python<'_>, m: &PyModule) -> PyResult<()> { +fn maturin_starter(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_wrapped(wrap_pymodule!(submodule::submodule))?; // Inserting to sys.modules allows importing submodules nicely from Python // e.g. from maturin_starter.submodule import SubmoduleClass - let sys = PyModule::import(py, "sys")?; - let sys_modules: &PyDict = sys.getattr("modules")?.downcast()?; + let sys = PyModule::import_bound(py, "sys")?; + let sys_modules: Bound<'_, PyDict> = sys.getattr("modules")?.downcast_into()?; sys_modules.set_item("maturin_starter.submodule", m.getattr("submodule")?)?; Ok(()) diff --git a/examples/maturin-starter/src/submodule.rs b/examples/maturin-starter/src/submodule.rs index 56540b2e469..f3eb174100b 100644 --- a/examples/maturin-starter/src/submodule.rs +++ b/examples/maturin-starter/src/submodule.rs @@ -16,7 +16,7 @@ impl SubmoduleClass { } #[pymodule] -pub fn submodule(_py: Python<'_>, m: &PyModule) -> PyResult<()> { +pub fn submodule(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; Ok(()) } diff --git a/examples/plugin/.template/pre-script.rhai b/examples/plugin/.template/pre-script.rhai index 94a61826dc2..7c2f375fbfb 100644 --- a/examples/plugin/.template/pre-script.rhai +++ b/examples/plugin/.template/pre-script.rhai @@ -1,4 +1,4 @@ -variable::set("PYO3_VERSION", "0.20.0"); +variable::set("PYO3_VERSION", "0.21.2"); file::rename(".template/Cargo.toml", "Cargo.toml"); file::rename(".template/plugin_api/Cargo.toml", "plugin_api/Cargo.toml"); file::delete(".template"); diff --git a/examples/plugin/plugin_api/src/lib.rs b/examples/plugin/plugin_api/src/lib.rs index 59aae55699d..580c85a8c8e 100644 --- a/examples/plugin/plugin_api/src/lib.rs +++ b/examples/plugin/plugin_api/src/lib.rs @@ -26,7 +26,7 @@ impl Gadget { /// A Python module for plugin interface types #[pymodule] -pub fn plugin_api(_py: Python<'_>, m: &PyModule) -> PyResult<()> { +pub fn plugin_api(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; Ok(()) } diff --git a/examples/plugin/src/main.rs b/examples/plugin/src/main.rs index b50b54548e5..5a54a1837cb 100644 --- a/examples/plugin/src/main.rs +++ b/examples/plugin/src/main.rs @@ -19,7 +19,7 @@ fn main() -> Result<(), Box> { // Now we can load our python_plugin/gadget_init_plugin.py file. // It can in turn import other stuff as it deems appropriate - let plugin = PyModule::import(py, "gadget_init_plugin")?; + let plugin = PyModule::import_bound(py, "gadget_init_plugin")?; // and call start function there, which will return a python reference to Gadget. // Gadget here is a "pyclass" object reference let gadget = plugin.getattr("start")?.call0()?; diff --git a/examples/setuptools-rust-starter/.template/pre-script.rhai b/examples/setuptools-rust-starter/.template/pre-script.rhai index e4ede9b7aff..dd2950665eb 100644 --- a/examples/setuptools-rust-starter/.template/pre-script.rhai +++ b/examples/setuptools-rust-starter/.template/pre-script.rhai @@ -1,4 +1,4 @@ -variable::set("PYO3_VERSION", "0.20.0"); +variable::set("PYO3_VERSION", "0.21.2"); file::rename(".template/Cargo.toml", "Cargo.toml"); file::rename(".template/setup.cfg", "setup.cfg"); file::delete(".template"); diff --git a/examples/setuptools-rust-starter/src/lib.rs b/examples/setuptools-rust-starter/src/lib.rs index fbfeccc1555..d31284be7a3 100644 --- a/examples/setuptools-rust-starter/src/lib.rs +++ b/examples/setuptools-rust-starter/src/lib.rs @@ -20,15 +20,15 @@ impl ExampleClass { /// An example module implemented in Rust using PyO3. #[pymodule] -fn _setuptools_rust_starter(py: Python<'_>, m: &PyModule) -> PyResult<()> { +fn _setuptools_rust_starter(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_wrapped(wrap_pymodule!(submodule::submodule))?; // Inserting to sys.modules allows importing submodules nicely from Python // e.g. from setuptools_rust_starter.submodule import SubmoduleClass - let sys = PyModule::import(py, "sys")?; - let sys_modules: &PyDict = sys.getattr("modules")?.downcast()?; + let sys = PyModule::import_bound(py, "sys")?; + let sys_modules: Bound<'_, PyDict> = sys.getattr("modules")?.downcast_into()?; sys_modules.set_item("setuptools_rust_starter.submodule", m.getattr("submodule")?)?; Ok(()) diff --git a/examples/setuptools-rust-starter/src/submodule.rs b/examples/setuptools-rust-starter/src/submodule.rs index 56540b2e469..f3eb174100b 100644 --- a/examples/setuptools-rust-starter/src/submodule.rs +++ b/examples/setuptools-rust-starter/src/submodule.rs @@ -16,7 +16,7 @@ impl SubmoduleClass { } #[pymodule] -pub fn submodule(_py: Python<'_>, m: &PyModule) -> PyResult<()> { +pub fn submodule(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; Ok(()) } diff --git a/examples/word-count/.template/pre-script.rhai b/examples/word-count/.template/pre-script.rhai index 5ba02b12ffd..37372854cd8 100644 --- a/examples/word-count/.template/pre-script.rhai +++ b/examples/word-count/.template/pre-script.rhai @@ -1,4 +1,4 @@ -variable::set("PYO3_VERSION", "0.20.0"); +variable::set("PYO3_VERSION", "0.21.2"); file::rename(".template/Cargo.toml", "Cargo.toml"); file::rename(".template/pyproject.toml", "pyproject.toml"); file::delete(".template"); diff --git a/examples/word-count/src/lib.rs b/examples/word-count/src/lib.rs index 95ffcff1d0b..c76af8b3de5 100644 --- a/examples/word-count/src/lib.rs +++ b/examples/word-count/src/lib.rs @@ -33,7 +33,7 @@ fn count_line(line: &str, needle: &str) -> usize { } #[pymodule] -fn word_count(_py: Python<'_>, m: &PyModule) -> PyResult<()> { +fn word_count(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(search, m)?)?; m.add_function(wrap_pyfunction!(search_sequential, m)?)?; m.add_function(wrap_pyfunction!(search_sequential_allow_threads, m)?)?; diff --git a/guide/book.toml b/guide/book.toml index 31fa4bb1587..bccc3506098 100644 --- a/guide/book.toml +++ b/guide/book.toml @@ -9,4 +9,4 @@ command = "python3 guide/pyo3_version.py" [output.html] git-repository-url = "https://github.com/PyO3/pyo3/tree/main/guide" edit-url-template = "https://github.com/PyO3/pyo3/edit/main/guide/{path}" -playground.runnable = false \ No newline at end of file +playground.runnable = false diff --git a/guide/pyclass_parameters.md b/guide/pyclass-parameters.md similarity index 89% rename from guide/pyclass_parameters.md rename to guide/pyclass-parameters.md index 35c54147df5..9bd0534ea5d 100644 --- a/guide/pyclass_parameters.md +++ b/guide/pyclass-parameters.md @@ -2,6 +2,7 @@ | Parameter | Description | | :- | :- | +| `constructor` | This is currently only allowed on [variants of complex enums][params-constructor]. It allows customization of the generated class constructor for each variant. It uses the same syntax and supports the same options as the `signature` attribute of functions and methods. | | `crate = "some::path"` | Path to import the `pyo3` crate, if it's not accessible at `::pyo3`. | | `dict` | Gives instances of this class an empty `__dict__` to store custom attributes. | | `extends = BaseType` | Use a custom baseclass. Defaults to [`PyAny`][params-1] | @@ -33,11 +34,12 @@ struct MyClass {} struct MyClass {} ``` -[params-1]: https://docs.rs/pyo3/latest/pyo3/struct.PyAny.html +[params-1]: https://docs.rs/pyo3/latest/pyo3/types/struct.PyAny.html [params-2]: https://en.wikipedia.org/wiki/Free_list [params-3]: https://doc.rust-lang.org/std/marker/trait.Send.html [params-4]: https://doc.rust-lang.org/std/rc/struct.Rc.html [params-5]: https://doc.rust-lang.org/std/sync/struct.Arc.html [params-6]: https://docs.python.org/3/library/weakref.html +[params-constructor]: https://pyo3.rs/latest/class.html#complex-enums [params-mapping]: https://pyo3.rs/latest/class/protocols.html#mapping--sequence-types [params-sequence]: https://pyo3.rs/latest/class/protocols.html#mapping--sequence-types diff --git a/guide/src/SUMMARY.md b/guide/src/SUMMARY.md index 34775a0d185..4c22c26f587 100644 --- a/guide/src/SUMMARY.md +++ b/guide/src/SUMMARY.md @@ -4,31 +4,34 @@ --- -- [Getting started](getting_started.md) -- [Python modules](module.md) -- [Python functions](function.md) - - [Function signatures](function/signature.md) - - [Error handling](function/error_handling.md) -- [Python classes](class.md) - - [Class customizations](class/protocols.md) - - [Basic object customization](class/object.md) - - [Emulating numeric types](class/numeric.md) - - [Emulating callable objects](class/call.md) +- [Getting started](getting-started.md) +- [Using Rust from Python](rust-from-python.md) + - [Python modules](module.md) + - [Python functions](function.md) + - [Function signatures](function/signature.md) + - [Error handling](function/error-handling.md) + - [Python classes](class.md) + - [Class customizations](class/protocols.md) + - [Basic object customization](class/object.md) + - [Emulating numeric types](class/numeric.md) + - [Emulating callable objects](class/call.md) +- [Calling Python from Rust](python-from-rust.md) + - [Python object types](types.md) + - [Python exceptions](exception.md) + - [Calling Python functions](python-from-rust/function-calls.md) + - [Executing existing Python code](python-from-rust/calling-existing-code.md) - [Type conversions](conversions.md) - [Mapping of Rust types to Python types](conversions/tables.md) - [Conversion traits](conversions/traits.md) -- [Python exceptions](exception.md) -- [Calling Python from Rust](python_from_rust.md) - [Using `async` and `await`](async-await.md) -- [GIL, mutability and object types](types.md) - [Parallelism](parallelism.md) - [Debugging](debugging.md) - [Features reference](features.md) - [Memory management](memory.md) - [Performance](performance.md) - [Advanced topics](advanced.md) -- [Building and distribution](building_and_distribution.md) - - [Supporting multiple Python versions](building_and_distribution/multiple_python_versions.md) +- [Building and distribution](building-and-distribution.md) + - [Supporting multiple Python versions](building-and-distribution/multiple-python-versions.md) - [Useful crates](ecosystem.md) - [Logging](ecosystem/logging.md) - [Using `async` and `await`](ecosystem/async-await.md) @@ -37,8 +40,8 @@ --- [Appendix A: Migration guide](migration.md) -[Appendix B: Trait bounds](trait_bounds.md) -[Appendix C: Python typing hints](python_typing_hints.md) +[Appendix B: Trait bounds](trait-bounds.md) +[Appendix C: Python typing hints](python-typing-hints.md) [CHANGELOG](changelog.md) --- diff --git a/guide/src/advanced.md b/guide/src/advanced.md index 8264c14dd17..61dc66382f4 100644 --- a/guide/src/advanced.md +++ b/guide/src/advanced.md @@ -5,10 +5,3 @@ PyO3 exposes much of Python's C API through the `ffi` module. The C API is naturally unsafe and requires you to manage reference counts, errors and specific invariants yourself. Please refer to the [C API Reference Manual](https://docs.python.org/3/c-api/) and [The Rustonomicon](https://doc.rust-lang.org/nightly/nomicon/ffi.html) before using any function from that API. - -## Memory management - -PyO3's `&PyAny` "owned references" and `Py` smart pointers are used to -access memory stored in Python's heap. This memory sometimes lives for longer -than expected because of differences in Rust and Python's memory models. See -the chapter on [memory management](./memory.md) for more information. diff --git a/guide/src/async-await.md b/guide/src/async-await.md index 1b8d038c6b6..8d38b1ef89f 100644 --- a/guide/src/async-await.md +++ b/guide/src/async-await.md @@ -6,11 +6,13 @@ ```rust # #![allow(dead_code)] +# #[cfg(feature = "experimental-async")] { use std::{thread, time::Duration}; use futures::channel::oneshot; use pyo3::prelude::*; #[pyfunction] +#[pyo3(signature=(seconds, result=None))] async fn sleep(seconds: f64, result: Option) -> Option { let (tx, rx) = oneshot::channel(); thread::spawn(move || { @@ -20,6 +22,7 @@ async fn sleep(seconds: f64, result: Option) -> Option { rx.await.unwrap(); result } +# } ``` *Python awaitables instantiated with this method can only be awaited in *asyncio* context. Other Python async runtime may be supported in the future.* @@ -28,15 +31,15 @@ async fn sleep(seconds: f64, result: Option) -> Option { Resulting future of an `async fn` decorated by `#[pyfunction]` must be `Send + 'static` to be embedded in a Python object. -As a consequence, `async fn` parameters and return types must also be `Send + 'static`, so it is not possible to have a signature like `async fn does_not_compile(arg: &PyAny, py: Python<'_>) -> &PyAny`. +As a consequence, `async fn` parameters and return types must also be `Send + 'static`, so it is not possible to have a signature like `async fn does_not_compile<'py>(arg: Bound<'py, PyAny>) -> Bound<'py, PyAny>`. -However, there is an exception for method receiver, so async methods can accept `&self`/`&mut self`. Note that this means that the class instance is borrowed for as long as the returned future is not completed, even across yield points and while waiting for I/O operations to complete. Hence, other methods cannot obtain exclusive borrows while the future is still being polled. This is the same as how async methods in Rust generally work but it is more problematic for Rust code interfacing with Python code due to pervasive shared mutability. This strongly suggests to prefer shared borrows `&self` to exclusive ones `&mut self` to avoid racy borrow check failures at runtime. +However, there is an exception for method receivers, so async methods can accept `&self`/`&mut self`. Note that this means that the class instance is borrowed for as long as the returned future is not completed, even across yield points and while waiting for I/O operations to complete. Hence, other methods cannot obtain exclusive borrows while the future is still being polled. This is the same as how async methods in Rust generally work but it is more problematic for Rust code interfacing with Python code due to pervasive shared mutability. This strongly suggests to prefer shared borrows `&self` over exclusive ones `&mut self` to avoid racy borrow check failures at runtime. ## Implicit GIL holding -Even if it is not possible to pass a `py: Python<'_>` parameter to `async fn`, the GIL is still held during the execution of the future – it's also the case for regular `fn` without `Python<'_>`/`&PyAny` parameter, yet the GIL is held. +Even if it is not possible to pass a `py: Python<'py>` parameter to `async fn`, the GIL is still held during the execution of the future – it's also the case for regular `fn` without `Python<'py>`/`Bound<'py, PyAny>` parameter, yet the GIL is held. -It is still possible to get a `Python` marker using [`Python::with_gil`]({{#PYO3_DOCS_URL}}/pyo3/struct.Python.html#method.with_gil); because `with_gil` is reentrant and optimized, the cost will be negligible. +It is still possible to get a `Python` marker using [`Python::with_gil`]({{#PYO3_DOCS_URL}}/pyo3/marker/struct.Python.html#method.with_gil); because `with_gil` is reentrant and optimized, the cost will be negligible. ## Release the GIL across `.await` @@ -45,7 +48,11 @@ There is currently no simple way to release the GIL when awaiting a future, *but Here is the advised workaround for now: ```rust,ignore -use std::{future::Future, pin::{Pin, pin}, task::{Context, Poll}}; +use std::{ + future::Future, + pin::{Pin, pin}, + task::{Context, Poll}, +}; use pyo3::prelude::*; struct AllowThreads(F); @@ -68,10 +75,11 @@ where ## Cancellation -Cancellation on the Python side can be caught using [`CancelHandle`]({{#PYO3_DOCS_URL}}/pyo3/coroutine/struct.CancelHandle.html) type, by annotating a function parameter with `#[pyo3(cancel_handle)]. +Cancellation on the Python side can be caught using [`CancelHandle`]({{#PYO3_DOCS_URL}}/pyo3/coroutine/struct.CancelHandle.html) type, by annotating a function parameter with `#[pyo3(cancel_handle)]`. ```rust # #![allow(dead_code)] +# #[cfg(feature = "experimental-async")] { use futures::FutureExt; use pyo3::prelude::*; use pyo3::coroutine::CancelHandle; @@ -83,11 +91,12 @@ async fn cancellable(#[pyo3(cancel_handle)] mut cancel: CancelHandle) { _ = cancel.cancelled().fuse() => println!("cancelled"), } } +# } ``` ## The `Coroutine` type -To make a Rust future awaitable in Python, PyO3 defines a [`Coroutine`]({{#PYO3_DOCS_URL}}/pyo3/coroutine/struct.Coroutine.html) type, which implements the Python [coroutine protocol](https://docs.python.org/3/library/collections.abc.html#collections.abc.Coroutine). +To make a Rust future awaitable in Python, PyO3 defines a [`Coroutine`]({{#PYO3_DOCS_URL}}/pyo3/coroutine/struct.Coroutine.html) type, which implements the Python [coroutine protocol](https://docs.python.org/3/library/collections.abc.html#collections.abc.Coroutine). Each `coroutine.send` call is translated to a `Future::poll` call. If a [`CancelHandle`]({{#PYO3_DOCS_URL}}/pyo3/coroutine/struct.CancelHandle.html) parameter is declared, the exception passed to `coroutine.throw` call is stored in it and can be retrieved with [`CancelHandle::cancelled`]({{#PYO3_DOCS_URL}}/pyo3/coroutine/struct.CancelHandle.html#method.cancelled); otherwise, it cancels the Rust future, and the exception is reraised; diff --git a/guide/src/building_and_distribution.md b/guide/src/building-and-distribution.md similarity index 95% rename from guide/src/building_and_distribution.md rename to guide/src/building-and-distribution.md index 8cb675f4303..33280a5a180 100644 --- a/guide/src/building_and_distribution.md +++ b/guide/src/building-and-distribution.md @@ -2,9 +2,9 @@ This chapter of the guide goes into detail on how to build and distribute projects using PyO3. The way to achieve this is very different depending on whether the project is a Python module implemented in Rust, or a Rust binary embedding Python. For both types of project there are also common problems such as the Python version to build for and the [linker](https://en.wikipedia.org/wiki/Linker_(computing)) arguments to use. -The material in this chapter is intended for users who have already read the PyO3 [README](#index.md). It covers in turn the choices that can be made for Python modules and for Rust binaries. There is also a section at the end about cross-compiling projects using PyO3. +The material in this chapter is intended for users who have already read the PyO3 [README](./index.md). It covers in turn the choices that can be made for Python modules and for Rust binaries. There is also a section at the end about cross-compiling projects using PyO3. -There is an additional sub-chapter dedicated to [supporting multiple Python versions](./building_and_distribution/multiple_python_versions.html). +There is an additional sub-chapter dedicated to [supporting multiple Python versions](./building-and-distribution/multiple-python-versions.md). ## Configuring the Python version @@ -151,7 +151,7 @@ rustflags = [ ] ``` -Alternatively, on rust >= 1.56, one can include in `build.rs`: +Alternatively, one can include in `build.rs`: ```rust fn main() { @@ -177,7 +177,7 @@ The downside of not linking to `libpython` is that binaries, tests, and examples By default, Python extension modules can only be used with the same Python version they were compiled against. For example, an extension module built for Python 3.5 can't be imported in Python 3.8. [PEP 384](https://www.python.org/dev/peps/pep-0384/) introduced the idea of the limited Python API, which would have a stable ABI enabling extension modules built with it to be used against multiple Python versions. This is also known as `abi3`. -The advantage of building extension modules using the limited Python API is that package vendors only need to build and distribute a single copy (for each OS / architecture), and users can install it on all Python versions from the [minimum version](#minimum-python-version-for-abi3) and up. The downside of this is that PyO3 can't use optimizations which rely on being compiled against a known exact Python version. It's up to you to decide whether this matters for your extension module. It's also possible to design your extension module such that you can distribute `abi3` wheels but allow users compiling from source to benefit from additional optimizations - see the [support for multiple python versions](./building_and_distribution/multiple_python_versions.html) section of this guide, in particular the `#[cfg(Py_LIMITED_API)]` flag. +The advantage of building extension modules using the limited Python API is that package vendors only need to build and distribute a single copy (for each OS / architecture), and users can install it on all Python versions from the [minimum version](#minimum-python-version-for-abi3) and up. The downside of this is that PyO3 can't use optimizations which rely on being compiled against a known exact Python version. It's up to you to decide whether this matters for your extension module. It's also possible to design your extension module such that you can distribute `abi3` wheels but allow users compiling from source to benefit from additional optimizations - see the [support for multiple python versions](./building-and-distribution/multiple-python-versions.md) section of this guide, in particular the `#[cfg(Py_LIMITED_API)]` flag. There are three steps involved in making use of `abi3` when building Python packages as wheels: @@ -198,7 +198,7 @@ See the [corresponding](https://github.com/PyO3/maturin/pull/353) [PRs](https:// Because a single `abi3` wheel can be used with many different Python versions, PyO3 has feature flags `abi3-py37`, `abi3-py38`, `abi3-py39` etc. to set the minimum required Python version for your `abi3` wheel. For example, if you set the `abi3-py37` feature, your extension wheel can be used on all Python 3 versions from Python 3.7 and up. `maturin` and `setuptools-rust` will give the wheel a name like `my-extension-1.0-cp37-abi3-manylinux2020_x86_64.whl`. -As your extension module may be run with multiple different Python versions you may occasionally find you need to check the Python version at runtime to customize behavior. See [the relevant section of this guide](./building_and_distribution/multiple_python_versions.html#checking-the-python-version-at-runtime) on supporting multiple Python versions at runtime. +As your extension module may be run with multiple different Python versions you may occasionally find you need to check the Python version at runtime to customize behavior. See [the relevant section of this guide](./building-and-distribution/multiple-python-versions.md#checking-the-python-version-at-runtime) on supporting multiple Python versions at runtime. PyO3 is only able to link your extension module to abi3 version up to and including your host Python version. E.g., if you set `abi3-py38` and try to compile the crate with a host of Python 3.7, the build will fail. @@ -232,7 +232,7 @@ not work when compiling for `abi3`. These are: If you want to embed the Python interpreter inside a Rust program, there are two modes in which this can be done: dynamically and statically. We'll cover each of these modes in the following sections. Each of them affect how you must distribute your program. Instead of learning how to do this yourself, you might want to consider using a project like [PyOxidizer] to ship your application and all of its dependencies in a single file. -PyO3 automatically switches between the two linking modes depending on whether the Python distribution you have configured PyO3 to use ([see above](#python-version)) contains a shared library or a static library. The static library is most often seen in Python distributions compiled from source without the `--enable-shared` configuration option. For example, this is the default for `pyenv` on macOS. +PyO3 automatically switches between the two linking modes depending on whether the Python distribution you have configured PyO3 to use ([see above](#configuring-the-python-version)) contains a shared library or a static library. The static library is most often seen in Python distributions compiled from source without the `--enable-shared` configuration option. ### Dynamically embedding the Python interpreter @@ -242,7 +242,7 @@ This mode of embedding works well for Rust tests which need access to the Python For distributing your program to non-technical users, you will have to consider including the Python shared library in your distribution as well as setting up wrapper scripts to set the right environment variables (such as `LD_LIBRARY_PATH` on UNIX, or `PATH` on Windows). -Note that PyPy cannot be embedded in Rust (or any other software). Support for this is tracked on the [PyPy issue tracker](https://foss.heptapod.net/pypy/pypy/-/issues/3286). +Note that PyPy cannot be embedded in Rust (or any other software). Support for this is tracked on the [PyPy issue tracker](https://github.com/pypy/pypy/issues/3836). ### Statically embedding the Python interpreter @@ -285,7 +285,7 @@ Thanks to Rust's great cross-compilation support, cross-compiling using PyO3 is * A toolchain for your target. * The appropriate options in your Cargo `.config` for the platform you're targeting and the toolchain you are using. * A Python interpreter that's already been compiled for your target (optional when building "abi3" extension modules). -* A Python interpreter that is built for your host and available through the `PATH` or setting the [`PYO3_PYTHON`](#python-version) variable (optional when building "abi3" extension modules). +* A Python interpreter that is built for your host and available through the `PATH` or setting the [`PYO3_PYTHON`](#configuring-the-python-version) variable (optional when building "abi3" extension modules). After you've obtained the above, you can build a cross-compiled PyO3 module by using Cargo's `--target` flag. PyO3's build script will detect that you are attempting a cross-compile based on your host machine and the desired target. diff --git a/guide/src/building_and_distribution/multiple_python_versions.md b/guide/src/building-and-distribution/multiple-python-versions.md similarity index 94% rename from guide/src/building_and_distribution/multiple_python_versions.md rename to guide/src/building-and-distribution/multiple-python-versions.md index 43203686fc8..b328d236c51 100644 --- a/guide/src/building_and_distribution/multiple_python_versions.md +++ b/guide/src/building-and-distribution/multiple-python-versions.md @@ -85,7 +85,7 @@ This `#[cfg]` marks code which is running on PyPy. ## Checking the Python version at runtime -When building with PyO3's `abi3` feature, your extension module will be compiled against a specific [minimum version](../building_and_distribution.html#minimum-python-version-for-abi3) of Python, but may be running on newer Python versions. +When building with PyO3's `abi3` feature, your extension module will be compiled against a specific [minimum version](../building-and-distribution.md#minimum-python-version-for-abi3) of Python, but may be running on newer Python versions. For example with PyO3's `abi3-py38` feature, your extension will be compiled as if it were for Python 3.8. If you were using `pyo3-build-config`, `#[cfg(Py_3_8)]` would be present. Your user could freely install and run your abi3 extension on Python 3.9. @@ -104,5 +104,5 @@ Python::with_gil(|py| { }); ``` -[`Python::version()`]: {{#PYO3_DOCS_URL}}/pyo3/struct.Python.html#method.version -[`Python::version_info()`]: {{#PYO3_DOCS_URL}}/pyo3/struct.Python.html#method.version_info +[`Python::version()`]: {{#PYO3_DOCS_URL}}/pyo3/marker/struct.Python.html#method.version +[`Python::version_info()`]: {{#PYO3_DOCS_URL}}/pyo3/marker/struct.Python.html#method.version_info diff --git a/guide/src/class.md b/guide/src/class.md index e3f3502780b..57a5cf6d467 100644 --- a/guide/src/class.md +++ b/guide/src/class.md @@ -2,7 +2,7 @@ PyO3 exposes a group of attributes powered by Rust's proc macro system for defining Python classes as Rust structs. -The main attribute is `#[pyclass]`, which is placed upon a Rust `struct` or a fieldless `enum` (a.k.a. C-like enum) to generate a Python type for it. They will usually also have *one* `#[pymethods]`-annotated `impl` block for the struct, which is used to define Python methods and constants for the generated Python type. (If the [`multiple-pymethods`] feature is enabled, each `#[pyclass]` is allowed to have multiple `#[pymethods]` blocks.) `#[pymethods]` may also have implementations for Python magic methods such as `__str__`. +The main attribute is `#[pyclass]`, which is placed upon a Rust `struct` or `enum` to generate a Python type for it. They will usually also have *one* `#[pymethods]`-annotated `impl` block for the struct, which is used to define Python methods and constants for the generated Python type. (If the [`multiple-pymethods`] feature is enabled, each `#[pyclass]` is allowed to have multiple `#[pymethods]` blocks.) `#[pymethods]` may also have implementations for Python magic methods such as `__str__`. This chapter will discuss the functionality and configuration these attributes offer. Below is a list of links to the relevant section of this chapter for each: @@ -16,18 +16,18 @@ This chapter will discuss the functionality and configuration these attributes o - [`#[classmethod]`](#class-methods) - [`#[classattr]`](#class-attributes) - [`#[args]`](#method-arguments) -- [Magic methods and slots](class/protocols.html) +- [Magic methods and slots](class/protocols.md) - [Classes as function arguments](#classes-as-function-arguments) ## Defining a new class -To define a custom Python class, add the `#[pyclass]` attribute to a Rust struct or a fieldless enum. +To define a custom Python class, add the `#[pyclass]` attribute to a Rust struct or enum. ```rust # #![allow(dead_code)] use pyo3::prelude::*; #[pyclass] -struct Integer { +struct MyClass { inner: i32, } @@ -35,7 +35,15 @@ struct Integer { #[pyclass] struct Number(i32); -// PyO3 supports custom discriminants in enums +// PyO3 supports unit-only enums (which contain only unit variants) +// These simple enums behave similarly to Python's enumerations (enum.Enum) +#[pyclass] +enum MyEnum { + Variant, + OtherVariant = 30, // PyO3 supports custom discriminants. +} + +// PyO3 supports custom discriminants in unit-only enums #[pyclass] enum HttpResponse { Ok = 200, @@ -44,14 +52,22 @@ enum HttpResponse { // ... } +// PyO3 also supports enums with Struct and Tuple variants +// These complex enums have sligtly different behavior from the simple enums above +// They are meant to work with instance checks and match statement patterns +// The variants can be mixed and matched +// Struct variants have named fields while tuple enums generate generic names for fields in order _0, _1, _2, ... +// Apart from this both types are functionally identical #[pyclass] -enum MyEnum { - Variant, - OtherVariant = 30, // PyO3 supports custom discriminants. +enum Shape { + Circle { radius: f64 }, + Rectangle { width: f64, height: f64 }, + RegularPolygon(u32, f64), + Nothing(), } ``` -The above example generates implementations for [`PyTypeInfo`] and [`PyClass`] for `MyClass` and `MyEnum`. To see these generated implementations, refer to the [implementation details](#implementation-details) at the end of this chapter. +The above example generates implementations for [`PyTypeInfo`] and [`PyClass`] for `MyClass`, `Number`, `MyEnum`, `HttpResponse`, and `Shape`. To see these generated implementations, refer to the [implementation details](#implementation-details) at the end of this chapter. ### Restrictions @@ -69,6 +85,38 @@ When you need to share ownership of data between Python and Rust, instead of usi A Rust `struct Foo` with a generic parameter `T` generates new compiled implementations each time it is used with a different concrete type for `T`. These new implementations are generated by the compiler at each usage site. This is incompatible with wrapping `Foo` in Python, where there needs to be a single compiled implementation of `Foo` which is integrated with the Python interpreter. +Currently, the best alternative is to write a macro which expands to a new `#[pyclass]` for each instantiation you want: + +```rust +# #![allow(dead_code)] +use pyo3::prelude::*; + +struct GenericClass { + data: T, +} + +macro_rules! create_interface { + ($name: ident, $type: ident) => { + #[pyclass] + pub struct $name { + inner: GenericClass<$type>, + } + #[pymethods] + impl $name { + #[new] + pub fn new(data: $type) -> Self { + Self { + inner: GenericClass { data: data }, + } + } + } + }; +} + +create_interface!(IntClass, i64); +create_interface!(FloatClass, String); +``` + #### Must be Send Because Python objects are freely shared between threads by the Python interpreter, there is no guarantee which thread will eventually drop the object. Therefore all types annotated with `#[pyclass]` must implement `Send` (unless annotated with [`#[pyclass(unsendable)]`](#customizing-the-class)). @@ -80,6 +128,7 @@ To declare a constructor, you need to define a method and annotate it with the ` attribute. Only Python's `__new__` method can be specified, `__init__` is not available. ```rust +# #![allow(dead_code)] # use pyo3::prelude::*; # #[pyclass] # struct Number(i32); @@ -96,6 +145,7 @@ impl Number { Alternatively, if your `new` method may fail you can return `PyResult`. ```rust +# #![allow(dead_code)] # use pyo3::prelude::*; # use pyo3::exceptions::PyValueError; # #[pyclass] @@ -130,37 +180,33 @@ For arguments, see the [`Method arguments`](#method-arguments) section below. The next step is to create the module initializer and add our class to it: ```rust +# #![allow(dead_code)] # use pyo3::prelude::*; # #[pyclass] # struct Number(i32); # #[pymodule] -fn my_module(_py: Python<'_>, m: &PyModule) -> PyResult<()> { +fn my_module(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; Ok(()) } ``` -## PyCell and interior mutability +## Bound and interior mutability -You sometimes need to convert your `pyclass` into a Python object and access it -from Rust code (e.g., for testing it). -[`PyCell`] is the primary interface for that. +Often is useful to turn a `#[pyclass]` type `T` into a Python object and access it from Rust code. The [`Py`] and [`Bound<'py, T>`] smart pointers are the ways to represent a Python object in PyO3's API. More detail can be found about them [in the Python objects](./types.md#pyo3s-smart-pointers) section of the guide. -`PyCell` is always allocated in the Python heap, so Rust doesn't have ownership of it. -In other words, Rust code can only extract a `&PyCell`, not a `PyCell`. +Most Python objects do not offer exclusive (`&mut`) access (see the [section on Python's memory model](./python-from-rust.md#pythons-memory-model)). However, Rust structs wrapped as Python objects (called `pyclass` types) often *do* need `&mut` access. Due to the GIL, PyO3 *can* guarantee exclusive access to them. -Thus, to mutate data behind `&PyCell` safely, PyO3 employs the -[Interior Mutability Pattern](https://doc.rust-lang.org/book/ch15-05-interior-mutability.html) -like [`RefCell`]. +The Rust borrow checker cannot reason about `&mut` references once an object's ownership has been passed to the Python interpreter. This means that borrow checking is done at runtime using with a scheme very similar to `std::cell::RefCell`. This is known as [interior mutability](https://doc.rust-lang.org/book/ch15-05-interior-mutability.html). -Users who are familiar with `RefCell` can use `PyCell` just like `RefCell`. +Users who are familiar with `RefCell` can use `Py` and `Bound<'py, T>` just like `RefCell`. -For users who are not very familiar with `RefCell`, here is a reminder of Rust's rules of borrowing: +For users who are not very familiar with `RefCell`, here is a reminder of Rust's rules of borrowing: - At any given time, you can have either (but not both of) one mutable reference or any number of immutable references. -- References must always be valid. +- References can never outlast the data they refer to. -`PyCell`, like `RefCell`, ensures these borrowing rules by tracking references at runtime. +`Py` and `Bound<'py, T>`, like `RefCell`, ensure these borrowing rules by tracking references at runtime. ```rust # use pyo3::prelude::*; @@ -170,7 +216,7 @@ struct MyClass { num: i32, } Python::with_gil(|py| { - let obj = PyCell::new(py, MyClass { num: 3 }).unwrap(); + let obj = Bound::new(py, MyClass { num: 3 }).unwrap(); { let obj_ref = obj.borrow(); // Get PyRef assert_eq!(obj_ref.num, 3); @@ -185,15 +231,13 @@ Python::with_gil(|py| { assert!(obj.try_borrow_mut().is_err()); } - // You can convert `&PyCell` to a Python object + // You can convert `Bound` to a Python object pyo3::py_run!(py, obj, "assert obj.num == 5"); }); ``` -`&PyCell` is bounded by the same lifetime as a [`GILGuard`]. -To make the object longer lived (for example, to store it in a struct on the -Rust side), you can use `Py`, which stores an object longer than the GIL -lifetime, and therefore needs a `Python<'_>` token to access. +A `Bound<'py, T>` is restricted to the GIL lifetime `'py`. To make the object longer lived (for example, to store it in a struct on the +Rust side), use `Py`. `Py` needs a `Python<'_>` token to allow access: ```rust # use pyo3::prelude::*; @@ -208,9 +252,9 @@ fn return_myclass() -> Py { let obj = return_myclass(); -Python::with_gil(|py| { - let cell = obj.as_ref(py); // Py::as_ref returns &PyCell - let obj_ref = cell.borrow(); // Get PyRef +Python::with_gil(move |py| { + let bound = obj.bind(py); // Py::bind returns &Bound<'py, MyClass> + let obj_ref = bound.borrow(); // Get PyRef assert_eq!(obj_ref.num, 1); }); ``` @@ -219,7 +263,7 @@ Python::with_gil(|py| { As detailed above, runtime borrow checking is currently enabled by default. But a class can opt of out it by declaring itself `frozen`. It can still use interior mutability via standard Rust types like `RefCell` or `Mutex`, but it is not bound to the implementation provided by PyO3 and can choose the most appropriate strategy on field-by-field basis. -Classes which are `frozen` and also `Sync`, e.g. they do use `Mutex` but not `RefCell`, can be accessed without needing the Python GIL via the `PyCell::get` and `Py::get` methods: +Classes which are `frozen` and also `Sync`, e.g. they do use `Mutex` but not `RefCell`, can be accessed without needing the Python GIL via the `Bound::get` and `Py::get` methods: ```rust use std::sync::atomic::{AtomicUsize, Ordering}; @@ -239,13 +283,15 @@ let py_counter: Py = Python::with_gil(|py| { }); py_counter.get().value.fetch_add(1, Ordering::Relaxed); + +Python::with_gil(move |_py| drop(py_counter)); ``` Frozen classes are likely to become the default thereby guiding the PyO3 ecosystem towards a more deliberate application of interior mutability. Eventually, this should enable further optimizations of PyO3's internals and avoid downstream code paying the cost of interior mutability when it is not actually required. ## Customizing the class -{{#include ../pyclass_parameters.md}} +{{#include ../pyclass-parameters.md}} These parameters are covered in various sections of this guide. @@ -351,11 +397,11 @@ impl SubSubClass { } } # Python::with_gil(|py| { -# let subsub = pyo3::PyCell::new(py, SubSubClass::new()).unwrap(); +# let subsub = pyo3::Py::new(py, SubSubClass::new()).unwrap(); # pyo3::py_run!(py, subsub, "assert subsub.method3() == 3000"); # let subsub = SubSubClass::factory_method(py, 2).unwrap(); # let subsubsub = SubSubClass::factory_method(py, 3).unwrap(); -# let cls = py.get_type::(); +# let cls = py.get_type_bound::(); # pyo3::py_run!(py, subsub cls, "assert not isinstance(subsub, cls)"); # pyo3::py_run!(py, subsubsub cls, "assert isinstance(subsubsub, cls)"); # }); @@ -365,8 +411,9 @@ You can inherit native types such as `PyDict`, if they implement [`PySizedLayout`]({{#PYO3_DOCS_URL}}/pyo3/type_object/trait.PySizedLayout.html). This is not supported when building for the Python limited API (aka the `abi3` feature of PyO3). -However, because of some technical problems, we don't currently provide safe upcasting methods for types -that inherit native types. Even in such cases, you can unsafely get a base class by raw pointer conversion. +To convert between the Rust type and its native base class, you can take +`slf` as a Python object. To access the Rust fields use `slf.borrow()` or +`slf.borrow_mut()`, and to access the base class use `slf.downcast::()`. ```rust # #[cfg(not(Py_LIMITED_API))] { @@ -387,15 +434,14 @@ impl DictWithCounter { Self::default() } - fn set(mut self_: PyRefMut<'_, Self>, key: String, value: &PyAny) -> PyResult<()> { - self_.counter.entry(key.clone()).or_insert(0); - let py = self_.py(); - let dict: &PyDict = unsafe { py.from_borrowed_ptr_or_err(self_.as_ptr())? }; + fn set(slf: &Bound<'_, Self>, key: String, value: Bound<'_, PyAny>) -> PyResult<()> { + slf.borrow_mut().counter.entry(key.clone()).or_insert(0); + let dict = slf.downcast::()?; dict.set_item(key, value) } } # Python::with_gil(|py| { -# let cnt = pyo3::PyCell::new(py, DictWithCounter::new()).unwrap(); +# let cnt = pyo3::Py::new(py, DictWithCounter::new()).unwrap(); # pyo3::py_run!(py, cnt, "cnt.set('abc', 10); assert cnt['abc'] == 10") # }); # } @@ -444,14 +490,14 @@ struct MyDict { impl MyDict { #[new] #[pyo3(signature = (*args, **kwargs))] - fn new(args: &PyAny, kwargs: Option<&PyAny>) -> Self { + fn new(args: &Bound<'_, PyAny>, kwargs: Option<&Bound<'_, PyAny>>) -> Self { Self { private: 0 } } // some custom methods that use `private` here... } # Python::with_gil(|py| { -# let cls = py.get_type::(); +# let cls = py.get_type_bound::(); # pyo3::py_run!(py, cls, "cls(a=1, b=2)") # }); # } @@ -645,7 +691,7 @@ This is the equivalent of the Python decorator `@classmethod`. #[pymethods] impl MyClass { #[classmethod] - fn cls_method(cls: &PyType) -> PyResult { + fn cls_method(cls: &Bound<'_, PyType>) -> PyResult { Ok(10) } } @@ -655,7 +701,7 @@ Declares a class method callable from Python. * The first parameter is the type object of the class on which the method is called. This may be the type object of a derived class. -* The first parameter implicitly has type `&PyType`. +* The first parameter implicitly has type `&Bound<'_, PyType>`. * For details on `parameter-list`, see the documentation of `Method arguments` section. * The return type must be `PyResult` or `T` for some `T` that implements `IntoPy`. @@ -663,6 +709,7 @@ Declares a class method callable from Python. To create a constructor which takes a positional class argument, you can combine the `#[classmethod]` and `#[new]` modifiers: ```rust +# #![allow(dead_code)] # use pyo3::prelude::*; # use pyo3::types::PyType; # #[pyclass] @@ -672,10 +719,10 @@ To create a constructor which takes a positional class argument, you can combine impl BaseClass { #[new] #[classmethod] - fn py_new<'p>(cls: &'p PyType, py: Python<'p>) -> PyResult { + fn py_new(cls: &Bound<'_, PyType>) -> PyResult { // Get an abstract attribute (presumably) declared on a subclass of this class. - let subclass_attr = cls.getattr("a_class_attr")?; - Ok(Self(subclass_attr.to_object(py))) + let subclass_attr: Bound<'_, PyAny> = cls.getattr("a_class_attr")?; + Ok(Self(subclass_attr.unbind())) } } ``` @@ -720,7 +767,7 @@ impl MyClass { } Python::with_gil(|py| { - let my_class = py.get_type::(); + let my_class = py.get_type_bound::(); pyo3::py_run!(py, my_class, "assert my_class.my_attribute == 'hello'") }); ``` @@ -754,23 +801,23 @@ struct MyClass { my_field: i32, } -// Take a GIL-bound reference when the underlying `PyCell` is irrelevant. +// Take a reference when the underlying `Bound` is irrelevant. #[pyfunction] fn increment_field(my_class: &mut MyClass) { my_class.my_field += 1; } -// Take a GIL-bound reference wrapper when borrowing should be automatic, -// but interaction with the underlying `PyCell` is desired. +// Take a reference wrapper when borrowing should be automatic, +// but interaction with the underlying `Bound` is desired. #[pyfunction] fn print_field(my_class: PyRef<'_, MyClass>) { println!("{}", my_class.my_field); } -// Take a GIL-bound reference to the underlying cell +// Take a reference to the underlying Bound // when borrowing needs to be managed manually. #[pyfunction] -fn increment_then_print_field(my_class: &PyCell) { +fn increment_then_print_field(my_class: &Bound<'_, MyClass>) { my_class.borrow_mut().my_field += 1; println!("{}", my_class.borrow().my_field); @@ -829,9 +876,9 @@ impl MyClass { fn method( &mut self, num: i32, - py_args: &PyTuple, + py_args: &Bound<'_, PyTuple>, name: &str, - py_kwargs: Option<&PyDict>, + py_kwargs: Option<&Bound<'_, PyDict>>, ) -> String { let num_before = self.num; self.num = num; @@ -854,9 +901,7 @@ py_args=('World', 666), py_kwargs=Some({'x': 44, 'y': 55}), name=Hello, num=44, py_args=(), py_kwargs=None, name=World, num=-1, num_before=44 ``` -## Making class method signatures available to Python - -The [`text_signature = "..."`](./function.md#text_signature) option for `#[pyfunction]` also works for `#[pymethods]`: +The [`#[pyo3(text_signature = "...")`](./function/signature.md#overriding-the-generated-signature) option for `#[pyfunction]` also works for `#[pymethods]`. ```rust # #![allow(dead_code)] @@ -881,7 +926,7 @@ impl MyClass { // similarly for classmethod arguments, use $cls #[classmethod] #[pyo3(text_signature = "($cls, e, f)")] - fn my_class_method(cls: &PyType, e: i32, f: i32) -> i32 { + fn my_class_method(cls: &Bound<'_, PyType>, e: i32, f: i32) -> i32 { e + f } #[staticmethod] @@ -893,8 +938,8 @@ impl MyClass { # # fn main() -> PyResult<()> { # Python::with_gil(|py| { -# let inspect = PyModule::import(py, "inspect")?.getattr("signature")?; -# let module = PyModule::new(py, "my_module")?; +# let inspect = PyModule::import_bound(py, "inspect")?.getattr("signature")?; +# let module = PyModule::new_bound(py, "my_module")?; # module.add_class::()?; # let class = module.getattr("MyClass")?; # @@ -903,7 +948,7 @@ impl MyClass { # assert_eq!(doc, ""); # # let sig: String = inspect -# .call1((class,))? +# .call1((&class,))? # .call_method0("__str__")? # .extract()?; # assert_eq!(sig, "(c, d)"); @@ -911,7 +956,7 @@ impl MyClass { # let doc: String = class.getattr("__doc__")?.extract()?; # assert_eq!(doc, ""); # -# inspect.call1((class,)).expect_err("`text_signature` on classes is not compatible with compilation in `abi3` mode until Python 3.10 or greater"); +# inspect.call1((&class,)).expect_err("`text_signature` on classes is not compatible with compilation in `abi3` mode until Python 3.10 or greater"); # } # # { @@ -958,9 +1003,53 @@ impl MyClass { Note that `text_signature` on `#[new]` is not compatible with compilation in `abi3` mode until Python 3.10 or greater. -## #[pyclass] enums +### Method receivers and lifetime elision + +PyO3 supports writing instance methods using the normal method receivers for shared `&self` and unique `&mut self` references. This interacts with [lifetime elision][lifetime-elision] insofar as the lifetime of a such a receiver is assigned to all elided output lifetime parameters. + +This is a good default for general Rust code where return values are more likely to borrow from the receiver than from the other arguments, if they contain any lifetimes at all. However, when returning bound references `Bound<'py, T>` in PyO3-based code, the GIL lifetime `'py` should usually be derived from a GIL token `py: Python<'py>` passed as an argument instead of the receiver. + +Specifically, signatures like + +```rust,ignore +fn frobnicate(&self, py: Python) -> Bound; +``` + +will not work as they are inferred as + +```rust,ignore +fn frobnicate<'a, 'py>(&'a self, py: Python<'py>) -> Bound<'a, Foo>; +``` + +instead of the intended + +```rust,ignore +fn frobnicate<'a, 'py>(&'a self, py: Python<'py>) -> Bound<'py, Foo>; +``` + +and should usually be written as + +```rust,ignore +fn frobnicate<'py>(&self, py: Python<'py>) -> Bound<'py, Foo>; +``` + +The same problem does not exist for `#[pyfunction]`s as the special case for receiver lifetimes does not apply and indeed a signature like + +```rust,ignore +fn frobnicate(bar: &Bar, py: Python) -> Bound; +``` + +will yield compiler error [E0106 "missing lifetime specifier"][compiler-error-e0106]. + +## `#[pyclass]` enums + +Enum support in PyO3 comes in two flavors, depending on what kind of variants the enum has: simple and complex. + +### Simple enums + +A simple enum (a.k.a. C-like enum) has only unit variants. -Currently PyO3 only supports fieldless enums. PyO3 adds a class attribute for each variant, so you can access them in Python without defining `#[new]`. PyO3 also provides default implementations of `__richcmp__` and `__int__`, so they can be compared using `==`: +PyO3 adds a class attribute for each variant, so you can access them in Python without defining `#[new]`. PyO3 also provides default implementations of `__richcmp__` and `__int__`, so they can be compared using `==`: ```rust # use pyo3::prelude::*; @@ -973,7 +1062,7 @@ enum MyEnum { Python::with_gil(|py| { let x = Py::new(py, MyEnum::Variant).unwrap(); let y = Py::new(py, MyEnum::OtherVariant).unwrap(); - let cls = py.get_type::(); + let cls = py.get_type_bound::(); pyo3::py_run!(py, x y cls, r#" assert x == cls.Variant assert y == cls.OtherVariant @@ -982,7 +1071,7 @@ Python::with_gil(|py| { }) ``` -You can also convert your enums into `int`: +You can also convert your simple enums into `int`: ```rust # use pyo3::prelude::*; @@ -993,7 +1082,7 @@ enum MyEnum { } Python::with_gil(|py| { - let cls = py.get_type::(); + let cls = py.get_type_bound::(); let x = MyEnum::Variant as i32; // The exact value is assigned by the compiler. pyo3::py_run!(py, cls x, r#" assert int(cls.Variant) == x @@ -1015,7 +1104,7 @@ enum MyEnum{ } Python::with_gil(|py| { - let cls = py.get_type::(); + let cls = py.get_type_bound::(); let x = Py::new(py, MyEnum::Variant).unwrap(); pyo3::py_run!(py, cls x, r#" assert repr(x) == 'MyEnum.Variant' @@ -1041,7 +1130,7 @@ impl MyEnum { } Python::with_gil(|py| { - let cls = py.get_type::(); + let cls = py.get_type_bound::(); pyo3::py_run!(py, cls, "assert repr(cls.Answer) == '42'") }) ``` @@ -1058,7 +1147,7 @@ enum MyEnum { Python::with_gil(|py| { let x = Py::new(py, MyEnum::Variant).unwrap(); - let cls = py.get_type::(); + let cls = py.get_type_bound::(); pyo3::py_run!(py, x cls, r#" assert repr(x) == 'RenamedEnum.UPPERCASE' assert x == cls.UPPERCASE @@ -1090,6 +1179,115 @@ enum BadSubclass { `#[pyclass]` enums are currently not interoperable with `IntEnum` in Python. +### Complex enums + +An enum is complex if it has any non-unit (struct or tuple) variants. + +PyO3 supports only struct and tuple variants in a complex enum. Unit variants aren't supported at present (the recommendation is to use an empty tuple enum instead). + +PyO3 adds a class attribute for each variant, which may be used to construct values and in match patterns. PyO3 also provides getter methods for all fields of each variant. + +```rust +# use pyo3::prelude::*; +#[pyclass] +enum Shape { + Circle { radius: f64 }, + Rectangle { width: f64, height: f64 }, + RegularPolygon(u32, f64), + Nothing { }, +} + +# #[cfg(Py_3_10)] +Python::with_gil(|py| { + let circle = Shape::Circle { radius: 10.0 }.into_py(py); + let square = Shape::RegularPolygon(4, 10.0).into_py(py); + let cls = py.get_type_bound::(); + pyo3::py_run!(py, circle square cls, r#" + assert isinstance(circle, cls) + assert isinstance(circle, cls.Circle) + assert circle.radius == 10.0 + + assert isinstance(square, cls) + assert isinstance(square, cls.RegularPolygon) + assert square[0] == 4 # Gets _0 field + assert square[1] == 10.0 # Gets _1 field + + def count_vertices(cls, shape): + match shape: + case cls.Circle(): + return 0 + case cls.Rectangle(): + return 4 + case cls.RegularPolygon(n): + return n + case cls.Nothing(): + return 0 + + assert count_vertices(cls, circle) == 0 + assert count_vertices(cls, square) == 4 + "#) +}) +``` + +WARNING: `Py::new` and `.into_py` are currently inconsistent. Note how the constructed value is _not_ an instance of the specific variant. For this reason, constructing values is only recommended using `.into_py`. + +```rust +# use pyo3::prelude::*; +#[pyclass] +enum MyEnum { + Variant { i: i32 }, +} + +Python::with_gil(|py| { + let x = Py::new(py, MyEnum::Variant { i: 42 }).unwrap(); + let cls = py.get_type_bound::(); + pyo3::py_run!(py, x cls, r#" + assert isinstance(x, cls) + assert not isinstance(x, cls.Variant) + "#) +}) +``` + +The constructor of each generated class can be customized using the `#[pyo3(constructor = (...))]` attribute. This uses the same syntax as the [`#[pyo3(signature = (...))]`](function/signature.md) +attribute on function and methods and supports the same options. To apply this attribute simply place it on top of a variant in a `#[pyclass]` complex enum as shown below: + +```rust +# use pyo3::prelude::*; +#[pyclass] +enum Shape { + #[pyo3(constructor = (radius=1.0))] + Circle { radius: f64 }, + #[pyo3(constructor = (*, width, height))] + Rectangle { width: f64, height: f64 }, + #[pyo3(constructor = (side_count, radius=1.0))] + RegularPolygon { side_count: u32, radius: f64 }, + Nothing { }, +} + +# #[cfg(Py_3_10)] +Python::with_gil(|py| { + let cls = py.get_type_bound::(); + pyo3::py_run!(py, cls, r#" + circle = cls.Circle() + assert isinstance(circle, cls) + assert isinstance(circle, cls.Circle) + assert circle.radius == 1.0 + + square = cls.Rectangle(width = 1, height = 1) + assert isinstance(square, cls) + assert isinstance(square, cls.Rectangle) + assert square.width == 1 + assert square.height == 1 + + hexagon = cls.RegularPolygon(6) + assert isinstance(hexagon, cls) + assert isinstance(hexagon, cls.RegularPolygon) + assert hexagon.side_count == 6 + assert hexagon.radius == 1 + "#) +}) +``` + ## Implementation details The `#[pyclass]` macros rely on a lot of conditional code generation: each `#[pyclass]` can optionally have a `#[pymethods]` block. @@ -1108,6 +1306,11 @@ struct MyClass { # #[allow(dead_code)] num: i32, } + +impl pyo3::types::DerefToPyAny for MyClass {} + +# #[allow(deprecated)] +# #[cfg(feature = "gil-refs")] unsafe impl pyo3::type_object::HasPyGilRef for MyClass { type AsRefTarget = pyo3::PyCell; } @@ -1131,7 +1334,7 @@ impl<'a, 'py> pyo3::impl_::extract_argument::PyFunctionArgument<'a, 'py> for &'a type Holder = ::std::option::Option>; #[inline] - fn extract(obj: &'py pyo3::PyAny, holder: &'a mut Self::Holder) -> pyo3::PyResult { + fn extract(obj: &'a pyo3::Bound<'py, PyAny>, holder: &'a mut Self::Holder) -> pyo3::PyResult { pyo3::impl_::extract_argument::extract_pyclass_ref(obj, holder) } } @@ -1141,7 +1344,7 @@ impl<'a, 'py> pyo3::impl_::extract_argument::PyFunctionArgument<'a, 'py> for &'a type Holder = ::std::option::Option>; #[inline] - fn extract(obj: &'py pyo3::PyAny, holder: &'a mut Self::Holder) -> pyo3::PyResult { + fn extract(obj: &'a pyo3::Bound<'py, PyAny>, holder: &'a mut Self::Holder) -> pyo3::PyResult { pyo3::impl_::extract_argument::extract_pyclass_ref_mut(obj, holder) } } @@ -1155,6 +1358,8 @@ impl pyo3::IntoPy for MyClass { impl pyo3::impl_::pyclass::PyClassImpl for MyClass { const IS_BASETYPE: bool = false; const IS_SUBCLASS: bool = false; + const IS_MAPPING: bool = false; + const IS_SEQUENCE: bool = false; type BaseType = PyAny; type ThreadChecker = pyo3::impl_::pyclass::SendablePyClass; type PyClassMutability = <::PyClassMutability as pyo3::impl_::pycell::PyClassMutability>::MutableChild; @@ -1180,24 +1385,23 @@ impl pyo3::impl_::pyclass::PyClassImpl for MyClass { static DOC: pyo3::sync::GILOnceCell<::std::borrow::Cow<'static, ::std::ffi::CStr>> = pyo3::sync::GILOnceCell::new(); DOC.get_or_try_init(py, || { let collector = PyClassImplCollector::::new(); - build_pyclass_doc(::NAME, "", None.or_else(|| collector.new_text_signature())) + build_pyclass_doc(::NAME, "\0", collector.new_text_signature()) }).map(::std::ops::Deref::deref) } } # Python::with_gil(|py| { -# let cls = py.get_type::(); +# let cls = py.get_type_bound::(); # pyo3::py_run!(py, cls, "assert cls.__name__ == 'MyClass'") # }); # } ``` -[`GILGuard`]: {{#PYO3_DOCS_URL}}/pyo3/struct.GILGuard.html [`PyTypeInfo`]: {{#PYO3_DOCS_URL}}/pyo3/type_object/trait.PyTypeInfo.html [`Py`]: {{#PYO3_DOCS_URL}}/pyo3/struct.Py.html -[`PyCell`]: {{#PYO3_DOCS_URL}}/pyo3/pycell/struct.PyCell.html +[`Bound<'_, T>`]: {{#PYO3_DOCS_URL}}/pyo3/struct.Bound.html [`PyClass`]: {{#PYO3_DOCS_URL}}/pyo3/pyclass/trait.PyClass.html [`PyRef`]: {{#PYO3_DOCS_URL}}/pyo3/pycell/struct.PyRef.html [`PyRefMut`]: {{#PYO3_DOCS_URL}}/pyo3/pycell/struct.PyRefMut.html @@ -1209,3 +1413,6 @@ impl pyo3::impl_::pyclass::PyClassImpl for MyClass { [classattr]: https://docs.python.org/3/tutorial/classes.html#class-and-instance-variables [`multiple-pymethods`]: features.md#multiple-pymethods + +[lifetime-elision]: https://doc.rust-lang.org/reference/lifetime-elision.html +[compiler-error-e0106]: https://doc.rust-lang.org/error_codes/E0106.html diff --git a/guide/src/class/call.md b/guide/src/class/call.md index 3b20986239b..0890df9561a 100644 --- a/guide/src/class/call.md +++ b/guide/src/class/call.md @@ -75,8 +75,8 @@ A [previous implementation] used a normal `u64`, which meant it required a `&mut fn __call__( &mut self, py: Python<'_>, - args: &PyTuple, - kwargs: Option<&PyDict>, + args: &Bound<'_, PyTuple>, + kwargs: Option<&Bound<'_, PyDict>>, ) -> PyResult> { self.count += 1; let name = self.wraps.getattr(py, "__name__")?; diff --git a/guide/src/class/numeric.md b/guide/src/class/numeric.md index 10a06e7c02c..361d2fb6d36 100644 --- a/guide/src/class/numeric.md +++ b/guide/src/class/numeric.md @@ -35,7 +35,7 @@ and cast it to an `i32`. # #![allow(dead_code)] use pyo3::prelude::*; -fn wrap(obj: &PyAny) -> Result { +fn wrap(obj: &Bound<'_, PyAny>) -> PyResult { let val = obj.call_method1("__and__", (0xFFFFFFFF_u32,))?; let val: u32 = val.extract()?; // 👇 This intentionally overflows! @@ -48,7 +48,7 @@ We also add documentation, via `///` comments, which are visible to Python users # #![allow(dead_code)] use pyo3::prelude::*; -fn wrap(obj: &PyAny) -> Result { +fn wrap(obj: &Bound<'_, PyAny>) -> PyResult { let val = obj.call_method1("__and__", (0xFFFFFFFF_u32,))?; let val: u32 = val.extract()?; Ok(val as i32) @@ -170,8 +170,8 @@ impl Number { self.0 as f64 } - fn __complex__<'py>(&self, py: Python<'py>) -> &'py PyComplex { - PyComplex::from_doubles(py, self.0 as f64, 0.0) + fn __complex__<'py>(&self, py: Python<'py>) -> Bound<'py, PyComplex> { + PyComplex::from_doubles_bound(py, self.0 as f64, 0.0) } } ``` @@ -206,14 +206,13 @@ assert hash_djb2('l50_50') == Number(-1152549421) ```rust use std::collections::hash_map::DefaultHasher; use std::hash::{Hash, Hasher}; -use std::convert::TryInto; use pyo3::exceptions::{PyValueError, PyZeroDivisionError}; use pyo3::prelude::*; use pyo3::class::basic::CompareOp; use pyo3::types::PyComplex; -fn wrap(obj: &PyAny) -> Result { +fn wrap(obj: &Bound<'_, PyAny>) -> PyResult { let val = obj.call_method1("__and__", (0xFFFFFFFF_u32,))?; let val: u32 = val.extract()?; Ok(val as i32) @@ -230,7 +229,7 @@ impl Number { Self(value) } - fn __repr__(slf: &PyCell) -> PyResult { + fn __repr__(slf: &Bound<'_, Self>) -> PyResult { // Get the class name dynamically in case `Number` is subclassed let class_name: String = slf.get_type().qualname()?; Ok(format!("{}({})", class_name, slf.borrow().0)) @@ -321,13 +320,13 @@ impl Number { self.0 as f64 } - fn __complex__<'py>(&self, py: Python<'py>) -> &'py PyComplex { - PyComplex::from_doubles(py, self.0 as f64, 0.0) + fn __complex__<'py>(&self, py: Python<'py>) -> Bound<'py, PyComplex> { + PyComplex::from_doubles_bound(py, self.0 as f64, 0.0) } } #[pymodule] -fn my_module(_py: Python<'_>, m: &PyModule) -> PyResult<()> { +fn my_module(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; Ok(()) } @@ -387,10 +386,10 @@ fn my_module(_py: Python<'_>, m: &PyModule) -> PyResult<()> { # # fn main() -> PyResult<()> { # Python::with_gil(|py| -> PyResult<()> { -# let globals = PyModule::import(py, "__main__")?.dict(); -# globals.set_item("Number", Number::type_object(py))?; +# let globals = PyModule::import_bound(py, "__main__")?.dict(); +# globals.set_item("Number", Number::type_object_bound(py))?; # -# py.run(SCRIPT, Some(globals), None)?; +# py.run_bound(SCRIPT, Some(&globals), None)?; # Ok(()) # }) # } @@ -412,8 +411,8 @@ the contracts of this function. Let's review those contracts: - The GIL must be held. If it's not, calling this function causes a data race. - The pointer must be valid, i.e. it must be properly aligned and point to a valid Python object. -Let's create that helper function. The signature has to be `fn(&PyAny) -> PyResult`. -- `&PyAny` represents a checked borrowed reference, so the pointer derived from it is valid (and not null). +Let's create that helper function. The signature has to be `fn(&Bound<'_, PyAny>) -> PyResult`. +- `&Bound<'_, PyAny>` represents a checked borrowed reference, so the pointer derived from it is valid (and not null). - Whenever we have borrowed references to Python objects in scope, it is guaranteed that the GIL is held. This reference is also where we can get a [`Python`] token to use in our call to [`PyErr::take`]. ```rust @@ -422,7 +421,7 @@ use std::os::raw::c_ulong; use pyo3::prelude::*; use pyo3::ffi; -fn wrap(obj: &PyAny) -> Result { +fn wrap(obj: &Bound<'_, PyAny>) -> Result { let py: Python<'_> = obj.py(); unsafe { @@ -441,6 +440,6 @@ fn wrap(obj: &PyAny) -> Result { ``` [`PyErr::take`]: {{#PYO3_DOCS_URL}}/pyo3/prelude/struct.PyErr.html#method.take -[`Python`]: {{#PYO3_DOCS_URL}}/pyo3/struct.Python.html +[`Python`]: {{#PYO3_DOCS_URL}}/pyo3/marker/struct.Python.html [`FromPyObject`]: {{#PYO3_DOCS_URL}}/pyo3/conversion/trait.FromPyObject.html [`pyo3::ffi::PyLong_AsUnsignedLongMask`]: {{#PYO3_DOCS_URL}}/pyo3/ffi/fn.PyLong_AsUnsignedLongMask.html diff --git a/guide/src/class/object.md b/guide/src/class/object.md index cfaa8bb1ddd..db6cc7d3234 100644 --- a/guide/src/class/object.md +++ b/guide/src/class/object.md @@ -3,6 +3,7 @@ Recall the `Number` class from the previous chapter: ```rust +# #![allow(dead_code)] use pyo3::prelude::*; #[pyclass] @@ -17,7 +18,7 @@ impl Number { } #[pymodule] -fn my_module(_py: Python<'_>, m: &PyModule) -> PyResult<()> { +fn my_module(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; Ok(()) } @@ -75,7 +76,7 @@ In the `__repr__`, we used a hard-coded class name. This is sometimes not ideal, because if the class is subclassed in Python, we would like the repr to reflect the subclass name. This is typically done in Python code by accessing `self.__class__.__name__`. In order to be able to access the Python type information -*and* the Rust struct, we need to use a `PyCell` as the `self` argument. +*and* the Rust struct, we need to use a `Bound` as the `self` argument. ```rust # use pyo3::prelude::*; @@ -85,7 +86,7 @@ the subclass name. This is typically done in Python code by accessing # #[pymethods] impl Number { - fn __repr__(slf: &PyCell) -> PyResult { + fn __repr__(slf: &Bound<'_, Self>) -> PyResult { // This is the equivalent of `self.__class__.__name__` in Python. let class_name: String = slf.get_type().qualname()?; // To access fields of the Rust struct, we need to borrow the `PyCell`. @@ -216,8 +217,8 @@ impl Number { # fn main() -> PyResult<()> { # Python::with_gil(|py| { -# let x = PyCell::new(py, Number(4))?; -# let y = PyCell::new(py, Number(4))?; +# let x = &Bound::new(py, Number(4))?; +# let y = &Bound::new(py, Number(4))?; # assert!(x.eq(y)?); # assert!(!x.ne(y)?); # Ok(()) @@ -262,7 +263,7 @@ impl Number { Self(value) } - fn __repr__(slf: &PyCell) -> PyResult { + fn __repr__(slf: &Bound<'_, Self>) -> PyResult { let class_name: String = slf.get_type().qualname()?; Ok(format!("{}({})", class_name, slf.borrow().0)) } @@ -294,7 +295,7 @@ impl Number { } #[pymodule] -fn my_module(_py: Python<'_>, m: &PyModule) -> PyResult<()> { +fn my_module(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; Ok(()) } diff --git a/guide/src/class/protocols.md b/guide/src/class/protocols.md index 4577a5102b6..4e5f6010e6d 100644 --- a/guide/src/class/protocols.md +++ b/guide/src/class/protocols.md @@ -1,23 +1,30 @@ -# Magic methods and slots +# Class customizations -Python's object model defines several protocols for different object behavior, such as the sequence, mapping, and number protocols. You may be familiar with implementing these protocols in Python classes by "magic" methods, such as `__str__` or `__repr__`. Because of the double-underscores surrounding their name, these are also known as "dunder" methods. +Python's object model defines several protocols for different object behavior, such as the sequence, mapping, and number protocols. Python classes support these protocols by implementing "magic" methods, such as `__str__` or `__repr__`. Because of the double-underscores surrounding their name, these are also known as "dunder" methods. -In the Python C-API which PyO3 is implemented upon, many of these magic methods have to be placed into special "slots" on the class type object, as covered in the previous section. +PyO3 makes it possible for every magic method to be implemented in `#[pymethods]` just as they would be done in a regular Python class, with a few notable differences: +- `__new__` and `__init__` are replaced by the [`#[new]` attribute](../class.md#constructor). +- `__del__` is not yet supported, but may be in the future. +- `__buffer__` and `__release_buffer__` are currently not supported and instead PyO3 supports [`__getbuffer__` and `__releasebuffer__`](#buffer-objects) methods (these predate [PEP 688](https://peps.python.org/pep-0688/#python-level-buffer-protocol)), again this may change in the future. +- PyO3 adds [`__traverse__` and `__clear__`](#garbage-collector-integration) methods for controlling garbage collection. +- The Python C-API which PyO3 is implemented upon requires many magic methods to have a specific function signature in C and be placed into special "slots" on the class type object. This limits the allowed argument and return types for these methods. They are listed in detail in the section below. -If a function name in `#[pymethods]` is a recognised magic method, it will be automatically placed into the correct slot in the Python type object. The function name is taken from the usual rules for naming `#[pymethods]`: the `#[pyo3(name = "...")]` attribute is used if present, otherwise the Rust function name is used. +If a magic method is not on the list above (for example `__init_subclass__`), then it should just work in PyO3. If this is not the case, please file a bug report. -The magic methods handled by PyO3 are very similar to the standard Python ones on [this page](https://docs.python.org/3/reference/datamodel.html#special-method-names) - in particular they are the the subset which have slots as [defined here](https://docs.python.org/3/c-api/typeobj.html). Some of the slots do not have a magic method in Python, which leads to a few additional magic methods defined only in PyO3: - - Magic methods for garbage collection - - Magic methods for the buffer protocol +## Magic Methods handled by PyO3 + +If a function name in `#[pymethods]` is a magic method which is known to need special handling, it will be automatically placed into the correct slot in the Python type object. The function name is taken from the usual rules for naming `#[pymethods]`: the `#[pyo3(name = "...")]` attribute is used if present, otherwise the Rust function name is used. + +The magic methods handled by PyO3 are very similar to the standard Python ones on [this page](https://docs.python.org/3/reference/datamodel.html#special-method-names) - in particular they are the subset which have slots as [defined here](https://docs.python.org/3/c-api/typeobj.html). When PyO3 handles a magic method, a couple of changes apply compared to other `#[pymethods]`: - The Rust function signature is restricted to match the magic method. - The `#[pyo3(signature = (...)]` and `#[pyo3(text_signature = "...")]` attributes are not allowed. -The following sections list of all magic methods PyO3 currently handles. The +The following sections list all magic methods for which PyO3 implements the necessary special handling. The given signatures should be interpreted as follows: - All methods take a receiver as first argument, shown as ``. It can be - `&self`, `&mut self` or a `PyCell` reference like `self_: PyRef<'_, Self>` and + `&self`, `&mut self` or a `Bound` reference like `self_: PyRef<'_, Self>` and `self_: PyRefMut<'_, Self>`, as described [here](../class.md#inheritance). - An optional `Python<'py>` argument is always allowed as the first argument. - Return values can be optionally wrapped in `PyResult`. @@ -31,7 +38,6 @@ given signatures should be interpreted as follows: checked by the Python interpreter. For example, `__str__` needs to return a string object. This is indicated by `object (Python type)`. - ### Basic object customization - `__str__() -> object (str)` @@ -103,7 +109,7 @@ given signatures should be interpreted as follows: match op { CompareOp::Eq => (self.0 == other.0).into_py(py), CompareOp::Ne => (self.0 != other.0).into_py(py), - _ => py.NotImplemented().into(), + _ => py.NotImplemented(), } } } @@ -207,7 +213,7 @@ impl Container { # Python::with_gil(|py| { # let container = Container { iter: vec![1, 2, 3, 4] }; -# let inst = pyo3::PyCell::new(py, container).unwrap(); +# let inst = pyo3::Py::new(py, container).unwrap(); # pyo3::py_run!(py, inst, "assert list(inst) == [1, 2, 3, 4]"); # pyo3::py_run!(py, inst, "assert list(iter(iter(inst))) == [1, 2, 3, 4]"); # }); @@ -450,7 +456,7 @@ Usually, an implementation of `__traverse__` should do nothing but calls to `vis Most importantly, safe access to the GIL is prohibited inside implementations of `__traverse__`, i.e. `Python::with_gil` will panic. -> Note: these methods are part of the C API, PyPy does not necessarily honor them. If you are building for PyPy you should measure memory consumption to make sure you do not have runaway memory growth. See [this issue on the PyPy bug tracker](https://foss.heptapod.net/pypy/pypy/-/issues/3899). +> Note: these methods are part of the C API, PyPy does not necessarily honor them. If you are building for PyPy you should measure memory consumption to make sure you do not have runaway memory growth. See [this issue on the PyPy bug tracker](https://github.com/pypy/pypy/issues/3848). [`IterNextOutput`]: {{#PYO3_DOCS_URL}}/pyo3/pyclass/enum.IterNextOutput.html [`PySequence`]: {{#PYO3_DOCS_URL}}/pyo3/types/struct.PySequence.html diff --git a/guide/src/conversions/tables.md b/guide/src/conversions/tables.md index f9d716b324d..208e61671ec 100644 --- a/guide/src/conversions/tables.md +++ b/guide/src/conversions/tables.md @@ -12,49 +12,50 @@ The table below contains the Python type and the corresponding function argument | Python | Rust | Rust (Python-native) | | ------------- |:-------------------------------:|:--------------------:| -| `object` | - | `&PyAny` | -| `str` | `String`, `Cow`, `&str`, `char`, `OsString`, `PathBuf`, `Path` | `&PyString`, `&PyUnicode` | -| `bytes` | `Vec`, `&[u8]`, `Cow<[u8]>` | `&PyBytes` | -| `bool` | `bool` | `&PyBool` | -| `int` | `i8`, `u8`, `i16`, `u16`, `i32`, `u32`, `i64`, `u64`, `i128`, `u128`, `isize`, `usize`, `num_bigint::BigInt`[^1], `num_bigint::BigUint`[^1] | `&PyLong` | -| `float` | `f32`, `f64` | `&PyFloat` | -| `complex` | `num_complex::Complex`[^2] | `&PyComplex` | -| `list[T]` | `Vec` | `&PyList` | -| `dict[K, V]` | `HashMap`, `BTreeMap`, `hashbrown::HashMap`[^3], `indexmap::IndexMap`[^4] | `&PyDict` | -| `tuple[T, U]` | `(T, U)`, `Vec` | `&PyTuple` | -| `set[T]` | `HashSet`, `BTreeSet`, `hashbrown::HashSet`[^3] | `&PySet` | -| `frozenset[T]` | `HashSet`, `BTreeSet`, `hashbrown::HashSet`[^3] | `&PyFrozenSet` | -| `bytearray` | `Vec`, `Cow<[u8]>` | `&PyByteArray` | -| `slice` | - | `&PySlice` | -| `type` | - | `&PyType` | -| `module` | - | `&PyModule` | +| `object` | - | `PyAny` | +| `str` | `String`, `Cow`, `&str`, `char`, `OsString`, `PathBuf`, `Path` | `PyString`, `PyUnicode` | +| `bytes` | `Vec`, `&[u8]`, `Cow<[u8]>` | `PyBytes` | +| `bool` | `bool` | `PyBool` | +| `int` | `i8`, `u8`, `i16`, `u16`, `i32`, `u32`, `i64`, `u64`, `i128`, `u128`, `isize`, `usize`, `num_bigint::BigInt`[^1], `num_bigint::BigUint`[^1] | `PyLong` | +| `float` | `f32`, `f64` | `PyFloat` | +| `complex` | `num_complex::Complex`[^2] | `PyComplex` | +| `fractions.Fraction`| `num_rational::Ratio`[^8] | - | +| `list[T]` | `Vec` | `PyList` | +| `dict[K, V]` | `HashMap`, `BTreeMap`, `hashbrown::HashMap`[^3], `indexmap::IndexMap`[^4] | `PyDict` | +| `tuple[T, U]` | `(T, U)`, `Vec` | `PyTuple` | +| `set[T]` | `HashSet`, `BTreeSet`, `hashbrown::HashSet`[^3] | `PySet` | +| `frozenset[T]` | `HashSet`, `BTreeSet`, `hashbrown::HashSet`[^3] | `PyFrozenSet` | +| `bytearray` | `Vec`, `Cow<[u8]>` | `PyByteArray` | +| `slice` | - | `PySlice` | +| `type` | - | `PyType` | +| `module` | - | `PyModule` | | `collections.abc.Buffer` | - | `PyBuffer` | -| `datetime.datetime` | - | `&PyDateTime` | -| `datetime.date` | - | `&PyDate` | -| `datetime.time` | - | `&PyTime` | -| `datetime.tzinfo` | - | `&PyTzInfo` | -| `datetime.timedelta` | `Duration` | `&PyDelta` | -| `decimal.Decimal` | `rust_decimal::Decimal`[^5] | - | +| `datetime.datetime` | `SystemTime`, `chrono::DateTime`[^5], `chrono::NaiveDateTime`[^5] | `PyDateTime` | +| `datetime.date` | `chrono::NaiveDate`[^5] | `PyDate` | +| `datetime.time` | `chrono::NaiveTime`[^5] | `PyTime` | +| `datetime.tzinfo` | `chrono::FixedOffset`[^5], `chrono::Utc`[^5], `chrono_tz::TimeZone`[^6] | `PyTzInfo` | +| `datetime.timedelta` | `Duration`, `chrono::Duration`[^5] | `PyDelta` | +| `decimal.Decimal` | `rust_decimal::Decimal`[^7] | - | | `ipaddress.IPv4Address` | `std::net::IpAddr`, `std::net::IpV4Addr` | - | | `ipaddress.IPv6Address` | `std::net::IpAddr`, `std::net::IpV6Addr` | - | -| `os.PathLike ` | `PathBuf`, `Path` | `&PyString`, `&PyUnicode` | -| `pathlib.Path` | `PathBuf`, `Path` | `&PyString`, `&PyUnicode` | +| `os.PathLike ` | `PathBuf`, `Path` | `PyString`, `PyUnicode` | +| `pathlib.Path` | `PathBuf`, `Path` | `PyString`, `PyUnicode` | | `typing.Optional[T]` | `Option` | - | -| `typing.Sequence[T]` | `Vec` | `&PySequence` | +| `typing.Sequence[T]` | `Vec` | `PySequence` | | `typing.Mapping[K, V]` | `HashMap`, `BTreeMap`, `hashbrown::HashMap`[^3], `indexmap::IndexMap`[^4] | `&PyMapping` | -| `typing.Iterator[Any]` | - | `&PyIterator` | -| `typing.Union[...]` | See [`#[derive(FromPyObject)]`](traits.html#deriving-a-hrefhttpsdocsrspyo3latestpyo3conversiontraitfrompyobjecthtmlfrompyobjecta-for-enums) | - | +| `typing.Iterator[Any]` | - | `PyIterator` | +| `typing.Union[...]` | See [`#[derive(FromPyObject)]`](traits.md#deriving-frompyobject-for-enums) | - | -There are also a few special types related to the GIL and Rust-defined `#[pyclass]`es which may come in useful: +It is also worth remembering the following special types: -| What | Description | -| ------------- | ------------------------------------- | -| `Python` | A GIL token, used to pass to PyO3 constructors to prove ownership of the GIL | -| `Py` | A Python object isolated from the GIL lifetime. This can be sent to other threads. | -| `PyObject` | An alias for `Py` | -| `&PyCell` | A `#[pyclass]` value owned by Python. | -| `PyRef` | A `#[pyclass]` borrowed immutably. | -| `PyRefMut` | A `#[pyclass]` borrowed mutably. | +| What | Description | +| ---------------- | ------------------------------------- | +| `Python<'py>` | A GIL token, used to pass to PyO3 constructors to prove ownership of the GIL. | +| `Bound<'py, T>` | A Python object connected to the GIL lifetime. This provides access to most of PyO3's APIs. | +| `Py` | A Python object isolated from the GIL lifetime. This can be sent to other threads. | +| `PyObject` | An alias for `Py` | +| `PyRef` | A `#[pyclass]` borrowed immutably. | +| `PyRefMut` | A `#[pyclass]` borrowed mutably. | For more detail on accepting `#[pyclass]` values as function arguments, see [the section of this guide on Python Classes](../class.md). @@ -72,9 +73,9 @@ For most PyO3 usage the conversion cost is worth paying to get these benefits. A ### Returning Rust values to Python -When returning values from functions callable from Python, Python-native types (`&PyAny`, `&PyDict` etc.) can be used with zero cost. +When returning values from functions callable from Python, [PyO3's smart pointers](../types.md#pyo3s-smart-pointers) (`Py`, `Bound<'py, T>`, and `Borrowed<'a, 'py, T>`) can be used with zero cost. -Because these types are references, in some situations the Rust compiler may ask for lifetime annotations. If this is the case, you should use `Py`, `Py` etc. instead - which are also zero-cost. For all of these Python-native types `T`, `Py` can be created from `T` with an `.into()` conversion. +Because `Bound<'py, T>` and `Borrowed<'a, 'py, T>` have lifetime parameters, the Rust compiler may ask for lifetime annotations to be added to your function. See the [section of the guide dedicated to this](../types.md#function-argument-lifetimes). If your function is fallible, it should return `PyResult` or `Result` where `E` implements `From for PyErr`. This will raise a `Python` exception if the `Err` variant is returned. @@ -95,7 +96,8 @@ Finally, the following Rust types are also able to convert to Python as return v | `BTreeMap` | `Dict[K, V]` | | `HashSet` | `Set[T]` | | `BTreeSet` | `Set[T]` | -| `&PyCell` | `T` | +| `Py` | `T` | +| `Bound` | `T` | | `PyRef` | `T` | | `PyRefMut` | `T` | @@ -107,4 +109,10 @@ Finally, the following Rust types are also able to convert to Python as return v [^4]: Requires the `indexmap` optional feature. -[^5]: Requires the `rust_decimal` optional feature. +[^5]: Requires the `chrono` optional feature. + +[^6]: Requires the `chrono-tz` optional feature. + +[^7]: Requires the `rust_decimal` optional feature. + +[^8]: Requires the `num-rational` optional feature. diff --git a/guide/src/conversions/traits.md b/guide/src/conversions/traits.md index fd136c83747..95d16faaaa6 100644 --- a/guide/src/conversions/traits.md +++ b/guide/src/conversions/traits.md @@ -13,7 +13,7 @@ fails, so usually you will use something like # use pyo3::types::PyList; # fn main() -> PyResult<()> { # Python::with_gil(|py| { -# let list = PyList::new(py, b"foo"); +# let list = PyList::new_bound(py, b"foo"); let v: Vec = list.extract()?; # assert_eq!(&v, &[102, 111, 111]); # Ok(()) @@ -54,7 +54,7 @@ struct RustyStruct { # # fn main() -> PyResult<()> { # Python::with_gil(|py| -> PyResult<()> { -# let module = PyModule::from_code( +# let module = PyModule::from_code_bound( # py, # "class Foo: # def __init__(self): @@ -86,7 +86,7 @@ struct RustyStruct { # use pyo3::types::PyDict; # fn main() -> PyResult<()> { # Python::with_gil(|py| -> PyResult<()> { -# let dict = PyDict::new(py); +# let dict = PyDict::new_bound(py); # dict.set_item("my_string", "test")?; # # let rustystruct: RustyStruct = dict.extract()?; @@ -111,7 +111,7 @@ struct RustyStruct { # # fn main() -> PyResult<()> { # Python::with_gil(|py| -> PyResult<()> { -# let module = PyModule::from_code( +# let module = PyModule::from_code_bound( # py, # "class Foo(dict): # def __init__(self): @@ -155,7 +155,7 @@ struct RustyStruct { # # fn main() -> PyResult<()> { # Python::with_gil(|py| -> PyResult<()> { -# let py_dict = py.eval("{'foo': 'foo', 'bar': 'bar', 'foobar': 'foobar'}", None, None)?; +# let py_dict = py.eval_bound("{'foo': 'foo', 'bar': 'bar', 'foobar': 'foobar'}", None, None)?; # let rustystruct: RustyStruct = py_dict.extract()?; # assert_eq!(rustystruct.foo, "foo"); # assert_eq!(rustystruct.bar, "bar"); @@ -181,7 +181,7 @@ struct RustyTuple(String, String); # use pyo3::types::PyTuple; # fn main() -> PyResult<()> { # Python::with_gil(|py| -> PyResult<()> { -# let tuple = PyTuple::new(py, vec!["test", "test2"]); +# let tuple = PyTuple::new_bound(py, vec!["test", "test2"]); # # let rustytuple: RustyTuple = tuple.extract()?; # assert_eq!(rustytuple.0, "test"); @@ -204,7 +204,7 @@ struct RustyTuple((String,)); # use pyo3::types::PyTuple; # fn main() -> PyResult<()> { # Python::with_gil(|py| -> PyResult<()> { -# let tuple = PyTuple::new(py, vec!["test"]); +# let tuple = PyTuple::new_bound(py, vec!["test"]); # # let rustytuple: RustyTuple = tuple.extract()?; # assert_eq!((rustytuple.0).0, "test"); @@ -236,7 +236,7 @@ struct RustyTransparentStruct { # use pyo3::types::PyString; # fn main() -> PyResult<()> { # Python::with_gil(|py| -> PyResult<()> { -# let s = PyString::new(py, "test"); +# let s = PyString::new_bound(py, "test"); # # let tup: RustyTransparentTupleStruct = s.extract()?; # assert_eq!(tup.0, "test"); @@ -265,7 +265,7 @@ use pyo3::prelude::*; #[derive(FromPyObject)] # #[derive(Debug)] -enum RustyEnum<'a> { +enum RustyEnum<'py> { Int(usize), // input is a positive int String(String), // input is a string IntTuple(usize, usize), // input is a 2-tuple with positive ints @@ -284,7 +284,7 @@ enum RustyEnum<'a> { b: usize, }, #[pyo3(transparent)] - CatchAll(&'a PyAny), // This extraction never fails + CatchAll(Bound<'py, PyAny>), // This extraction never fails } # # use pyo3::types::{PyBytes, PyString}; @@ -303,7 +303,7 @@ enum RustyEnum<'a> { # ); # } # { -# let thing = PyString::new(py, "text"); +# let thing = PyString::new_bound(py, "text"); # let rust_thing: RustyEnum<'_> = thing.extract()?; # # assert_eq!( @@ -339,7 +339,7 @@ enum RustyEnum<'a> { # ); # } # { -# let module = PyModule::from_code( +# let module = PyModule::from_code_bound( # py, # "class Foo(dict): # def __init__(self): @@ -364,7 +364,7 @@ enum RustyEnum<'a> { # } # # { -# let module = PyModule::from_code( +# let module = PyModule::from_code_bound( # py, # "class Foo(dict): # def __init__(self): @@ -388,13 +388,13 @@ enum RustyEnum<'a> { # } # # { -# let thing = PyBytes::new(py, b"text"); +# let thing = PyBytes::new_bound(py, b"text"); # let rust_thing: RustyEnum<'_> = thing.extract()?; # # assert_eq!( # b"text", # match rust_thing { -# RustyEnum::CatchAll(i) => i.downcast::()?.as_bytes(), +# RustyEnum::CatchAll(ref i) => i.downcast::()?.as_bytes(), # other => unreachable!("Error extracting: {:?}", other), # } # ); @@ -482,9 +482,9 @@ If the input is neither a string nor an integer, the error message will be: - retrieve the field from a mapping, possibly with the custom key specified as an argument. - can be any literal that implements `ToBorrowedObject` - `pyo3(from_py_with = "...")` - - apply a custom function to convert the field from Python the desired Rust type. + - apply a custom function to convert the field from Python the desired Rust type. - the argument must be the name of the function as a string. - - the function signature must be `fn(&PyAny) -> PyResult` where `T` is the Rust type of the argument. + - the function signature must be `fn(&Bound) -> PyResult` where `T` is the Rust type of the argument. ### `IntoPy` @@ -499,7 +499,7 @@ _without_ having a unique python type. ```rust use pyo3::prelude::*; - +# #[allow(dead_code)] struct MyPyObjectWrapper(PyObject); impl IntoPy for MyPyObjectWrapper { diff --git a/guide/src/debugging.md b/guide/src/debugging.md index e861f454a07..00c22631c3b 100644 --- a/guide/src/debugging.md +++ b/guide/src/debugging.md @@ -16,7 +16,7 @@ You can also debug classic `!`-macros by adding `-Z trace-macros`: cargo rustc --profile=check -- -Z unstable-options --pretty=expanded -Z trace-macros > expanded.rs; rustfmt expanded.rs ``` -See [cargo expand](https://github.com/dtolnay/cargo-expand) for a more elaborate version of those commands. +Note that those commands require using the nightly build of rust and may occasionally have bugs. See [cargo expand](https://github.com/dtolnay/cargo-expand) for a more elaborate and stable version of those commands. ## Running with Valgrind diff --git a/guide/src/ecosystem/async-await.md b/guide/src/ecosystem/async-await.md index f537ab90df1..9c0ad19bdef 100644 --- a/guide/src/ecosystem/async-await.md +++ b/guide/src/ecosystem/async-await.md @@ -120,7 +120,7 @@ Export an async function that makes use of `async-std`: use pyo3::{prelude::*, wrap_pyfunction}; #[pyfunction] -fn rust_sleep(py: Python<'_>) -> PyResult<&PyAny> { +fn rust_sleep(py: Python<'_>) -> PyResult<&Bound<'_, PyAny>> { pyo3_asyncio::async_std::future_into_py(py, async { async_std::task::sleep(std::time::Duration::from_secs(1)).await; Ok(Python::with_gil(|py| py.None())) @@ -128,7 +128,7 @@ fn rust_sleep(py: Python<'_>) -> PyResult<&PyAny> { } #[pymodule] -fn my_async_module(py: Python<'_>, m: &PyModule) -> PyResult<()> { +fn my_async_module(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(rust_sleep, m)?)?; Ok(()) @@ -143,7 +143,7 @@ If you want to use `tokio` instead, here's what your module should look like: use pyo3::{prelude::*, wrap_pyfunction}; #[pyfunction] -fn rust_sleep(py: Python<'_>) -> PyResult<&PyAny> { +fn rust_sleep(py: Python<'_>) -> PyResult<&Bound<'_, PyAny>>> { pyo3_asyncio::tokio::future_into_py(py, async { tokio::time::sleep(std::time::Duration::from_secs(1)).await; Ok(Python::with_gil(|py| py.None())) @@ -151,7 +151,7 @@ fn rust_sleep(py: Python<'_>) -> PyResult<&PyAny> { } #[pymodule] -fn my_async_module(py: Python<'_>, m: &PyModule) -> PyResult<()> { +fn my_async_module(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(rust_sleep, m)?)?; Ok(()) } @@ -197,7 +197,7 @@ to do something special with the object that it returns. Normally in Python, that something special is the `await` keyword, but in order to await this coroutine in Rust, we first need to convert it into Rust's version of a `coroutine`: a `Future`. That's where `pyo3-asyncio` comes in. -[`pyo3_asyncio::into_future`](https://docs.rs/pyo3-asyncio/latest/pyo3_asyncio/fn.into_future.html) +[`pyo3_asyncio::async_std::into_future`](https://docs.rs/pyo3-asyncio/latest/pyo3_asyncio/async_std/fn.into_future.html) performs this conversion for us. The following example uses `into_future` to call the `py_sleep` function shown above and then await the @@ -233,7 +233,7 @@ a coroutine argument: ```rust #[pyfunction] -fn await_coro(coro: &PyAny) -> PyResult<()> { +fn await_coro(coro: &Bound<'_, PyAny>>) -> PyResult<()> { // convert the coroutine into a Rust future using the // async_std runtime let f = pyo3_asyncio::async_std::into_future(coro)?; @@ -261,7 +261,7 @@ If for you wanted to pass a callable function to the `#[pyfunction]` instead, (i ```rust #[pyfunction] -fn await_coro(callable: &PyAny) -> PyResult<()> { +fn await_coro(callable: &Bound<'_, PyAny>>) -> PyResult<()> { // get the coroutine by calling the callable let coro = callable.call0()?; @@ -317,7 +317,7 @@ async fn rust_sleep() { } #[pyfunction] -fn call_rust_sleep(py: Python<'_>) -> PyResult<&PyAny> { +fn call_rust_sleep(py: Python<'_>) -> PyResult<&Bound<'_, PyAny>>> { pyo3_asyncio::async_std::future_into_py(py, async move { rust_sleep().await; Ok(Python::with_gil(|py| py.None())) @@ -349,7 +349,7 @@ implementations _prefer_ control over the main thread, this can still make some Because Python needs to control the main thread, we can't use the convenient proc macros from Rust runtimes to handle the `main` function or `#[test]` functions. Instead, the initialization for PyO3 has to be done from the `main` function and the main -thread must block on [`pyo3_asyncio::run_forever`](https://docs.rs/pyo3-asyncio/latest/pyo3_asyncio/fn.run_forever.html) or [`pyo3_asyncio::async_std::run_until_complete`](https://docs.rs/pyo3-asyncio/latest/pyo3_asyncio/async_std/fn.run_until_complete.html). +thread must block on [`pyo3_asyncio::async_std::run_until_complete`](https://docs.rs/pyo3-asyncio/latest/pyo3_asyncio/async_std/fn.run_until_complete.html). Because we have to block on one of those functions, we can't use [`#[async_std::main]`](https://docs.rs/async-std/latest/async_std/attr.main.html) or [`#[tokio::main]`](https://docs.rs/tokio/1.1.0/tokio/attr.main.html) since it's not a good idea to make long blocking calls during an async function. @@ -467,7 +467,7 @@ tokio = "1.4" use pyo3::{prelude::*, wrap_pyfunction}; #[pyfunction] -fn rust_sleep(py: Python<'_>) -> PyResult<&PyAny> { +fn rust_sleep(py: Python<'_>) -> PyResult<&Bound<'_, PyAny>>> { pyo3_asyncio::tokio::future_into_py(py, async { tokio::time::sleep(std::time::Duration::from_secs(1)).await; Ok(Python::with_gil(|py| py.None())) @@ -475,7 +475,7 @@ fn rust_sleep(py: Python<'_>) -> PyResult<&PyAny> { } #[pymodule] -fn my_async_module(_py: Python<'_>, m: &PyModule) -> PyResult<()> { +fn my_async_module(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(rust_sleep, m)?)?; Ok(()) diff --git a/guide/src/ecosystem/logging.md b/guide/src/ecosystem/logging.md index 2e7d4a087c6..da95c4a7cd2 100644 --- a/guide/src/ecosystem/logging.md +++ b/guide/src/ecosystem/logging.md @@ -30,11 +30,11 @@ fn log_something() { } #[pymodule] -fn my_module(_py: Python<'_>, m: &PyModule) -> PyResult<()> { +fn my_module(m: &Bound<'_, PyModule>) -> PyResult<()> { // A good place to install the Rust -> Python logger. pyo3_log::init(); - m.add_function(wrap_pyfunction!(log_something))?; + m.add_function(wrap_pyfunction!(log_something, m)?)?; Ok(()) } ``` diff --git a/guide/src/exception.md b/guide/src/exception.md index 756477957b8..1a68e24086f 100644 --- a/guide/src/exception.md +++ b/guide/src/exception.md @@ -24,7 +24,7 @@ use pyo3::exceptions::PyException; create_exception!(mymodule, CustomError, PyException); Python::with_gil(|py| { - let ctx = [("CustomError", py.get_type::())].into_py_dict(py); + let ctx = [("CustomError", py.get_type_bound::())].into_py_dict_bound(py); pyo3::py_run!( py, *ctx, @@ -44,9 +44,9 @@ use pyo3::exceptions::PyException; pyo3::create_exception!(mymodule, CustomError, PyException); #[pymodule] -fn mymodule(py: Python<'_>, m: &PyModule) -> PyResult<()> { +fn mymodule(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { // ... other elements added to module ... - m.add("CustomError", py.get_type::())?; + m.add("CustomError", py.get_type_bound::())?; Ok(()) } @@ -54,7 +54,7 @@ fn mymodule(py: Python<'_>, m: &PyModule) -> PyResult<()> { ## Raising an exception -As described in the [function error handling](./function/error_handling.md) chapter, to raise an exception from a `#[pyfunction]` or `#[pymethods]`, return an `Err(PyErr)`. PyO3 will automatically raise this exception for you when returning the result to Python. +As described in the [function error handling](./function/error-handling.md) chapter, to raise an exception from a `#[pyfunction]` or `#[pymethods]`, return an `Err(PyErr)`. PyO3 will automatically raise this exception for you when returning the result to Python. You can also manually write and fetch errors in the Python interpreter's global state: @@ -75,12 +75,12 @@ Python has an [`isinstance`](https://docs.python.org/3/library/functions.html#is In PyO3 every object has the [`PyAny::is_instance`] and [`PyAny::is_instance_of`] methods which do the same thing. ```rust -use pyo3::Python; +use pyo3::prelude::*; use pyo3::types::{PyBool, PyList}; Python::with_gil(|py| { - assert!(PyBool::new(py, true).is_instance_of::()); - let list = PyList::new(py, &[1, 2, 3, 4]); + assert!(PyBool::new_bound(py, true).is_instance_of::()); + let list = PyList::new_bound(py, &[1, 2, 3, 4]); assert!(!list.is_instance_of::()); assert!(list.is_instance_of::()); }); @@ -111,7 +111,7 @@ mod io { pyo3::import_exception!(io, UnsupportedOperation); } -fn tell(file: &PyAny) -> PyResult { +fn tell(file: &Bound<'_, PyAny>) -> PyResult { match file.call_method0("tell") { Err(_) => Err(io::UnsupportedOperation::new_err("not supported: tell")), Ok(x) => x.extract::(), @@ -128,5 +128,5 @@ defines exceptions for several standard library modules. [`PyErr`]: {{#PYO3_DOCS_URL}}/pyo3/struct.PyErr.html [`PyResult`]: {{#PYO3_DOCS_URL}}/pyo3/type.PyResult.html [`PyErr::from_value`]: {{#PYO3_DOCS_URL}}/pyo3/struct.PyErr.html#method.from_value -[`PyAny::is_instance`]: {{#PYO3_DOCS_URL}}/pyo3/struct.PyAny.html#method.is_instance -[`PyAny::is_instance_of`]: {{#PYO3_DOCS_URL}}/pyo3/struct.PyAny.html#method.is_instance_of +[`PyAny::is_instance`]: {{#PYO3_DOCS_URL}}/pyo3/types/trait.PyAnyMethods.html#tymethod.is_instance +[`PyAny::is_instance_of`]: {{#PYO3_DOCS_URL}}/pyo3/types/trait.PyAnyMethods.html#tymethod.is_instance_of diff --git a/guide/src/faq.md b/guide/src/faq.md index 8308acc64de..b79641a3803 100644 --- a/guide/src/faq.md +++ b/guide/src/faq.md @@ -1,5 +1,7 @@ # Frequently Asked Questions and troubleshooting +Sorry that you're having trouble using PyO3. If you can't find the answer to your problem in the list below, you can also reach out for help on [GitHub Discussions](https://github.com/PyO3/pyo3/discussions) and on [Discord](https://discord.gg/33kcChzH7f). + ## I'm experiencing deadlocks using PyO3 with lazy_static or once_cell! `lazy_static` and `once_cell::sync` both use locks to ensure that initialization is performed only by a single thread. Because the Python GIL is an additional lock this can lead to deadlocks in the following way: @@ -125,12 +127,10 @@ If you don't want that cloning to happen, a workaround is to allocate the field ```rust # use pyo3::prelude::*; #[pyclass] -#[derive(Clone)] struct Inner {/* fields omitted */} #[pyclass] struct Outer { - #[pyo3(get)] inner: Py, } @@ -142,6 +142,11 @@ impl Outer { inner: Py::new(py, Inner {})?, }) } + + #[getter] + fn inner(&self, py: Python<'_>) -> Py { + self.inner.clone_ref(py) + } } ``` This time `a` and `b` *are* the same object: @@ -161,7 +166,7 @@ b: ``` The downside to this approach is that any Rust code working on the `Outer` struct now has to acquire the GIL to do anything with its field. -## I want to use the `pyo3` crate re-exported from from dependency but the proc-macros fail! +## I want to use the `pyo3` crate re-exported from dependency but the proc-macros fail! All PyO3 proc-macros (`#[pyclass]`, `#[pyfunction]`, `#[derive(FromPyObject)]` and so on) expect the `pyo3` crate to be available under that name in your crate @@ -183,7 +188,7 @@ struct MyClass; ## I'm trying to call Python from Rust but I get `STATUS_DLL_NOT_FOUND` or `STATUS_ENTRYPOINT_NOT_FOUND`! -This happens on Windows when linking to the python DLL fails or the wrong one is linked. The Python DLL on Windows will usually be called something like: +This happens on Windows when linking to the python DLL fails or the wrong one is linked. The Python DLL on Windows will usually be called something like: - `python3X.dll` for Python 3.X, e.g. `python310.dll` for Python 3.10 - `python3.dll` when using PyO3's `abi3` feature diff --git a/guide/src/features.md b/guide/src/features.md index 3306a0b859d..ef49f317804 100644 --- a/guide/src/features.md +++ b/guide/src/features.md @@ -12,7 +12,7 @@ This feature is required when building a Python extension module using PyO3. It tells PyO3's build script to skip linking against `libpython.so` on Unix platforms, where this must not be done. -See the [building and distribution](building_and_distribution.md#linking) section for further detail. +See the [building and distribution](building-and-distribution.md#the-extension-module-feature) section for further detail. ### `abi3` @@ -20,7 +20,7 @@ This feature is used when building Python extension modules to create wheels whi It restricts PyO3's API to a subset of the full Python API which is guaranteed by [PEP 384](https://www.python.org/dev/peps/pep-0384/) to be forwards-compatible with future Python versions. -See the [building and distribution](building_and_distribution.md#py_limited_apiabi3) section for further detail. +See the [building and distribution](building-and-distribution.md#py_limited_apiabi3) section for further detail. ### The `abi3-pyXY` features @@ -28,7 +28,7 @@ See the [building and distribution](building_and_distribution.md#py_limited_apia These features are extensions of the `abi3` feature to specify the exact minimum Python version which the multiple-version-wheel will support. -See the [building and distribution](building_and_distribution.md#minimum-python-version-for-abi3) section for further detail. +See the [building and distribution](building-and-distribution.md#minimum-python-version-for-abi3) section for further detail. ### `generate-import-lib` @@ -38,25 +38,51 @@ for MinGW-w64 and MSVC (cross-)compile targets. Enabling it allows to (cross-)compile extension modules to any Windows targets without having to install the Windows Python distribution files for the target. -See the [building and distribution](building_and_distribution.md#building-abi3-extensions-without-a-python-interpreter) +See the [building and distribution](building-and-distribution.md#building-abi3-extensions-without-a-python-interpreter) section for further detail. ## Features for embedding Python in Rust ### `auto-initialize` -This feature changes [`Python::with_gil`]({{#PYO3_DOCS_URL}}/pyo3/struct.Python.html#method.with_gil) to automatically initialize a Python interpreter (by calling [`prepare_freethreaded_python`]({{#PYO3_DOCS_URL}}/pyo3/fn.prepare_freethreaded_python.html)) if needed. +This feature changes [`Python::with_gil`]({{#PYO3_DOCS_URL}}/pyo3/marker/struct.Python.html#method.with_gil) to automatically initialize a Python interpreter (by calling [`prepare_freethreaded_python`]({{#PYO3_DOCS_URL}}/pyo3/fn.prepare_freethreaded_python.html)) if needed. If you do not enable this feature, you should call `pyo3::prepare_freethreaded_python()` before attempting to call any other Python APIs. ## Advanced Features +### `experimental-async` + +This feature adds support for `async fn` in `#[pyfunction]` and `#[pymethods]`. + +The feature has some unfinished refinements and performance improvements. To help finish this off, see [issue #1632](https://github.com/PyO3/pyo3/issues/1632) and its associated draft PRs. + +### `experimental-declarative-modules` + +This feature allows to declare Python modules using `#[pymodule] mod my_module { ... }` syntax. + +The feature has some unfinished refinements and edge cases. To help finish this off, see [issue #3900](https://github.com/PyO3/pyo3/issues/3900). + ### `experimental-inspect` This feature adds the `pyo3::inspect` module, as well as `IntoPy::type_output` and `FromPyObject::type_input` APIs to produce Python type "annotations" for Rust types. This is a first step towards adding first-class support for generating type annotations automatically in PyO3, however work is needed to finish this off. All feedback and offers of help welcome on [issue #2454](https://github.com/PyO3/pyo3/issues/2454). +### `gil-refs` + +This feature is a backwards-compatibility feature to allow continued use of the "GIL Refs" APIs deprecated in PyO3 0.21. These APIs have performance drawbacks and soundness edge cases which the newer `Bound` smart pointer and accompanying APIs resolve. + +This feature and the APIs it enables is expected to be removed in a future PyO3 version. + +### `py-clone` + +This feature was introduced to ease migration. It was found that delayed reference counts cannot be made sound and hence `Clon`ing an instance of `Py` must panic without the GIL being held. To avoid migrations introducing new panics without warning, the `Clone` implementation itself is now gated behind this feature. + +### `pyo3_disable_reference_pool` + +This is a performance-oriented conditional compilation flag, e.g. [set via `$RUSTFLAGS`][set-configuration-options], which disabled the global reference pool and the assocaited overhead for the crossing the Python-Rust boundary. However, if enabled, `Drop`ping an instance of `Py` without the GIL being held will abort the process. + ### `macros` This feature enables a dependency on the `pyo3-macros` crate, which provides the procedural macros portion of PyO3's API: @@ -98,16 +124,22 @@ Adds a dependency on [anyhow](https://docs.rs/anyhow). Enables a conversion from ### `chrono` Adds a dependency on [chrono](https://docs.rs/chrono). Enables a conversion from [chrono](https://docs.rs/chrono)'s types to python: -- [Duration](https://docs.rs/chrono/latest/chrono/struct.Duration.html) -> [`PyDelta`]({{#PYO3_DOCS_URL}}/pyo3/types/struct.PyDelta.html) +- [TimeDelta](https://docs.rs/chrono/latest/chrono/struct.TimeDelta.html) -> [`PyDelta`]({{#PYO3_DOCS_URL}}/pyo3/types/struct.PyDelta.html) - [FixedOffset](https://docs.rs/chrono/latest/chrono/offset/struct.FixedOffset.html) -> [`PyDelta`]({{#PYO3_DOCS_URL}}/pyo3/types/struct.PyDelta.html) - [Utc](https://docs.rs/chrono/latest/chrono/offset/struct.Utc.html) -> [`PyTzInfo`]({{#PYO3_DOCS_URL}}/pyo3/types/struct.PyTzInfo.html) - [NaiveDate](https://docs.rs/chrono/latest/chrono/naive/struct.NaiveDate.html) -> [`PyDate`]({{#PYO3_DOCS_URL}}/pyo3/types/struct.PyDate.html) - [NaiveTime](https://docs.rs/chrono/latest/chrono/naive/struct.NaiveTime.html) -> [`PyTime`]({{#PYO3_DOCS_URL}}/pyo3/types/struct.PyTime.html) - [DateTime](https://docs.rs/chrono/latest/chrono/struct.DateTime.html) -> [`PyDateTime`]({{#PYO3_DOCS_URL}}/pyo3/types/struct.PyDateTime.html) +### `chrono-tz` + +Adds a dependency on [chrono-tz](https://docs.rs/chrono-tz). +Enables conversion from and to [`Tz`](https://docs.rs/chrono-tz/latest/chrono_tz/enum.Tz.html). +It requires at least Python 3.9. + ### `either` -Adds a dependency on [either](https://docs.rs/either). Enables a conversions into [either](https://docs.rs/either)’s [`Either`](https://docs.rs/either/latest/either/struct.Report.html) type. +Adds a dependency on [either](https://docs.rs/either). Enables a conversions into [either](https://docs.rs/either)’s [`Either`](https://docs.rs/either/latest/either/enum.Either.html) type. ### `eyre` @@ -123,12 +155,16 @@ Adds a dependency on [indexmap](https://docs.rs/indexmap) and enables conversion ### `num-bigint` -Adds a dependency on [num-bigint](https://docs.rs/num-bigint) and enables conversions into its [`BigInt`](https://docs.rs/num-bigint/latest/num_bigint/struct.BigInt.html) and [`BigUint`](https://docs.rs/num-bigint/latest/num_bigint/struct.BigUInt.html) types. +Adds a dependency on [num-bigint](https://docs.rs/num-bigint) and enables conversions into its [`BigInt`](https://docs.rs/num-bigint/latest/num_bigint/struct.BigInt.html) and [`BigUint`](https://docs.rs/num-bigint/latest/num_bigint/struct.BigUint.html) types. ### `num-complex` Adds a dependency on [num-complex](https://docs.rs/num-complex) and enables conversions into its [`Complex`](https://docs.rs/num-complex/latest/num_complex/struct.Complex.html) type. +### `num-rational` + +Adds a dependency on [num-rational](https://docs.rs/num-rational) and enables conversions into its [`Ratio`](https://docs.rs/num-rational/latest/num_rational/struct.Ratio.html) type. + ### `rust_decimal` Adds a dependency on [rust_decimal](https://docs.rs/rust_decimal) and enables conversions into its [`Decimal`](https://docs.rs/rust_decimal/latest/rust_decimal/struct.Decimal.html) type. @@ -163,3 +199,5 @@ struct User { ### `smallvec` Adds a dependency on [smallvec](https://docs.rs/smallvec) and enables conversions into its [`SmallVec`](https://docs.rs/smallvec/latest/smallvec/struct.SmallVec.html) type. + +[set-configuration-options]: https://doc.rust-lang.org/reference/conditional-compilation.html#set-configuration-options diff --git a/guide/src/function-calls.md b/guide/src/function-calls.md new file mode 100644 index 00000000000..e5c9c1ed9b3 --- /dev/null +++ b/guide/src/function-calls.md @@ -0,0 +1 @@ +# Calling Python functions diff --git a/guide/src/function.md b/guide/src/function.md index 49ec716af56..86ac4c89b46 100644 --- a/guide/src/function.md +++ b/guide/src/function.md @@ -13,7 +13,7 @@ fn double(x: usize) -> usize { } #[pymodule] -fn my_extension(py: Python<'_>, m: &PyModule) -> PyResult<()> { +fn my_extension(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(double, m)?)?; Ok(()) } @@ -38,7 +38,7 @@ There are also additional sections on the following topics: The `#[pyo3]` attribute can be used to modify properties of the generated Python function. It can take any combination of the following options: - - `#[pyo3(name = "...")]` + - `#[pyo3(name = "...")]` Overrides the name exposed to Python. @@ -55,7 +55,7 @@ The `#[pyo3]` attribute can be used to modify properties of the generated Python } #[pymodule] - fn module_with_functions(py: Python<'_>, m: &PyModule) -> PyResult<()> { + fn module_with_functions(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(no_args_py, m)?)?; Ok(()) } @@ -67,31 +67,34 @@ The `#[pyo3]` attribute can be used to modify properties of the generated Python # }); ``` - - `#[pyo3(signature = (...))]` + - `#[pyo3(signature = (...))]` Defines the function signature in Python. See [Function Signatures](./function/signature.md). - - `#[pyo3(text_signature = "...")]` + - `#[pyo3(text_signature = "...")]` Overrides the PyO3-generated function signature visible in Python tooling (such as via [`inspect.signature`]). See the [corresponding topic in the Function Signatures subchapter](./function/signature.md#making-the-function-signature-available-to-python). - - `#[pyo3(pass_module)]` + - `#[pyo3(pass_module)]` - Set this option to make PyO3 pass the containing module as the first argument to the function. It is then possible to use the module in the function body. The first argument **must** be of type `&PyModule`. + Set this option to make PyO3 pass the containing module as the first argument to the function. It is then possible to use the module in the function body. The first argument **must** be of type `&Bound<'_, PyModule>`, `Bound<'_, PyModule>`, or `Py`. The following example creates a function `pyfunction_with_module` which returns the containing module's name (i.e. `module_with_fn`): ```rust use pyo3::prelude::*; + use pyo3::types::PyString; #[pyfunction] #[pyo3(pass_module)] - fn pyfunction_with_module(module: &PyModule) -> PyResult<&str> { + fn pyfunction_with_module<'py>( + module: &Bound<'py, PyModule>, + ) -> PyResult> { module.name() } #[pymodule] - fn module_with_fn(py: Python<'_>, m: &PyModule) -> PyResult<()> { + fn module_with_fn(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(pyfunction_with_module, m)?) } ``` @@ -100,16 +103,16 @@ The `#[pyo3]` attribute can be used to modify properties of the generated Python The `#[pyo3]` attribute can be used on individual arguments to modify properties of them in the generated function. It can take any combination of the following options: - - `#[pyo3(from_py_with = "...")]` + - `#[pyo3(from_py_with = "...")]` - Set this on an option to specify a custom function to convert the function argument from Python to the desired Rust type, instead of using the default `FromPyObject` extraction. The function signature must be `fn(&PyAny) -> PyResult` where `T` is the Rust type of the argument. + Set this on an option to specify a custom function to convert the function argument from Python to the desired Rust type, instead of using the default `FromPyObject` extraction. The function signature must be `fn(&Bound<'_, PyAny>) -> PyResult` where `T` is the Rust type of the argument. The following example uses `from_py_with` to convert the input Python object to its length: ```rust use pyo3::prelude::*; - fn get_length(obj: &PyAny) -> PyResult { + fn get_length(obj: &Bound<'_, PyAny>) -> PyResult { let length = obj.len()?; Ok(length) } @@ -120,7 +123,7 @@ The `#[pyo3]` attribute can be used on individual arguments to modify properties } # Python::with_gil(|py| { - # let f = pyo3::wrap_pyfunction!(object_length)(py).unwrap(); + # let f = pyo3::wrap_pyfunction_bound!(object_length)(py).unwrap(); # assert_eq!(f.call1((vec![1, 2, 3],)).unwrap().extract::().unwrap(), 3); # }); ``` @@ -133,11 +136,11 @@ You can pass Python `def`'d functions and built-in functions to Rust functions [ corresponds to regular Python functions while [`PyCFunction`] describes built-ins such as `repr()`. -You can also use [`PyAny::is_callable`] to check if you have a callable object. `is_callable` will -return `true` for functions (including lambdas), methods and objects with a `__call__` method. -You can call the object with [`PyAny::call`] with the args as first parameter and the kwargs -(or `None`) as second parameter. There are also [`PyAny::call0`] with no args and [`PyAny::call1`] -with only positional args. +You can also use [`Bound<'_, PyAny>::is_callable`] to check if you have a callable object. `is_callable` +will return `true` for functions (including lambdas), methods and objects with a `__call__` method. +You can call the object with [`Bound<'_, PyAny>::call`] with the args as first parameter and the kwargs +(or `None`) as second parameter. There are also [`Bound<'_, PyAny>::call0`] with no args and +[`Bound<'_, PyAny>::call1`] with only positional args. ### Calling Rust functions in Python @@ -148,11 +151,10 @@ The ways to convert a Rust function into a Python object vary depending on the f - use a `#[pyclass]` struct which stores the function as a field and implement `__call__` to call the stored function. - use `PyCFunction::new_closure` to create an object directly from the function. -[`PyAny::is_callable`]: {{#PYO3_DOCS_URL}}/pyo3/struct.PyAny.html#tymethod.is_callable -[`PyAny::call`]: {{#PYO3_DOCS_URL}}/pyo3/struct.PyAny.html#tymethod.call -[`PyAny::call0`]: {{#PYO3_DOCS_URL}}/pyo3/struct.PyAny.html#tymethod.call0 -[`PyAny::call1`]: {{#PYO3_DOCS_URL}}/pyo3/struct.PyAny.html#tymethod.call1 -[`PyObject`]: {{#PYO3_DOCS_URL}}/pyo3/type.PyObject.html +[`Bound<'_, PyAny>::is_callable`]: {{#PYO3_DOCS_URL}}/pyo3/prelude/trait.PyAnyMethods.html#tymethod.is_callable +[`Bound<'_, PyAny>::call`]: {{#PYO3_DOCS_URL}}/pyo3/prelude/trait.PyAnyMethods.html#tymethod.call +[`Bound<'_, PyAny>::call0`]: {{#PYO3_DOCS_URL}}/pyo3/prelude/trait.PyAnyMethods.html#tymethod.call0 +[`Bound<'_, PyAny>::call1`]: {{#PYO3_DOCS_URL}}/pyo3/prelude/trait.PyAnyMethods.html#tymethod.call1 [`wrap_pyfunction!`]: {{#PYO3_DOCS_URL}}/pyo3/macro.wrap_pyfunction.html [`PyFunction`]: {{#PYO3_DOCS_URL}}/pyo3/types/struct.PyFunction.html [`PyCFunction`]: {{#PYO3_DOCS_URL}}/pyo3/types/struct.PyCFunction.html @@ -165,8 +167,8 @@ Python argument passing convention.) It then embeds the call to the Rust functio FFI-wrapper function. This wrapper handles extraction of the regular arguments and the keyword arguments from the input `PyObject`s. -The `wrap_pyfunction` macro can be used to directly get a `PyCFunction` given a -`#[pyfunction]` and a `PyModule`: `wrap_pyfunction!(rust_fun, module)`. +The `wrap_pyfunction` macro can be used to directly get a `Bound` given a +`#[pyfunction]` and a `Bound`: `wrap_pyfunction!(rust_fun, module)`. ## `#[pyfn]` shorthand @@ -179,7 +181,7 @@ An example of `#[pyfn]` is below: use pyo3::prelude::*; #[pymodule] -fn my_extension(py: Python<'_>, m: &PyModule) -> PyResult<()> { +fn my_extension(m: &Bound<'_, PyModule>) -> PyResult<()> { #[pyfn(m)] fn double(x: usize) -> usize { x * 2 @@ -196,7 +198,7 @@ documented in the rest of this chapter. The code above is expanded to the follow use pyo3::prelude::*; #[pymodule] -fn my_extension(py: Python<'_>, m: &PyModule) -> PyResult<()> { +fn my_extension(m: &Bound<'_, PyModule>) -> PyResult<()> { #[pyfunction] fn double(x: usize) -> usize { x * 2 diff --git a/guide/src/function/error_handling.md b/guide/src/function/error-handling.md similarity index 95% rename from guide/src/function/error_handling.md rename to guide/src/function/error-handling.md index b0f63885cdf..f55fee90e54 100644 --- a/guide/src/function/error_handling.md +++ b/guide/src/function/error-handling.md @@ -23,7 +23,7 @@ In summary: As indicated in the previous section, when a `PyResult` containing an `Err` crosses from Rust to Python, PyO3 will raise the exception contained within. -Accordingly, to raise an exception from a `#[pyfunction]`, change the return type `T` to `PyResult`. When the function returns an `Err` it will raise a Python exception. (Other `Result` types can be used as long as the error `E` has a `From` conversion for `PyErr`, see [implementing a conversion](#implementing-an-error-conversion) below.) +Accordingly, to raise an exception from a `#[pyfunction]`, change the return type `T` to `PyResult`. When the function returns an `Err` it will raise a Python exception. (Other `Result` types can be used as long as the error `E` has a `From` conversion for `PyErr`, see [custom Rust error types](#custom-rust-error-types) below.) This also works for functions in `#[pymethods]`. @@ -44,7 +44,7 @@ fn check_positive(x: i32) -> PyResult<()> { # # fn main(){ # Python::with_gil(|py|{ -# let fun = pyo3::wrap_pyfunction!(check_positive, py).unwrap(); +# let fun = pyo3::wrap_pyfunction_bound!(check_positive, py).unwrap(); # fun.call1((-1,)).unwrap_err(); # fun.call1((1,)).unwrap(); # }); @@ -72,7 +72,7 @@ fn parse_int(x: &str) -> Result { # fn main() { # Python::with_gil(|py| { -# let fun = pyo3::wrap_pyfunction!(parse_int, py).unwrap(); +# let fun = pyo3::wrap_pyfunction_bound!(parse_int, py).unwrap(); # let value: usize = fun.call1(("5",)).unwrap().extract().unwrap(); # assert_eq!(value, 5); # }); @@ -132,7 +132,7 @@ fn connect(s: String) -> Result<(), CustomIOError> { fn main() { Python::with_gil(|py| { - let fun = pyo3::wrap_pyfunction!(connect, py).unwrap(); + let fun = pyo3::wrap_pyfunction_bound!(connect, py).unwrap(); let err = fun.call1(("0.0.0.0",)).unwrap_err(); assert!(err.is_instance_of::(py)); }); @@ -224,7 +224,7 @@ fn wrapped_get_x() -> Result { # fn main() { # Python::with_gil(|py| { -# let fun = pyo3::wrap_pyfunction!(wrapped_get_x, py).unwrap(); +# let fun = pyo3::wrap_pyfunction_bound!(wrapped_get_x, py).unwrap(); # let value: usize = fun.call0().unwrap().extract().unwrap(); # assert_eq!(value, 5); # }); @@ -234,8 +234,6 @@ fn wrapped_get_x() -> Result { [`From`]: https://doc.rust-lang.org/stable/std/convert/trait.From.html [`Result`]: https://doc.rust-lang.org/stable/std/result/enum.Result.html - -[`PyResult`]: {{#PYO3_DOCS_URL}}/pyo3/prelude/type.PyResult.html [`PyResult`]: {{#PYO3_DOCS_URL}}/pyo3/prelude/type.PyResult.html [`PyErr`]: {{#PYO3_DOCS_URL}}/pyo3/struct.PyErr.html [`pyo3::exceptions`]: {{#PYO3_DOCS_URL}}/pyo3/exceptions/index.html diff --git a/guide/src/function/signature.md b/guide/src/function/signature.md index 599a2f73f8a..69949220be6 100644 --- a/guide/src/function/signature.md +++ b/guide/src/function/signature.md @@ -2,9 +2,9 @@ The `#[pyfunction]` attribute also accepts parameters to control how the generated Python function accepts arguments. Just like in Python, arguments can be positional-only, keyword-only, or accept either. `*args` lists and `**kwargs` dicts can also be accepted. These parameters also work for `#[pymethods]` which will be introduced in the [Python Classes](../class.md) section of the guide. -Like Python, by default PyO3 accepts all arguments as either positional or keyword arguments. Most arguments are required by default, except for trailing `Option<_>` arguments, which are [implicitly given a default of `None`](#trailing-optional-arguments). There are two ways to modify this behaviour: - - The `#[pyo3(signature = (...))]` option which allows writing a signature in Python syntax. - - Extra arguments directly to `#[pyfunction]`. (See deprecated form) +Like Python, by default PyO3 accepts all arguments as either positional or keyword arguments. Most arguments are required by default, except for trailing `Option<_>` arguments, which are [implicitly given a default of `None`](#trailing-optional-arguments). This behaviour can be configured by the `#[pyo3(signature = (...))]` option which allows writing a signature in Python syntax. + +This section of the guide goes into detail about use of the `#[pyo3(signature = (...))]` option and its related option `#[pyo3(text_signature = "...")]` ## Using `#[pyo3(signature = (...))]` @@ -16,12 +16,12 @@ use pyo3::types::PyDict; #[pyfunction] #[pyo3(signature = (**kwds))] -fn num_kwds(kwds: Option<&PyDict>) -> usize { +fn num_kwds(kwds: Option<&Bound<'_, PyDict>>) -> usize { kwds.map_or(0, |dict| dict.len()) } #[pymodule] -fn module_with_functions(py: Python<'_>, m: &PyModule) -> PyResult<()> { +fn module_with_functions(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(num_kwds, m)?).unwrap(); Ok(()) } @@ -31,8 +31,8 @@ Just like in Python, the following constructs can be part of the signature:: * `/`: positional-only arguments separator, each parameter defined before `/` is a positional-only parameter. * `*`: var arguments separator, each parameter defined after `*` is a keyword-only parameter. - * `*args`: "args" is var args. Type of the `args` parameter has to be `&PyTuple`. - * `**kwargs`: "kwargs" receives keyword arguments. The type of the `kwargs` parameter has to be `Option<&PyDict>`. + * `*args`: "args" is var args. Type of the `args` parameter has to be `&Bound<'_, PyTuple>`. + * `**kwargs`: "kwargs" receives keyword arguments. The type of the `kwargs` parameter has to be `Option<&Bound<'_, PyDict>>`. * `arg=Value`: arguments with default value. If the `arg` argument is defined after var arguments, it is treated as a keyword-only argument. Note that `Value` has to be valid rust code, PyO3 just inserts it into the generated @@ -59,9 +59,9 @@ impl MyClass { fn method( &mut self, num: i32, - py_args: &PyTuple, + py_args: &Bound<'_, PyTuple>, name: &str, - py_kwargs: Option<&PyDict>, + py_kwargs: Option<&Bound<'_, PyDict>>, ) -> String { let num_before = self.num; self.num = num; @@ -121,9 +121,22 @@ num=-1 ## Trailing optional arguments +
+ +⚠️ Warning: This behaviour is being phased out 🛠️ + +The special casing of trailing optional arguments is deprecated. In a future `pyo3` version, arguments of type `Option<..>` will share the same behaviour as other arguments, they are required unless a default is set using `#[pyo3(signature = (...))]`. + +This is done to better align the Python and Rust definition of such functions and make it more intuitive to rewrite them from Python in Rust. Specifically `def some_fn(a: int, b: Optional[int]): ...` will not automatically default `b` to `none`, but requires an explicit default if desired, where as in current `pyo3` it is handled the other way around. + +During the migration window a `#[pyo3(signature = (...))]` will be required to silence the deprecation warning. After support for trailing optional arguments is fully removed, the signature attribute can be removed if all arguments should be required. +
+ + As a convenience, functions without a `#[pyo3(signature = (...))]` option will treat trailing `Option` arguments as having a default of `None`. In the example below, PyO3 will create `increment` with a signature of `increment(x, amount=None)`. ```rust +#![allow(deprecated)] use pyo3::prelude::*; /// Returns a copy of `x` increased by `amount`. @@ -136,9 +149,9 @@ fn increment(x: u64, amount: Option) -> u64 { # # fn main() -> PyResult<()> { # Python::with_gil(|py| { -# let fun = pyo3::wrap_pyfunction!(increment, py)?; +# let fun = pyo3::wrap_pyfunction_bound!(increment, py)?; # -# let inspect = PyModule::import(py, "inspect")?.getattr("signature")?; +# let inspect = PyModule::import_bound(py, "inspect")?.getattr("signature")?; # let sig: String = inspect # .call1((fun,))? # .call_method0("__str__")? @@ -164,9 +177,9 @@ fn increment(x: u64, amount: Option) -> u64 { # # fn main() -> PyResult<()> { # Python::with_gil(|py| { -# let fun = pyo3::wrap_pyfunction!(increment, py)?; +# let fun = pyo3::wrap_pyfunction_bound!(increment, py)?; # -# let inspect = PyModule::import(py, "inspect")?.getattr("signature")?; +# let inspect = PyModule::import_bound(py, "inspect")?.getattr("signature")?; # let sig: String = inspect # .call1((fun,))? # .call_method0("__str__")? @@ -204,12 +217,12 @@ fn add(a: u64, b: u64) -> u64 { # # fn main() -> PyResult<()> { # Python::with_gil(|py| { -# let fun = pyo3::wrap_pyfunction!(add, py)?; +# let fun = pyo3::wrap_pyfunction_bound!(add, py)?; # # let doc: String = fun.getattr("__doc__")?.extract()?; # assert_eq!(doc, "This function adds two unsigned 64-bit integers."); # -# let inspect = PyModule::import(py, "inspect")?.getattr("signature")?; +# let inspect = PyModule::import_bound(py, "inspect")?.getattr("signature")?; # let sig: String = inspect # .call1((fun,))? # .call_method0("__str__")? @@ -252,12 +265,12 @@ fn add(a: u64, b: u64) -> u64 { # # fn main() -> PyResult<()> { # Python::with_gil(|py| { -# let fun = pyo3::wrap_pyfunction!(add, py)?; +# let fun = pyo3::wrap_pyfunction_bound!(add, py)?; # # let doc: String = fun.getattr("__doc__")?.extract()?; # assert_eq!(doc, "This function adds two unsigned 64-bit integers."); # -# let inspect = PyModule::import(py, "inspect")?.getattr("signature")?; +# let inspect = PyModule::import_bound(py, "inspect")?.getattr("signature")?; # let sig: String = inspect # .call1((fun,))? # .call_method0("__str__")? @@ -269,7 +282,7 @@ fn add(a: u64, b: u64) -> u64 { # } ``` -PyO3 will include the contents of the annotation unmodified as the `__text_signature`. Below shows how IPython will now present this (see the default value of 0 for b): +PyO3 will include the contents of the annotation unmodified as the `__text_signature__`. Below shows how IPython will now present this (see the default value of 0 for b): ```text >>> pyo3_test.add.__text_signature__ @@ -294,7 +307,7 @@ fn add(a: u64, b: u64) -> u64 { # # fn main() -> PyResult<()> { # Python::with_gil(|py| { -# let fun = pyo3::wrap_pyfunction!(add, py)?; +# let fun = pyo3::wrap_pyfunction_bound!(add, py)?; # # let doc: String = fun.getattr("__doc__")?.extract()?; # assert_eq!(doc, "This function adds two unsigned 64-bit integers."); diff --git a/guide/src/getting_started.md b/guide/src/getting-started.md similarity index 78% rename from guide/src/getting_started.md rename to guide/src/getting-started.md index 22bce336d80..ede48d50c33 100644 --- a/guide/src/getting_started.md +++ b/guide/src/getting-started.md @@ -2,9 +2,11 @@ To get started using PyO3 you will need three things: a Rust toolchain, a Python environment, and a way to build. We'll cover each of these below. +> If you'd like to chat to the PyO3 maintainers and other PyO3 users, consider joining the [PyO3 Discord server](https://discord.gg/33kcChzH7f). We're keen to hear about your experience getting started, so we can make PyO3 as accessible as possible for everyone! + ## Rust -First, make sure you have Rust installed on your system. If you haven't already done so, try following the instructions [here](https://www.rust-lang.org/tools/install). PyO3 runs on both the `stable` and `nightly` versions so you can choose whichever one fits you best. The minimum required Rust version is 1.56. +First, make sure you have Rust installed on your system. If you haven't already done so, try following the instructions [here](https://www.rust-lang.org/tools/install). PyO3 runs on both the `stable` and `nightly` versions so you can choose whichever one fits you best. The minimum required Rust version is 1.63. If you can run `rustc --version` and the version is new enough you're good to go! @@ -16,22 +18,17 @@ To use PyO3, you need at least Python 3.7. While you can simply use the default While you can use any virtualenv manager you like, we recommend the use of `pyenv` in particular if you want to develop or test for multiple different Python versions, so that is what the examples in this book will use. The installation instructions for `pyenv` can be found [here](https://github.com/pyenv/pyenv#getting-pyenv). (Note: To get the `pyenv activate` and `pyenv virtualenv` commands, you will also need to install the [`pyenv-virtualenv`](https://github.com/pyenv/pyenv-virtualenv) plugin. The [pyenv installer](https://github.com/pyenv/pyenv-installer#installation--update--uninstallation) will install both together.) -If you intend to run Python from Rust (for example in unit tests) you should set the following environment variable when installing a new Python version using `pyenv`: -```bash -PYTHON_CONFIGURE_OPTS="--enable-shared" -``` +It can be useful to keep the sources used when installing using `pyenv` so that future debugging can see the original source files. This can be done by passing the `--keep` flag as part of the `pyenv install` command. For example: ```bash -env PYTHON_CONFIGURE_OPTS="--enable-shared" pyenv install 3.12 +pyenv install 3.12 --keep ``` -You can read more about `pyenv`'s configuration options [here](https://github.com/pyenv/pyenv/blob/master/plugins/python-build/README.md#building-with---enable-shared). - ### Building -There are a number of build and Python package management systems such as [`setuptools-rust`](https://github.com/PyO3/setuptools-rust) or [manually](https://pyo3.rs/latest/building_and_distribution.html#manual-builds). We recommend the use of `maturin`, which you can install [here](https://maturin.rs/installation.html). It is developed to work with PyO3 and provides the most "batteries included" experience, especially if you are aiming to publish to PyPI. `maturin` is just a Python package, so you can add it in the same you already install Python packages. +There are a number of build and Python package management systems such as [`setuptools-rust`](https://github.com/PyO3/setuptools-rust) or [manually](./building-and-distribution.md#manual-builds). We recommend the use of `maturin`, which you can install [here](https://maturin.rs/installation.html). It is developed to work with PyO3 and provides the most "batteries included" experience, especially if you are aiming to publish to PyPI. `maturin` is just a Python package, so you can add it in the same way you already install Python packages. System Python: ```bash @@ -161,7 +158,7 @@ fn sum_as_string(a: usize, b: usize) -> PyResult { /// the `lib.name` setting in the `Cargo.toml`, else Python will not be able to /// import the module. #[pymodule] -fn pyo3_example(_py: Python<'_>, m: &PyModule) -> PyResult<()> { +fn pyo3_example(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(sum_as_string, m)?)?; Ok(()) } @@ -178,4 +175,8 @@ $ python '25' ``` -For more instructions on how to use Python code from Rust, see the [Python from Rust](python_from_rust.md) page. +For more instructions on how to use Python code from Rust, see the [Python from Rust](python-from-rust.md) page. + +## Maturin Import Hook + +In development, any changes in the code would require running `maturin develop` before testing. To streamline the development process, you may want to install [Maturin Import Hook](https://github.com/PyO3/maturin-import-hook) which will run `maturin develop` automatically when the library with code changes is being imported. diff --git a/guide/src/index.md b/guide/src/index.md index 80534caea5f..fe8b76b69c9 100644 --- a/guide/src/index.md +++ b/guide/src/index.md @@ -2,6 +2,23 @@ Welcome to the PyO3 user guide! This book is a companion to [PyO3's API docs](https://docs.rs/pyo3). It contains examples and documentation to explain all of PyO3's use cases in detail. +The rough order of material in this user guide is as follows: + 1. Getting started + 2. Wrapping Rust code for use from Python + 3. How to use Python code from Rust + 4. Remaining topics which go into advanced concepts in detail + Please choose from the chapters on the left to jump to individual topics, or continue below to start with PyO3's README. +
+ +⚠️ Warning: API update in progress 🛠️ + +PyO3 0.21 has introduced a significant new API, termed the "Bound" API after the new smart pointer `Bound`. + +While most of this guide has been updated to the new API, it is possible some stray references to the older "GIL Refs" API such as `&PyAny` remain. +
+ +
+ {{#include ../../README.md}} diff --git a/guide/src/memory.md b/guide/src/memory.md index f9201e3f003..38a31f4d0ef 100644 --- a/guide/src/memory.md +++ b/guide/src/memory.md @@ -1,5 +1,16 @@ # Memory management +
+ +⚠️ Warning: API update in progress 🛠️ + +PyO3 0.21 has introduced a significant new API, termed the "Bound" API after the new smart pointer `Bound`. + +This section on memory management is heavily weighted towards the now-deprecated "GIL Refs" API, which suffered from the drawbacks detailed here as well as CPU overheads. + +See [the smart pointer types](./types.md#pyo3s-smart-pointers) for description on the new, simplified, memory model of the Bound API, which is built as a thin wrapper on Python reference counting. +
+ Rust and Python have very different notions of memory management. Rust has a strict memory model with concepts of ownership, borrowing, and lifetimes, where memory is freed at predictable points in program execution. Python has @@ -10,12 +21,12 @@ Memory in Python is freed eventually by the garbage collector, but not usually in a predictable way. PyO3 bridges the Rust and Python memory models with two different strategies for -accessing memory allocated on Python's heap from inside Rust. These are -GIL-bound, or "owned" references, and GIL-independent `Py` smart pointers. +accessing memory allocated on Python's heap from inside Rust. These are +GIL Refs such as `&'py PyAny`, and GIL-independent `Py` smart pointers. ## GIL-bound memory -PyO3's GIL-bound, "owned references" (`&PyAny` etc.) make PyO3 more ergonomic to +PyO3's GIL Refs such as `&'py PyAny` make PyO3 more ergonomic to use by ensuring that their lifetime can never be longer than the duration the Python GIL is held. This means that most of PyO3's API can assume the GIL is held. (If PyO3 could not assume this, every PyO3 API would need to take a @@ -23,11 +34,16 @@ held. (If PyO3 could not assume this, every PyO3 API would need to take a very simple and easy-to-understand programs like this: ```rust +# #![allow(unused_imports)] # use pyo3::prelude::*; # use pyo3::types::PyString; # fn main() -> PyResult<()> { +# #[cfg(feature = "gil-refs")] Python::with_gil(|py| -> PyResult<()> { - let hello: &PyString = py.eval("\"Hello World!\"", None, None)?.extract()?; + #[allow(deprecated)] // py.eval() is part of the GIL Refs API + let hello = py + .eval("\"Hello World!\"", None, None)? + .downcast::()?; println!("Python says: {}", hello); Ok(()) })?; @@ -43,12 +59,17 @@ it owns are decreased, releasing them to the Python garbage collector. Most of the time we don't have to think about this, but consider the following: ```rust +# #![allow(unused_imports)] # use pyo3::prelude::*; # use pyo3::types::PyString; # fn main() -> PyResult<()> { +# #[cfg(feature = "gil-refs")] Python::with_gil(|py| -> PyResult<()> { for _ in 0..10 { - let hello: &PyString = py.eval("\"Hello World!\"", None, None)?.extract()?; + #[allow(deprecated)] // py.eval() is part of the GIL Refs API + let hello = py + .eval("\"Hello World!\"", None, None)? + .downcast::()?; println!("Python says: {}", hello); } // There are 10 copies of `hello` on Python's heap here. @@ -67,16 +88,29 @@ bound to the `GILPool`, not the for loop. The `GILPool` isn't dropped until the end of the `with_gil()` closure, at which point the 10 copies of `hello` are finally released to the Python garbage collector. +
+ +⚠️ Warning: `GILPool` is no longer the preferred way to manage memory with PyO3 🛠️ + +PyO3 0.21 has introduced a new API known as the Bound API, which doesn't have the same surprising results. Instead, each `Bound` smart pointer releases the Python reference immediately on drop. See [the smart pointer types](./types.md#pyo3s-smart-pointers) for more details. +
+ + In general we don't want unbounded memory growth during loops! One workaround is to acquire and release the GIL with each iteration of the loop. ```rust +# #![allow(unused_imports)] # use pyo3::prelude::*; # use pyo3::types::PyString; # fn main() -> PyResult<()> { +# #[cfg(feature = "gil-refs")] for _ in 0..10 { Python::with_gil(|py| -> PyResult<()> { - let hello: &PyString = py.eval("\"Hello World!\"", None, None)?.extract()?; + #[allow(deprecated)] // py.eval() is part of the GIL Refs API + let hello = py + .eval("\"Hello World!\"", None, None)? + .downcast::()?; println!("Python says: {}", hello); Ok(()) })?; // only one copy of `hello` at a time @@ -90,14 +124,20 @@ times. Another workaround is to work with the `GILPool` object directly, but this is unsafe. ```rust +# #![allow(unused_imports)] # use pyo3::prelude::*; # use pyo3::types::PyString; # fn main() -> PyResult<()> { +# #[cfg(feature = "gil-refs")] Python::with_gil(|py| -> PyResult<()> { for _ in 0..10 { + #[allow(deprecated)] // `new_pool` is not needed in code not using the GIL Refs API let pool = unsafe { py.new_pool() }; let py = pool.python(); - let hello: &PyString = py.eval("\"Hello World!\"", None, None)?.extract()?; + #[allow(deprecated)] // py.eval() is part of the GIL Refs API + let hello = py + .eval("\"Hello World!\"", None, None)? + .downcast::()?; println!("Python says: {}", hello); } Ok(()) @@ -114,8 +154,7 @@ at the end of each loop iteration, before the `with_gil()` closure ends. When doing this, you must be very careful to ensure that once the `GILPool` is dropped you do not retain access to any owned references created after the -`GILPool` was created. Read the -[documentation for `Python::new_pool()`]({{#PYO3_DOCS_URL}}/pyo3/prelude/struct.Python.html#method.new_pool) +`GILPool` was created. Read the documentation for `Python::new_pool()` for more information on safety. This memory management can also be applicable when writing extension modules. @@ -124,7 +163,12 @@ function call, releasing objects when the function returns. Most functions only a few objects, meaning this doesn't have a significant impact. Occasionally functions with long complex loops may need to use `Python::new_pool` as shown above. -This behavior may change in future, see [issue #1056](https://github.com/PyO3/pyo3/issues/1056). +
+ +⚠️ Warning: `GILPool` is no longer the preferred way to manage memory with PyO3 🛠️ + +PyO3 0.21 has introduced a new API known as the Bound API, which doesn't have the same surprising results. Instead, each `Bound` smart pointer releases the Python reference immediately on drop. See [the smart pointer types](./types.md#pyo3s-smart-pointers) for more details. +
## GIL-independent memory @@ -140,12 +184,18 @@ What happens to the memory when the last `Py` is dropped and its reference count reaches zero? It depends whether or not we are holding the GIL. ```rust +# #![allow(unused_imports)] # use pyo3::prelude::*; # use pyo3::types::PyString; # fn main() -> PyResult<()> { +# #[cfg(feature = "gil-refs")] Python::with_gil(|py| -> PyResult<()> { + #[allow(deprecated)] // py.eval() is part of the GIL Refs API let hello: Py = py.eval("\"Hello World!\"", None, None)?.extract()?; - println!("Python says: {}", hello.as_ref(py)); + #[allow(deprecated)] // as_ref is part of the GIL Refs API + { + println!("Python says: {}", hello.as_ref(py)); + } Ok(()) })?; # Ok(()) @@ -162,16 +212,23 @@ This example wasn't very interesting. We could have just used a GIL-bound we are *not* holding the GIL? ```rust +# #![allow(unused_imports, dead_code)] +# #[cfg(not(pyo3_disable_reference_pool))] { # use pyo3::prelude::*; # use pyo3::types::PyString; # fn main() -> PyResult<()> { +# #[cfg(feature = "gil-refs")] +# { let hello: Py = Python::with_gil(|py| { + #[allow(deprecated)] // py.eval() is part of the GIL Refs API py.eval("\"Hello World!\"", None, None)?.extract() })?; // Do some stuff... // Now sometime later in the program we want to access `hello`. Python::with_gil(|py| { - println!("Python says: {}", hello.as_ref(py)); + #[allow(deprecated)] // as_ref is part of the deprecated "GIL Refs" API. + let hello = hello.as_ref(py); + println!("Python says: {}", hello); }); // Now we're done with `hello`. drop(hello); // Memory *not* released here. @@ -180,30 +237,41 @@ Python::with_gil(|py| // Memory for `hello` is released here. # () ); +# } # Ok(()) # } +# } ``` When `hello` is dropped *nothing* happens to the pointed-to memory on Python's heap because nothing _can_ happen if we're not holding the GIL. Fortunately, -the memory isn't leaked. PyO3 keeps track of the memory internally and will -release it the next time we acquire the GIL. +the memory isn't leaked. If the `pyo3_disable_reference_pool` conditional compilation flag +is not enabled, PyO3 keeps track of the memory internally and will release it +the next time we acquire the GIL. We can avoid the delay in releasing memory if we are careful to drop the `Py` while the GIL is held. ```rust +# #![allow(unused_imports)] # use pyo3::prelude::*; # use pyo3::types::PyString; # fn main() -> PyResult<()> { +# #[cfg(feature = "gil-refs")] +# { +#[allow(deprecated)] // py.eval() is part of the GIL Refs API let hello: Py = Python::with_gil(|py| py.eval("\"Hello World!\"", None, None)?.extract())?; // Do some stuff... // Now sometime later in the program: Python::with_gil(|py| { - println!("Python says: {}", hello.as_ref(py)); + #[allow(deprecated)] // as_ref is part of the GIL Refs API + { + println!("Python says: {}", hello.as_ref(py)); + } drop(hello); // Memory released here. }); +# } # Ok(()) # } ``` @@ -215,19 +283,27 @@ that rather than being released immediately, the memory will not be released until the GIL is dropped. ```rust +# #![allow(unused_imports)] # use pyo3::prelude::*; # use pyo3::types::PyString; # fn main() -> PyResult<()> { +# #[cfg(feature = "gil-refs")] +# { +#[allow(deprecated)] // py.eval() is part of the GIL Refs API let hello: Py = Python::with_gil(|py| py.eval("\"Hello World!\"", None, None)?.extract())?; // Do some stuff... // Now sometime later in the program: Python::with_gil(|py| { - println!("Python says: {}", hello.into_ref(py)); + #[allow(deprecated)] // into_ref is part of the GIL Refs API + { + println!("Python says: {}", hello.into_ref(py)); + } // Memory not released yet. // Do more stuff... // Memory released here at end of `with_gil()` closure. }); +# } # Ok(()) # } ``` diff --git a/guide/src/migration.md b/guide/src/migration.md index fa144432d83..dac08d0bad1 100644 --- a/guide/src/migration.md +++ b/guide/src/migration.md @@ -3,19 +3,108 @@ This guide can help you upgrade code through breaking changes from one PyO3 version to the next. For a detailed list of all changes, see the [CHANGELOG](changelog.md). +## from 0.21.* to 0.22 + +### Deprecation of implicit default for trailing optional arguments +
+Click to expand + +With `pyo3` 0.22 the implicit `None` default for trailing `Option` type argument is deprecated. To migrate, place a `#[pyo3(signature = (...))]` attribute on affected functions or methods and specify the desired behavior. +The migration warning specifies the corresponding signature to keep the current behavior. With 0.23 the signature will be required for any function containing `Option` type parameters to prevent accidental +and unnoticed changes in behavior. With 0.24 this restriction will be lifted again and `Option` type arguments will be treated as any other argument _without_ special handling. + +Before: + +```rust +# #![allow(deprecated, dead_code)] +# use pyo3::prelude::*; +#[pyfunction] +fn increment(x: u64, amount: Option) -> u64 { + x + amount.unwrap_or(1) +} +``` + +After: + +```rust +# #![allow(dead_code)] +# use pyo3::prelude::*; +#[pyfunction] +#[pyo3(signature = (x, amount=None))] +fn increment(x: u64, amount: Option) -> u64 { + x + amount.unwrap_or(1) +} +``` +
+ +### `Py::clone` is now gated behind the `py-clone` feature +
+Click to expand +If you rely on `impl Clone for Py` to fulfil trait requirements imposed by existing Rust code written without PyO3-based code in mind, the newly introduced feature `py-clone` must be enabled. + +However, take care to note that the behaviour is different from previous versions. If `Clone` was called without the GIL being held, we tried to delay the application of these reference count increments until PyO3-based code would re-acquire it. This turned out to be impossible to implement in a sound manner and hence was removed. Now, if `Clone` is called without the GIL being held, we panic instead for which calling code might not be prepared. + +Related to this, we also added a `pyo3_disable_reference_pool` conditional compilation flag which removes the infrastructure necessary to apply delayed reference count decrements implied by `impl Drop for Py`. They do not appear to be a soundness hazard as they should lead to memory leaks in the worst case. However, the global synchronization adds significant overhead to cross the Python-Rust boundary. Enabling this feature will remove these costs and make the `Drop` implementation abort the process if called without the GIL being held instead. +
+ ## from 0.20.* to 0.21 +
+Click to expand + +PyO3 0.21 introduces a new `Bound<'py, T>` smart pointer which replaces the existing "GIL Refs" API to interact with Python objects. For example, in PyO3 0.20 the reference `&'py PyAny` would be used to interact with Python objects. In PyO3 0.21 the updated type is `Bound<'py, PyAny>`. Making this change moves Rust ownership semantics out of PyO3's internals and into user code. This change fixes [a known soundness edge case of interaction with gevent](https://github.com/PyO3/pyo3/issues/3668) as well as improves CPU and [memory performance](https://github.com/PyO3/pyo3/issues/1056). For a full history of discussion see https://github.com/PyO3/pyo3/issues/3382. + +The "GIL Ref" `&'py PyAny` and similar types such as `&'py PyDict` continue to be available as a deprecated API. Due to the advantages of the new API it is advised that all users make the effort to upgrade as soon as possible. + +In addition to the major API type overhaul, PyO3 has needed to make a few small breaking adjustments to other APIs to close correctness and soundness gaps. + +The recommended steps to update to PyO3 0.21 is as follows: + 1. Enable the `gil-refs` feature to silence deprecations related to the API change + 2. Fix all other PyO3 0.21 migration steps + 3. Disable the `gil-refs` feature and migrate off the deprecated APIs + +The following sections are laid out in this order. +
+ +### Enable the `gil-refs` feature +
+Click to expand + +To make the transition for the PyO3 ecosystem away from the GIL Refs API as smooth as possible, in PyO3 0.21 no APIs consuming or producing GIL Refs have been altered. Instead, variants using `Bound` smart pointers have been introduced, for example `PyTuple::new_bound` which returns `Bound` is the replacement form of `PyTuple::new`. The GIL Ref APIs have been deprecated, but to make migration easier it is possible to disable these deprecation warnings by enabling the `gil-refs` feature. + +> The one single exception where an existing API was changed in-place is the `pyo3::intern!` macro. Almost all uses of this macro did not need to update code to account it changing to return `&Bound` immediately, and adding an `intern_bound!` replacement was perceived as adding more work for users. + +It is recommended that users do this as a first step of updating to PyO3 0.21 so that the deprecation warnings do not get in the way of resolving the rest of the migration steps. + +Before: + +```toml +# Cargo.toml +[dependencies] +pyo3 = "0.20" +``` + +After: + +```toml +# Cargo.toml +[dependencies] +pyo3 = { version = "0.21", features = ["gil-refs"] } +``` +
### `PyTypeInfo` and `PyTryFrom` have been adjusted +
+Click to expand -The `PyTryFrom` trait has aged poorly, its [`try_from`] method now conflicts with `try_from` in the 2021 edition prelude. A lot of its functionality was also duplicated with `PyTypeInfo`. +The `PyTryFrom` trait has aged poorly, its `try_from` method now conflicts with `TryFrom::try_from` in the 2021 edition prelude. A lot of its functionality was also duplicated with `PyTypeInfo`. -To tighten up the PyO3 traits ahead of [a proposed upcoming API change](https://github.com/PyO3/pyo3/issues/3382) the `PyTypeInfo` trait has had a simpler companion `PyTypeCheck`. The methods [`PyAny::downcast`]({{#PYO3_DOCS_URL}}/pyo3/types/struct.PyAny.html#method.downcast) and [`PyAny::downcast_exact`]({{#PYO3_DOCS_URL}}/pyo3/types/struct.PyAny.html#method.downcast_exact) no longer use `PyTryFrom` as a bound, instead using `PyTypeCheck` and `PyTypeInfo` respectively. +To tighten up the PyO3 traits as part of the deprecation of the GIL Refs API the `PyTypeInfo` trait has had a simpler companion `PyTypeCheck`. The methods `PyAny::downcast` and `PyAny::downcast_exact` no longer use `PyTryFrom` as a bound, instead using `PyTypeCheck` and `PyTypeInfo` respectively. To migrate, switch all type casts to use `obj.downcast()` instead of `try_from(obj)` (and similar for `downcast_exact`). Before: -```rust +```rust,ignore # #![allow(deprecated)] # use pyo3::prelude::*; # use pyo3::types::{PyInt, PyList}; @@ -30,56 +119,29 @@ Python::with_gil(|py| { After: -```rust +```rust,ignore # use pyo3::prelude::*; # use pyo3::types::{PyInt, PyList}; # fn main() -> PyResult<()> { Python::with_gil(|py| { + // Note that PyList::new is deprecated for PyList::new_bound as part of the GIL Refs API removal, + // see the section below on migration to Bound. + #[allow(deprecated)] let list = PyList::new(py, 0..5); let b = list.get_item(0).unwrap().downcast::()?; Ok(()) }) # } ``` - -### `py.None()`, `py.NotImplemented()` and `py.Ellipsis()` now return typed singletons - -Previously `py.None()`, `py.NotImplemented()` and `py.Ellipsis()` would return `PyObject`. This had a few downsides: - - `PyObject` does not carry static type information - - `PyObject` takes ownership of a reference to the singletons, adding refcounting performance overhead - - `PyObject` is not gil-bound, meaning follow up method calls might again need `py`, causing repetition - -To avoid these downsides, these methods now return typed gil-bound references to the singletons, e.g. `py.None()` returns `&PyNone`. These typed singletons all implement `Into`, so migration is straightforward. - -Before: - -```rust,compile_fail -# use pyo3::prelude::*; -Python::with_gil(|py| { - let a: PyObject = py.None(); - - let b: &PyAny = py.None().as_ref(py); // or into_ref(py) -}); -``` - -After: - -```rust -# use pyo3::prelude::*; -Python::with_gil(|py| { - // For uses needing a PyObject, add `.into()` - let a: PyObject = py.None().into(); - - // For uses needing &PyAny, remove `.as_ref(py)` - let b: &PyAny = py.None(); -}); -``` +
### `Python::allow_threads` was split into `Python::safe_allow_threads` and `Python::unsafe_allow_threads` TODO ### `Iter(A)NextOutput` are deprecated +
+Click to expand The `__next__` and `__anext__` magic methods can now return any type convertible into Python objects directly just like all other `#[pymethods]`. The `IterNextOutput` used by `__next__` and `IterANextOutput` used by `__anext__` are subsequently deprecated. Most importantly, this change allows returning an awaitable from `__anext__` without non-sensically wrapping it into `Yield` or `Some`. Only the return types `Option` and `Result, E>` are still handled in a special manner where `Some(val)` yields `val` and `None` stops iteration. @@ -187,7 +249,9 @@ struct PyClassAsyncIter { impl PyClassAsyncIter { fn __anext__(&mut self) -> PyClassAwaitable { self.number += 1; - PyClassAwaitable { number: self.number } + PyClassAwaitable { + number: self.number, + } } fn __aiter__(slf: Py) -> Py { @@ -195,23 +259,207 @@ impl PyClassAsyncIter { } } ``` +
### `PyType::name` has been renamed to `PyType::qualname` +
+Click to expand `PyType::name` has been renamed to `PyType::qualname` to indicate that it does indeed return the [qualified name](https://docs.python.org/3/glossary.html#term-qualified-name), matching the `__qualname__` attribute. The newly added `PyType::name` yields the full name including the module name now which corresponds to `__module__.__name__` on the level of attributes. +
+ +### `PyCell` has been deprecated +
+Click to expand + +Interactions with Python objects implemented in Rust no longer need to go though `PyCell`. Instead iteractions with Python object now consistently go through `Bound` or `Py` independently of whether `T` is native Python object or a `#[pyclass]` implemented in Rust. Use `Bound::new` or `Py::new` respectively to create and `Bound::borrow(_mut)` / `Py::borrow(_mut)` to borrow the Rust object. +
+ +### Migrating from the GIL Refs API to `Bound` +
+Click to expand + +To minimise breakage of code using the GIL Refs API, the `Bound` smart pointer has been introduced by adding complements to all functions which accept or return GIL Refs. This allows code to migrate by replacing the deprecated APIs with the new ones. + +To identify what to migrate, temporarily switch off the `gil-refs` feature to see deprecation warnings on [almost](#cases-where-pyo3-cannot-emit-gil-ref-deprecation-warnings) all uses of APIs accepting and producing GIL Refs . Over one or more PRs it should be possible to follow the deprecation hints to update code. Depending on your development environment, switching off the `gil-refs` feature may introduce [some very targeted breakages](#deactivating-the-gil-refs-feature), so you may need to fixup those first. + +For example, the following APIs have gained updated variants: +- `PyList::new`, `PyTyple::new` and similar constructors have replacements `PyList::new_bound`, `PyTuple::new_bound` etc. +- `FromPyObject::extract` has a new `FromPyObject::extract_bound` (see the section below) +- The `PyTypeInfo` trait has had new `_bound` methods added to accept / return `Bound`. + +Because the new `Bound` API brings ownership out of the PyO3 framework and into user code, there are a few places where user code is expected to need to adjust while switching to the new API: +- Code will need to add the occasional `&` to borrow the new smart pointer as `&Bound` to pass these types around (or use `.clone()` at the very small cost of increasing the Python reference count) +- `Bound` and `Bound` cannot support indexing with `list[0]`, you should use `list.get_item(0)` instead. +- `Bound::iter_borrowed` is slightly more efficient than `Bound::iter`. The default iteration of `Bound` cannot return borrowed references because Rust does not (yet) have "lending iterators". Similarly `Bound::get_borrowed_item` is more efficient than `Bound::get_item` for the same reason. +- `&Bound` does not implement `FromPyObject` (although it might be possible to do this in the future once the GIL Refs API is completely removed). Use `bound_any.downcast::()` instead of `bound_any.extract::<&Bound>()`. +- `Bound::to_str` now borrows from the `Bound` rather than from the `'py` lifetime, so code will need to store the smart pointer as a value in some cases where previously `&PyString` was just used as a temporary. (There are some more details relating to this in [the section below](#deactivating-the-gil-refs-feature).) +- `.extract::<&str>()` now borrows from the source Python object. The simplest way to update is to change to `.extract::()`, which retains ownership of the Python reference. See more information [in the section on deactivating the `gil-refs` feature](#deactivating-the-gil-refs-feature). + +To convert between `&PyAny` and `&Bound` use the `as_borrowed()` method: + +```rust,ignore +let gil_ref: &PyAny = ...; +let bound: &Bound = &gil_ref.as_borrowed(); +``` + +To convert between `Py` and `Bound` use the `bind()` / `into_bound()` methods, and `as_unbound()` / `unbind()` to go back from `Bound` to `Py`. + +```rust,ignore +let obj: Py = ...; +let bound: &Bound<'py, PyList> = obj.bind(py); +let bound: Bound<'py, PyList> = obj.into_bound(py); + +let obj: &Py = bound.as_unbound(); +let obj: Py = bound.unbind(); +``` + +
+ +⚠️ Warning: dangling pointer trap 💣 + +> Because of the ownership changes, code which uses `.as_ptr()` to convert `&PyAny` and other GIL Refs to a `*mut pyo3_ffi::PyObject` should take care to avoid creating dangling pointers now that `Bound` carries ownership. +> +> For example, the following pattern with `Option<&PyAny>` can easily create a dangling pointer when migrating to the `Bound` smart pointer: +> +> ```rust,ignore +> let opt: Option<&PyAny> = ...; +> let p: *mut ffi::PyObject = opt.map_or(std::ptr::null_mut(), |any| any.as_ptr()); +> ``` +> +> The correct way to migrate this code is to use `.as_ref()` to avoid dropping the `Bound` in the `map_or` closure: +> +> ```rust,ignore +> let opt: Option> = ...; +> let p: *mut ffi::PyObject = opt.as_ref().map_or(std::ptr::null_mut(), Bound::as_ptr); +> ``` +
+ +#### Migrating `FromPyObject` implementations + +`FromPyObject` has had a new method `extract_bound` which takes `&Bound<'py, PyAny>` as an argument instead of `&PyAny`. Both `extract` and `extract_bound` have been given default implementations in terms of the other, to avoid breaking code immediately on update to 0.21. + +All implementations of `FromPyObject` should be switched from `extract` to `extract_bound`. + +Before: + +```rust,ignore +impl<'py> FromPyObject<'py> for MyType { + fn extract(obj: &'py PyAny) -> PyResult { + /* ... */ + } +} +``` + +After: + +```rust,ignore +impl<'py> FromPyObject<'py> for MyType { + fn extract_bound(obj: &Bound<'py, PyAny>) -> PyResult { + /* ... */ + } +} +``` + +The expectation is that in 0.22 `extract_bound` will have the default implementation removed and in 0.23 `extract` will be removed. + +#### Cases where PyO3 cannot emit GIL Ref deprecation warnings + +Despite a large amount of deprecations warnings produced by PyO3 to aid with the transition from GIL Refs to the Bound API, there are a few cases where PyO3 cannot automatically warn on uses of GIL Refs. It is worth checking for these cases manually after the deprecation warnings have all been addressed: + +- Individual implementations of the `FromPyObject` trait cannot be deprecated, so PyO3 cannot warn about uses of code patterns like `.extract<&PyAny>()` which produce a GIL Ref. +- GIL Refs in `#[pyfunction]` arguments emit a warning, but if the GIL Ref is wrapped inside another container such as `Vec<&PyAny>` then PyO3 cannot warn against this. +- The `wrap_pyfunction!(function)(py)` deferred argument form of the `wrap_pyfunction` macro taking `py: Python<'py>` produces a GIL Ref, and due to limitations in type inference PyO3 cannot warn against this specific case. + +
+ +### Deactivating the `gil-refs` feature +
+Click to expand + +As a final step of migration, deactivating the `gil-refs` feature will set up code for best performance and is intended to set up a forward-compatible API for PyO3 0.22. + +At this point code that needed to manage GIL Ref memory can safely remove uses of `GILPool` (which are constructed by calls to `Python::new_pool` and `Python::with_pool`). Deprecation warnings will highlight these cases. + +There is just one case of code that changes upon disabling these features: `FromPyObject` trait implementations for types that borrow directly from the input data cannot be implemented by PyO3 without GIL Refs (while the GIL Refs API is in the process of being removed). The main types affected are `&str`, `Cow<'_, str>`, `&[u8]`, `Cow<'_, u8>`. + +To make PyO3's core functionality continue to work while the GIL Refs API is in the process of being removed, disabling the `gil-refs` feature moves the implementations of `FromPyObject` for `&str`, `Cow<'_, str>`, `&[u8]`, `Cow<'_, u8>` to a new temporary trait `FromPyObjectBound`. This trait is the expected future form of `FromPyObject` and has an additional lifetime `'a` to enable these types to borrow data from Python objects. + +PyO3 0.21 has introduced the [`PyBackedStr`]({{#PYO3_DOCS_URL}}/pyo3/pybacked/struct.PyBackedStr.html) and [`PyBackedBytes`]({{#PYO3_DOCS_URL}}/pyo3/pybacked/struct.PyBackedBytes.html) types to help with this case. The easiest way to avoid lifetime challenges from extracting `&str` is to use these. For more complex types like `Vec<&str>`, is now impossible to extract directly from a Python object and `Vec` is the recommended upgrade path. + +A key thing to note here is because extracting to these types now ties them to the input lifetime, some extremely common patterns may need to be split into multiple Rust lines. For example, the following snippet of calling `.extract::<&str>()` directly on the result of `.getattr()` needs to be adjusted when deactivating the `gil-refs` feature. + +Before: + +```rust +# #[cfg(feature = "gil-refs")] { +# use pyo3::prelude::*; +# use pyo3::types::{PyList, PyType}; +# fn example<'py>(py: Python<'py>) -> PyResult<()> { +#[allow(deprecated)] // GIL Ref API +let obj: &'py PyType = py.get_type::(); +let name: &'py str = obj.getattr("__name__")?.extract()?; +assert_eq!(name, "list"); +# Ok(()) +# } +# Python::with_gil(example).unwrap(); +# } +``` + +After: + +```rust +# #[cfg(any(not(Py_LIMITED_API), Py_3_10))] { +# use pyo3::prelude::*; +# use pyo3::types::{PyList, PyType}; +# fn example<'py>(py: Python<'py>) -> PyResult<()> { +let obj: Bound<'py, PyType> = py.get_type_bound::(); +let name_obj: Bound<'py, PyAny> = obj.getattr("__name__")?; +// the lifetime of the data is no longer `'py` but the much shorter +// lifetime of the `name_obj` smart pointer above +let name: &'_ str = name_obj.extract()?; +assert_eq!(name, "list"); +# Ok(()) +# } +# Python::with_gil(example).unwrap(); +# } +``` + +To avoid needing to worry about lifetimes at all, it is also possible to use the new `PyBackedStr` type, which stores a reference to the Python `str` without a lifetime attachment. In particular, `PyBackedStr` helps for `abi3` builds for Python older than 3.10. Due to limitations in the `abi3` CPython API for those older versions, PyO3 cannot offer a `FromPyObjectBound` implementation for `&str` on those versions. The easiest way to migrate for older `abi3` builds is to replace any cases of `.extract::<&str>()` with `.extract::()`. Alternatively, use `.extract::>()`, `.extract::()` to copy the data into Rust. + +The following example uses the same snippet as those just above, but this time the final extracted type is `PyBackedStr`: + +```rust +# use pyo3::prelude::*; +# use pyo3::types::{PyList, PyType}; +# fn example<'py>(py: Python<'py>) -> PyResult<()> { +use pyo3::pybacked::PyBackedStr; +let obj: Bound<'py, PyType> = py.get_type_bound::(); +let name: PyBackedStr = obj.getattr("__name__")?.extract()?; +assert_eq!(&*name, "list"); +# Ok(()) +# } +# Python::with_gil(example).unwrap(); +``` ## from 0.19.* to 0.20 ### Drop support for older technologies +
+Click to expand PyO3 0.20 has increased minimum Rust version to 1.56. This enables use of newer language features and simplifies maintenance of the project. +
### `PyDict::get_item` now returns a `Result` +
+Click to expand `PyDict::get_item` in PyO3 0.19 and older was implemented using a Python API which would suppress all exceptions and return `None` in those cases. This included errors in `__hash__` and `__eq__` implementations of the key being looked up. Newer recommendations by the Python core developers advise against using these APIs which suppress exceptions, instead allowing exceptions to bubble upwards. `PyDict::get_item_with_error` already implemented this recommended behavior, so that API has been renamed to `PyDict::get_item`. + Before: ```rust,ignore @@ -228,14 +476,17 @@ Python::with_gil(|py| { // `b` is not in the dictionary assert!(dict.get_item("b").is_none()); // `dict` is not hashable, so this fails with a `TypeError` - assert!(dict.get_item_with_error(dict).unwrap_err().is_instance_of::(py)); + assert!(dict + .get_item_with_error(dict) + .unwrap_err() + .is_instance_of::(py)); }); # } ``` After: -```rust +```rust,ignore use pyo3::prelude::*; use pyo3::exceptions::PyTypeError; use pyo3::types::{PyDict, IntoPyDict}; @@ -249,14 +500,20 @@ Python::with_gil(|py| -> PyResult<()> { // `b` is not in the dictionary assert!(dict.get_item("b")?.is_none()); // `dict` is not hashable, so this fails with a `TypeError` - assert!(dict.get_item(dict).unwrap_err().is_instance_of::(py)); + assert!(dict + .get_item(dict) + .unwrap_err() + .is_instance_of::(py)); Ok(()) }); # } ``` +
### Required arguments are no longer accepted after optional arguments +
+Click to expand [Trailing `Option` arguments](./function/signature.md#trailing-optional-arguments) have an automatic default of `None`. To avoid unwanted changes when modifying function signatures, in PyO3 0.18 it was deprecated to have a required argument after an `Option` argument without using `#[pyo3(signature = (...))]` to specify the intended defaults. In PyO3 0.20, this becomes a hard error. @@ -281,8 +538,11 @@ fn x_or_y(x: Option, y: u64) -> u64 { x.unwrap_or(y) } ``` +
### Remove deprecated function forms +
+Click to expand In PyO3 0.18 the `#[args]` attribute for `#[pymethods]`, and directly specifying the function signature in `#[pyfunction]`, was deprecated. This functionality has been removed in PyO3 0.20. @@ -309,17 +569,27 @@ fn add(a: u64, b: u64) -> u64 { } ``` +
+ ### `IntoPyPointer` trait removed +
+Click to expand The trait `IntoPyPointer`, which provided the `into_ptr` method on many types, has been removed. `into_ptr` is now available as an inherent method on all types that previously implemented this trait. +
### `AsPyPointer` now `unsafe` trait +
+Click to expand The trait `AsPyPointer` is now `unsafe trait`, meaning any external implementation of it must be marked as `unsafe impl`, and ensure that they uphold the invariant of returning valid pointers. +
## from 0.18.* to 0.19 ### Access to `Python` inside `__traverse__` implementations are now forbidden +
+Click to expand During `__traverse__` implementations for Python's Garbage Collection it is forbidden to do anything other than visit the members of the `#[pyclass]` being traversed. This means making Python function calls or other API calls are forbidden. @@ -339,12 +609,15 @@ impl SomeClass { } } ``` +
### Smarter `anyhow::Error` / `eyre::Report` conversion when inner error is "simple" `PyErr` +
+Click to expand When converting from `anyhow::Error` or `eyre::Report` to `PyErr`, if the inner error is a "simple" `PyErr` (with no source error), then the inner error will be used directly as the `PyErr` instead of wrapping it in a new `PyRuntimeError` with the original information converted into a string. -```rust +```rust,ignore # #[cfg(feature = "anyhow")] # #[allow(dead_code)] # mod anyhow_only { @@ -355,7 +628,7 @@ fn raise_err() -> anyhow::Result<()> { Err(PyValueError::new_err("original error message").into()) } -# fn main() { +fn main() { Python::with_gil(|py| { let rs_func = wrap_pyfunction!(raise_err, py).unwrap(); pyo3::py_run!( @@ -378,8 +651,11 @@ Before, the above code would have printed `RuntimeError('ValueError: original er After, the same code will print `ValueError: original error message`, which is more straightforward. However, if the `anyhow::Error` or `eyre::Report` has a source, then the original exception will still be wrapped in a `PyRuntimeError`. +
### The deprecated `Python::acquire_gil` was removed and `Python::with_gil` must be used instead +
+Click to expand While the API provided by [`Python::acquire_gil`](https://docs.rs/pyo3/0.18.3/pyo3/marker/struct.Python.html#method.acquire_gil) seems convenient, it is somewhat brittle as the design of the GIL token [`Python`](https://docs.rs/pyo3/0.18.3/pyo3/marker/struct.Python.html) relies on proper nesting and panics if not used correctly, e.g. @@ -411,9 +687,9 @@ drop(first); drop(second); ``` -The replacement is [`Python::with_gil`]() which is more cumbersome but enforces the proper nesting by design, e.g. +The replacement is [`Python::with_gil`](https://docs.rs/pyo3/0.18.3/pyo3/marker/struct.Python.html#method.with_gil) which is more cumbersome but enforces the proper nesting by design, e.g. -```rust +```rust,ignore # #![allow(dead_code)] # use pyo3::prelude::*; @@ -438,7 +714,7 @@ let second = Python::with_gil(|py| Object::new(py)); drop(first); drop(second); -// Or it ensure releasing the inner lock before the outer one. +// Or it ensures releasing the inner lock before the outer one. Python::with_gil(|py| { let first = Object::new(py); let second = Python::with_gil(|py| Object::new(py)); @@ -448,10 +724,13 @@ Python::with_gil(|py| { ``` Furthermore, `Python::acquire_gil` provides ownership of a `GILGuard` which can be freely stored and passed around. This is usually not helpful as it may keep the lock held for a long time thereby blocking progress in other parts of the program. Due to the generative lifetime attached to the GIL token supplied by `Python::with_gil`, the problem is avoided as the GIL token can only be passed down the call chain. Often, this issue can also be avoided entirely as any GIL-bound reference `&'py PyAny` implies access to a GIL token `Python<'py>` via the [`PyAny::py`](https://docs.rs/pyo3/latest/pyo3/types/struct.PyAny.html#method.py) method. +
## from 0.17.* to 0.18 ### Required arguments after `Option<_>` arguments will no longer be automatically inferred +
+Click to expand In `#[pyfunction]` and `#[pymethods]`, if a "required" function input such as `i32` came after an `Option<_>` input, then the `Option<_>` would be implicitly treated as required. (All trailing `Option<_>` arguments were treated as optional with a default value of `None`). @@ -481,8 +760,11 @@ fn required_argument_after_option_a(x: Option, y: i32) {} #[pyfunction(signature = (x=None, y=0))] fn required_argument_after_option_b(x: Option, y: i32) {} ``` +
### `__text_signature__` is now automatically generated for `#[pyfunction]` and `#[pymethods]` +
+Click to expand The [`#[pyo3(text_signature = "...")]` option](./function/signature.md#making-the-function-signature-available-to-python) was previously the only supported way to set the `__text_signature__` attribute on generated Python functions. @@ -505,17 +787,20 @@ fn function_with_defaults(a: i32, b: i32, c: i32) {} # fn main() { # Python::with_gil(|py| { -# let simple = wrap_pyfunction!(simple_function, py).unwrap(); +# let simple = wrap_pyfunction_bound!(simple_function, py).unwrap(); # assert_eq!(simple.getattr("__text_signature__").unwrap().to_string(), "(a, b, c)"); -# let defaulted = wrap_pyfunction!(function_with_defaults, py).unwrap(); +# let defaulted = wrap_pyfunction_bound!(function_with_defaults, py).unwrap(); # assert_eq!(defaulted.getattr("__text_signature__").unwrap().to_string(), "(a, b=1, c=2)"); # }) # } ``` +
## from 0.16.* to 0.17 ### Type checks have been changed for `PyMapping` and `PySequence` types +
+Click to expand Previously the type checks for `PyMapping` and `PySequence` (implemented in `PyTryFrom`) used the Python C-API functions `PyMapping_Check` and `PySequence_Check`. @@ -565,13 +850,19 @@ assert!(m.as_ref(py).downcast::().is_ok()); ``` Note that this requirement may go away in the future when a pyclass is able to inherit from the abstract base class directly (see [pyo3/pyo3#991](https://github.com/PyO3/pyo3/issues/991)). +
### The `multiple-pymethods` feature now requires Rust 1.62 +
+Click to expand Due to limitations in the `inventory` crate which the `multiple-pymethods` feature depends on, this feature now requires Rust 1.62. For more information see [dtolnay/inventory#32](https://github.com/dtolnay/inventory/issues/32). +
### Added `impl IntoPy> for &str` +
+Click to expand This may cause inference errors. @@ -598,12 +889,18 @@ Python::with_gil(|py| { }); # } ``` +
### The `pyproto` feature is now disabled by default +
+Click to expand In preparation for removing the deprecated `#[pyproto]` attribute macro in a future PyO3 version, it is now gated behind an opt-in feature flag. This also gives a slight saving to compile times for code which does not use the deprecated macro. +
### `PyTypeObject` trait has been deprecated +
+Click to expand The `PyTypeObject` trait already was near-useless; almost all functionality was already on the `PyTypeInfo` trait, which `PyTypeObject` had a blanket implementation based upon. In PyO3 0.17 the final method, `PyTypeObject::type_object` was moved to `PyTypeInfo::type_object`. @@ -611,7 +908,7 @@ To migrate, update trait bounds and imports from `PyTypeObject` to `PyTypeInfo`. Before: -```rust,compile_fail +```rust,ignore use pyo3::Python; use pyo3::type_object::PyTypeObject; use pyo3::types::PyType; @@ -623,7 +920,7 @@ fn get_type_object(py: Python<'_>) -> &PyType { After -```rust +```rust,ignore use pyo3::{Python, PyTypeInfo}; use pyo3::types::PyType; @@ -633,22 +930,34 @@ fn get_type_object(py: Python<'_>) -> &PyType { # Python::with_gil(|py| { get_type_object::(py); }); ``` +
### `impl IntoPy for [T; N]` now requires `T: IntoPy` rather than `T: ToPyObject` +
+Click to expand If this leads to errors, simply implement `IntoPy`. Because pyclasses already implement `IntoPy`, you probably don't need to worry about this. +
### Each `#[pymodule]` can now only be initialized once per process +
+Click to expand To make PyO3 modules sound in the presence of Python sub-interpreters, for now it has been necessary to explicitly disable the ability to initialize a `#[pymodule]` more than once in the same process. Attempting to do this will now raise an `ImportError`. +
## from 0.15.* to 0.16 ### Drop support for older technologies +
+Click to expand PyO3 0.16 has increased minimum Rust version to 1.48 and minimum Python version to 3.7. This enables use of newer language features (enabling some of the other additions in 0.16) and simplifies maintenance of the project. +
### `#[pyproto]` has been deprecated +
+Click to expand In PyO3 0.15, the `#[pymethods]` attribute macro gained support for implementing "magic methods" such as `__str__` (aka "dunder" methods). This implementation was not quite finalized at the time, with a few edge cases to be decided upon. The existing `#[pyproto]` attribute macro was left untouched, because it covered these edge cases. @@ -701,8 +1010,11 @@ impl MyClass { } } ``` +
### Removed `PartialEq` for object wrappers +
+Click to expand The Python object wrappers `Py` and `PyAny` had implementations of `PartialEq` so that `object_a == object_b` would compare the Python objects for pointer @@ -714,8 +1026,11 @@ wrapper type for `object_a` and `object_b`; you can now directly compare a To check for Python object equality (the Python `==` operator), use the new method `eq()`. +
### Container magic methods now match Python behavior +
+Click to expand In PyO3 0.15, `__getitem__`, `__setitem__` and `__delitem__` in `#[pymethods]` would generate only the _mapping_ implementation for a `#[pyclass]`. To match the Python behavior, these methods now generate both the _mapping_ **and** _sequence_ implementations. @@ -744,8 +1059,11 @@ The `__len__` and `__getitem__` methods are also used to implement a Python [map Because there is no such distinction from Python, implementing these methods will fill the mapping and sequence slots simultaneously. A Python class with `__len__` implemented, for example, will have both the `sq_length` and `mp_length` slots filled. The PyO3 behavior in 0.16 has been changed to be closer to this Python behavior by default. +
### `wrap_pymodule!` and `wrap_pyfunction!` now respect privacy correctly +
+Click to expand Prior to PyO3 0.16 the `wrap_pymodule!` and `wrap_pyfunction!` macros could use modules and functions whose defining `fn` was not reachable according Rust privacy rules. @@ -773,7 +1091,7 @@ fn my_module(_py: Python<'_>, m: &PyModule) -> PyResult<()> { To fix it, make the private submodule visible, e.g. with `pub` or `pub(crate)`. -```rust +```rust,ignore mod foo { use pyo3::prelude::*; @@ -793,10 +1111,13 @@ fn my_module(_py: Python<'_>, m: &PyModule) -> PyResult<()> { Ok(()) } ``` +
## from 0.14.* to 0.15 ### Changes in sequence indexing +
+Click to expand For all types that take sequence indices (`PyList`, `PyTuple` and `PySequence`), the API has been made consistent to only take `usize` indices, for consistency @@ -816,7 +1137,8 @@ An additional advantage of using Rust's indexing conventions for these types is that these types can now also support Rust's indexing operators as part of a consistent API: -```rust +```rust,ignore +#![allow(deprecated)] use pyo3::{Python, types::PyList}; Python::with_gil(|py| { @@ -824,20 +1146,29 @@ Python::with_gil(|py| { assert_eq!(list[0..2].to_string(), "[1, 2]"); }); ``` +
## from 0.13.* to 0.14 ### `auto-initialize` feature is now opt-in +
+Click to expand For projects embedding Python in Rust, PyO3 no longer automatically initializes a Python interpreter on the first call to `Python::with_gil` (or `Python::acquire_gil`) unless the [`auto-initialize` feature](features.md#auto-initialize) is enabled. +
### New `multiple-pymethods` feature +
+Click to expand `#[pymethods]` have been reworked with a simpler default implementation which removes the dependency on the `inventory` crate. This reduces dependencies and compile times for the majority of users. The limitation of the new default implementation is that it cannot support multiple `#[pymethods]` blocks for the same `#[pyclass]`. If you need this functionality, you must enable the `multiple-pymethods` feature which will switch `#[pymethods]` to the inventory-based implementation. +
### Deprecated `#[pyproto]` methods +
+Click to expand Some protocol (aka `__dunder__`) methods such as `__bytes__` and `__format__` have been possible to implement two ways in PyO3 for some time: via a `#[pyproto]` (e.g. `PyObjectProtocol` for the methods listed here), or by writing them directly in `#[pymethods]`. This is only true for a handful of the `#[pyproto]` methods (for technical reasons to do with the way PyO3 currently interacts with the Python C-API). @@ -877,14 +1208,20 @@ impl MyClass { } } ``` +
## from 0.12.* to 0.13 ### Minimum Rust version increased to Rust 1.45 +
+Click to expand PyO3 `0.13` makes use of new Rust language features stabilized between Rust 1.40 and Rust 1.45. If you are using a Rust compiler older than Rust 1.45, you will need to update your toolchain to be able to continue using PyO3. +
### Runtime changes to support the CPython limited API +
+Click to expand In PyO3 `0.13` support was added for compiling against the CPython limited API. This had a number of implications for _all_ PyO3 users, described here. @@ -893,10 +1230,13 @@ The largest of these is that all types created from PyO3 are what CPython calls - If you wish to subclass one of these types _from Rust_ you must mark it `#[pyclass(subclass)]`, as you would if you wished to allow subclassing it from Python code. - Type objects are now mutable - Python code can set attributes on them. - `__module__` on types without `#[pyclass(module="mymodule")]` no longer returns `builtins`, it now raises `AttributeError`. +
## from 0.11.* to 0.12 ### `PyErr` has been reworked +
+Click to expand In PyO3 `0.12` the `PyErr` type has been re-implemented to be significantly more compatible with the standard Rust error handling ecosystem. Specifically `PyErr` now implements @@ -905,28 +1245,40 @@ the standard Rust error handling ecosystem. Specifically `PyErr` now implements While this has necessitated the removal of a number of APIs, the resulting `PyErr` type should now be much more easier to work with. The following sections list the changes in detail and how to migrate to the new APIs. +
#### `PyErr::new` and `PyErr::from_type` now require `Send + Sync` for their argument +
+Click to expand For most uses no change will be needed. If you are trying to construct `PyErr` from a value that is not `Send + Sync`, you will need to first create the Python object and then use `PyErr::from_instance`. Similarly, any types which implemented `PyErrArguments` will now need to be `Send + Sync`. +
#### `PyErr`'s contents are now private +
+Click to expand It is no longer possible to access the fields `.ptype`, `.pvalue` and `.ptraceback` of a `PyErr`. You should instead now use the new methods `PyErr::ptype`, `PyErr::pvalue` and `PyErr::ptraceback`. +
#### `PyErrValue` and `PyErr::from_value` have been removed +
+Click to expand As these were part the internals of `PyErr` which have been reworked, these APIs no longer exist. If you used this API, it is recommended to use `PyException::new_err` (see [the section on Exception types](#exception-types-have-been-reworked)). +
#### `Into>` for `PyErr` has been removed +
+Click to expand This implementation was redundant. Just construct the `Result::Err` variant directly. @@ -940,8 +1292,11 @@ After (also using the new reworked exception types; see the following section): # use pyo3::{PyResult, exceptions::PyTypeError}; let result: PyResult<()> = Err(PyTypeError::new_err("error message")); ``` +
### Exception types have been reworked +
+Click to expand Previously exception types were zero-sized marker types purely used to construct `PyErr`. In PyO3 0.12, these types have been replaced with full definitions and are usable in the same way as `PyAny`, `PyDict` etc. This @@ -949,13 +1304,13 @@ makes it possible to interact with Python exception objects. The new types also have names starting with the "Py" prefix. For example, before: -```rust,compile_fail +```rust,ignore let err: PyErr = TypeError::py_err("error message"); ``` After: -```rust,compile_fail +```rust,ignore # use pyo3::{PyErr, PyResult, Python, type_object::PyTypeObject}; # use pyo3::exceptions::{PyBaseException, PyTypeError}; # Python::with_gil(|py| -> PyResult<()> { @@ -973,8 +1328,12 @@ assert_eq!( # Ok(()) # }).unwrap(); ``` +
### `FromPy` has been removed +
+Click to expand + To simplify the PyO3 conversion traits, the `FromPy` trait has been removed. Previously there were two ways to define the to-Python conversion for a type: `FromPy for PyObject` and `IntoPy for T`. @@ -997,6 +1356,7 @@ impl FromPy for PyObject { After ```rust # use pyo3::prelude::*; +# #[allow(dead_code)] struct MyPyObjectWrapper(PyObject); impl IntoPy for MyPyObjectWrapper { @@ -1023,12 +1383,20 @@ After: let obj: PyObject = 1.234.into_py(py); # }) ``` +
### `PyObject` is now a type alias of `Py` +
+Click to expand + This should change very little from a usage perspective. If you implemented traits for both `PyObject` and `Py`, you may find you can just remove the `PyObject` implementation. +
### `AsPyRef` has been removed +
+Click to expand + As `PyObject` has been changed to be just a type alias, the only remaining implementor of `AsPyRef` was `Py`. This removed the need for a trait, so the `AsPyRef::as_ref` method has been moved to `Py::as_ref`. @@ -1037,7 +1405,7 @@ This should require no code changes except removing `use pyo3::AsPyRef` for code `pyo3::prelude::*`. Before: -```rust,compile_fail +```rust,ignore use pyo3::{AsPyRef, Py, types::PyList}; # pyo3::Python::with_gil(|py| { let list_py: Py = PyList::empty(py).into(); @@ -1046,20 +1414,28 @@ let list_ref: &PyList = list_py.as_ref(py); ``` After: -```rust +```rust,ignore use pyo3::{Py, types::PyList}; # pyo3::Python::with_gil(|py| { let list_py: Py = PyList::empty(py).into(); let list_ref: &PyList = list_py.as_ref(py); # }) ``` +
## from 0.10.* to 0.11 ### Stable Rust +
+Click to expand + PyO3 now supports the stable Rust toolchain. The minimum required version is 1.39.0. +
### `#[pyclass]` structs must now be `Send` or `unsendable` +
+Click to expand + Because `#[pyclass]` structs can be sent between threads by the Python interpreter, they must implement `Send` or declared as `unsendable` (by `#[pyclass(unsendable)]`). Note that `unsendable` is added in PyO3 `0.11.1` and `Send` is always required in PyO3 `0.11.0`. @@ -1126,36 +1502,44 @@ There can be two fixes: pointers: Vec<*mut std::os::raw::c_char>, } ``` +
### All `PyObject` and `Py` methods now take `Python` as an argument +
+Click to expand + Previously, a few methods such as `Object::get_refcnt` did not take `Python` as an argument (to ensure that the Python GIL was held by the current thread). Technically, this was not sound. To migrate, just pass a `py` argument to any calls to these methods. Before: -```rust,ignore +```rust,compile_fail # pyo3::Python::with_gil(|py| { py.None().get_refcnt(); # }) ``` After: -```rust,compile_fail +```rust # pyo3::Python::with_gil(|py| { py.None().get_refcnt(py); # }) ``` +
## from 0.9.* to 0.10 ### `ObjectProtocol` is removed +
+Click to expand + All methods are moved to [`PyAny`]. And since now all native types (e.g., `PyList`) implements `Deref`, all you need to do is remove `ObjectProtocol` from your code. Or if you use `ObjectProtocol` by `use pyo3::prelude::*`, you have to do nothing. Before: -```rust,compile_fail +```rust,compile_fail,ignore use pyo3::ObjectProtocol; # pyo3::Python::with_gil(|py| { @@ -1166,21 +1550,29 @@ assert_eq!(hi.len().unwrap(), 5); ``` After: -```rust +```rust,ignore # pyo3::Python::with_gil(|py| { let obj = py.eval("lambda: 'Hi :)'", None, None).unwrap(); let hi: &pyo3::types::PyString = obj.call0().unwrap().downcast().unwrap(); assert_eq!(hi.len().unwrap(), 5); # }) ``` +
### No `#![feature(specialization)]` in user code +
+Click to expand + While PyO3 itself still requires specialization and nightly Rust, now you don't have to use `#![feature(specialization)]` in your crate. +
## from 0.8.* to 0.9 ### `#[new]` interface +
+Click to expand + [`PyRawObject`](https://docs.rs/pyo3/0.8.5/pyo3/type_object/struct.PyRawObject.html) is now removed and our syntax for constructors has changed. @@ -1214,10 +1606,14 @@ impl MyClass { ``` Basically you can return `Self` or `Result` directly. -For more, see [the constructor section](class.html#constructor) of this guide. +For more, see [the constructor section](class.md#constructor) of this guide. +
### PyCell -PyO3 0.9 introduces [`PyCell`], which is a [`RefCell`]-like object wrapper +
+Click to expand + +PyO3 0.9 introduces `PyCell`, which is a [`RefCell`]-like object wrapper for ensuring Rust's rules regarding aliasing of references are upheld. For more detail, see the [Rust Book's section on Rust's rules of references](https://doc.rust-lang.org/book/ch04-02-references-and-borrowing.html#the-rules-of-references) @@ -1246,7 +1642,7 @@ impl Names { } } # Python::with_gil(|py| { -# let names = PyCell::new(py, Names::new()).unwrap(); +# let names = Py::new(py, Names::new()).unwrap(); # pyo3::py_run!(py, names, r" # try: # names.merge(names) @@ -1266,7 +1662,7 @@ However, for `#[pyproto]` and some functions, you need to manually fix the code. In 0.8 object creation was done with `PyRef::new` and `PyRefMut::new`. In 0.9 these have both been removed. To upgrade code, please use -[`PyCell::new`]({{#PYO3_DOCS_URL}}/pyo3/pycell/struct.PyCell.html#method.new) instead. +`PyCell::new` instead. If you need [`PyRef`] or [`PyRefMut`], just call `.borrow()` or `.borrow_mut()` on the newly-created `PyCell`. @@ -1281,7 +1677,7 @@ let obj_ref = PyRef::new(py, MyClass {}).unwrap(); ``` After: -```rust +```rust,ignore # use pyo3::prelude::*; # #[pyclass] # struct MyClass {} @@ -1305,7 +1701,7 @@ let obj_ref_mut: &mut MyClass = obj.extract().unwrap(); ``` After: -```rust +```rust,ignore # use pyo3::prelude::*; # use pyo3::types::IntoPyDict; # #[pyclass] #[derive(Clone)] struct MyClass {} @@ -1367,10 +1763,35 @@ impl PySequenceProtocol for ByteSequence { } } ``` +
+ + [`FromPyObject`]: {{#PYO3_DOCS_URL}}/pyo3/conversion/trait.FromPyObject.html [`PyAny`]: {{#PYO3_DOCS_URL}}/pyo3/types/struct.PyAny.html -[`PyCell`]: {{#PYO3_DOCS_URL}}/pyo3/pycell/struct.PyCell.html [`PyBorrowMutError`]: {{#PYO3_DOCS_URL}}/pyo3/pycell/struct.PyBorrowMutError.html [`PyRef`]: {{#PYO3_DOCS_URL}}/pyo3/pycell/struct.PyRef.html [`PyRefMut`]: {{#PYO3_DOCS_URL}}/pyo3/pycell/struct.PyRef.html diff --git a/guide/src/module.md b/guide/src/module.md index 9e52ab93e2b..c9c7f78aaf5 100644 --- a/guide/src/module.md +++ b/guide/src/module.md @@ -12,7 +12,7 @@ fn double(x: usize) -> usize { /// This module is implemented in Rust. #[pymodule] -fn my_extension(py: Python<'_>, m: &PyModule) -> PyResult<()> { +fn my_extension(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(double, m)?)?; Ok(()) } @@ -34,7 +34,7 @@ fn double(x: usize) -> usize { #[pymodule] #[pyo3(name = "custom_name")] -fn my_extension(py: Python<'_>, m: &PyModule) -> PyResult<()> { +fn my_extension(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(double, m)?)?; Ok(()) } @@ -45,7 +45,7 @@ file. Otherwise, you will get an import error in Python with the following messa `ImportError: dynamic module does not define module export function (PyInit_name_of_your_module)` To import the module, either: - - copy the shared library as described in [Manual builds](building_and_distribution.html#manual-builds), or + - copy the shared library as described in [Manual builds](building-and-distribution.md#manual-builds), or - use a tool, e.g. `maturin develop` with [maturin](https://github.com/PyO3/maturin) or `python setup.py develop` with [setuptools-rust](https://github.com/PyO3/setuptools-rust). @@ -65,22 +65,22 @@ print(my_extension.__doc__) ## Python submodules You can create a module hierarchy within a single extension module by using -[`PyModule.add_submodule()`]({{#PYO3_DOCS_URL}}/pyo3/prelude/struct.PyModule.html#method.add_submodule). +[`Bound<'_, PyModule>::add_submodule()`]({{#PYO3_DOCS_URL}}/pyo3/prelude/trait.PyModuleMethods.html#tymethod.add_submodule). For example, you could define the modules `parent_module` and `parent_module.child_module`. ```rust use pyo3::prelude::*; #[pymodule] -fn parent_module(py: Python<'_>, m: &PyModule) -> PyResult<()> { - register_child_module(py, m)?; +fn parent_module(m: &Bound<'_, PyModule>) -> PyResult<()> { + register_child_module(m)?; Ok(()) } -fn register_child_module(py: Python<'_>, parent_module: &PyModule) -> PyResult<()> { - let child_module = PyModule::new(py, "child_module")?; - child_module.add_function(wrap_pyfunction!(func, child_module)?)?; - parent_module.add_submodule(child_module)?; +fn register_child_module(parent_module: &Bound<'_, PyModule>) -> PyResult<()> { + let child_module = PyModule::new_bound(parent_module.py(), "child_module")?; + child_module.add_function(wrap_pyfunction!(func, &child_module)?)?; + parent_module.add_submodule(&child_module)?; Ok(()) } @@ -93,9 +93,9 @@ fn func() -> String { # use pyo3::wrap_pymodule; # use pyo3::types::IntoPyDict; # let parent_module = wrap_pymodule!(parent_module)(py); -# let ctx = [("parent_module", parent_module)].into_py_dict(py); +# let ctx = [("parent_module", parent_module)].into_py_dict_bound(py); # -# py.run("assert parent_module.child_module.func() == 'func'", None, Some(&ctx)).unwrap(); +# py.run_bound("assert parent_module.child_module.func() == 'func'", None, Some(&ctx)).unwrap(); # }) ``` @@ -105,3 +105,55 @@ submodules by using `from parent_module import child_module`. For more informati [#1517](https://github.com/PyO3/pyo3/issues/1517#issuecomment-808664021). It is not necessary to add `#[pymodule]` on nested modules, which is only required on the top-level module. + +## Declarative modules (experimental) + +Another syntax based on Rust inline modules is also available to declare modules. +The `experimental-declarative-modules` feature must be enabled to use it. + +For example: +```rust +# #[cfg(feature = "experimental-declarative-modules")] +# mod declarative_module_test { +use pyo3::prelude::*; + +#[pyfunction] +fn double(x: usize) -> usize { + x * 2 +} + +#[pymodule] +mod my_extension { + use super::*; + + #[pymodule_export] + use super::double; // Exports the double function as part of the module + + #[pyfunction] // This will be part of the module + fn triple(x: usize) -> usize { + x * 3 + } + + #[pyclass] // This will be part of the module + struct Unit; + + #[pymodule] + mod submodule { + // This is a submodule + } + + #[pymodule_init] + fn init(m: &Bound<'_, PyModule>) -> PyResult<()> { + // Arbitrary code to run at the module initialization + m.add("double2", m.getattr("double")?)?; + Ok(()) + } +} +# } +``` + +Some changes are planned to this feature before stabilization, like automatically +filling submodules into `sys.modules` to allow easier imports (see [issue #759](https://github.com/PyO3/pyo3/issues/759)) +and filling the `module` argument of inlined `#[pyclass]` automatically with the proper module name. +Macro names might also change. +See [issue #3900](https://github.com/PyO3/pyo3/issues/3900) to track this feature progress. diff --git a/guide/src/parallelism.md b/guide/src/parallelism.md index ac255c7ae6c..4d88facc469 100644 --- a/guide/src/parallelism.md +++ b/guide/src/parallelism.md @@ -1,6 +1,6 @@ # Parallelism -CPython has the infamous [Global Interpreter Lock](https://docs.python.org/3/glossary.html#term-global-interpreter-lock), which prevents several threads from executing Python bytecode in parallel. This makes threading in Python a bad fit for [CPU-bound](https://stackoverflow.com/questions/868568/) tasks and often forces developers to accept the overhead of multiprocessing. +CPython has the infamous [Global Interpreter Lock](https://docs.python.org/3/glossary.html#term-global-interpreter-lock), which prevents several threads from executing Python bytecode in parallel. This makes threading in Python a bad fit for [CPU-bound](https://en.wikipedia.org/wiki/CPU-bound) tasks and often forces developers to accept the overhead of multiprocessing. In PyO3 parallelism can be easily achieved in Rust-only code. Let's take a look at our [word-count](https://github.com/PyO3/pyo3/blob/main/examples/word-count/src/lib.rs) example, where we have a `search` function that utilizes the [rayon](https://github.com/rayon-rs/rayon) crate to count words in parallel. ```rust,no_run @@ -117,4 +117,4 @@ test_word_count_python_sequential 27.3985 (15.82) 45.452 You can see that the Python threaded version is not much slower than the Rust sequential version, which means compared to an execution on a single CPU core the speed has doubled. -[`Python::allow_threads`]: {{#PYO3_DOCS_URL}}/pyo3/struct.Python.html#method.allow_threads +[`Python::allow_threads`]: {{#PYO3_DOCS_URL}}/pyo3/marker/struct.Python.html#method.allow_threads diff --git a/guide/src/performance.md b/guide/src/performance.md index 23fb59c4e90..b3d160fe6b1 100644 --- a/guide/src/performance.md +++ b/guide/src/performance.md @@ -4,26 +4,26 @@ To achieve the best possible performance, it is useful to be aware of several tr ## `extract` versus `downcast` -Pythonic API implemented using PyO3 are often polymorphic, i.e. they will accept `&PyAny` and try to turn this into multiple more concrete types to which the requested operation is applied. This often leads to chains of calls to `extract`, e.g. +Pythonic API implemented using PyO3 are often polymorphic, i.e. they will accept `&Bound<'_, PyAny>` and try to turn this into multiple more concrete types to which the requested operation is applied. This often leads to chains of calls to `extract`, e.g. ```rust # #![allow(dead_code)] # use pyo3::prelude::*; # use pyo3::{exceptions::PyTypeError, types::PyList}; -fn frobnicate_list(list: &PyList) -> PyResult<&PyAny> { +fn frobnicate_list<'py>(list: &Bound<'_, PyList>) -> PyResult> { todo!() } -fn frobnicate_vec(vec: Vec<&PyAny>) -> PyResult<&PyAny> { +fn frobnicate_vec<'py>(vec: Vec>) -> PyResult> { todo!() } #[pyfunction] -fn frobnicate(value: &PyAny) -> PyResult<&PyAny> { - if let Ok(list) = value.extract::<&PyList>() { - frobnicate_list(list) - } else if let Ok(vec) = value.extract::>() { +fn frobnicate<'py>(value: &Bound<'py, PyAny>) -> PyResult> { + if let Ok(list) = value.extract::>() { + frobnicate_list(&list) + } else if let Ok(vec) = value.extract::>>() { frobnicate_vec(vec) } else { Err(PyTypeError::new_err("Cannot frobnicate that type.")) @@ -37,15 +37,15 @@ This suboptimal as the `FromPyObject` trait requires `extract` to have a `Res # #![allow(dead_code)] # use pyo3::prelude::*; # use pyo3::{exceptions::PyTypeError, types::PyList}; -# fn frobnicate_list(list: &PyList) -> PyResult<&PyAny> { todo!() } -# fn frobnicate_vec(vec: Vec<&PyAny>) -> PyResult<&PyAny> { todo!() } +# fn frobnicate_list<'py>(list: &Bound<'_, PyList>) -> PyResult> { todo!() } +# fn frobnicate_vec<'py>(vec: Vec>) -> PyResult> { todo!() } # #[pyfunction] -fn frobnicate(value: &PyAny) -> PyResult<&PyAny> { +fn frobnicate<'py>(value: &Bound<'py, PyAny>) -> PyResult> { // Use `downcast` instead of `extract` as turning `PyDowncastError` into `PyErr` is quite costly. if let Ok(list) = value.downcast::() { frobnicate_list(list) - } else if let Ok(vec) = value.extract::>() { + } else if let Ok(vec) = value.extract::>>() { frobnicate_vec(vec) } else { Err(PyTypeError::new_err("Cannot frobnicate that type.")) @@ -53,9 +53,9 @@ fn frobnicate(value: &PyAny) -> PyResult<&PyAny> { } ``` -## Access to GIL-bound reference implies access to GIL token +## Access to Bound implies access to GIL token -Calling `Python::with_gil` is effectively a no-op when the GIL is already held, but checking that this is the case still has a cost. If an existing GIL token can not be accessed, for example when implementing a pre-existing trait, but a GIL-bound reference is available, this cost can be avoided by exploiting that access to GIL-bound reference gives zero-cost access to a GIL token via `PyAny::py`. +Calling `Python::with_gil` is effectively a no-op when the GIL is already held, but checking that this is the case still has a cost. If an existing GIL token can not be accessed, for example when implementing a pre-existing trait, but a GIL-bound reference is available, this cost can be avoided by exploiting that access to GIL-bound reference gives zero-cost access to a GIL token via `Bound::py`. For example, instead of writing @@ -66,29 +66,77 @@ For example, instead of writing struct Foo(Py); -struct FooRef<'a>(&'a PyList); +struct FooBound<'py>(Bound<'py, PyList>); -impl PartialEq for FooRef<'_> { +impl PartialEq for FooBound<'_> { fn eq(&self, other: &Foo) -> bool { - Python::with_gil(|py| self.0.len() == other.0.as_ref(py).len()) + Python::with_gil(|py| { + let len = other.0.bind(py).len(); + self.0.len() == len + }) } } ``` -use more efficient +use the more efficient ```rust # #![allow(dead_code)] # use pyo3::prelude::*; # use pyo3::types::PyList; # struct Foo(Py); -# struct FooRef<'a>(&'a PyList); +# struct FooBound<'py>(Bound<'py, PyList>); # -impl PartialEq for FooRef<'_> { +impl PartialEq for FooBound<'_> { fn eq(&self, other: &Foo) -> bool { - // Access to `&'a PyAny` implies access to `Python<'a>`. + // Access to `&Bound<'py, PyAny>` implies access to `Python<'py>`. let py = self.0.py(); - self.0.len() == other.0.as_ref(py).len() + let len = other.0.bind(py).len(); + self.0.len() == len } } ``` + +## Disable the global reference pool + +PyO3 uses global mutable state to keep track of deferred reference count updates implied by `impl Drop for Py` being called without the GIL being held. The necessary synchronization to obtain and apply these reference count updates when PyO3-based code next acquires the GIL is somewhat expensive and can become a significant part of the cost of crossing the Python-Rust boundary. + +This functionality can be avoided by setting the `pyo3_disable_reference_pool` conditional compilation flag. This removes the global reference pool and the associated costs completely. However, it does _not_ remove the `Drop` implementation for `Py` which is necessary to interoperate with existing Rust code written without PyO3-based code in mind. To stay compatible with the wider Rust ecosystem in these cases, we keep the implementation but abort when `Drop` is called without the GIL being held. If `pyo3_leak_on_drop_without_reference_pool` is additionally enabled, objects dropped without the GIL being held will be leaked instead which is always sound but might have determinal effects like resource exhaustion in the long term. + +This limitation is important to keep in mind when this setting is used, especially when embedding Python code into a Rust application as it is quite easy to accidentally drop a `Py` (or types containing it like `PyErr`, `PyBackedStr` or `PyBackedBytes`) returned from `Python::with_gil` without making sure to re-acquire the GIL beforehand. For example, the following code + +```rust,ignore +# use pyo3::prelude::*; +# use pyo3::types::PyList; +let numbers: Py = Python::with_gil(|py| PyList::empty_bound(py).unbind()); + +Python::with_gil(|py| { + numbers.bind(py).append(23).unwrap(); +}); + +Python::with_gil(|py| { + numbers.bind(py).append(42).unwrap(); +}); +``` + +will abort if the list not explicitly disposed via + +```rust +# use pyo3::prelude::*; +# use pyo3::types::PyList; +let numbers: Py = Python::with_gil(|py| PyList::empty_bound(py).unbind()); + +Python::with_gil(|py| { + numbers.bind(py).append(23).unwrap(); +}); + +Python::with_gil(|py| { + numbers.bind(py).append(42).unwrap(); +}); + +Python::with_gil(move |py| { + drop(numbers); +}); +``` + +[conditional-compilation]: https://doc.rust-lang.org/reference/conditional-compilation.html diff --git a/guide/src/python-from-rust.md b/guide/src/python-from-rust.md new file mode 100644 index 00000000000..ee618f3fa47 --- /dev/null +++ b/guide/src/python-from-rust.md @@ -0,0 +1,46 @@ +# Calling Python in Rust code + +This chapter of the guide documents some ways to interact with Python code from Rust. + +Below is an introduction to the `'py` lifetime and some general remarks about how PyO3's API reasons about Python code. + +The subchapters also cover the following topics: + - Python object types available in PyO3's API + - How to work with Python exceptions + - How to call Python functions + - How to execute existing Python code + +## The `'py` lifetime + +To safely interact with the Python interpreter a Rust thread must have a corresponding Python thread state and hold the [Global Interpreter Lock (GIL)](#the-global-interpreter-lock). PyO3 has a `Python<'py>` token that is used to prove that these conditions +are met. Its lifetime `'py` is a central part of PyO3's API. + +The `Python<'py>` token serves three purposes: + +* It provides global APIs for the Python interpreter, such as [`py.eval_bound()`][eval] and [`py.import_bound()`][import]. +* It can be passed to functions that require a proof of holding the GIL, such as [`Py::clone_ref`][clone_ref]. +* Its lifetime `'py` is used to bind many of PyO3's types to the Python interpreter, such as [`Bound<'py, T>`][Bound]. + +PyO3's types that are bound to the `'py` lifetime, for example `Bound<'py, T>`, all contain a `Python<'py>` token. This means they have full access to the Python interpreter and offer a complete API for interacting with Python objects. + +Consult [PyO3's API documentation][obtaining-py] to learn how to acquire one of these tokens. + +### The Global Interpreter Lock + +Concurrent programming in Python is aided by the Global Interpreter Lock (GIL), which ensures that only one Python thread can use the Python interpreter and its API at the same time. This allows it to be used to synchronize code. See the [`pyo3::sync`] module for synchronization tools PyO3 offers that are based on the GIL's guarantees. + +Non-Python operations (system calls and native Rust code) can unlock the GIL. See [the section on parallelism](parallelism.md) for how to do that using PyO3's API. + +## Python's memory model + +Python's memory model differs from Rust's memory model in two key ways: +- There is no concept of ownership; all Python objects are shared and usually implemented via reference counting +- There is no concept of exclusive (`&mut`) references; any reference can mutate a Python object + +PyO3's API reflects this by providing [smart pointer][smart-pointers] types, `Py`, `Bound<'py, T>`, and (the very rarely used) `Borrowed<'a, 'py, T>`. These smart pointers all use Python reference counting. See the [subchapter on types](./types.md) for more detail on these types. + +Because of the lack of exclusive `&mut` references, PyO3's APIs for Python objects, for example [`PyListMethods::append`], use shared references. This is safe because Python objects have internal mechanisms to prevent data races (as of time of writing, the Python GIL). + +[smart-pointers]: https://doc.rust-lang.org/book/ch15-00-smart-pointers.html +[obtaining-py]: {{#PYO3_DOCS_URL}}/pyo3/marker/struct.Python.html#obtaining-a-python-token +[`pyo3::sync`]: {{#PYO3_DOCS_URL}}/pyo3/sync/index.html diff --git a/guide/src/python_from_rust.md b/guide/src/python-from-rust/calling-existing-code.md similarity index 56% rename from guide/src/python_from_rust.md rename to guide/src/python-from-rust/calling-existing-code.md index ed51b772d2d..9c0f592451f 100644 --- a/guide/src/python_from_rust.md +++ b/guide/src/python-from-rust/calling-existing-code.md @@ -1,123 +1,10 @@ -# Calling Python in Rust code - -This chapter of the guide documents some ways to interact with Python code from Rust: - - How to call Python functions - - How to execute existing Python code - -## Calling Python functions - -Any Python-native object reference (such as `&PyAny`, `&PyList`, or `&PyCell`) can be used to call Python functions. - -PyO3 offers two APIs to make function calls: - -* [`call`]({{#PYO3_DOCS_URL}}/pyo3/types/struct.PyAny.html#method.call) - call any callable Python object. -* [`call_method`]({{#PYO3_DOCS_URL}}/pyo3/types/struct.PyAny.html#method.call_method) - call a method on the Python object. - -Both of these APIs take `args` and `kwargs` arguments (for positional and keyword arguments respectively). There are variants for less complex calls: - -* [`call1`]({{#PYO3_DOCS_URL}}/pyo3/types/struct.PyAny.html#method.call1) and [`call_method1`]({{#PYO3_DOCS_URL}}/pyo3/types/struct.PyAny.html#method.call_method1) to call only with positional `args`. -* [`call0`]({{#PYO3_DOCS_URL}}/pyo3/types/struct.PyAny.html#method.call0) and [`call_method0`]({{#PYO3_DOCS_URL}}/pyo3/types/struct.PyAny.html#method.call_method0) to call with no arguments. - -For convenience the [`Py`](types.html#pyt-and-pyobject) smart pointer also exposes these same six API methods, but needs a `Python` token as an additional first argument to prove the GIL is held. - -The example below calls a Python function behind a `PyObject` (aka `Py`) reference: - -```rust -use pyo3::prelude::*; -use pyo3::types::PyTuple; - -fn main() -> PyResult<()> { - let arg1 = "arg1"; - let arg2 = "arg2"; - let arg3 = "arg3"; - - Python::with_gil(|py| { - let fun: Py = PyModule::from_code( - py, - "def example(*args, **kwargs): - if args != (): - print('called with args', args) - if kwargs != {}: - print('called with kwargs', kwargs) - if args == () and kwargs == {}: - print('called with no arguments')", - "", - "", - )? - .getattr("example")? - .into(); - - // call object without any arguments - fun.call0(py)?; - - // call object with PyTuple - let args = PyTuple::new(py, &[arg1, arg2, arg3]); - fun.call1(py, args)?; - - // pass arguments as rust tuple - let args = (arg1, arg2, arg3); - fun.call1(py, args)?; - Ok(()) - }) -} -``` - -### Creating keyword arguments - -For the `call` and `call_method` APIs, `kwargs` can be `None` or `Some(&PyDict)`. You can use the [`IntoPyDict`]({{#PYO3_DOCS_URL}}/pyo3/types/trait.IntoPyDict.html) trait to convert other dict-like containers, e.g. `HashMap` or `BTreeMap`, as well as tuples with up to 10 elements and `Vec`s where each element is a two-element tuple. - -```rust -use pyo3::prelude::*; -use pyo3::types::IntoPyDict; -use std::collections::HashMap; - -fn main() -> PyResult<()> { - let key1 = "key1"; - let val1 = 1; - let key2 = "key2"; - let val2 = 2; - - Python::with_gil(|py| { - let fun: Py = PyModule::from_code( - py, - "def example(*args, **kwargs): - if args != (): - print('called with args', args) - if kwargs != {}: - print('called with kwargs', kwargs) - if args == () and kwargs == {}: - print('called with no arguments')", - "", - "", - )? - .getattr("example")? - .into(); - - // call object with PyDict - let kwargs = [(key1, val1)].into_py_dict(py); - fun.call(py, (), Some(kwargs))?; - - // pass arguments as Vec - let kwargs = vec![(key1, val1), (key2, val2)]; - fun.call(py, (), Some(kwargs.into_py_dict(py)))?; - - // pass arguments as HashMap - let mut kwargs = HashMap::<&str, i32>::new(); - kwargs.insert(key1, 1); - fun.call(py, (), Some(kwargs.into_py_dict(py)))?; - - Ok(()) - }) -} -``` - -## Executing existing Python code +# Executing existing Python code If you already have some existing Python code that you need to execute from Rust, the following FAQs can help you select the right PyO3 functionality for your situation: -### Want to access Python APIs? Then use `PyModule::import`. +## Want to access Python APIs? Then use `PyModule::import_bound`. -[`Pymodule::import`]({{#PYO3_DOCS_URL}}/pyo3/types/struct.PyModule.html#method.import) can +[`PyModule::import_bound`]({{#PYO3_DOCS_URL}}/pyo3/types/struct.PyModule.html#method.import_bound) can be used to get handle to a Python module from Rust. You can use this to import and use any Python module available in your environment. @@ -126,7 +13,7 @@ use pyo3::prelude::*; fn main() -> PyResult<()> { Python::with_gil(|py| { - let builtins = PyModule::import(py, "builtins")?; + let builtins = PyModule::import_bound(py, "builtins")?; let total: i32 = builtins .getattr("sum")? .call1((vec![1, 2, 3],))? @@ -137,11 +24,11 @@ fn main() -> PyResult<()> { } ``` -### Want to run just an expression? Then use `eval`. +## Want to run just an expression? Then use `eval_bound`. -[`Python::eval`]({{#PYO3_DOCS_URL}}/pyo3/marker/struct.Python.html#method.eval) is +[`Python::eval_bound`]({{#PYO3_DOCS_URL}}/pyo3/marker/struct.Python.html#method.eval_bound) is a method to execute a [Python expression](https://docs.python.org/3.7/reference/expressions.html) -and return the evaluated value as a `&PyAny` object. +and return the evaluated value as a `Bound<'py, PyAny>` object. ```rust use pyo3::prelude::*; @@ -149,7 +36,7 @@ use pyo3::prelude::*; # fn main() -> Result<(), ()> { Python::with_gil(|py| { let result = py - .eval("[i * 10 for i in range(5)]", None, None) + .eval_bound("[i * 10 for i in range(5)]", None, None) .map_err(|e| { e.print_and_set_sys_last_vars(py); })?; @@ -160,20 +47,20 @@ Python::with_gil(|py| { # } ``` -### Want to run statements? Then use `run`. +## Want to run statements? Then use `run_bound`. -[`Python::run`] is a method to execute one or more +[`Python::run_bound`] is a method to execute one or more [Python statements](https://docs.python.org/3.7/reference/simple_stmts.html). This method returns nothing (like any Python statement), but you can get access to manipulated objects via the `locals` dict. -You can also use the [`py_run!`] macro, which is a shorthand for [`Python::run`]. +You can also use the [`py_run!`] macro, which is a shorthand for [`Python::run_bound`]. Since [`py_run!`] panics on exceptions, we recommend you use this macro only for quickly testing your Python extensions. ```rust use pyo3::prelude::*; -use pyo3::{PyCell, py_run}; +use pyo3::py_run; # fn main() { #[pyclass] @@ -198,7 +85,7 @@ Python::with_gil(|py| { id: 34, name: "Yu".to_string(), }; - let userdata = PyCell::new(py, userdata).unwrap(); + let userdata = Py::new(py, userdata).unwrap(); let userdata_as_tuple = (34, "Yu"); py_run!(py, userdata userdata_as_tuple, r#" assert repr(userdata) == "User Yu(id: 34)" @@ -208,9 +95,9 @@ assert userdata.as_tuple() == userdata_as_tuple # } ``` -## You have a Python file or code snippet? Then use `PyModule::from_code`. +## You have a Python file or code snippet? Then use `PyModule::from_code_bound`. -[`PyModule::from_code`]({{#PYO3_DOCS_URL}}/pyo3/types/struct.PyModule.html#method.from_code) +[`PyModule::from_code_bound`]({{#PYO3_DOCS_URL}}/pyo3/types/struct.PyModule.html#method.from_code_bound) can be used to generate a Python module which can then be used just as if it was imported with `PyModule::import`. @@ -218,14 +105,11 @@ can be used to generate a Python module which can then be used just as if it was to this function! ```rust -use pyo3::{ - prelude::*, - types::{IntoPyDict, PyModule}, -}; +use pyo3::{prelude::*, types::IntoPyDict}; # fn main() -> PyResult<()> { Python::with_gil(|py| { - let activators = PyModule::from_code( + let activators = PyModule::from_code_bound( py, r#" def relu(x): @@ -242,10 +126,10 @@ def leaky_relu(x, slope=0.01): let relu_result: f64 = activators.getattr("relu")?.call1((-1.0,))?.extract()?; assert_eq!(relu_result, 0.0); - let kwargs = [("slope", 0.2)].into_py_dict(py); + let kwargs = [("slope", 0.2)].into_py_dict_bound(py); let lrelu_result: f64 = activators .getattr("leaky_relu")? - .call((-1.0,), Some(kwargs))? + .call((-1.0,), Some(&kwargs))? .extract()?; assert_eq!(lrelu_result, -0.2); # Ok(()) @@ -253,7 +137,7 @@ def leaky_relu(x, slope=0.01): # } ``` -### Want to embed Python in Rust with additional modules? +## Want to embed Python in Rust with additional modules? Python maintains the `sys.modules` dict as a cache of all imported modules. An import in Python will first attempt to lookup the module from this dict, @@ -275,19 +159,19 @@ fn add_one(x: i64) -> i64 { } #[pymodule] -fn foo(_py: Python<'_>, foo_module: &PyModule) -> PyResult<()> { +fn foo(foo_module: &Bound<'_, PyModule>) -> PyResult<()> { foo_module.add_function(wrap_pyfunction!(add_one, foo_module)?)?; Ok(()) } fn main() -> PyResult<()> { pyo3::append_to_inittab!(foo); - Python::with_gil(|py| Python::run(py, "import foo; foo.add_one(6)", None, None)) + Python::with_gil(|py| Python::run_bound(py, "import foo; foo.add_one(6)", None, None)) } ``` If `append_to_inittab` cannot be used due to constraints in the program, -an alternative is to create a module using [`PyModule::new`] +an alternative is to create a module using [`PyModule::new_bound`] and insert it manually into `sys.modules`: ```rust @@ -302,23 +186,23 @@ pub fn add_one(x: i64) -> i64 { fn main() -> PyResult<()> { Python::with_gil(|py| { // Create new module - let foo_module = PyModule::new(py, "foo")?; - foo_module.add_function(wrap_pyfunction!(add_one, foo_module)?)?; + let foo_module = PyModule::new_bound(py, "foo")?; + foo_module.add_function(wrap_pyfunction!(add_one, &foo_module)?)?; // Import and get sys.modules - let sys = PyModule::import(py, "sys")?; - let py_modules: &PyDict = sys.getattr("modules")?.downcast()?; + let sys = PyModule::import_bound(py, "sys")?; + let py_modules: Bound<'_, PyDict> = sys.getattr("modules")?.downcast_into()?; // Insert foo into sys.modules py_modules.set_item("foo", foo_module)?; // Now we can import + run our python code - Python::run(py, "import foo; foo.add_one(6)", None, None) + Python::run_bound(py, "import foo; foo.add_one(6)", None, None) }) } ``` -### Include multiple Python files +## Include multiple Python files You can include a file at compile time by using [`std::include_str`](https://doc.rust-lang.org/std/macro.include_str.html) macro. @@ -373,8 +257,8 @@ fn main() -> PyResult<()> { )); let py_app = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/python_app/app.py")); let from_python = Python::with_gil(|py| -> PyResult> { - PyModule::from_code(py, py_foo, "utils.foo", "utils.foo")?; - let app: Py = PyModule::from_code(py, py_app, "", "")? + PyModule::from_code_bound(py, py_foo, "utils.foo", "utils.foo")?; + let app: Py = PyModule::from_code_bound(py, py_app, "", "")? .getattr("run")? .into(); app.call0(py) @@ -406,9 +290,12 @@ fn main() -> PyResult<()> { let path = Path::new("/usr/share/python_app"); let py_app = fs::read_to_string(path.join("app.py"))?; let from_python = Python::with_gil(|py| -> PyResult> { - let syspath: &PyList = py.import("sys")?.getattr("path")?.downcast()?; + let syspath = py + .import_bound("sys")? + .getattr("path")? + .downcast_into::()?; syspath.insert(0, &path)?; - let app: Py = PyModule::from_code(py, &py_app, "", "")? + let app: Py = PyModule::from_code_bound(py, &py_app, "", "")? .getattr("run")? .into(); app.call0(py) @@ -420,7 +307,7 @@ fn main() -> PyResult<()> { ``` -[`Python::run`]: {{#PYO3_DOCS_URL}}/pyo3/struct.Python.html#method.run +[`Python::run`]: {{#PYO3_DOCS_URL}}/pyo3/marker/struct.Python.html#method.run [`py_run!`]: {{#PYO3_DOCS_URL}}/pyo3/macro.py_run.html ## Need to use a context manager from Rust? @@ -429,11 +316,10 @@ Use context managers by directly invoking `__enter__` and `__exit__`. ```rust use pyo3::prelude::*; -use pyo3::types::PyModule; fn main() { Python::with_gil(|py| { - let custom_manager = PyModule::from_code( + let custom_manager = PyModule::from_code_bound( py, r#" class House(object): @@ -458,7 +344,7 @@ class House(object): house.call_method0("__enter__").unwrap(); - let result = py.eval("undefined_variable + 1", None, None); + let result = py.eval_bound("undefined_variable + 1", None, None); // If the eval threw an exception we'll pass it through to the context manager. // Otherwise, __exit__ is called with empty arguments (Python "None"). @@ -471,7 +357,14 @@ class House(object): } Err(e) => { house - .call_method1("__exit__", (e.get_type(py), e.value(py), e.traceback(py))) + .call_method1( + "__exit__", + ( + e.get_type_bound(py), + e.value_bound(py), + e.traceback_bound(py), + ), + ) .unwrap(); } } @@ -479,5 +372,26 @@ class House(object): } ``` +## Handling system signals/interrupts (Ctrl-C) + +The best way to handle system signals when running Rust code is to periodically call `Python::check_signals` to handle any signals captured by Python's signal handler. See also [the FAQ entry](../faq.md#ctrl-c-doesnt-do-anything-while-my-rust-code-is-executing). + +Alternatively, set Python's `signal` module to take the default action for a signal: + +```rust +use pyo3::prelude::*; + +# fn main() -> PyResult<()> { +Python::with_gil(|py| -> PyResult<()> { + let signal = py.import_bound("signal")?; + // Set SIGINT to have the default action + signal + .getattr("signal")? + .call1((signal.getattr("SIGINT")?, signal.getattr("SIG_DFL")?))?; + Ok(()) +}) +# } +``` + -[`PyModule::new`]: {{#PYO3_DOCS_URL}}/pyo3/types/struct.PyModule.html#method.new +[`PyModule::new_bound`]: {{#PYO3_DOCS_URL}}/pyo3/types/struct.PyModule.html#method.new_bound diff --git a/guide/src/python-from-rust/function-calls.md b/guide/src/python-from-rust/function-calls.md new file mode 100644 index 00000000000..c19d6fafabc --- /dev/null +++ b/guide/src/python-from-rust/function-calls.md @@ -0,0 +1,114 @@ +# Calling Python functions + +The `Bound<'py, T>` smart pointer (such as `Bound<'py, PyAny>`, `Bound<'py, PyList>`, or `Bound<'py, MyClass>`) can be used to call Python functions. + +PyO3 offers two APIs to make function calls: + +* [`call`]({{#PYO3_DOCS_URL}}/pyo3/types/trait.PyAnyMethods.html#tymethod.call) - call any callable Python object. +* [`call_method`]({{#PYO3_DOCS_URL}}/pyo3/types/trait.PyAnyMethods.html#tymethod.call_method) - call a method on the Python object. + +Both of these APIs take `args` and `kwargs` arguments (for positional and keyword arguments respectively). There are variants for less complex calls: + +* [`call1`]({{#PYO3_DOCS_URL}}/pyo3/types/trait.PyAnyMethods.html#tymethod.call1) and [`call_method1`]({{#PYO3_DOCS_URL}}/pyo3/types/trait.PyAnyMethods.html#tymethod.call_method1) to call only with positional `args`. +* [`call0`]({{#PYO3_DOCS_URL}}/pyo3/types/trait.PyAnyMethods.html#tymethod.call0) and [`call_method0`]({{#PYO3_DOCS_URL}}/pyo3/types/trait.PyAnyMethods.html#tymethod.call_method0) to call with no arguments. + +For convenience the [`Py`](../types.md#pyt-and-pyobject) smart pointer also exposes these same six API methods, but needs a `Python` token as an additional first argument to prove the GIL is held. + +The example below calls a Python function behind a `PyObject` (aka `Py`) reference: + +```rust +use pyo3::prelude::*; +use pyo3::types::PyTuple; + +fn main() -> PyResult<()> { + let arg1 = "arg1"; + let arg2 = "arg2"; + let arg3 = "arg3"; + + Python::with_gil(|py| { + let fun: Py = PyModule::from_code_bound( + py, + "def example(*args, **kwargs): + if args != (): + print('called with args', args) + if kwargs != {}: + print('called with kwargs', kwargs) + if args == () and kwargs == {}: + print('called with no arguments')", + "", + "", + )? + .getattr("example")? + .into(); + + // call object without any arguments + fun.call0(py)?; + + // pass object with Rust tuple of positional arguments + let args = (arg1, arg2, arg3); + fun.call1(py, args)?; + + // call object with Python tuple of positional arguments + let args = PyTuple::new_bound(py, &[arg1, arg2, arg3]); + fun.call1(py, args)?; + Ok(()) + }) +} +``` + +## Creating keyword arguments + +For the `call` and `call_method` APIs, `kwargs` are `Option<&Bound<'py, PyDict>>`, so can either be `None` or `Some(&dict)`. You can use the [`IntoPyDict`]({{#PYO3_DOCS_URL}}/pyo3/types/trait.IntoPyDict.html) trait to convert other dict-like containers, e.g. `HashMap` or `BTreeMap`, as well as tuples with up to 10 elements and `Vec`s where each element is a two-element tuple. + +```rust +use pyo3::prelude::*; +use pyo3::types::IntoPyDict; +use std::collections::HashMap; + +fn main() -> PyResult<()> { + let key1 = "key1"; + let val1 = 1; + let key2 = "key2"; + let val2 = 2; + + Python::with_gil(|py| { + let fun: Py = PyModule::from_code_bound( + py, + "def example(*args, **kwargs): + if args != (): + print('called with args', args) + if kwargs != {}: + print('called with kwargs', kwargs) + if args == () and kwargs == {}: + print('called with no arguments')", + "", + "", + )? + .getattr("example")? + .into(); + + // call object with PyDict + let kwargs = [(key1, val1)].into_py_dict_bound(py); + fun.call_bound(py, (), Some(&kwargs))?; + + // pass arguments as Vec + let kwargs = vec![(key1, val1), (key2, val2)]; + fun.call_bound(py, (), Some(&kwargs.into_py_dict_bound(py)))?; + + // pass arguments as HashMap + let mut kwargs = HashMap::<&str, i32>::new(); + kwargs.insert(key1, 1); + fun.call_bound(py, (), Some(&kwargs.into_py_dict_bound(py)))?; + + Ok(()) + }) +} +``` + +
+ +During PyO3's [migration from "GIL Refs" to the `Bound` smart pointer](../migration.md#migrating-from-the-gil-refs-api-to-boundt), `Py::call` is temporarily named [`Py::call_bound`]({{#PYO3_DOCS_URL}}/pyo3/struct.Py.html#method.call_bound) (and `call_method` is temporarily `call_method_bound`). + +(This temporary naming is only the case for the `Py` smart pointer. The methods on the `&PyAny` GIL Ref such as `call` have not been given replacements, and the methods on the `Bound` smart pointer such as [`Bound::call`]({{#PYO3_DOCS_URL}}/pyo3/types/trait.PyAnyMethods.html#tymethod.call) already use follow the newest API conventions.) + +
diff --git a/guide/src/python_typing_hints.md b/guide/src/python-typing-hints.md similarity index 100% rename from guide/src/python_typing_hints.md rename to guide/src/python-typing-hints.md diff --git a/guide/src/rust-from-python.md b/guide/src/rust-from-python.md new file mode 100644 index 00000000000..470d5719098 --- /dev/null +++ b/guide/src/rust-from-python.md @@ -0,0 +1,13 @@ +# Using Rust from Python + +This chapter of the guide is dedicated to explaining how to wrap Rust code into Python objects. + +PyO3 uses Rust's "procedural macros" to provide a powerful yet simple API to denote what Rust code should map into Python objects. + +The three types of Python objects which PyO3 can produce are: + +- Python modules, via the `#[pymodule]` macro +- Python functions, via the `#[pyfunction]` macro +- Python classes, via the `#[pyclass]` macro (plus `#[pymethods]` to define methods for those clases) + +The following subchapters go through each of these in turn. diff --git a/guide/src/trait_bounds.md b/guide/src/trait-bounds.md similarity index 90% rename from guide/src/trait_bounds.md rename to guide/src/trait-bounds.md index 7d6e6ea44a4..eb67bc42413 100644 --- a/guide/src/trait_bounds.md +++ b/guide/src/trait-bounds.md @@ -1,6 +1,6 @@ # Using in Python a Rust function with trait bounds -PyO3 allows for easy conversion from Rust to Python for certain functions and classes (see the [conversion table](conversions/tables.html). +PyO3 allows for easy conversion from Rust to Python for certain functions and classes (see the [conversion table](conversions/tables.md)). However, it is not always straightforward to convert Rust code that requires a given trait implementation as an argument. This tutorial explains how to convert a Rust function that takes a trait as argument for use in Python with classes implementing the same methods as the trait. @@ -64,7 +64,9 @@ class Model: The following wrapper will call the Python model from Rust, using a struct to hold the model as a `PyAny` object: ```rust +# #![allow(dead_code)] use pyo3::prelude::*; +use pyo3::types::PyList; # pub trait Model { # fn set_variables(&mut self, inputs: &Vec); @@ -80,11 +82,9 @@ impl Model for UserModel { fn set_variables(&mut self, var: &Vec) { println!("Rust calling Python to set the variables"); Python::with_gil(|py| { - let values: Vec = var.clone(); - let list: PyObject = values.into_py(py); - let py_model = self.model.as_ref(py); - py_model - .call_method("set_variables", (list,), None) + self.model + .bind(py) + .call_method("set_variables", (PyList::new_bound(py, var),), None) .unwrap(); }) } @@ -93,7 +93,7 @@ impl Model for UserModel { println!("Rust calling Python to get the results"); Python::with_gil(|py| { self.model - .as_ref(py) + .bind(py) .call_method("get_results", (), None) .unwrap() .extract() @@ -105,7 +105,7 @@ impl Model for UserModel { println!("Rust calling Python to perform the computation"); Python::with_gil(|py| { self.model - .as_ref(py) + .bind(py) .call_method("compute", (), None) .unwrap(); }) @@ -131,7 +131,7 @@ struct UserModel { } #[pymodule] -fn trait_exposure(_py: Python<'_>, m: &PyModule) -> PyResult<()> { +fn trait_exposure(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; Ok(()) } @@ -162,7 +162,9 @@ However, we can write a second wrapper around these functions to call them direc This wrapper will also perform the type conversions between Python and Rust. ```rust +# #![allow(dead_code)] # use pyo3::prelude::*; +# use pyo3::types::PyList; # # pub trait Model { # fn set_variables(&mut self, inputs: &Vec); @@ -179,11 +181,8 @@ This wrapper will also perform the type conversions between Python and Rust. # fn set_variables(&mut self, var: &Vec) { # println!("Rust calling Python to set the variables"); # Python::with_gil(|py| { -# let values: Vec = var.clone(); -# let list: PyObject = values.into_py(py); -# let py_model = self.model.as_ref(py); -# py_model -# .call_method("set_variables", (list,), None) +# self.model.bind(py) +# .call_method("set_variables", (PyList::new_bound(py, var),), None) # .unwrap(); # }) # } @@ -192,7 +191,7 @@ This wrapper will also perform the type conversions between Python and Rust. # println!("Rust calling Python to get the results"); # Python::with_gil(|py| { # self.model -# .as_ref(py) +# .bind(py) # .call_method("get_results", (), None) # .unwrap() # .extract() @@ -204,7 +203,7 @@ This wrapper will also perform the type conversions between Python and Rust. # println!("Rust calling Python to perform the computation"); # Python::with_gil(|py| { # self.model -# .as_ref(py) +# .bind(py) # .call_method("compute", (), None) # .unwrap(); # }) @@ -330,7 +329,9 @@ Let's modify the code performing the type conversion to give a helpful error mes We used in our `get_results` method the following call that performs the type conversion: ```rust +# #![allow(dead_code)] # use pyo3::prelude::*; +# use pyo3::types::PyList; # # pub trait Model { # fn set_variables(&mut self, inputs: &Vec); @@ -348,7 +349,7 @@ impl Model for UserModel { println!("Rust calling Python to get the results"); Python::with_gil(|py| { self.model - .as_ref(py) + .bind(py) .call_method("get_results", (), None) .unwrap() .extract() @@ -358,11 +359,8 @@ impl Model for UserModel { # fn set_variables(&mut self, var: &Vec) { # println!("Rust calling Python to set the variables"); # Python::with_gil(|py| { -# let values: Vec = var.clone(); -# let list: PyObject = values.into_py(py); -# let py_model = self.model.as_ref(py); -# py_model -# .call_method("set_variables", (list,), None) +# self.model.bind(py) +# .call_method("set_variables", (PyList::new_bound(py, var),), None) # .unwrap(); # }) # } @@ -371,7 +369,7 @@ impl Model for UserModel { # println!("Rust calling Python to perform the computation"); # Python::with_gil(|py| { # self.model -# .as_ref(py) +# .bind(py) # .call_method("compute", (), None) # .unwrap(); # }) @@ -382,7 +380,9 @@ impl Model for UserModel { Let's break it down in order to perform better error handling: ```rust +# #![allow(dead_code)] # use pyo3::prelude::*; +# use pyo3::types::PyList; # # pub trait Model { # fn set_variables(&mut self, inputs: &Vec); @@ -399,9 +399,9 @@ impl Model for UserModel { fn get_results(&self) -> Vec { println!("Get results from Rust calling Python"); Python::with_gil(|py| { - let py_result: &PyAny = self + let py_result: Bound<'_, PyAny> = self .model - .as_ref(py) + .bind(py) .call_method("get_results", (), None) .unwrap(); @@ -418,11 +418,8 @@ impl Model for UserModel { # fn set_variables(&mut self, var: &Vec) { # println!("Rust calling Python to set the variables"); # Python::with_gil(|py| { -# let values: Vec = var.clone(); -# let list: PyObject = values.into_py(py); -# let py_model = self.model.as_ref(py); -# py_model -# .call_method("set_variables", (list,), None) +# let py_model = self.model.bind(py) +# .call_method("set_variables", (PyList::new_bound(py, var),), None) # .unwrap(); # }) # } @@ -431,7 +428,7 @@ impl Model for UserModel { # println!("Rust calling Python to perform the computation"); # Python::with_gil(|py| { # self.model -# .as_ref(py) +# .bind(py) # .call_method("compute", (), None) # .unwrap(); # }) @@ -460,7 +457,9 @@ Because of this, we can write a function wrapper that takes the `UserModel`--whi It is also required to make the struct public. ```rust +# #![allow(dead_code)] use pyo3::prelude::*; +use pyo3::types::PyList; pub trait Model { fn set_variables(&mut self, var: &Vec); @@ -484,7 +483,7 @@ pub struct UserModel { } #[pymodule] -fn trait_exposure(_py: Python<'_>, m: &PyModule) -> PyResult<()> { +fn trait_exposure(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_function(wrap_pyfunction!(solve_wrapper, m)?)?; Ok(()) @@ -516,11 +515,9 @@ impl Model for UserModel { fn set_variables(&mut self, var: &Vec) { println!("Rust calling Python to set the variables"); Python::with_gil(|py| { - let values: Vec = var.clone(); - let list: PyObject = values.into_py(py); - let py_model = self.model.as_ref(py); - py_model - .call_method("set_variables", (list,), None) + self.model + .bind(py) + .call_method("set_variables", (PyList::new_bound(py, var),), None) .unwrap(); }) } @@ -528,9 +525,9 @@ impl Model for UserModel { fn get_results(&self) -> Vec { println!("Get results from Rust calling Python"); Python::with_gil(|py| { - let py_result: &PyAny = self + let py_result: Bound<'_, PyAny> = self .model - .as_ref(py) + .bind(py) .call_method("get_results", (), None) .unwrap(); @@ -549,7 +546,7 @@ impl Model for UserModel { println!("Rust calling Python to perform the computation"); Python::with_gil(|py| { self.model - .as_ref(py) + .bind(py) .call_method("compute", (), None) .unwrap(); }) diff --git a/guide/src/types.md b/guide/src/types.md index 8bbc48d3aa4..2a13c241de1 100644 --- a/guide/src/types.md +++ b/guide/src/types.md @@ -1,66 +1,328 @@ -# GIL lifetimes, mutability and Python object types +# Python object types -On first glance, PyO3 provides a huge number of different types that can be used -to wrap or refer to Python objects. This page delves into the details and gives -an overview of their intended meaning, with examples when each type is best -used. +PyO3 offers two main sets of types to interact with Python objects. This section of the guide expands into detail about these types and how to choose which to use. +The first set of types is are the [smart pointers][smart-pointers] which all Python objects are wrapped in. These are `Py`, `Bound<'py, T>`, and `Borrowed<'a, 'py, T>`. The [first section below](#pyo3s-smart-pointers) expands on each of these in detail and why there are three of them. -## The Python GIL, mutability, and Rust types +The second set of types are types which fill in the generic parameter `T` of the smart pointers. The most common is `PyAny`, which represents any Python object (similar to Python's `typing.Any`). There are also concrete types for many Python built-in types, such as `PyList`, `PyDict`, and `PyTuple`. User defined `#[pyclass]` types also fit this category. The [second section below](#concrete-python-types) expands on how to use these types. -Since Python has no concept of ownership, and works solely with boxed objects, -any Python object can be referenced any number of times, and mutation is allowed -from any reference. +Before PyO3 0.21, PyO3's main API to interact with Python objects was a deprecated API known as the "GIL Refs" API, containing reference types such as `&PyAny`, `&PyList`, and `&PyCell` for user-defined `#[pyclass]` types. The [third section below](#the-gil-refs-api) details this deprecated API. -The situation is helped a little by the Global Interpreter Lock (GIL), which -ensures that only one thread can use the Python interpreter and its API at the -same time, while non-Python operations (system calls and extension code) can -unlock the GIL. (See [the section on parallelism](parallelism.md) for how to do -that in PyO3.) +## PyO3's smart pointers -In PyO3, holding the GIL is modeled by acquiring a token of the type -`Python<'py>`, which serves three purposes: +PyO3's API offers three generic smart pointers: `Py`, `Bound<'py, T>` and `Borrowed<'a, 'py, T>`. For each of these the type parameter `T` will be filled by a [concrete Python type](#concrete-python-types). For example, a Python list object can be represented by `Py`, `Bound<'py, PyList>`, and `Borrowed<'a, 'py, PyList>`. -* It provides some global API for the Python interpreter, such as - [`eval`][eval]. -* It can be passed to functions that require a proof of holding the GIL, - such as [`Py::clone_ref`][clone_ref]. -* Its lifetime can be used to create Rust references that implicitly guarantee - holding the GIL, such as [`&'py PyAny`][PyAny]. +These smart pointers behave differently due to their lifetime parameters. `Py` has no lifetime parameters, `Bound<'py, T>` has [the `'py` lifetime](./python-from-rust.md#the-py-lifetime) as a parameter, and `Borrowed<'a, 'py, T>` has the `'py` lifetime plus an additional lifetime `'a` to denote the lifetime it is borrowing data for. (You can read more about these lifetimes in the subsections below). -The latter two points are the reason why some APIs in PyO3 require the `py: -Python` argument, while others don't. +Python objects are reference counted, like [`std::sync::Arc`](https://doc.rust-lang.org/stable/std/sync/struct.Arc.html). A major reason for these smart pointers is to bring Python's reference counting to a Rust API. -The PyO3 API for Python objects is written such that instead of requiring a -mutable Rust reference for mutating operations such as -[`PyList::append`][PyList_append], a shared reference (which, in turn, can only -be created through `Python<'_>` with a GIL lifetime) is sufficient. +The recommendation of when to use each of these smart pointers is as follows: -However, Rust structs wrapped as Python objects (called `pyclass` types) usually -*do* need `&mut` access. Due to the GIL, PyO3 *can* guarantee thread-safe access -to them, but it cannot statically guarantee uniqueness of `&mut` references once -an object's ownership has been passed to the Python interpreter, ensuring -references is done at runtime using `PyCell`, a scheme very similar to -`std::cell::RefCell`. +- Use `Bound<'py, T>` for as much as possible, as it offers the most efficient and complete API. +- Use `Py` mostly just for storage inside Rust `struct`s which do not want to or can't add a lifetime parameter for `Bound<'py, T>`. +- `Borrowed<'a, 'py, T>` is almost never used. It is occasionally present at the boundary between Rust and the Python interpreter, for example when borrowing data from Python tuples (which is safe because they are immutable). -### Accessing the Python GIL +The sections below also explain these smart pointers in a little more detail. -To get hold of a `Python<'py>` token to prove the GIL is held, consult [PyO3's documentation][obtaining-py]. +### `Py` (and `PyObject`) -## Object types +[`Py`][Py] is the foundational smart pointer in PyO3's API. The type parameter `T` denotes the type of the Python object. Very frequently this is `PyAny`, meaning any Python object. This is so common that `Py` has a type alias `PyObject`. -### [`PyAny`][PyAny] +Because `Py` is not bound to [the `'py` lifetime](./python-from-rust.md#the-py-lifetime), it is the type to use when storing a Python object inside a Rust `struct` or `enum` which do not want to have a lifetime parameter. In particular, [`#[pyclass]`][pyclass] types are not permitted to have a lifetime, so `Py` is the correct type to store Python objects inside them. + +The lack of binding to the `'py` lifetime also carries drawbacks: + - Almost all methods on `Py` require a `Python<'py>` token as the first argument + - Other functionality, such as [`Drop`][Drop], needs to check at runtime for attachment to the Python GIL, at a small performance cost + +Because of the drawbacks `Bound<'py, T>` is preferred for many of PyO3's APIs. In particular, `Bound<'py, T>` is the better for function arguments. + +To convert a `Py` into a `Bound<'py, T>`, the `Py::bind` and `Py::into_bound` methods are available. `Bound<'py, T>` can be converted back into `Py` using [`Bound::unbind`]. + +### `Bound<'py, T>` + +[`Bound<'py, T>`][Bound] is the counterpart to `Py` which is also bound to the `'py` lifetime. It can be thought of as equivalent to the Rust tuple `(Python<'py>, Py)`. + +By having the binding to the `'py` lifetime, `Bound<'py, T>` can offer the complete PyO3 API at maximum efficiency. This means that in almost all cases where `Py` is not necessary for lifetime reasons, `Bound<'py, T>` should be used. + +`Bound<'py, T>` engages in Python reference counting. This means that `Bound<'py, T>` owns a Python object. Rust code which just wants to borrow a Python object should use a shared reference `&Bound<'py, T>`. Just like `std::sync::Arc`, using `.clone()` and `drop()` will cheaply increment and decrement the reference count of the object (just in this case, the reference counting is implemented by the Python interpreter itself). + +To give an example of how `Bound<'py, T>` is PyO3's primary API type, consider the following Python code: + +```python +def example(): + x = list() # create a Python list + x.append(1) # append the integer 1 to it + y = x # create a second reference to the list + del x # delete the original reference +``` + +Using PyO3's API, and in particular `Bound<'py, PyList>`, this code translates into the following Rust code: + +```rust +use pyo3::prelude::*; +use pyo3::types::PyList; + +fn example<'py>(py: Python<'py>) -> PyResult<()> { + let x: Bound<'py, PyList> = PyList::empty_bound(py); + x.append(1)?; + let y: Bound<'py, PyList> = x.clone(); // y is a new reference to the same list + drop(x); // release the original reference x + Ok(()) +} +# Python::with_gil(example).unwrap(); +``` + +Or, without the type annotations: + +```rust +use pyo3::prelude::*; +use pyo3::types::PyList; + +fn example(py: Python<'_>) -> PyResult<()> { + let x = PyList::empty_bound(py); + x.append(1)?; + let y = x.clone(); + drop(x); + Ok(()) +} +# Python::with_gil(example).unwrap(); +``` + +#### Function argument lifetimes + +Because the `'py` lifetime often appears in many function arguments as part of the `Bound<'py, T>` smart pointer, the Rust compiler will often require annotations of input and output lifetimes. This occurs when the function output has at least one lifetime, and there is more than one lifetime present on the inputs. + +To demonstrate, consider this function which takes accepts Python objects and applies the [Python `+` operation][PyAnyMethods::add] to them: + +```rust,compile_fail +# use pyo3::prelude::*; +fn add(left: &'_ Bound<'_, PyAny>, right: &'_ Bound<'_, PyAny>) -> PyResult> { + left.add(right) +} +``` + +Because the Python `+` operation might raise an exception, this function returns `PyResult>`. It doesn't need ownership of the inputs, so it takes `&Bound<'_, PyAny>` shared references. To demonstrate the point, all lifetimes have used the wildcard `'_` to allow the Rust compiler to attempt to infer them. Because there are four input lifetimes (two lifetimes of the shared references, and two `'py` lifetimes unnamed inside the `Bound<'_, PyAny>` pointers), the compiler cannot reason about which must be connected to the output. + +The correct way to solve this is to add the `'py` lifetime as a parameter for the function, and name all the `'py` lifetimes inside the `Bound<'py, PyAny>` smart pointers. For the shared references, it's also fine to reduce `&'_` to just `&`. The working end result is below: + +```rust +# use pyo3::prelude::*; +fn add<'py>( + left: &Bound<'py, PyAny>, + right: &Bound<'py, PyAny>, +) -> PyResult> { + left.add(right) +} +# Python::with_gil(|py| { +# let s = pyo3::types::PyString::new_bound(py, "s"); +# assert!(add(&s, &s).unwrap().eq("ss").unwrap()); +# }) +``` + +If naming the `'py` lifetime adds unwanted complexity to the function signature, it is also acceptable to return `PyObject` (aka `Py`), which has no lifetime. The cost is instead paid by a slight increase in implementation complexity, as seen by the introduction of a call to [`Bound::unbind`]: + +```rust +# use pyo3::prelude::*; +fn add(left: &Bound<'_, PyAny>, right: &Bound<'_, PyAny>) -> PyResult { + let output: Bound<'_, PyAny> = left.add(right)?; + Ok(output.unbind()) +} +# Python::with_gil(|py| { +# let s = pyo3::types::PyString::new_bound(py, "s"); +# assert!(add(&s, &s).unwrap().bind(py).eq("ss").unwrap()); +# }) +``` + +### `Borrowed<'a, 'py, T>` + +[`Borrowed<'a, 'py, T>`][Borrowed] is an advanced type used just occasionally at the edge of interaction with the Python interpreter. It can be thought of as analogous to the shared reference `&'a Bound<'py, T>`. The difference is that `Borrowed<'a, 'py, T>` is just a smart pointer rather than a reference-to-a-smart-pointer, which is a helpful reduction in indirection in specific interactions with the Python interpreter. + +`Borrowed<'a, 'py, T>` dereferences to `Bound<'py, T>`, so all methods on `Bound<'py, T>` are available on `Borrowed<'a, 'py, T>`. + +An example where `Borrowed<'a, 'py, T>` is used is in [`PyTupleMethods::get_borrowed_item`]({{#PYO3_DOCS_URL}}/pyo3/types/trait.PyTupleMethods.html#tymethod.get_item): + +```rust +use pyo3::prelude::*; +use pyo3::types::PyTuple; + +# fn example<'py>(py: Python<'py>) -> PyResult<()> { +// Create a new tuple with the elements (0, 1, 2) +let t = PyTuple::new_bound(py, [0, 1, 2]); +for i in 0..=2 { + let entry: Borrowed<'_, 'py, PyAny> = t.get_borrowed_item(i)?; + // `PyAnyMethods::extract` is available on `Borrowed` + // via the dereference to `Bound` + let value: usize = entry.extract()?; + assert_eq!(i, value); +} +# Ok(()) +# } +# Python::with_gil(example).unwrap(); +``` + +### Casting between smart pointer types + +To convert between `Py` and `Bound<'py, T>` use the `bind()` / `into_bound()` methods. Use the `as_unbound()` / `unbind()` methods to go back from `Bound<'py, T>` to `Py`. + +```rust,ignore +let obj: Py = ...; +let bound: &Bound<'py, PyAny> = obj.bind(py); +let bound: Bound<'py, PyAny> = obj.into_bound(py); + +let obj: &Py = bound.as_unbound(); +let obj: Py = bound.unbind(); +``` + +To convert between `Bound<'py, T>` and `Borrowed<'a, 'py, T>` use the `as_borrowed()` method. `Borrowed<'a, 'py, T>` has a deref coercion to `Bound<'py, T>`. Use the `to_owned()` method to increment the Python reference count and to create a new `Bound<'py, T>` from the `Borrowed<'a, 'py, T>`. + +```rust,ignore +let bound: Bound<'py, PyAny> = ...; +let borrowed: Borrowed<'_, 'py, PyAny> = bound.as_borrowed(); + +// deref coercion +let bound: &Bound<'py, PyAny> = &borrowed; + +// create a new Bound by increase the Python reference count +let bound: Bound<'py, PyAny> = borrowed.to_owned(); +``` + +To convert between `Py` and `Borrowed<'a, 'py, T>` use the `bind_borrowed()` method. Use either `as_unbound()` or `.to_owned().unbind()` to go back to `Py` from `Borrowed<'a, 'py, T>`, via `Bound<'py, T>`. + +```rust,ignore +let obj: Py = ...; +let borrowed: Borrowed<'_, 'py, PyAny> = bound.as_borrowed(); + +// via deref coercion to Bound and then using Bound::as_unbound +let obj: &Py = borrowed.as_unbound(); + +// via a new Bound by increasing the Python reference count, and unbind it +let obj: Py = borrowed.to_owned().unbind(). +``` + +## Concrete Python types + +In all of `Py`, `Bound<'py, T>`, and `Borrowed<'a, 'py, T>`, the type parameter `T` denotes the type of the Python object referred to by the smart pointer. + +This parameter `T` can be filled by: + - [`PyAny`][PyAny], which represents any Python object, + - Native Python types such as `PyList`, `PyTuple`, and `PyDict`, and + - [`#[pyclass]`][pyclass] types defined from Rust + +The following subsections covers some further detail about how to work with these types: +- the APIs that are available for these concrete types, +- how to cast `Bound<'py, T>` to a specific concrete type, and +- how to get Rust data out of a `Bound<'py, T>`. -**Represents:** a Python object of unspecified type, restricted to a GIL -lifetime. Currently, `PyAny` can only ever occur as a reference, `&PyAny`. +### Using APIs for concrete Python types -**Used:** Whenever you want to refer to some Python object and will have the -GIL for the whole duration you need to access that object. For example, -intermediate values and arguments to `pyfunction`s or `pymethod`s implemented -in Rust where any type is allowed. +Each concrete Python type such as `PyAny`, `PyTuple` and `PyDict` exposes its API on the corresponding bound smart pointer `Bound<'py, PyAny>`, `Bound<'py, PyTuple>` and `Bound<'py, PyDict>`. + +Each type's API is exposed as a trait: [`PyAnyMethods`], [`PyTupleMethods`], [`PyDictMethods`], and so on for all concrete types. Using traits rather than associated methods on the `Bound` smart pointer is done for a couple of reasons: +- Clarity of documentation: each trait gets its own documentation page in the PyO3 API docs. If all methods were on the `Bound` smart pointer directly, the vast majority of PyO3's API would be on a single, extremely long, documentation page. +- Consistency: downstream code implementing Rust APIs for existing Python types can also follow this pattern of using a trait. Downstream code would not be allowed to add new associated methods directly on the `Bound` type. +- Future design: it is hoped that a future Rust with [arbitrary self types](https://github.com/rust-lang/rust/issues/44874) will remove the need for these traits in favour of placing the methods directly on `PyAny`, `PyTuple`, `PyDict`, and so on. + +These traits are all included in the `pyo3::prelude` module, so with the glob import `use pyo3::prelude::*` the full PyO3 API is made available to downstream code. + +The following function accesses the first item in the input Python list, using the `.get_item()` method from the `PyListMethods` trait: + +```rust +use pyo3::prelude::*; +use pyo3::types::PyList; + +fn get_first_item<'py>(list: &Bound<'py, PyList>) -> PyResult> { + list.get_item(0) +} +# Python::with_gil(|py| { +# let l = PyList::new_bound(py, ["hello world"]); +# assert!(get_first_item(&l).unwrap().eq("hello world").unwrap()); +# }) +``` -Many general methods for interacting with Python objects are on the `PyAny` struct, -such as `getattr`, `setattr`, and `.call`. +### Casting between Python object types + +To cast `Bound<'py, T>` smart pointers to some other type, use the [`.downcast()`][PyAnyMethods::downcast] family of functions. This converts `&Bound<'py, T>` to a different `&Bound<'py, U>`, without transferring ownership. There is also [`.downcast_into()`][PyAnyMethods::downcast_into] to convert `Bound<'py, T>` to `Bound<'py, U>` with transfer of ownership. These methods are available for all types `T` which implement the [`PyTypeCheck`] trait. + +Casting to `Bound<'py, PyAny>` can be done with `.as_any()` or `.into_any()`. + +For example, the following snippet shows how to cast `Bound<'py, PyAny>` to `Bound<'py, PyTuple>`: + +```rust +# use pyo3::prelude::*; +# use pyo3::types::PyTuple; +# fn example<'py>(py: Python<'py>) -> PyResult<()> { +// create a new Python `tuple`, and use `.into_any()` to erase the type +let obj: Bound<'py, PyAny> = PyTuple::empty_bound(py).into_any(); + +// use `.downcast()` to cast to `PyTuple` without transferring ownership +let _: &Bound<'py, PyTuple> = obj.downcast()?; + +// use `.downcast_into()` to cast to `PyTuple` with transfer of ownership +let _: Bound<'py, PyTuple> = obj.downcast_into()?; +# Ok(()) +# } +# Python::with_gil(example).unwrap() +``` + +Custom [`#[pyclass]`][pyclass] types implement [`PyTypeCheck`], so `.downcast()` also works for these types. The snippet below is the same as the snippet above casting instead to a custom type `MyClass`: + +```rust +use pyo3::prelude::*; + +#[pyclass] +struct MyClass {} + +# fn example<'py>(py: Python<'py>) -> PyResult<()> { +// create a new Python `tuple`, and use `.into_any()` to erase the type +let obj: Bound<'py, PyAny> = Bound::new(py, MyClass {})?.into_any(); + +// use `.downcast()` to cast to `MyClass` without transferring ownership +let _: &Bound<'py, MyClass> = obj.downcast()?; + +// use `.downcast_into()` to cast to `MyClass` with transfer of ownership +let _: Bound<'py, MyClass> = obj.downcast_into()?; +# Ok(()) +# } +# Python::with_gil(example).unwrap() +``` + +### Extracting Rust data from Python objects + +To extract Rust data from Python objects, use [`.extract()`][PyAnyMethods::extract] instead of `.downcast()`. This method is available for all types which implement the [`FromPyObject`] trait. + +For example, the following snippet extracts a Rust tuple of integers from a Python tuple: + +```rust +# use pyo3::prelude::*; +# use pyo3::types::PyTuple; +# fn example<'py>(py: Python<'py>) -> PyResult<()> { +// create a new Python `tuple`, and use `.into_any()` to erase the type +let obj: Bound<'py, PyAny> = PyTuple::new_bound(py, [1, 2, 3]).into_any(); + +// extracting the Python `tuple` to a rust `(i32, i32, i32)` tuple +let (x, y, z) = obj.extract::<(i32, i32, i32)>()?; +assert_eq!((x, y, z), (1, 2, 3)); +# Ok(()) +# } +# Python::with_gil(example).unwrap() +``` + +To avoid copying data, [`#[pyclass]`][pyclass] types can directly reference Rust data stored within the Python objects without needing to `.extract()`. See the [corresponding documentation in the class section of the guide](./class.md#bound-and-interior-mutability) +for more detail. + +## The GIL Refs API + +The GIL Refs API was PyO3's primary API prior to PyO3 0.21. The main difference was that instead of the `Bound<'py, PyAny>` smart pointer, the "GIL Reference" `&'py PyAny` was used. (This was similar for other Python types.) + +As of PyO3 0.21, the GIL Refs API is deprecated. See the [migration guide](./migration.md#from-020-to-021) for details on how to upgrade. + +The following sections note some historical detail about the GIL Refs API. + +### [`PyAny`][PyAny] + +**Represented:** a Python object of unspecified type. In the GIL Refs API, this was only accessed as the GIL Ref `&'py PyAny`. + +**Used:** `&'py PyAny` was used to refer to some Python object when the GIL lifetime was available for the whole duration access was needed. For example, intermediate values and arguments to `pyfunction`s or `pymethod`s implemented in Rust where any type is allowed. **Conversions:** @@ -68,9 +330,12 @@ For a `&PyAny` object reference `any` where the underlying object is a Python-na a list: ```rust +# #![allow(unused_imports)] # use pyo3::prelude::*; # use pyo3::types::PyList; +# #[cfg(feature = "gil-refs")] # Python::with_gil(|py| -> PyResult<()> { +#[allow(deprecated)] // PyList::empty is part of the deprecated "GIL Refs" API. let obj: &PyAny = PyList::empty(py); // To &PyList with PyAny::downcast @@ -88,12 +353,16 @@ let _: Py = obj.extract()?; For a `&PyAny` object reference `any` where the underlying object is a `#[pyclass]`: ```rust +# #![allow(unused_imports)] # use pyo3::prelude::*; # #[pyclass] #[derive(Clone)] struct MyClass { } +# #[cfg(feature = "gil-refs")] # Python::with_gil(|py| -> PyResult<()> { +#[allow(deprecated)] // into_ref is part of the deprecated GIL Refs API let obj: &PyAny = Py::new(py, MyClass {})?.into_ref(py); // To &PyCell with PyAny::downcast +#[allow(deprecated)] // &PyCell is part of the deprecated GIL Refs API let _: &PyCell = obj.downcast()?; // To Py (aka PyObject) with .into() @@ -114,25 +383,23 @@ let _: PyRefMut<'_, MyClass> = obj.extract()?; ### `PyTuple`, `PyDict`, and many more -**Represents:** a native Python object of known type, restricted to a GIL -lifetime just like `PyAny`. +**Represented:** a native Python object of known type. In the GIL Refs API, they were only accessed as the GIL Refs `&'py PyTuple`, `&'py PyDict`. -**Used:** Whenever you want to operate with native Python types while holding -the GIL. Like `PyAny`, this is the most convenient form to use for function -arguments and intermediate values. +**Used:** `&'py PyTuple` and similar were used to operate with native Python types while holding the GIL. Like `PyAny`, this is the most convenient form to use for function arguments and intermediate values. -These types all implement `Deref`, so they all expose the same -methods which can be found on `PyAny`. +These GIL Refs implement `Deref`, so they all expose the same methods which can be found on `PyAny`. -To see all Python types exposed by `PyO3` you should consult the -[`pyo3::types`][pyo3::types] module. +To see all Python types exposed by `PyO3` consult the [`pyo3::types`][pyo3::types] module. **Conversions:** ```rust +# #![allow(unused_imports)] # use pyo3::prelude::*; # use pyo3::types::PyList; +# #[cfg(feature = "gil-refs")] # Python::with_gil(|py| -> PyResult<()> { +#[allow(deprecated)] // PyList::empty is part of the deprecated "GIL Refs" API. let list = PyList::empty(py); // Use methods from PyAny on all Python types with Deref implementation @@ -142,6 +409,7 @@ let _ = list.repr()?; let _: &PyAny = list; // To &PyAny explicitly with .as_ref() +#[allow(deprecated)] // as_ref is part of the deprecated "GIL Refs" API. let _: &PyAny = list.as_ref(); // To Py with .into() or Py::from() @@ -155,7 +423,7 @@ let _: PyObject = list.into(); ### `Py` and `PyObject` -**Represents:** a GIL-independent reference to a Python object. This can be a Python native type +**Represented:** a GIL-independent reference to a Python object. This can be a Python native type (like `PyTuple`), or a `pyclass` type implemented in Rust. The most commonly-used variant, `Py`, is also known as `PyObject`. @@ -165,81 +433,25 @@ Python-Rust FFI boundary, or returning objects from functions implemented in Rus Can be cloned using Python reference counts with `.clone()`. -**Conversions:** - -For a `Py`, the conversions are as below: - -```rust -# use pyo3::prelude::*; -# use pyo3::types::PyList; -# Python::with_gil(|py| { -let list: Py = PyList::empty(py).into(); - -// To &PyList with Py::as_ref() (borrows from the Py) -let _: &PyList = list.as_ref(py); - -# let list_clone = list.clone(); // Because `.into_ref()` will consume `list`. -// To &PyList with Py::into_ref() (moves the pointer into PyO3's object storage) -let _: &PyList = list.into_ref(py); - -# let list = list_clone; -// To Py (aka PyObject) with .into() -let _: Py = list.into(); -# }) -``` - -For a `#[pyclass] struct MyClass`, the conversions for `Py` are below: - -```rust -# use pyo3::prelude::*; -# Python::with_gil(|py| { -# #[pyclass] struct MyClass { } -# Python::with_gil(|py| -> PyResult<()> { -let my_class: Py = Py::new(py, MyClass { })?; - -// To &PyCell with Py::as_ref() (borrows from the Py) -let _: &PyCell = my_class.as_ref(py); - -# let my_class_clone = my_class.clone(); // Because `.into_ref()` will consume `my_class`. -// To &PyCell with Py::into_ref() (moves the pointer into PyO3's object storage) -let _: &PyCell = my_class.into_ref(py); - -# let my_class = my_class_clone.clone(); -// To Py (aka PyObject) with .into_py(py) -let _: Py = my_class.into_py(py); - -# let my_class = my_class_clone; -// To PyRef<'_, MyClass> with Py::borrow or Py::try_borrow -let _: PyRef<'_, MyClass> = my_class.try_borrow(py)?; - -// To PyRefMut<'_, MyClass> with Py::borrow_mut or Py::try_borrow_mut -let _: PyRefMut<'_, MyClass> = my_class.try_borrow_mut(py)?; -# Ok(()) -# }).unwrap(); -# }); -``` - ### `PyCell` -**Represents:** a reference to a Rust object (instance of `PyClass`) which is -wrapped in a Python object. The cell part is an analog to stdlib's -[`RefCell`][RefCell] to allow access to `&mut` references. +**Represented:** a reference to a Rust object (instance of `PyClass`) wrapped in a Python object. The cell part is an analog to stdlib's [`RefCell`][RefCell] to allow access to `&mut` references. -**Used:** for accessing pure-Rust API of the instance (members and functions -taking `&SomeType` or `&mut SomeType`) while maintaining the aliasing rules of -Rust references. +**Used:** for accessing pure-Rust API of the instance (members and functions taking `&SomeType` or `&mut SomeType`) while maintaining the aliasing rules of Rust references. -Like PyO3's Python native types, `PyCell` implements `Deref`, -so it also exposes all of the methods on `PyAny`. +Like PyO3's Python native types, the GIL Ref `&PyCell` implements `Deref`, so it also exposed all of the methods on `PyAny`. **Conversions:** -`PyCell` can be used to access `&T` and `&mut T` via `PyRef` and `PyRefMut` respectively. +`PyCell` was used to access `&T` and `&mut T` via `PyRef` and `PyRefMut` respectively. ```rust +#![allow(unused_imports)] # use pyo3::prelude::*; # #[pyclass] struct MyClass { } +# #[cfg(feature = "gil-refs")] # Python::with_gil(|py| -> PyResult<()> { +#[allow(deprecated)] // &PyCell is part of the deprecated GIL Refs API let cell: &PyCell = PyCell::new(py, MyClass {})?; // To PyRef with .borrow() or .try_borrow() @@ -254,12 +466,15 @@ let _: &mut MyClass = &mut *py_ref_mut; # }).unwrap(); ``` -`PyCell` can also be accessed like a Python-native type. +`PyCell` was also accessed like a Python-native type. ```rust +#![allow(unused_imports)] # use pyo3::prelude::*; # #[pyclass] struct MyClass { } +# #[cfg(feature = "gil-refs")] # Python::with_gil(|py| -> PyResult<()> { +#[allow(deprecated)] // &PyCell is part of the deprecate GIL Refs API let cell: &PyCell = PyCell::new(py, MyClass {})?; // Use methods from PyAny on PyCell with Deref implementation @@ -269,36 +484,30 @@ let _ = cell.repr()?; let _: &PyAny = cell; // To &PyAny explicitly with .as_ref() +#[allow(deprecated)] // as_ref is part of the deprecated "GIL Refs" API. let _: &PyAny = cell.as_ref(); # Ok(()) # }).unwrap(); ``` -### `PyRef` and `PyRefMut` - -**Represents:** reference wrapper types employed by `PyCell` to keep track of -borrows, analog to `Ref` and `RefMut` used by `RefCell`. - -**Used:** while borrowing a `PyCell`. They can also be used with `.extract()` -on types like `Py` and `PyAny` to get a reference quickly. - - -## Related traits and types - -### `PyClass` - -This trait marks structs defined in Rust that are also usable as Python classes, -usually defined using the `#[pyclass]` macro. - -### `PyNativeType` - -This trait marks structs that mirror native Python types, such as `PyList`. - - +[Bound]: {{#PYO3_DOCS_URL}}/pyo3/struct.Bound.html +[`Bound::unbind`]: {{#PYO3_DOCS_URL}}/pyo3/struct.Bound.html#method.unbind +[Py]: {{#PYO3_DOCS_URL}}/pyo3/struct.Py.html +[PyAnyMethods::add]: {{#PYO3_DOCS_URL}}/pyo3/types/trait.PyAnyMethods.html#tymethod.add +[PyAnyMethods::extract]: {{#PYO3_DOCS_URL}}/pyo3/types/trait.PyAnyMethods.html#tymethod.extract +[PyAnyMethods::downcast]: {{#PYO3_DOCS_URL}}/pyo3/types/trait.PyAnyMethods.html#tymethod.downcast +[PyAnyMethods::downcast_into]: {{#PYO3_DOCS_URL}}/pyo3/types/trait.PyAnyMethods.html#tymethod.downcast_into +[`PyTypeCheck`]: {{#PYO3_DOCS_URL}}/pyo3/type_object/trait.PyTypeCheck.html +[`PyAnyMethods`]: {{#PYO3_DOCS_URL}}/pyo3/types/trait.PyAnyMethods.html +[`PyDictMethods`]: {{#PYO3_DOCS_URL}}/pyo3/types/trait.PyDictMethods.html +[`PyTupleMethods`]: {{#PYO3_DOCS_URL}}/pyo3/types/trait.PyTupleMethods.html +[pyclass]: class.md +[Borrowed]: {{#PYO3_DOCS_URL}}/pyo3/struct.Borrowed.html +[Drop]: https://doc.rust-lang.org/std/ops/trait.Drop.html [eval]: {{#PYO3_DOCS_URL}}/pyo3/marker/struct.Python.html#method.eval [clone_ref]: {{#PYO3_DOCS_URL}}/pyo3/struct.Py.html#method.clone_ref [pyo3::types]: {{#PYO3_DOCS_URL}}/pyo3/types/index.html [PyAny]: {{#PYO3_DOCS_URL}}/pyo3/types/struct.PyAny.html [PyList_append]: {{#PYO3_DOCS_URL}}/pyo3/types/struct.PyList.html#method.append [RefCell]: https://doc.rust-lang.org/std/cell/struct.RefCell.html -[obtaining-py]: {{#PYO3_DOCS_URL}}/pyo3/marker/struct.Python.html#obtaining-a-python-token +[smart-pointers]: https://doc.rust-lang.org/book/ch15-00-smart-pointers.html diff --git a/newsfragments/3456.added.md b/newsfragments/3456.added.md deleted file mode 100644 index 6e9376ba65d..00000000000 --- a/newsfragments/3456.added.md +++ /dev/null @@ -1 +0,0 @@ -Add optional conversion support for `either::Either` sum type (under "either" feature). diff --git a/newsfragments/3507.added.md b/newsfragments/3507.added.md deleted file mode 100644 index 2068ab4c3f7..00000000000 --- a/newsfragments/3507.added.md +++ /dev/null @@ -1 +0,0 @@ -Add `smallvec` feature to add `ToPyObject`, `IntoPy` and `FromPyObject` implementations for `smallvec::SmallVec`. diff --git a/newsfragments/3512.fixed.md b/newsfragments/3512.fixed.md deleted file mode 100644 index 39b8087669e..00000000000 --- a/newsfragments/3512.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Fix minimum version specification for optional `chrono` dependency diff --git a/newsfragments/3514.added.md b/newsfragments/3514.added.md deleted file mode 100644 index 7fbf662b2ec..00000000000 --- a/newsfragments/3514.added.md +++ /dev/null @@ -1 +0,0 @@ -Add `PyMemoryView` type. diff --git a/newsfragments/3532.changed.md b/newsfragments/3532.changed.md deleted file mode 100644 index b65f240931e..00000000000 --- a/newsfragments/3532.changed.md +++ /dev/null @@ -1 +0,0 @@ -- `PyDict::from_sequence` now takes a single argument of type `&PyAny` (previously took two arguments `Python` and `PyObject`). diff --git a/newsfragments/3540.added.md b/newsfragments/3540.added.md deleted file mode 100644 index 2b113193bef..00000000000 --- a/newsfragments/3540.added.md +++ /dev/null @@ -1 +0,0 @@ -Support `async fn` in macros with coroutine implementation \ No newline at end of file diff --git a/newsfragments/3556.added.md b/newsfragments/3556.added.md deleted file mode 100644 index 014908a1bf5..00000000000 --- a/newsfragments/3556.added.md +++ /dev/null @@ -1 +0,0 @@ -Add `take` and `into_inner` methods to `GILOnceCell` \ No newline at end of file diff --git a/newsfragments/3564.fixed.md b/newsfragments/3564.fixed.md deleted file mode 100644 index 83e4dba05bb..00000000000 --- a/newsfragments/3564.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Silenced new `clippy::unnecessary_fallible_conversions` warning when using a `Py` `self` receiver diff --git a/newsfragments/3577.added.md b/newsfragments/3577.added.md deleted file mode 100644 index 632274984ec..00000000000 --- a/newsfragments/3577.added.md +++ /dev/null @@ -1 +0,0 @@ -Implement `PyTypeInfo` for `PyEllipsis`, `PyNone` and `PyNotImplemented`. diff --git a/newsfragments/3577.changed.md b/newsfragments/3577.changed.md deleted file mode 100644 index a7e6629d6a5..00000000000 --- a/newsfragments/3577.changed.md +++ /dev/null @@ -1 +0,0 @@ -Deprecate `Py::is_ellipsis` and `PyAny::is_ellipsis` in favour of `any.is(py.Ellipsis())`. diff --git a/newsfragments/3578.changed.md b/newsfragments/3578.changed.md deleted file mode 100644 index 53fc9943b43..00000000000 --- a/newsfragments/3578.changed.md +++ /dev/null @@ -1 +0,0 @@ -Change return type of `py.None()`, `py.NotImplemented()`, and `py.Ellipsis()` from `PyObject` to typed singletons (`&PyNone`, `&PyNotImplemented` and `PyEllipsis` respectively). diff --git a/newsfragments/3587.added.md b/newsfragments/3587.added.md deleted file mode 100644 index f8ea280dd25..00000000000 --- a/newsfragments/3587.added.md +++ /dev/null @@ -1,2 +0,0 @@ -- Classmethods can now receive `Py` as their first argument -- Function annotated with `pass_module` can now receive `Py` as their first argument \ No newline at end of file diff --git a/newsfragments/3588.added.md b/newsfragments/3588.added.md deleted file mode 100644 index acddf296a6f..00000000000 --- a/newsfragments/3588.added.md +++ /dev/null @@ -1 +0,0 @@ -Add `__name__`/`__qualname__` attributes to `Coroutine` \ No newline at end of file diff --git a/newsfragments/3599.added.md b/newsfragments/3599.added.md deleted file mode 100644 index 36078fbcdb6..00000000000 --- a/newsfragments/3599.added.md +++ /dev/null @@ -1 +0,0 @@ -Add `coroutine::CancelHandle` to catch coroutine cancellation \ No newline at end of file diff --git a/newsfragments/3600.changed.md b/newsfragments/3600.changed.md deleted file mode 100644 index c8701ef4b25..00000000000 --- a/newsfragments/3600.changed.md +++ /dev/null @@ -1 +0,0 @@ -Split some `PyTypeInfo` functionality into new traits `HasPyGilRef` and `PyTypeCheck`. diff --git a/newsfragments/3601.changed.md b/newsfragments/3601.changed.md deleted file mode 100644 index 413765ecad5..00000000000 --- a/newsfragments/3601.changed.md +++ /dev/null @@ -1 +0,0 @@ -Deprecate `PyTryFrom` and `PyTryInto` traits in favor of `any.downcast()` via the `PyTypeCheck` and `PyTypeInfo` traits. diff --git a/newsfragments/3603.removed.md b/newsfragments/3603.removed.md deleted file mode 100644 index e8f5004e3b9..00000000000 --- a/newsfragments/3603.removed.md +++ /dev/null @@ -1 +0,0 @@ -Remove all functionality deprecated in PyO3 0.19. diff --git a/newsfragments/3609.changed.md b/newsfragments/3609.changed.md deleted file mode 100644 index 7979ea71960..00000000000 --- a/newsfragments/3609.changed.md +++ /dev/null @@ -1 +0,0 @@ -Allow async methods to accept `&self`/`&mut self` \ No newline at end of file diff --git a/newsfragments/3616.added.md b/newsfragments/3616.added.md deleted file mode 100644 index 532dc6e56c1..00000000000 --- a/newsfragments/3616.added.md +++ /dev/null @@ -1 +0,0 @@ -Add `traverse` method to `GILProtected` diff --git a/newsfragments/3619.fixed.md b/newsfragments/3619.fixed.md deleted file mode 100644 index 690542409f4..00000000000 --- a/newsfragments/3619.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Use portable-atomic to support platforms without 64-bit atomics diff --git a/newsfragments/3632.added.md b/newsfragments/3632.added.md deleted file mode 100644 index d9c954fa0b4..00000000000 --- a/newsfragments/3632.added.md +++ /dev/null @@ -1 +0,0 @@ -Add support for extracting Rust set types from `frozenset`. diff --git a/newsfragments/3638.changed.md b/newsfragments/3638.changed.md deleted file mode 100644 index 6bdafde8422..00000000000 --- a/newsfragments/3638.changed.md +++ /dev/null @@ -1 +0,0 @@ -Values of type `bool` can now be extracted from NumPy's `bool_`. diff --git a/newsfragments/3653.changed.md b/newsfragments/3653.changed.md deleted file mode 100644 index 75fea03cb71..00000000000 --- a/newsfragments/3653.changed.md +++ /dev/null @@ -1 +0,0 @@ -Add `AsRefSource` to `PyNativeType`. diff --git a/newsfragments/3657.changed.md b/newsfragments/3657.changed.md deleted file mode 100644 index 0a519b09d62..00000000000 --- a/newsfragments/3657.changed.md +++ /dev/null @@ -1 +0,0 @@ -Changed `.is_true` to `.is_truthy` on `PyAny` and `Py` to clarify that the test is not based on identity with or equality to the True singleton. diff --git a/newsfragments/3660.changed.md b/newsfragments/3660.changed.md deleted file mode 100644 index 8b4a3f734e1..00000000000 --- a/newsfragments/3660.changed.md +++ /dev/null @@ -1 +0,0 @@ -`PyType::name` is now `PyType::qualname` whereas `PyType::name` efficiently accesses the full name which includes the module name. diff --git a/newsfragments/3661.changed.md b/newsfragments/3661.changed.md deleted file mode 100644 index 8245a6f1a80..00000000000 --- a/newsfragments/3661.changed.md +++ /dev/null @@ -1 +0,0 @@ -The `Iter(A)NextOutput` types are now deprecated and `__(a)next__` can directly return anything which can be converted into Python objects, i.e. awaitables do not need to be wrapped into `IterANextOutput` or `Option` any more. `Option` can still be used as well and returning `None` will trigger the fast path for `__next__`, stopping iteration without having to raise a `StopIteration` exception. diff --git a/newsfragments/3663.changed.md b/newsfragments/3663.changed.md deleted file mode 100644 index 13c07e01f2d..00000000000 --- a/newsfragments/3663.changed.md +++ /dev/null @@ -1 +0,0 @@ -Implements `FromPyObject` on `chrono::DateTime` for all `Tz` and not only `FixedOffset` and `Utc` \ No newline at end of file diff --git a/newsfragments/3664.changed.md b/newsfragments/3664.changed.md deleted file mode 100644 index 3a167d2f9d2..00000000000 --- a/newsfragments/3664.changed.md +++ /dev/null @@ -1 +0,0 @@ -`chrono` conversions are compatible with `abi3` \ No newline at end of file diff --git a/newsfragments/3670.added.md b/newsfragments/3670.added.md deleted file mode 100644 index a524261e9d9..00000000000 --- a/newsfragments/3670.added.md +++ /dev/null @@ -1 +0,0 @@ -`FromPyObject`, `IntoPy` and `ToPyObject` are implemented on `std::duration::Duration` \ No newline at end of file diff --git a/newsfragments/3677.added.md b/newsfragments/3677.added.md deleted file mode 100644 index 3e6bc56d582..00000000000 --- a/newsfragments/3677.added.md +++ /dev/null @@ -1 +0,0 @@ -Add `PyString::to_cow`. Add `Py::to_str`, `Py::to_cow`, and `Py::to_string_lossy`, as ways to access Python string data safely beyond the GIL lifetime. diff --git a/newsfragments/3679.changed.md b/newsfragments/3679.changed.md deleted file mode 100644 index 2e8e28a9d61..00000000000 --- a/newsfragments/3679.changed.md +++ /dev/null @@ -1 +0,0 @@ -Add lifetime parameter to `PyTzInfoAccess` trait and change the return type of `PyTzInfoAccess::get_tzinfo` to `Option>`. For the deprecated gil-ref API, the trait is now implemented for `&'py PyTime` and `&'py PyDateTime` instead of `PyTime` and `PyDate`. diff --git a/newsfragments/3686.added.md b/newsfragments/3686.added.md deleted file mode 100644 index f808df3685a..00000000000 --- a/newsfragments/3686.added.md +++ /dev/null @@ -1 +0,0 @@ -Add `Bound` and `Borrowed` smart pointers as a new API for accessing Python objects. diff --git a/newsfragments/3687.added.md b/newsfragments/3687.added.md deleted file mode 100644 index a6df28d939f..00000000000 --- a/newsfragments/3687.added.md +++ /dev/null @@ -1 +0,0 @@ -Added `abi3-py312` feature diff --git a/newsfragments/3689.changed.md b/newsfragments/3689.changed.md deleted file mode 100644 index 30928e82f64..00000000000 --- a/newsfragments/3689.changed.md +++ /dev/null @@ -1 +0,0 @@ -Calls to `__traverse__` become no-ops for unsendable pyclasses if on the wrong thread, thereby avoiding hard aborts at the cost of potential leakage. diff --git a/newsfragments/3761.changed.md b/newsfragments/3761.changed.md new file mode 100644 index 00000000000..fd0847211d8 --- /dev/null +++ b/newsfragments/3761.changed.md @@ -0,0 +1 @@ +Change the type of `PySliceIndices::slicelength` and the `length` parameter of `PySlice::indices()`. diff --git a/newsfragments/3966.packaging.md b/newsfragments/3966.packaging.md new file mode 100644 index 00000000000..81220bc4e85 --- /dev/null +++ b/newsfragments/3966.packaging.md @@ -0,0 +1 @@ +Update `heck` dependency to 0.5. diff --git a/newsfragments/4061.packaging.md b/newsfragments/4061.packaging.md new file mode 100644 index 00000000000..5e51f50290d --- /dev/null +++ b/newsfragments/4061.packaging.md @@ -0,0 +1 @@ +Extend range of supported versions of `chrono-tz` optional dependency to include version 0.10. diff --git a/newsfragments/4072.added.md b/newsfragments/4072.added.md new file mode 100644 index 00000000000..23207c849d8 --- /dev/null +++ b/newsfragments/4072.added.md @@ -0,0 +1 @@ +Support `#[pyclass]` on enums that have tuple variants. \ No newline at end of file diff --git a/newsfragments/4078.changed.md b/newsfragments/4078.changed.md new file mode 100644 index 00000000000..45f160f5556 --- /dev/null +++ b/newsfragments/4078.changed.md @@ -0,0 +1 @@ +deprecate implicit default for trailing optional arguments diff --git a/newsfragments/4079.added.md b/newsfragments/4079.added.md new file mode 100644 index 00000000000..afe26728f9a --- /dev/null +++ b/newsfragments/4079.added.md @@ -0,0 +1 @@ +Added support for scientific notation in `Decimal` conversion diff --git a/newsfragments/4086.fixed.md b/newsfragments/4086.fixed.md new file mode 100644 index 00000000000..e9cae7733f9 --- /dev/null +++ b/newsfragments/4086.fixed.md @@ -0,0 +1 @@ +Fixes a compile error when exporting an exception created with `create_exception!` living in a different Rust module using the `declarative-module` feature. \ No newline at end of file diff --git a/newsfragments/4095.added.md b/newsfragments/4095.added.md new file mode 100644 index 00000000000..c9940f70f12 --- /dev/null +++ b/newsfragments/4095.added.md @@ -0,0 +1 @@ +Add `pyo3_disable_reference_pool` conditional compilation flag to avoid the overhead of the global reference pool at the cost of known limitations as explained in the performance section of the guide. diff --git a/newsfragments/4095.changed.md b/newsfragments/4095.changed.md new file mode 100644 index 00000000000..7f155ae04ef --- /dev/null +++ b/newsfragments/4095.changed.md @@ -0,0 +1 @@ +`Clone`ing pointers into the Python heap has been moved behind the `py-clone` feature, as it must panic without the GIL being held as a soundness fix. diff --git a/newsfragments/4098.changed.md b/newsfragments/4098.changed.md new file mode 100644 index 00000000000..5df526a52e3 --- /dev/null +++ b/newsfragments/4098.changed.md @@ -0,0 +1 @@ +Add `#[track_caller]` to all `Py`, `Bound<'py, T>` and `Borrowed<'a, 'py, T>` methods which can panic. diff --git a/newsfragments/4100.changed.md b/newsfragments/4100.changed.md new file mode 100644 index 00000000000..13fd2e02aa8 --- /dev/null +++ b/newsfragments/4100.changed.md @@ -0,0 +1 @@ +Change `PyAnyMethods::dir` to be fallible and return `PyResult>` (and similar for `PyAny::dir`). diff --git a/newsfragments/4104.fixed.md b/newsfragments/4104.fixed.md new file mode 100644 index 00000000000..3eff1654b4f --- /dev/null +++ b/newsfragments/4104.fixed.md @@ -0,0 +1 @@ +Changes definitions of `PY_VECTORCALL_ARGUMENTS_OFFSET` and `PyVectorcall_NARGS` to fix a false-positive assertion. diff --git a/newsfragments/4116.fixed.md b/newsfragments/4116.fixed.md new file mode 100644 index 00000000000..63531aceb39 --- /dev/null +++ b/newsfragments/4116.fixed.md @@ -0,0 +1 @@ +Disable `PyUnicode_DATA` on PyPy: Not exposed by PyPy. diff --git a/newsfragments/4117.fixed.md b/newsfragments/4117.fixed.md new file mode 100644 index 00000000000..c3bb4c144b6 --- /dev/null +++ b/newsfragments/4117.fixed.md @@ -0,0 +1 @@ +Correctly handle `#[pyo3(from_py_with = ...)]` attribute on dunder (`__magic__`) method arguments instead of silently ignoring it. diff --git a/newsfragments/4129.changed.md b/newsfragments/4129.changed.md new file mode 100644 index 00000000000..6818181ad0c --- /dev/null +++ b/newsfragments/4129.changed.md @@ -0,0 +1 @@ +Raised the MSRV to 1.63 diff --git a/newsfragments/4148.added.md b/newsfragments/4148.added.md new file mode 100644 index 00000000000..16da3d2db37 --- /dev/null +++ b/newsfragments/4148.added.md @@ -0,0 +1 @@ +Conversion between [num-rational](https://github.com/rust-num/num-rational) and Python's fractions.Fraction. diff --git a/newsfragments/4158.added.md b/newsfragments/4158.added.md new file mode 100644 index 00000000000..42e6d3ff4b4 --- /dev/null +++ b/newsfragments/4158.added.md @@ -0,0 +1 @@ +Added `#[pyo3(constructor = (...))]` to customize the generated constructors for complex enum variants diff --git a/newsfragments/4184.packaging.md b/newsfragments/4184.packaging.md new file mode 100644 index 00000000000..c12302a7029 --- /dev/null +++ b/newsfragments/4184.packaging.md @@ -0,0 +1 @@ +Support Python 3.13. diff --git a/newsfragments/4197.added.md b/newsfragments/4197.added.md new file mode 100644 index 00000000000..5652028cb76 --- /dev/null +++ b/newsfragments/4197.added.md @@ -0,0 +1 @@ +Add `PyTypeMethods::mro` and `PyTypeMethods::bases`. diff --git a/noxfile.py b/noxfile.py index a4e3297857b..f447d2665f3 100644 --- a/noxfile.py +++ b/noxfile.py @@ -1,21 +1,36 @@ +from contextlib import contextmanager import json import os import re +import shutil import subprocess import sys import tempfile from functools import lru_cache from glob import glob from pathlib import Path -from typing import Any, Callable, Dict, List, Optional, Tuple +from typing import Any, Callable, Dict, Iterator, List, Optional, Tuple import nox +import nox.command + +try: + import tomllib as toml +except ImportError: + try: + import toml + except ImportError: + toml = None nox.options.sessions = ["test", "clippy", "rustfmt", "ruff", "docs"] PYO3_DIR = Path(__file__).parent -PY_VERSIONS = ("3.7", "3.8", "3.9", "3.10", "3.11", "3.12") +PYO3_TARGET = Path(os.environ.get("CARGO_TARGET_DIR", PYO3_DIR / "target")).absolute() +PYO3_GUIDE_SRC = PYO3_DIR / "guide" / "src" +PYO3_GUIDE_TARGET = PYO3_TARGET / "guide" +PYO3_DOCS_TARGET = PYO3_TARGET / "doc" +PY_VERSIONS = ("3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13") PYPY_VERSIONS = ("3.7", "3.8", "3.9", "3.10") @@ -36,6 +51,7 @@ def test_rust(session: nox.Session): _run_cargo_test(session, features="abi3") if "skip-full" not in session.posargs: _run_cargo_test(session, features="full") + _run_cargo_test(session, features="full gil-refs") _run_cargo_test(session, features="abi3 full") @@ -51,6 +67,14 @@ def coverage(session: nox.Session) -> None: session.env.update(_get_coverage_env()) _run_cargo(session, "llvm-cov", "clean", "--workspace") test(session) + + cov_format = "codecov" + output_file = "coverage.json" + + if "lcov" in session.posargs: + cov_format = "lcov" + output_file = "lcov.info" + _run_cargo( session, "llvm-cov", @@ -60,9 +84,9 @@ def coverage(session: nox.Session) -> None: "--package=pyo3-macros", "--package=pyo3-ffi", "report", - "--codecov", + f"--{cov_format}", "--output-path", - "coverage.json", + output_file, ) @@ -100,7 +124,7 @@ def _clippy(session: nox.Session, *, env: Dict[str, str] = None) -> bool: "--deny=warnings", env=env, ) - except Exception: + except nox.command.CommandFailed: success = False return success @@ -337,6 +361,7 @@ def docs(session: nox.Session) -> None: rustdoc_flags.append(session.env.get("RUSTDOCFLAGS", "")) session.env["RUSTDOCFLAGS"] = " ".join(rustdoc_flags) + shutil.rmtree(PYO3_DOCS_TARGET, ignore_errors=True) _run_cargo( session, *toolchain_flags, @@ -352,7 +377,51 @@ def docs(session: nox.Session) -> None: @nox.session(name="build-guide", venv_backend="none") def build_guide(session: nox.Session): - _run(session, "mdbook", "build", "-d", "../target/guide", "guide", *session.posargs) + shutil.rmtree(PYO3_GUIDE_TARGET, ignore_errors=True) + _run(session, "mdbook", "build", "-d", PYO3_GUIDE_TARGET, "guide", *session.posargs) + for license in ("LICENSE-APACHE", "LICENSE-MIT"): + target_file = PYO3_GUIDE_TARGET / license + target_file.unlink(missing_ok=True) + shutil.copy(PYO3_DIR / license, target_file) + + +@nox.session(name="check-guide", venv_backend="none") +def check_guide(session: nox.Session): + # reuse other sessions, but with default args + posargs = [*session.posargs] + del session.posargs[:] + build_guide(session) + docs(session) + session.posargs.extend(posargs) + + remaps = { + f"file://{PYO3_GUIDE_SRC}/([^/]*/)*?%7B%7B#PYO3_DOCS_URL}}}}": f"file://{PYO3_DOCS_TARGET}", + "%7B%7B#PYO3_DOCS_VERSION}}": "latest", + } + remap_args = [] + for key, value in remaps.items(): + remap_args.extend(("--remap", f"{key} {value}")) + # check all links in the guide + _run( + session, + "lychee", + "--include-fragments", + str(PYO3_GUIDE_SRC), + *remap_args, + *session.posargs, + ) + # check external links in the docs + # (intra-doc links are checked by rustdoc) + _run( + session, + "lychee", + str(PYO3_DOCS_TARGET), + f"--remap=https://pyo3.rs/main/ file://{PYO3_GUIDE_TARGET}/", + f"--remap=https://pyo3.rs/latest/ file://{PYO3_GUIDE_TARGET}/", + f"--exclude=file://{PYO3_DOCS_TARGET}", + "--exclude=http://www.adobe.com/", + *session.posargs, + ) @nox.session(name="format-guide", venv_backend="none") @@ -432,10 +501,11 @@ def address_sanitizer(session: nox.Session): @nox.session(name="check-changelog") def check_changelog(session: nox.Session): - event_path = os.environ.get("GITHUB_EVENT_PATH") - if event_path is None: + if not _is_github_actions(): session.error("Can only check changelog on github actions") + event_path = os.environ["GITHUB_EVENT_PATH"] + with open(event_path) as event_file: event = json.load(event_file) @@ -477,10 +547,8 @@ def check_changelog(session: nox.Session): def set_minimal_package_versions(session: nox.Session): from collections import defaultdict - try: - import tomllib as toml - except ImportError: - import toml + if toml is None: + session.error("requires Python 3.11 or `toml` to be installed") projects = ( None, @@ -490,23 +558,13 @@ def set_minimal_package_versions(session: nox.Session): "examples/word-count", ) min_pkg_versions = { - "rust_decimal": "1.26.1", - "csv": "1.1.6", - "indexmap": "1.6.2", - "hashbrown": "0.9.1", - "log": "0.4.17", - "once_cell": "1.17.2", - "rayon": "1.6.1", - "rayon-core": "1.10.2", - "regex": "1.7.3", - "proptest": "1.0.0", - "chrono": "0.4.25", - "byteorder": "1.4.3", - "crossbeam-channel": "0.5.8", - "crossbeam-deque": "0.8.3", - "crossbeam-epoch": "0.9.15", - "crossbeam-utils": "0.8.16", - "scoped-tls": "1.0.0", + "regex": "1.9.6", + "proptest": "1.2.0", + "trybuild": "1.0.89", + "eyre": "0.6.8", + "allocator-api2": "0.2.10", + # TODO is this necessary? + # "scoped-tls": "1.0.0", } # run cargo update first to ensure that everything is at highest @@ -565,9 +623,122 @@ def ffi_check(session: nox.Session): _run_cargo(session, "run", _FFI_CHECK) +@nox.session(name="test-version-limits") +def test_version_limits(session: nox.Session): + env = os.environ.copy() + with _config_file() as config_file: + env["PYO3_CONFIG_FILE"] = config_file.name + + assert "3.6" not in PY_VERSIONS + config_file.set("CPython", "3.6") + _run_cargo(session, "check", env=env, expect_error=True) + + assert "3.14" not in PY_VERSIONS + config_file.set("CPython", "3.14") + _run_cargo(session, "check", env=env, expect_error=True) + + # 3.14 CPython should build with forward compatibility + env["PYO3_USE_ABI3_FORWARD_COMPATIBILITY"] = "1" + _run_cargo(session, "check", env=env) + + assert "3.6" not in PYPY_VERSIONS + config_file.set("PyPy", "3.6") + _run_cargo(session, "check", env=env, expect_error=True) + + assert "3.11" not in PYPY_VERSIONS + config_file.set("PyPy", "3.11") + _run_cargo(session, "check", env=env, expect_error=True) + + +@nox.session(name="check-feature-powerset", venv_backend="none") +def check_feature_powerset(session: nox.Session): + if toml is None: + session.error("requires Python 3.11 or `toml` to be installed") + + cargo_toml = toml.loads((PYO3_DIR / "Cargo.toml").read_text()) + + EXCLUDED_FROM_FULL = { + "nightly", + "gil-refs", + "extension-module", + "full", + "default", + "auto-initialize", + "generate-import-lib", + "multiple-pymethods", # TODO add this after MSRV 1.62 + } + + features = cargo_toml["features"] + + full_feature = set(features["full"]) + abi3_features = {feature for feature in features if feature.startswith("abi3")} + abi3_version_features = abi3_features - {"abi3"} + + expected_full_feature = features.keys() - EXCLUDED_FROM_FULL - abi3_features + + uncovered_features = expected_full_feature - full_feature + if uncovered_features: + session.error( + f"some features missing from `full` meta feature: {uncovered_features}" + ) + + experimental_features = { + feature for feature in features if feature.startswith("experimental-") + } + full_without_experimental = full_feature - experimental_features + + if len(experimental_features) >= 2: + # justification: we always assume that feature within these groups are + # mutually exclusive to simplify CI + features_to_group = [ + full_without_experimental, + experimental_features, + ] + elif len(experimental_features) == 1: + # no need to make an experimental features group + features_to_group = [full_without_experimental] + else: + session.error("no experimental features exist; please simplify the noxfile") + + features_to_skip = [ + *(EXCLUDED_FROM_FULL - {"gil-refs"}), + *abi3_version_features, + ] + + # deny warnings + env = os.environ.copy() + rust_flags = env.get("RUSTFLAGS", "") + env["RUSTFLAGS"] = f"{rust_flags} -Dwarnings" + + comma_join = ",".join + _run_cargo( + session, + "hack", + "--feature-powerset", + '--optional-deps=""', + f'--skip="{comma_join(features_to_skip)}"', + *(f"--group-features={comma_join(group)}" for group in features_to_group), + "check", + "--all-targets", + env=env, + ) + + +@nox.session(name="update-ui-tests", venv_backend="none") +def update_ui_tests(session: nox.Session): + env = os.environ.copy() + env["TRYBUILD"] = "overwrite" + command = ["test", "--test", "test_compile_error"] + _run_cargo(session, *command, env=env) + _run_cargo(session, *command, "--features=full", env=env) + _run_cargo(session, *command, "--features=abi3,full", env=env) + + def _build_docs_for_ffi_check(session: nox.Session) -> None: # pyo3-ffi-check needs to scrape docs of pyo3-ffi - _run_cargo(session, "doc", _FFI_CHECK, "-p", "pyo3-ffi", "--no-deps") + env = os.environ.copy() + env["PYO3_PYTHON"] = sys.executable + _run_cargo(session, "doc", _FFI_CHECK, "-p", "pyo3-ffi", "--no-deps", env=env) @lru_cache() @@ -605,8 +776,8 @@ def _get_feature_sets() -> Tuple[Tuple[str, ...], ...]: "--no-default-features", "--features=abi3", ), - ("--features=full multiple-pymethods",), - ("--features=abi3 full multiple-pymethods",), + ("--features=full gil-refs multiple-pymethods",), + ("--features=abi3 full gil-refs multiple-pymethods",), ) else: return ( @@ -615,8 +786,8 @@ def _get_feature_sets() -> Tuple[Tuple[str, ...], ...]: "--no-default-features", "--features=abi3", ), - ("--features=full",), - ("--features=abi3 full",), + ("--features=full gil-refs",), + ("--features=abi3 full gil-refs",), ) @@ -645,15 +816,33 @@ def _get_coverage_env() -> Dict[str, str]: def _run(session: nox.Session, *args: str, **kwargs: Any) -> None: """Wrapper for _run(session, which creates nice groups on GitHub Actions.""" - if "GITHUB_ACTIONS" in os.environ: + is_github_actions = _is_github_actions() + failed = False + if is_github_actions: # Insert ::group:: at the start of nox's command line output print("::group::", end="", flush=True, file=sys.stderr) - session.run(*args, **kwargs) - if "GITHUB_ACTIONS" in os.environ: - print("::endgroup::", file=sys.stderr) - - -def _run_cargo(session: nox.Session, *args: str, **kwargs: Any) -> None: + try: + session.run(*args, **kwargs) + except nox.command.CommandFailed: + failed = True + raise + finally: + if is_github_actions: + print("::endgroup::", file=sys.stderr) + # Defer the error message until after the group to make them easier + # to find in the log + if failed: + command = " ".join(args) + print(f"::error::`{command}` failed", file=sys.stderr) + + +def _run_cargo( + session: nox.Session, *args: str, expect_error: bool = False, **kwargs: Any +) -> None: + if expect_error: + if "success_codes" in kwargs: + raise ValueError("expect_error overrides success_codes") + kwargs["success_codes"] = [101] _run(session, "cargo", *args, **kwargs, external=True) @@ -701,24 +890,14 @@ def _get_output(*args: str) -> str: def _for_all_version_configs( session: nox.Session, job: Callable[[Dict[str, str]], None] ) -> None: - with tempfile.NamedTemporaryFile("r+") as config: - env = os.environ.copy() - env["PYO3_CONFIG_FILE"] = config.name - - def _job_with_config(implementation, version) -> bool: - config.seek(0) - config.truncate(0) - config.write( - f"""\ -implementation={implementation} -version={version} -suppress_build_script_link_lines=true -""" - ) - config.flush() + env = os.environ.copy() + with _config_file() as config_file: + env["PYO3_CONFIG_FILE"] = config_file.name + def _job_with_config(implementation, version): session.log(f"{implementation} {version}") - return job(env) + config_file.set(implementation, version) + job(env) for version in PY_VERSIONS: _job_with_config("CPython", version) @@ -727,5 +906,38 @@ def _job_with_config(implementation, version) -> bool: _job_with_config("PyPy", version) +class _ConfigFile: + def __init__(self, config_file) -> None: + self._config_file = config_file + + def set(self, implementation: str, version: str) -> None: + """Set the contents of this config file to the given implementation and version.""" + self._config_file.seek(0) + self._config_file.truncate(0) + self._config_file.write( + f"""\ +implementation={implementation} +version={version} +suppress_build_script_link_lines=true +""" + ) + self._config_file.flush() + + @property + def name(self) -> str: + return self._config_file.name + + +@contextmanager +def _config_file() -> Iterator[_ConfigFile]: + """Creates a temporary config file which can be repeatedly set to different values.""" + with tempfile.NamedTemporaryFile("r+") as config: + yield _ConfigFile(config) + + +def _is_github_actions() -> bool: + return "GITHUB_ACTIONS" in os.environ + + _BENCHES = "--manifest-path=pyo3-benches/Cargo.toml" _FFI_CHECK = "--manifest-path=pyo3-ffi-check/Cargo.toml" diff --git a/pyo3-benches/benches/bench_any.rs b/pyo3-benches/benches/bench_any.rs index bfd010efd19..b77ab9567a6 100644 --- a/pyo3-benches/benches/bench_any.rs +++ b/pyo3-benches/benches/bench_any.rs @@ -1,11 +1,11 @@ use codspeed_criterion_compat::{criterion_group, criterion_main, Bencher, Criterion}; use pyo3::{ + prelude::*, types::{ PyBool, PyByteArray, PyBytes, PyDict, PyFloat, PyFrozenSet, PyInt, PyList, PyMapping, PySequence, PySet, PyString, PyTuple, }, - PyAny, PyResult, Python, }; #[derive(PartialEq, Eq, Debug)] @@ -27,7 +27,7 @@ enum ObjectType { Unknown, } -fn find_object_type(obj: &PyAny) -> ObjectType { +fn find_object_type(obj: &Bound<'_, PyAny>) -> ObjectType { if obj.is_none() { ObjectType::None } else if obj.is_instance_of::() { @@ -63,17 +63,17 @@ fn find_object_type(obj: &PyAny) -> ObjectType { fn bench_identify_object_type(b: &mut Bencher<'_>) { Python::with_gil(|py| { - let obj = py.eval("object()", None, None).unwrap(); + let obj = py.eval_bound("object()", None, None).unwrap(); - b.iter(|| find_object_type(obj)); + b.iter(|| find_object_type(&obj)); - assert_eq!(find_object_type(obj), ObjectType::Unknown); + assert_eq!(find_object_type(&obj), ObjectType::Unknown); }); } fn bench_collect_generic_iterator(b: &mut Bencher<'_>) { Python::with_gil(|py| { - let collection = py.eval("list(range(1 << 20))", None, None).unwrap(); + let collection = py.eval_bound("list(range(1 << 20))", None, None).unwrap(); b.iter(|| { collection diff --git a/pyo3-benches/benches/bench_bigint.rs b/pyo3-benches/benches/bench_bigint.rs index d3c71629ba4..99635a70279 100644 --- a/pyo3-benches/benches/bench_bigint.rs +++ b/pyo3-benches/benches/bench_bigint.rs @@ -1,72 +1,59 @@ -use codspeed_criterion_compat::{black_box, criterion_group, criterion_main, Bencher, Criterion}; - -use pyo3::{types::PyDict, PyAny, Python}; +use std::hint::black_box; +use codspeed_criterion_compat::{criterion_group, criterion_main, Bencher, Criterion}; use num_bigint::BigInt; +use pyo3::prelude::*; +use pyo3::types::PyDict; + fn extract_bigint_extract_fail(bench: &mut Bencher<'_>) { Python::with_gil(|py| { - let d = PyDict::new(py) as &PyAny; + let d = PyDict::new_bound(py).into_any(); - bench.iter(|| match black_box(d).extract::() { + bench.iter(|| match black_box(&d).extract::() { Ok(v) => panic!("should err {}", v), - Err(e) => black_box(e), + Err(e) => e, }); }); } fn extract_bigint_small(bench: &mut Bencher<'_>) { Python::with_gil(|py| { - let int = py.eval("-42", None, None).unwrap(); + let int = py.eval_bound("-42", None, None).unwrap(); - bench.iter(|| { - let v = black_box(int).extract::().unwrap(); - black_box(v); - }); + bench.iter_with_large_drop(|| black_box(&int).extract::().unwrap()); }); } fn extract_bigint_big_negative(bench: &mut Bencher<'_>) { Python::with_gil(|py| { - let int = py.eval("-10**300", None, None).unwrap(); + let int = py.eval_bound("-10**300", None, None).unwrap(); - bench.iter(|| { - let v = black_box(int).extract::().unwrap(); - black_box(v); - }); + bench.iter_with_large_drop(|| black_box(&int).extract::().unwrap()); }); } fn extract_bigint_big_positive(bench: &mut Bencher<'_>) { Python::with_gil(|py| { - let int = py.eval("10**300", None, None).unwrap(); + let int = py.eval_bound("10**300", None, None).unwrap(); - bench.iter(|| { - let v = black_box(int).extract::().unwrap(); - black_box(v); - }); + bench.iter_with_large_drop(|| black_box(&int).extract::().unwrap()); }); } fn extract_bigint_huge_negative(bench: &mut Bencher<'_>) { Python::with_gil(|py| { - let int = py.eval("-10**3000", None, None).unwrap(); + let int = py.eval_bound("-10**3000", None, None).unwrap(); - bench.iter(|| { - let v = black_box(int).extract::().unwrap(); - black_box(v); - }); + bench.iter_with_large_drop(|| black_box(&int).extract::().unwrap()); }); } fn extract_bigint_huge_positive(bench: &mut Bencher<'_>) { Python::with_gil(|py| { - let int = py.eval("10**3000", None, None).unwrap(); + let int = py.eval_bound("10**3000", None, None).unwrap(); - bench.iter(|| { - let v = black_box(int).extract::().unwrap(); - black_box(v); - }); + bench.iter_with_large_drop(|| black_box(&int).extract::().unwrap()); }); } diff --git a/pyo3-benches/benches/bench_call.rs b/pyo3-benches/benches/bench_call.rs index 50772097961..8470c8768d3 100644 --- a/pyo3-benches/benches/bench_call.rs +++ b/pyo3-benches/benches/bench_call.rs @@ -1,10 +1,13 @@ +use std::hint::black_box; + use codspeed_criterion_compat::{criterion_group, criterion_main, Bencher, Criterion}; use pyo3::prelude::*; macro_rules! test_module { ($py:ident, $code:literal) => { - PyModule::from_code($py, $code, file!(), "test_module").expect("module creation failed") + PyModule::from_code_bound($py, $code, file!(), "test_module") + .expect("module creation failed") }; } @@ -12,11 +15,11 @@ fn bench_call_0(b: &mut Bencher<'_>) { Python::with_gil(|py| { let module = test_module!(py, "def foo(): pass"); - let foo_module = module.getattr("foo").unwrap(); + let foo_module = &module.getattr("foo").unwrap(); b.iter(|| { for _ in 0..1000 { - foo_module.call0().unwrap(); + black_box(foo_module).call0().unwrap(); } }); }) @@ -33,11 +36,11 @@ class Foo: " ); - let foo_module = module.getattr("Foo").unwrap().call0().unwrap(); + let foo_module = &module.getattr("Foo").unwrap().call0().unwrap(); b.iter(|| { for _ in 0..1000 { - foo_module.call_method0("foo").unwrap(); + black_box(foo_module).call_method0("foo").unwrap(); } }); }) diff --git a/pyo3-benches/benches/bench_comparisons.rs b/pyo3-benches/benches/bench_comparisons.rs index ffd4c1a452f..fbd473f06cf 100644 --- a/pyo3-benches/benches/bench_comparisons.rs +++ b/pyo3-benches/benches/bench_comparisons.rs @@ -45,8 +45,8 @@ impl OrderedRichcmp { fn bench_ordered_dunder_methods(b: &mut Bencher<'_>) { Python::with_gil(|py| { - let obj1 = Py::new(py, OrderedDunderMethods(0)).unwrap().into_ref(py); - let obj2 = Py::new(py, OrderedDunderMethods(1)).unwrap().into_ref(py); + let obj1 = &Bound::new(py, OrderedDunderMethods(0)).unwrap().into_any(); + let obj2 = &Bound::new(py, OrderedDunderMethods(1)).unwrap().into_any(); b.iter(|| obj2.gt(obj1).unwrap()); }); @@ -54,8 +54,8 @@ fn bench_ordered_dunder_methods(b: &mut Bencher<'_>) { fn bench_ordered_richcmp(b: &mut Bencher<'_>) { Python::with_gil(|py| { - let obj1 = Py::new(py, OrderedRichcmp(0)).unwrap().into_ref(py); - let obj2 = Py::new(py, OrderedRichcmp(1)).unwrap().into_ref(py); + let obj1 = &Bound::new(py, OrderedRichcmp(0)).unwrap().into_any(); + let obj2 = &Bound::new(py, OrderedRichcmp(1)).unwrap().into_any(); b.iter(|| obj2.gt(obj1).unwrap()); }); diff --git a/pyo3-benches/benches/bench_decimal.rs b/pyo3-benches/benches/bench_decimal.rs index 203756b54cc..53b79abbd38 100644 --- a/pyo3-benches/benches/bench_decimal.rs +++ b/pyo3-benches/benches/bench_decimal.rs @@ -1,26 +1,26 @@ -use codspeed_criterion_compat::{black_box, criterion_group, criterion_main, Bencher, Criterion}; +use std::hint::black_box; + +use codspeed_criterion_compat::{criterion_group, criterion_main, Bencher, Criterion}; +use rust_decimal::Decimal; use pyo3::prelude::*; use pyo3::types::PyDict; -use rust_decimal::Decimal; fn decimal_via_extract(b: &mut Bencher<'_>) { Python::with_gil(|py| { - let locals = PyDict::new(py); - py.run( + let locals = PyDict::new_bound(py); + py.run_bound( r#" import decimal py_dec = decimal.Decimal("0.0") "#, None, - Some(locals), + Some(&locals), ) .unwrap(); let py_dec = locals.get_item("py_dec").unwrap().unwrap(); - b.iter(|| { - let _: Decimal = black_box(py_dec).extract().unwrap(); - }); + b.iter(|| black_box(&py_dec).extract::().unwrap()); }) } diff --git a/pyo3-benches/benches/bench_dict.rs b/pyo3-benches/benches/bench_dict.rs index d1c23466a39..8c3dfe023c8 100644 --- a/pyo3-benches/benches/bench_dict.rs +++ b/pyo3-benches/benches/bench_dict.rs @@ -1,16 +1,18 @@ +use std::collections::{BTreeMap, HashMap}; +use std::hint::black_box; + use codspeed_criterion_compat::{criterion_group, criterion_main, Bencher, Criterion}; use pyo3::types::IntoPyDict; use pyo3::{prelude::*, types::PyMapping}; -use std::collections::{BTreeMap, HashMap}; fn iter_dict(b: &mut Bencher<'_>) { Python::with_gil(|py| { const LEN: usize = 100_000; - let dict = (0..LEN as u64).map(|i| (i, i * 2)).into_py_dict(py); + let dict = (0..LEN as u64).map(|i| (i, i * 2)).into_py_dict_bound(py); let mut sum = 0; b.iter(|| { - for (k, _v) in dict { + for (k, _v) in &dict { let i: u64 = k.extract().unwrap(); sum += i; } @@ -21,14 +23,14 @@ fn iter_dict(b: &mut Bencher<'_>) { fn dict_new(b: &mut Bencher<'_>) { Python::with_gil(|py| { const LEN: usize = 50_000; - b.iter(|| (0..LEN as u64).map(|i| (i, i * 2)).into_py_dict(py)); + b.iter_with_large_drop(|| (0..LEN as u64).map(|i| (i, i * 2)).into_py_dict_bound(py)); }); } fn dict_get_item(b: &mut Bencher<'_>) { Python::with_gil(|py| { const LEN: usize = 50_000; - let dict = (0..LEN as u64).map(|i| (i, i * 2)).into_py_dict(py); + let dict = (0..LEN as u64).map(|i| (i, i * 2)).into_py_dict_bound(py); let mut sum = 0; b.iter(|| { for i in 0..LEN { @@ -46,16 +48,16 @@ fn dict_get_item(b: &mut Bencher<'_>) { fn extract_hashmap(b: &mut Bencher<'_>) { Python::with_gil(|py| { const LEN: usize = 100_000; - let dict = (0..LEN as u64).map(|i| (i, i * 2)).into_py_dict(py); - b.iter(|| HashMap::::extract(dict)); + let dict = (0..LEN as u64).map(|i| (i, i * 2)).into_py_dict_bound(py); + b.iter(|| HashMap::::extract_bound(&dict)); }); } fn extract_btreemap(b: &mut Bencher<'_>) { Python::with_gil(|py| { const LEN: usize = 100_000; - let dict = (0..LEN as u64).map(|i| (i, i * 2)).into_py_dict(py); - b.iter(|| BTreeMap::::extract(dict)); + let dict = (0..LEN as u64).map(|i| (i, i * 2)).into_py_dict_bound(py); + b.iter(|| BTreeMap::::extract_bound(&dict)); }); } @@ -63,21 +65,16 @@ fn extract_btreemap(b: &mut Bencher<'_>) { fn extract_hashbrown_map(b: &mut Bencher<'_>) { Python::with_gil(|py| { const LEN: usize = 100_000; - let dict = (0..LEN as u64).map(|i| (i, i * 2)).into_py_dict(py); - b.iter(|| hashbrown::HashMap::::extract(dict)); + let dict = (0..LEN as u64).map(|i| (i, i * 2)).into_py_dict_bound(py); + b.iter(|| hashbrown::HashMap::::extract_bound(&dict)); }); } fn mapping_from_dict(b: &mut Bencher<'_>) { Python::with_gil(|py| { const LEN: usize = 100_000; - let dict = (0..LEN as u64) - .map(|i| (i, i * 2)) - .into_py_dict(py) - .to_object(py); - b.iter(|| { - let _: &PyMapping = dict.extract(py).unwrap(); - }); + let dict = &(0..LEN as u64).map(|i| (i, i * 2)).into_py_dict_bound(py); + b.iter(|| black_box(dict).downcast::().unwrap()); }); } @@ -87,11 +84,10 @@ fn criterion_benchmark(c: &mut Criterion) { c.bench_function("dict_get_item", dict_get_item); c.bench_function("extract_hashmap", extract_hashmap); c.bench_function("extract_btreemap", extract_btreemap); + c.bench_function("mapping_from_dict", mapping_from_dict); #[cfg(feature = "hashbrown")] c.bench_function("extract_hashbrown_map", extract_hashbrown_map); - - c.bench_function("mapping_from_dict", mapping_from_dict); } criterion_group!(benches, criterion_benchmark); diff --git a/pyo3-benches/benches/bench_extract.rs b/pyo3-benches/benches/bench_extract.rs index 6ff22d3834d..9bb7ef60ab4 100644 --- a/pyo3-benches/benches/bench_extract.rs +++ b/pyo3-benches/benches/bench_extract.rs @@ -1,96 +1,89 @@ -use codspeed_criterion_compat::{black_box, criterion_group, criterion_main, Bencher, Criterion}; +use std::hint::black_box; + +use codspeed_criterion_compat::{criterion_group, criterion_main, Bencher, Criterion}; use pyo3::{ + prelude::*, types::{PyDict, PyFloat, PyInt, PyString}, - IntoPy, PyAny, PyObject, Python, }; fn extract_str_extract_success(bench: &mut Bencher<'_>) { Python::with_gil(|py| { - let s = PyString::new(py, "Hello, World!") as &PyAny; + let s = PyString::new_bound(py, "Hello, World!").into_any(); - bench.iter(|| { - let v = black_box(s).extract::<&str>().unwrap(); - black_box(v); - }); + bench.iter(|| black_box(&s).extract::<&str>().unwrap()); }); } fn extract_str_extract_fail(bench: &mut Bencher<'_>) { Python::with_gil(|py| { - let d = PyDict::new(py) as &PyAny; + let d = PyDict::new_bound(py).into_any(); - bench.iter(|| match black_box(d).extract::<&str>() { + bench.iter(|| match black_box(&d).extract::<&str>() { Ok(v) => panic!("should err {}", v), - Err(e) => black_box(e), + Err(e) => e, }); }); } +#[cfg(any(Py_3_10, not(Py_LIMITED_API)))] fn extract_str_downcast_success(bench: &mut Bencher<'_>) { Python::with_gil(|py| { - let s = PyString::new(py, "Hello, World!") as &PyAny; + let s = PyString::new_bound(py, "Hello, World!").into_any(); bench.iter(|| { - let py_str = black_box(s).downcast::().unwrap(); - let v = py_str.to_str().unwrap(); - black_box(v); + let py_str = black_box(&s).downcast::().unwrap(); + py_str.to_str().unwrap() }); }); } fn extract_str_downcast_fail(bench: &mut Bencher<'_>) { Python::with_gil(|py| { - let d = PyDict::new(py) as &PyAny; + let d = PyDict::new_bound(py).into_any(); - bench.iter(|| match black_box(d).downcast::() { + bench.iter(|| match black_box(&d).downcast::() { Ok(v) => panic!("should err {}", v), - Err(e) => black_box(e), + Err(e) => e, }); }); } fn extract_int_extract_success(bench: &mut Bencher<'_>) { Python::with_gil(|py| { - let int_obj: PyObject = 123.into_py(py); - let int = int_obj.as_ref(py); + let int = 123.to_object(py).into_bound(py); - bench.iter(|| { - let v = black_box(int).extract::().unwrap(); - black_box(v); - }); + bench.iter(|| black_box(&int).extract::().unwrap()); }); } fn extract_int_extract_fail(bench: &mut Bencher<'_>) { Python::with_gil(|py| { - let d = PyDict::new(py) as &PyAny; + let d = PyDict::new_bound(py).into_any(); - bench.iter(|| match black_box(d).extract::() { + bench.iter(|| match black_box(&d).extract::() { Ok(v) => panic!("should err {}", v), - Err(e) => black_box(e), + Err(e) => e, }); }); } fn extract_int_downcast_success(bench: &mut Bencher<'_>) { Python::with_gil(|py| { - let int_obj: PyObject = 123.into_py(py); - let int = int_obj.as_ref(py); + let int = 123.to_object(py).into_bound(py); bench.iter(|| { - let py_int = black_box(int).downcast::().unwrap(); - let v = py_int.extract::().unwrap(); - black_box(v); + let py_int = black_box(&int).downcast::().unwrap(); + py_int.extract::().unwrap() }); }); } fn extract_int_downcast_fail(bench: &mut Bencher<'_>) { Python::with_gil(|py| { - let d = PyDict::new(py) as &PyAny; + let d = PyDict::new_bound(py).into_any(); - bench.iter(|| match black_box(d).downcast::() { + bench.iter(|| match black_box(&d).downcast::() { Ok(v) => panic!("should err {}", v), Err(e) => black_box(e), }); @@ -99,47 +92,41 @@ fn extract_int_downcast_fail(bench: &mut Bencher<'_>) { fn extract_float_extract_success(bench: &mut Bencher<'_>) { Python::with_gil(|py| { - let float_obj: PyObject = 23.42.into_py(py); - let float = float_obj.as_ref(py); + let float = 23.42.to_object(py).into_bound(py); - bench.iter(|| { - let v = black_box(float).extract::().unwrap(); - black_box(v); - }); + bench.iter(|| black_box(&float).extract::().unwrap()); }); } fn extract_float_extract_fail(bench: &mut Bencher<'_>) { Python::with_gil(|py| { - let d = PyDict::new(py) as &PyAny; + let d = PyDict::new_bound(py).into_any(); - bench.iter(|| match black_box(d).extract::() { + bench.iter(|| match black_box(&d).extract::() { Ok(v) => panic!("should err {}", v), - Err(e) => black_box(e), + Err(e) => e, }); }); } fn extract_float_downcast_success(bench: &mut Bencher<'_>) { Python::with_gil(|py| { - let float_obj: PyObject = 23.42.into_py(py); - let float = float_obj.as_ref(py); + let float = 23.42.to_object(py).into_bound(py); bench.iter(|| { - let py_int = black_box(float).downcast::().unwrap(); - let v = py_int.extract::().unwrap(); - black_box(v); + let py_float = black_box(&float).downcast::().unwrap(); + py_float.value() }); }); } fn extract_float_downcast_fail(bench: &mut Bencher<'_>) { Python::with_gil(|py| { - let d = PyDict::new(py) as &PyAny; + let d = PyDict::new_bound(py).into_any(); - bench.iter(|| match black_box(d).downcast::() { + bench.iter(|| match black_box(&d).downcast::() { Ok(v) => panic!("should err {}", v), - Err(e) => black_box(e), + Err(e) => e, }); }); } @@ -147,6 +134,7 @@ fn extract_float_downcast_fail(bench: &mut Bencher<'_>) { fn criterion_benchmark(c: &mut Criterion) { c.bench_function("extract_str_extract_success", extract_str_extract_success); c.bench_function("extract_str_extract_fail", extract_str_extract_fail); + #[cfg(any(Py_3_10, not(Py_LIMITED_API)))] c.bench_function("extract_str_downcast_success", extract_str_downcast_success); c.bench_function("extract_str_downcast_fail", extract_str_downcast_fail); c.bench_function("extract_int_extract_success", extract_int_extract_success); diff --git a/pyo3-benches/benches/bench_frompyobject.rs b/pyo3-benches/benches/bench_frompyobject.rs index 1ad53c717f3..f53f116a154 100644 --- a/pyo3-benches/benches/bench_frompyobject.rs +++ b/pyo3-benches/benches/bench_frompyobject.rs @@ -1,11 +1,14 @@ -use codspeed_criterion_compat::{black_box, criterion_group, criterion_main, Bencher, Criterion}; +use std::hint::black_box; + +use codspeed_criterion_compat::{criterion_group, criterion_main, Bencher, Criterion}; use pyo3::{ prelude::*, - types::{PyFloat, PyList, PyString}, + types::{PyList, PyString}, }; #[derive(FromPyObject)] +#[allow(dead_code)] enum ManyTypes { Int(i32), Bytes(Vec), @@ -14,88 +17,72 @@ enum ManyTypes { fn enum_from_pyobject(b: &mut Bencher<'_>) { Python::with_gil(|py| { - let obj = PyString::new(py, "hello world"); - b.iter(|| { - let _: ManyTypes = obj.extract().unwrap(); - }); + let any = PyString::new_bound(py, "hello world").into_any(); + + b.iter(|| black_box(&any).extract::().unwrap()); }) } fn list_via_downcast(b: &mut Bencher<'_>) { Python::with_gil(|py| { - let any: &PyAny = PyList::empty(py).into(); + let any = PyList::empty_bound(py).into_any(); - b.iter(|| { - let _list: &PyList = black_box(any).downcast().unwrap(); - }); + b.iter(|| black_box(&any).downcast::().unwrap()); }) } fn list_via_extract(b: &mut Bencher<'_>) { Python::with_gil(|py| { - let any: &PyAny = PyList::empty(py).into(); + let any = PyList::empty_bound(py).into_any(); - b.iter(|| { - let _list: &PyList = black_box(any).extract().unwrap(); - }); + b.iter(|| black_box(&any).extract::>().unwrap()); }) } fn not_a_list_via_downcast(b: &mut Bencher<'_>) { Python::with_gil(|py| { - let any: &PyAny = PyString::new(py, "foobar").into(); + let any = PyString::new_bound(py, "foobar").into_any(); - b.iter(|| { - black_box(any).downcast::().unwrap_err(); - }); + b.iter(|| black_box(&any).downcast::().unwrap_err()); }) } fn not_a_list_via_extract(b: &mut Bencher<'_>) { Python::with_gil(|py| { - let any: &PyAny = PyString::new(py, "foobar").into(); + let any = PyString::new_bound(py, "foobar").into_any(); - b.iter(|| { - black_box(any).extract::<&PyList>().unwrap_err(); - }); + b.iter(|| black_box(&any).extract::>().unwrap_err()); }) } #[derive(FromPyObject)] enum ListOrNotList<'a> { - List(&'a PyList), - NotList(&'a PyAny), + List(Bound<'a, PyList>), + NotList(Bound<'a, PyAny>), } fn not_a_list_via_extract_enum(b: &mut Bencher<'_>) { Python::with_gil(|py| { - let any: &PyAny = PyString::new(py, "foobar").into(); + let any = PyString::new_bound(py, "foobar").into_any(); - b.iter(|| match black_box(any).extract::>() { + b.iter(|| match black_box(&any).extract::>() { Ok(ListOrNotList::List(_list)) => panic!(), - Ok(ListOrNotList::NotList(_any)) => (), + Ok(ListOrNotList::NotList(any)) => any, Err(_) => panic!(), }); }) } -fn f64_from_pyobject(b: &mut Bencher<'_>) { - Python::with_gil(|py| { - let obj = PyFloat::new(py, 1.234); - b.iter(|| { - let _: f64 = obj.extract().unwrap(); - }); - }) -} - fn criterion_benchmark(c: &mut Criterion) { c.bench_function("enum_from_pyobject", enum_from_pyobject); + c.bench_function("list_via_downcast", list_via_downcast); + c.bench_function("list_via_extract", list_via_extract); + c.bench_function("not_a_list_via_downcast", not_a_list_via_downcast); c.bench_function("not_a_list_via_extract", not_a_list_via_extract); c.bench_function("not_a_list_via_extract_enum", not_a_list_via_extract_enum); - c.bench_function("f64_from_pyobject", f64_from_pyobject); } criterion_group!(benches, criterion_benchmark); diff --git a/pyo3-benches/benches/bench_gil.rs b/pyo3-benches/benches/bench_gil.rs index b1b2ca0f0c6..fcb7a278cf0 100644 --- a/pyo3-benches/benches/bench_gil.rs +++ b/pyo3-benches/benches/bench_gil.rs @@ -1,16 +1,7 @@ -use codspeed_criterion_compat::{ - black_box, criterion_group, criterion_main, BatchSize, Bencher, Criterion, -}; +use codspeed_criterion_compat::{criterion_group, criterion_main, Bencher, Criterion}; +use std::hint::black_box; -use pyo3::{prelude::*, GILPool}; - -fn bench_clean_gilpool_new(b: &mut Bencher<'_>) { - Python::with_gil(|_py| { - b.iter(|| { - let _ = unsafe { GILPool::new() }; - }); - }); -} +use pyo3::prelude::*; fn bench_clean_acquire_gil(b: &mut Bencher<'_>) { // Acquiring first GIL will also create a "clean" GILPool, so this measures the Python overhead. @@ -18,15 +9,9 @@ fn bench_clean_acquire_gil(b: &mut Bencher<'_>) { } fn bench_dirty_acquire_gil(b: &mut Bencher<'_>) { - let obj: PyObject = Python::with_gil(|py| py.None().into()); - b.iter_batched( - || { - // Clone and drop an object so that the GILPool has work to do. - let _ = obj.clone(); - }, - |_| Python::with_gil(|_| {}), - BatchSize::NumBatches(1), - ); + let obj = Python::with_gil(|py| py.None()); + // Drop the returned clone of the object so that the reference pool has work to do. + b.iter(|| Python::with_gil(|py| obj.clone_ref(py))); } fn bench_allow_threads(b: &mut Bencher<'_>) { @@ -43,7 +28,6 @@ fn bench_local_allow_threads(b: &mut Bencher<'_>) { } fn criterion_benchmark(c: &mut Criterion) { - c.bench_function("clean_gilpool_new", bench_clean_gilpool_new); c.bench_function("clean_acquire_gil", bench_clean_acquire_gil); c.bench_function("dirty_acquire_gil", bench_dirty_acquire_gil); c.bench_function("allow_threads", bench_allow_threads); diff --git a/pyo3-benches/benches/bench_intern.rs b/pyo3-benches/benches/bench_intern.rs index d8dd1b8fd30..f9f9162a5ee 100644 --- a/pyo3-benches/benches/bench_intern.rs +++ b/pyo3-benches/benches/bench_intern.rs @@ -1,3 +1,5 @@ +use std::hint::black_box; + use codspeed_criterion_compat::{criterion_group, criterion_main, Bencher, Criterion}; use pyo3::prelude::*; @@ -6,17 +8,17 @@ use pyo3::intern; fn getattr_direct(b: &mut Bencher<'_>) { Python::with_gil(|py| { - let sys = py.import("sys").unwrap(); + let sys = &py.import_bound("sys").unwrap(); - b.iter(|| sys.getattr("version").unwrap()); + b.iter(|| black_box(sys).getattr("version").unwrap()); }); } fn getattr_intern(b: &mut Bencher<'_>) { Python::with_gil(|py| { - let sys = py.import("sys").unwrap(); + let sys = &py.import_bound("sys").unwrap(); - b.iter(|| sys.getattr(intern!(py, "version")).unwrap()); + b.iter(|| black_box(sys).getattr(intern!(py, "version")).unwrap()); }); } diff --git a/pyo3-benches/benches/bench_list.rs b/pyo3-benches/benches/bench_list.rs index 34bf9d855e5..dcbdb4779cb 100644 --- a/pyo3-benches/benches/bench_list.rs +++ b/pyo3-benches/benches/bench_list.rs @@ -1,3 +1,5 @@ +use std::hint::black_box; + use codspeed_criterion_compat::{criterion_group, criterion_main, Bencher, Criterion}; use pyo3::prelude::*; @@ -6,10 +8,10 @@ use pyo3::types::{PyList, PySequence}; fn iter_list(b: &mut Bencher<'_>) { Python::with_gil(|py| { const LEN: usize = 100_000; - let list = PyList::new(py, 0..LEN); + let list = PyList::new_bound(py, 0..LEN); let mut sum = 0; b.iter(|| { - for x in list { + for x in &list { let i: u64 = x.extract().unwrap(); sum += i; } @@ -20,14 +22,14 @@ fn iter_list(b: &mut Bencher<'_>) { fn list_new(b: &mut Bencher<'_>) { Python::with_gil(|py| { const LEN: usize = 50_000; - b.iter(|| PyList::new(py, 0..LEN)); + b.iter_with_large_drop(|| PyList::new_bound(py, 0..LEN)); }); } fn list_get_item(b: &mut Bencher<'_>) { Python::with_gil(|py| { const LEN: usize = 50_000; - let list = PyList::new(py, 0..LEN); + let list = PyList::new_bound(py, 0..LEN); let mut sum = 0; b.iter(|| { for i in 0..LEN { @@ -41,7 +43,7 @@ fn list_get_item(b: &mut Bencher<'_>) { fn list_get_item_unchecked(b: &mut Bencher<'_>) { Python::with_gil(|py| { const LEN: usize = 50_000; - let list = PyList::new(py, 0..LEN); + let list = PyList::new_bound(py, 0..LEN); let mut sum = 0; b.iter(|| { for i in 0..LEN { @@ -56,10 +58,8 @@ fn list_get_item_unchecked(b: &mut Bencher<'_>) { fn sequence_from_list(b: &mut Bencher<'_>) { Python::with_gil(|py| { const LEN: usize = 50_000; - let list = PyList::new(py, 0..LEN).to_object(py); - b.iter(|| { - let _: &PySequence = list.extract(py).unwrap(); - }); + let list = &PyList::new_bound(py, 0..LEN); + b.iter(|| black_box(list).downcast::().unwrap()); }); } diff --git a/pyo3-benches/benches/bench_pyobject.rs b/pyo3-benches/benches/bench_pyobject.rs index 169cf1f0666..af25d61ce6a 100644 --- a/pyo3-benches/benches/bench_pyobject.rs +++ b/pyo3-benches/benches/bench_pyobject.rs @@ -6,7 +6,7 @@ fn drop_many_objects(b: &mut Bencher<'_>) { Python::with_gil(|py| { b.iter(|| { for _ in 0..1000 { - std::mem::drop(PyObject::from(py.None())); + std::mem::drop(py.None()); } }); }); diff --git a/pyo3-benches/benches/bench_set.rs b/pyo3-benches/benches/bench_set.rs index 0753a2f9979..18134a15bd5 100644 --- a/pyo3-benches/benches/bench_set.rs +++ b/pyo3-benches/benches/bench_set.rs @@ -2,7 +2,10 @@ use codspeed_criterion_compat::{criterion_group, criterion_main, Bencher, Criter use pyo3::prelude::*; use pyo3::types::PySet; -use std::collections::{BTreeSet, HashSet}; +use std::{ + collections::{BTreeSet, HashSet}, + hint::black_box, +}; fn set_new(b: &mut Bencher<'_>) { Python::with_gil(|py| { @@ -10,21 +13,17 @@ fn set_new(b: &mut Bencher<'_>) { // Create Python objects up-front, so that the benchmark doesn't need to include // the cost of allocating LEN Python integers let elements: Vec = (0..LEN).map(|i| i.into_py(py)).collect(); - b.iter(|| { - let pool = unsafe { py.new_pool() }; - PySet::new(py, &elements).unwrap(); - drop(pool); - }); + b.iter_with_large_drop(|| PySet::new_bound(py, &elements).unwrap()); }); } fn iter_set(b: &mut Bencher<'_>) { Python::with_gil(|py| { const LEN: usize = 100_000; - let set = PySet::new(py, &(0..LEN).collect::>()).unwrap(); + let set = PySet::new_bound(py, &(0..LEN).collect::>()).unwrap(); let mut sum = 0; b.iter(|| { - for x in set { + for x in &set { let i: u64 = x.extract().unwrap(); sum += i; } @@ -35,16 +34,20 @@ fn iter_set(b: &mut Bencher<'_>) { fn extract_hashset(b: &mut Bencher<'_>) { Python::with_gil(|py| { const LEN: usize = 100_000; - let set = PySet::new(py, &(0..LEN).collect::>()).unwrap(); - b.iter(|| HashSet::::extract(set)); + let any = PySet::new_bound(py, &(0..LEN).collect::>()) + .unwrap() + .into_any(); + b.iter_with_large_drop(|| black_box(&any).extract::>()); }); } fn extract_btreeset(b: &mut Bencher<'_>) { Python::with_gil(|py| { const LEN: usize = 100_000; - let set = PySet::new(py, &(0..LEN).collect::>()).unwrap(); - b.iter(|| BTreeSet::::extract(set)); + let any = PySet::new_bound(py, &(0..LEN).collect::>()) + .unwrap() + .into_any(); + b.iter_with_large_drop(|| black_box(&any).extract::>()); }); } @@ -52,8 +55,10 @@ fn extract_btreeset(b: &mut Bencher<'_>) { fn extract_hashbrown_set(b: &mut Bencher<'_>) { Python::with_gil(|py| { const LEN: usize = 100_000; - let set = PySet::new(py, &(0..LEN).collect::>()).unwrap(); - b.iter(|| hashbrown::HashSet::::extract(set)); + let any = PySet::new_bound(py, &(0..LEN).collect::>()) + .unwrap() + .into_any(); + b.iter_with_large_drop(|| black_box(&any).extract::>()); }); } diff --git a/pyo3-benches/benches/bench_tuple.rs b/pyo3-benches/benches/bench_tuple.rs index f224ee1bc4d..3c0b56a0234 100644 --- a/pyo3-benches/benches/bench_tuple.rs +++ b/pyo3-benches/benches/bench_tuple.rs @@ -1,3 +1,5 @@ +use std::hint::black_box; + use codspeed_criterion_compat::{criterion_group, criterion_main, Bencher, Criterion}; use pyo3::prelude::*; @@ -6,10 +8,10 @@ use pyo3::types::{PyList, PySequence, PyTuple}; fn iter_tuple(b: &mut Bencher<'_>) { Python::with_gil(|py| { const LEN: usize = 100_000; - let tuple = PyTuple::new(py, 0..LEN); + let tuple = PyTuple::new_bound(py, 0..LEN); let mut sum = 0; b.iter(|| { - for x in tuple { + for x in tuple.iter_borrowed() { let i: u64 = x.extract().unwrap(); sum += i; } @@ -20,14 +22,14 @@ fn iter_tuple(b: &mut Bencher<'_>) { fn tuple_new(b: &mut Bencher<'_>) { Python::with_gil(|py| { const LEN: usize = 50_000; - b.iter(|| PyTuple::new(py, 0..LEN)); + b.iter_with_large_drop(|| PyTuple::new_bound(py, 0..LEN)); }); } fn tuple_get_item(b: &mut Bencher<'_>) { Python::with_gil(|py| { const LEN: usize = 50_000; - let tuple = PyTuple::new(py, 0..LEN); + let tuple = PyTuple::new_bound(py, 0..LEN); let mut sum = 0; b.iter(|| { for i in 0..LEN { @@ -41,7 +43,7 @@ fn tuple_get_item(b: &mut Bencher<'_>) { fn tuple_get_item_unchecked(b: &mut Bencher<'_>) { Python::with_gil(|py| { const LEN: usize = 50_000; - let tuple = PyTuple::new(py, 0..LEN); + let tuple = PyTuple::new_bound(py, 0..LEN); let mut sum = 0; b.iter(|| { for i in 0..LEN { @@ -53,33 +55,63 @@ fn tuple_get_item_unchecked(b: &mut Bencher<'_>) { }); } -fn sequence_from_tuple(b: &mut Bencher<'_>) { +fn tuple_get_borrowed_item(b: &mut Bencher<'_>) { Python::with_gil(|py| { const LEN: usize = 50_000; - let tuple = PyTuple::new(py, 0..LEN).to_object(py); - b.iter(|| tuple.extract::<&PySequence>(py).unwrap()); + let tuple = PyTuple::new_bound(py, 0..LEN); + let mut sum = 0; + b.iter(|| { + for i in 0..LEN { + sum += tuple + .get_borrowed_item(i) + .unwrap() + .extract::() + .unwrap(); + } + }); }); } -fn tuple_new_list(b: &mut Bencher<'_>) { +#[cfg(not(any(Py_LIMITED_API, PyPy)))] +fn tuple_get_borrowed_item_unchecked(b: &mut Bencher<'_>) { Python::with_gil(|py| { const LEN: usize = 50_000; - let tuple = PyTuple::new(py, 0..LEN); + let tuple = PyTuple::new_bound(py, 0..LEN); + let mut sum = 0; b.iter(|| { - let _pool = unsafe { py.new_pool() }; - let _ = PyList::new(py, tuple); + for i in 0..LEN { + unsafe { + sum += tuple + .get_borrowed_item_unchecked(i) + .extract::() + .unwrap(); + } + } }); }); } +fn sequence_from_tuple(b: &mut Bencher<'_>) { + Python::with_gil(|py| { + const LEN: usize = 50_000; + let tuple = PyTuple::new_bound(py, 0..LEN).into_any(); + b.iter(|| black_box(&tuple).downcast::().unwrap()); + }); +} + +fn tuple_new_list(b: &mut Bencher<'_>) { + Python::with_gil(|py| { + const LEN: usize = 50_000; + let tuple = PyTuple::new_bound(py, 0..LEN); + b.iter_with_large_drop(|| PyList::new_bound(py, tuple.iter_borrowed())); + }); +} + fn tuple_to_list(b: &mut Bencher<'_>) { Python::with_gil(|py| { const LEN: usize = 50_000; - let tuple = PyTuple::new(py, 0..LEN); - b.iter(|| { - let _pool = unsafe { py.new_pool() }; - let _ = tuple.to_list(); - }); + let tuple = PyTuple::new_bound(py, 0..LEN); + b.iter_with_large_drop(|| tuple.to_list()); }); } @@ -95,6 +127,12 @@ fn criterion_benchmark(c: &mut Criterion) { c.bench_function("tuple_get_item", tuple_get_item); #[cfg(not(any(Py_LIMITED_API, PyPy)))] c.bench_function("tuple_get_item_unchecked", tuple_get_item_unchecked); + c.bench_function("tuple_get_borrowed_item", tuple_get_borrowed_item); + #[cfg(not(any(Py_LIMITED_API, PyPy)))] + c.bench_function( + "tuple_get_borrowed_item_unchecked", + tuple_get_borrowed_item_unchecked, + ); c.bench_function("sequence_from_tuple", sequence_from_tuple); c.bench_function("tuple_new_list", tuple_new_list); c.bench_function("tuple_to_list", tuple_to_list); diff --git a/pyo3-build-config/Cargo.toml b/pyo3-build-config/Cargo.toml index 702e99a4aac..600237f8646 100644 --- a/pyo3-build-config/Cargo.toml +++ b/pyo3-build-config/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3-build-config" -version = "0.21.0-dev" +version = "0.22.0-dev" description = "Build configuration for the PyO3 ecosystem" authors = ["PyO3 Project and Contributors "] keywords = ["pyo3", "python", "cpython", "ffi"] @@ -13,11 +13,11 @@ edition = "2021" [dependencies] once_cell = "1" python3-dll-a = { version = "0.2.6", optional = true } -target-lexicon = "0.12" +target-lexicon = "0.12.14" [build-dependencies] python3-dll-a = { version = "0.2.6", optional = true } -target-lexicon = "0.12" +target-lexicon = "0.12.14" [features] default = [] diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index e188767fcd1..35c300da190 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -8,7 +8,6 @@ mod import_lib; use std::{ collections::{HashMap, HashSet}, - convert::AsRef, env, ffi::{OsStr, OsString}, fmt::Display, @@ -31,10 +30,16 @@ use crate::{ }; /// Minimum Python version PyO3 supports. -const MINIMUM_SUPPORTED_VERSION: PythonVersion = PythonVersion { major: 3, minor: 7 }; +pub(crate) const MINIMUM_SUPPORTED_VERSION: PythonVersion = PythonVersion { major: 3, minor: 7 }; + +/// GraalPy may implement the same CPython version over multiple releases. +const MINIMUM_SUPPORTED_VERSION_GRAALPY: PythonVersion = PythonVersion { + major: 24, + minor: 0, +}; /// Maximum Python version that can be used as minimum required Python version with abi3. -const ABI3_MAX_MINOR: u8 = 12; +pub(crate) const ABI3_MAX_MINOR: u8 = 12; /// Gets an environment variable owned by cargo. /// @@ -174,6 +179,11 @@ impl InterpreterConfig { See https://foss.heptapod.net/pypy/pypy/-/issues/3397 for more information." )); } + } else if self.implementation.is_graalpy() { + println!("cargo:rustc-cfg=GraalPy"); + if self.abi3 { + warn!("GraalPy does not support abi3 so the build artifacts will be version-specific."); + } } else if self.abi3 { out.push("cargo:rustc-cfg=Py_LIMITED_API".to_owned()); } @@ -198,6 +208,12 @@ import sys from sysconfig import get_config_var, get_platform PYPY = platform.python_implementation() == "PyPy" +GRAALPY = platform.python_implementation() == "GraalVM" + +if GRAALPY: + graalpy_ver = map(int, __graalpython__.get_graalvm_version().split('.')); + print("graalpy_major", next(graalpy_ver)) + print("graalpy_minor", next(graalpy_ver)) # sys.base_prefix is missing on Python versions older than 3.3; this allows the script to continue # so that the version mismatch can be reported in a nicer way later. @@ -227,7 +243,7 @@ SHARED = bool(get_config_var("Py_ENABLE_SHARED")) print("implementation", platform.python_implementation()) print("version_major", sys.version_info[0]) print("version_minor", sys.version_info[1]) -print("shared", PYPY or ANACONDA or WINDOWS or FRAMEWORK or SHARED) +print("shared", PYPY or GRAALPY or ANACONDA or WINDOWS or FRAMEWORK or SHARED) print_if_set("ld_version", get_config_var("LDVERSION")) print_if_set("libdir", get_config_var("LIBDIR")) print_if_set("base_prefix", base_prefix) @@ -245,6 +261,23 @@ print("ext_suffix", get_config_var("EXT_SUFFIX")) interpreter.as_ref().display() ); + if let Some(value) = map.get("graalpy_major") { + let graalpy_version = PythonVersion { + major: value + .parse() + .context("failed to parse GraalPy major version")?, + minor: map["graalpy_minor"] + .parse() + .context("failed to parse GraalPy minor version")?, + }; + ensure!( + graalpy_version >= MINIMUM_SUPPORTED_VERSION_GRAALPY, + "At least GraalPy version {} needed, got {}", + MINIMUM_SUPPORTED_VERSION_GRAALPY, + graalpy_version + ); + }; + let shared = map["shared"].as_str() == "True"; let version = PythonVersion { @@ -589,7 +622,7 @@ print("ext_suffix", get_config_var("EXT_SUFFIX")) /// Lowers the configured version to the abi3 version, if set. fn fixup_for_abi3_version(&mut self, abi3_version: Option) -> Result<()> { // PyPy doesn't support abi3; don't adjust the version - if self.implementation.is_pypy() { + if self.implementation.is_pypy() || self.implementation.is_graalpy() { return Ok(()); } @@ -648,6 +681,7 @@ impl FromStr for PythonVersion { pub enum PythonImplementation { CPython, PyPy, + GraalPy, } impl PythonImplementation { @@ -656,12 +690,19 @@ impl PythonImplementation { self == PythonImplementation::PyPy } + #[doc(hidden)] + pub fn is_graalpy(self) -> bool { + self == PythonImplementation::GraalPy + } + #[doc(hidden)] pub fn from_soabi(soabi: &str) -> Result { if soabi.starts_with("pypy") { Ok(PythonImplementation::PyPy) } else if soabi.starts_with("cpython") { Ok(PythonImplementation::CPython) + } else if soabi.starts_with("graalpy") { + Ok(PythonImplementation::GraalPy) } else { bail!("unsupported Python interpreter"); } @@ -673,6 +714,7 @@ impl Display for PythonImplementation { match self { PythonImplementation::CPython => write!(f, "CPython"), PythonImplementation::PyPy => write!(f, "PyPy"), + PythonImplementation::GraalPy => write!(f, "GraalVM"), } } } @@ -683,6 +725,7 @@ impl FromStr for PythonImplementation { match s { "CPython" => Ok(PythonImplementation::CPython), "PyPy" => Ok(PythonImplementation::PyPy), + "GraalVM" => Ok(PythonImplementation::GraalPy), _ => bail!("unknown interpreter: {}", s), } } @@ -701,6 +744,7 @@ fn have_python_interpreter() -> bool { /// Must be called from a PyO3 crate build script. fn is_abi3() -> bool { cargo_env_var("CARGO_FEATURE_ABI3").is_some() + || env_var("PYO3_USE_ABI3_FORWARD_COMPATIBILITY").map_or(false, |os_str| os_str == "1") } /// Gets the minimum supported Python version from PyO3 `abi3-py*` features. @@ -731,6 +775,8 @@ pub fn is_linking_libpython() -> bool { /// Must be called from a PyO3 crate build script. fn is_linking_libpython_for_target(target: &Triple) -> bool { target.operating_system == OperatingSystem::Windows + // See https://github.com/PyO3/pyo3/issues/4068#issuecomment-2051159852 + || target.operating_system == OperatingSystem::Aix || target.environment == Environment::Android || target.environment == Environment::Androideabi || !is_extension_module() @@ -760,7 +806,7 @@ pub struct CrossCompileConfig { /// The version of the Python library to link against. version: Option, - /// The target Python implementation hint (CPython or PyPy) + /// The target Python implementation hint (CPython, PyPy, GraalPy, ...) implementation: Option, /// The compile target triple (e.g. aarch64-unknown-linux-gnu) @@ -1264,6 +1310,15 @@ fn is_pypy_lib_dir(path: &str, v: &Option) -> bool { path == "lib_pypy" || path.starts_with(&pypy_version_pat) } +fn is_graalpy_lib_dir(path: &str, v: &Option) -> bool { + let graalpy_version_pat = if let Some(v) = v { + format!("graalpy{}", v) + } else { + "graalpy2".into() + }; + path == "lib_graalpython" || path.starts_with(&graalpy_version_pat) +} + fn is_cpython_lib_dir(path: &str, v: &Option) -> bool { let cpython_version_pat = if let Some(v) = v { format!("python{}", v) @@ -1297,6 +1352,7 @@ fn search_lib_dir(path: impl AsRef, cross: &CrossCompileConfig) -> Vec Result Result InterpreterConfig { - // FIXME: PyPy does not support the Stable ABI yet. + // FIXME: PyPy & GraalPy do not support the Stable ABI. let implementation = PythonImplementation::CPython; let abi3 = true; @@ -1524,7 +1580,7 @@ fn default_lib_name_windows( // CPython bug: linking against python3_d.dll raises error // https://github.com/python/cpython/issues/101614 format!("python{}{}_d", version.major, version.minor) - } else if abi3 && !implementation.is_pypy() { + } else if abi3 && !(implementation.is_pypy() || implementation.is_graalpy()) { WINDOWS_ABI3_LIB_NAME.to_owned() } else if mingw { // https://packages.msys2.org/base/mingw-w64-python @@ -1562,6 +1618,7 @@ fn default_lib_name_unix( format!("pypy{}-c", version.major) } } + PythonImplementation::GraalPy => "python-native".to_string(), } } @@ -1662,7 +1719,9 @@ pub fn find_interpreter() -> Result { .find(|bin| { if let Ok(out) = Command::new(bin).arg("--version").output() { // begin with `Python 3.X.X :: additional info` - out.stdout.starts_with(b"Python 3") || out.stderr.starts_with(b"Python 3") + out.stdout.starts_with(b"Python 3") + || out.stderr.starts_with(b"Python 3") + || out.stdout.starts_with(b"GraalPy 3") } else { false } @@ -1784,7 +1843,6 @@ fn unescape(escaped: &str) -> Vec { #[cfg(test)] mod tests { - use std::iter::FromIterator; use target_lexicon::triple; use super::*; diff --git a/pyo3-build-config/src/import_lib.rs b/pyo3-build-config/src/import_lib.rs index dc21638c3db..0925a861b5b 100644 --- a/pyo3-build-config/src/import_lib.rs +++ b/pyo3-build-config/src/import_lib.rs @@ -7,7 +7,7 @@ use python3_dll_a::ImportLibraryGenerator; use target_lexicon::{Architecture, OperatingSystem, Triple}; use super::{PythonImplementation, PythonVersion}; -use crate::errors::{Context, Result}; +use crate::errors::{Context, Error, Result}; /// Generates the `python3.dll` or `pythonXY.dll` import library for Windows targets. /// @@ -42,6 +42,9 @@ pub(super) fn generate_import_lib( let implementation = match py_impl { PythonImplementation::CPython => python3_dll_a::PythonImplementation::CPython, PythonImplementation::PyPy => python3_dll_a::PythonImplementation::PyPy, + PythonImplementation::GraalPy => { + return Err(Error::from("No support for GraalPy on Windows")) + } }; ImportLibraryGenerator::new(&arch, &env) diff --git a/pyo3-build-config/src/lib.rs b/pyo3-build-config/src/lib.rs index 576dd37024b..2b5e76e4b95 100644 --- a/pyo3-build-config/src/lib.rs +++ b/pyo3-build-config/src/lib.rs @@ -18,7 +18,6 @@ use std::{ use std::{env, process::Command, str::FromStr}; -#[cfg(feature = "resolve-config")] use once_cell::sync::OnceCell; pub use impl_::{ @@ -38,10 +37,12 @@ use target_lexicon::OperatingSystem; /// | `#[cfg(Py_3_7)]`, `#[cfg(Py_3_8)]`, `#[cfg(Py_3_9)]`, `#[cfg(Py_3_10)]` | These attributes mark code only for a given Python version and up. For example, `#[cfg(Py_3_7)]` marks code which can run on Python 3.7 **and newer**. | /// | `#[cfg(Py_LIMITED_API)]` | This marks code which is run when compiling with PyO3's `abi3` feature enabled. | /// | `#[cfg(PyPy)]` | This marks code which is run when compiling for PyPy. | +/// | `#[cfg(GraalPy)]` | This marks code which is run when compiling for GraalPy. | /// -/// For examples of how to use these attributes, [see PyO3's guide](https://pyo3.rs/latest/building_and_distribution/multiple_python_versions.html). +/// For examples of how to use these attributes, [see PyO3's guide](https://pyo3.rs/latest/building-and-distribution/multiple_python_versions.html). #[cfg(feature = "resolve-config")] pub fn use_pyo3_cfgs() { + print_expected_cfgs(); for cargo_command in get().build_script_outputs() { println!("{}", cargo_command) } @@ -85,6 +86,8 @@ pub fn get() -> &'static InterpreterConfig { .map(|path| path.exists()) .unwrap_or(false); + // CONFIG_FILE is generated in build.rs, so it's content can vary + #[allow(unknown_lints, clippy::const_is_empty)] if let Some(interpreter_config) = InterpreterConfig::from_cargo_dep_env() { interpreter_config } else if !CONFIG_FILE.is_empty() { @@ -131,28 +134,43 @@ fn resolve_cross_compile_config_path() -> Option { /// so this function is unstable. #[doc(hidden)] pub fn print_feature_cfgs() { - fn rustc_minor_version() -> Option { - let rustc = env::var_os("RUSTC")?; - let output = Command::new(rustc).arg("--version").output().ok()?; - let version = core::str::from_utf8(&output.stdout).ok()?; - let mut pieces = version.split('.'); - if pieces.next() != Some("rustc 1") { - return None; - } - pieces.next()?.parse().ok() - } - let rustc_minor_version = rustc_minor_version().unwrap_or(0); - // Enable use of const initializer for thread_local! on Rust 1.59 and greater - if rustc_minor_version >= 59 { - println!("cargo:rustc-cfg=thread_local_const_init"); - } - // invalid_from_utf8 lint was added in Rust 1.74 if rustc_minor_version >= 74 { println!("cargo:rustc-cfg=invalid_from_utf8_lint"); } + + if rustc_minor_version >= 78 { + println!("cargo:rustc-cfg=diagnostic_namespace"); + } +} + +/// Registers `pyo3`s config names as reachable cfg expressions +/// +/// - +/// - +#[doc(hidden)] +pub fn print_expected_cfgs() { + if rustc_minor_version().map_or(false, |version| version < 80) { + // rustc 1.80.0 stabilized `rustc-check-cfg` feature, don't emit before + return; + } + + println!("cargo:rustc-check-cfg=cfg(Py_LIMITED_API)"); + println!("cargo:rustc-check-cfg=cfg(PyPy)"); + println!("cargo:rustc-check-cfg=cfg(GraalPy)"); + println!("cargo:rustc-check-cfg=cfg(py_sys_config, values(\"Py_DEBUG\", \"Py_REF_DEBUG\", \"Py_TRACE_REFS\", \"COUNT_ALLOCS\"))"); + println!("cargo:rustc-check-cfg=cfg(invalid_from_utf8_lint)"); + println!("cargo:rustc-check-cfg=cfg(pyo3_disable_reference_pool)"); + println!("cargo:rustc-check-cfg=cfg(pyo3_leak_on_drop_without_reference_pool)"); + println!("cargo:rustc-check-cfg=cfg(diagnostic_namespace)"); + + // allow `Py_3_*` cfgs from the minimum supported version up to the + // maximum minor version (+1 for development for the next) + for i in impl_::MINIMUM_SUPPORTED_VERSION.minor..=impl_::ABI3_MAX_MINOR + 1 { + println!("cargo:rustc-check-cfg=cfg(Py_3_{i})"); + } } /// Private exports used in PyO3's build.rs @@ -181,6 +199,8 @@ pub mod pyo3_build_script_impl { /// correct value for CARGO_CFG_TARGET_OS). #[cfg(feature = "resolve-config")] pub fn resolve_interpreter_config() -> Result { + // CONFIG_FILE is generated in build.rs, so it's content can vary + #[allow(unknown_lints, clippy::const_is_empty)] if !CONFIG_FILE.is_empty() { let mut interperter_config = InterpreterConfig::from_reader(Cursor::new(CONFIG_FILE))?; interperter_config.generate_import_libs()?; @@ -211,6 +231,20 @@ pub mod pyo3_build_script_impl { } } +fn rustc_minor_version() -> Option { + static RUSTC_MINOR_VERSION: OnceCell> = OnceCell::new(); + *RUSTC_MINOR_VERSION.get_or_init(|| { + let rustc = env::var_os("RUSTC")?; + let output = Command::new(rustc).arg("--version").output().ok()?; + let version = core::str::from_utf8(&output.stdout).ok()?; + let mut pieces = version.split('.'); + if pieces.next() != Some("rustc 1") { + return None; + } + pieces.next()?.parse().ok() + }) +} + #[cfg(test)] mod tests { use super::*; diff --git a/pyo3-ffi/Cargo.toml b/pyo3-ffi/Cargo.toml index bc40d86ca19..865da93926a 100644 --- a/pyo3-ffi/Cargo.toml +++ b/pyo3-ffi/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3-ffi" -version = "0.21.0-dev" +version = "0.22.0-dev" description = "Python-API bindings for the PyO3 ecosystem" authors = ["PyO3 Project and Contributors "] keywords = ["pyo3", "python", "cpython", "ffi"] @@ -38,7 +38,7 @@ abi3-py312 = ["abi3", "pyo3-build-config/abi3-py312"] generate-import-lib = ["pyo3-build-config/python3-dll-a"] [build-dependencies] -pyo3-build-config = { path = "../pyo3-build-config", version = "0.21.0-dev", features = ["resolve-config"] } +pyo3-build-config = { path = "../pyo3-build-config", version = "=0.22.0-dev", features = ["resolve-config"] } [lints] workspace = true diff --git a/pyo3-ffi/README.md b/pyo3-ffi/README.md index 030c59d449c..283a7072357 100644 --- a/pyo3-ffi/README.md +++ b/pyo3-ffi/README.md @@ -14,7 +14,7 @@ Manual][capi] for up-to-date documentation. PyO3 supports the following software versions: - Python 3.7 and up (CPython and PyPy) - - Rust 1.56 and up + - Rust 1.63 and up # Example: Building Python Native modules @@ -184,7 +184,7 @@ can be easily converted to rust as well. [`maturin`]: https://github.com/PyO3/maturin "Build and publish crates with pyo3, rust-cpython and cffi bindings as well as rust binaries as python packages" [`pyo3-build-config`]: https://docs.rs/pyo3-build-config [feature flags]: https://doc.rust-lang.org/cargo/reference/features.html "Features - The Cargo Book" -[manual_builds]: https://pyo3.rs/latest/building_and_distribution.html#manual-builds "Manual builds - Building and Distribution - PyO3 user guide" +[manual_builds]: https://pyo3.rs/latest/building-and-distribution.html#manual-builds "Manual builds - Building and Distribution - PyO3 user guide" [setuptools-rust]: https://github.com/PyO3/setuptools-rust "Setuptools plugin for Rust extensions" [PEP 384]: https://www.python.org/dev/peps/pep-0384 "PEP 384 -- Defining a Stable ABI" [Features chapter of the guide]: https://pyo3.rs/latest/features.html#features-reference "Features Reference - PyO3 user guide" diff --git a/pyo3-ffi/build.rs b/pyo3-ffi/build.rs index 940d9808648..0f4931d6dc7 100644 --- a/pyo3-ffi/build.rs +++ b/pyo3-ffi/build.rs @@ -4,18 +4,105 @@ use pyo3_build_config::{ cargo_env_var, env_var, errors::Result, is_linking_libpython, resolve_interpreter_config, InterpreterConfig, PythonVersion, }, + PythonImplementation, }; /// Minimum Python version PyO3 supports. -const MINIMUM_SUPPORTED_VERSION: PythonVersion = PythonVersion { major: 3, minor: 7 }; +struct SupportedVersions { + min: PythonVersion, + max: PythonVersion, +} + +const SUPPORTED_VERSIONS_CPYTHON: SupportedVersions = SupportedVersions { + min: PythonVersion { major: 3, minor: 7 }, + max: PythonVersion { + major: 3, + minor: 13, + }, +}; + +const SUPPORTED_VERSIONS_PYPY: SupportedVersions = SupportedVersions { + min: PythonVersion { major: 3, minor: 7 }, + max: PythonVersion { + major: 3, + minor: 10, + }, +}; + +const SUPPORTED_VERSIONS_GRAALPY: SupportedVersions = SupportedVersions { + min: PythonVersion { + major: 3, + minor: 10, + }, + max: PythonVersion { + major: 3, + minor: 11, + }, +}; fn ensure_python_version(interpreter_config: &InterpreterConfig) -> Result<()> { - ensure!( - interpreter_config.version >= MINIMUM_SUPPORTED_VERSION, - "the configured Python interpreter version ({}) is lower than PyO3's minimum supported version ({})", - interpreter_config.version, - MINIMUM_SUPPORTED_VERSION, - ); + // This is an undocumented env var which is only really intended to be used in CI / for testing + // and development. + if std::env::var("UNSAFE_PYO3_SKIP_VERSION_CHECK").as_deref() == Ok("1") { + return Ok(()); + } + + match interpreter_config.implementation { + PythonImplementation::CPython => { + let versions = SUPPORTED_VERSIONS_CPYTHON; + ensure!( + interpreter_config.version >= versions.min, + "the configured Python interpreter version ({}) is lower than PyO3's minimum supported version ({})", + interpreter_config.version, + versions.min, + ); + ensure!( + interpreter_config.version <= versions.max || env_var("PYO3_USE_ABI3_FORWARD_COMPATIBILITY").map_or(false, |os_str| os_str == "1"), + "the configured Python interpreter version ({}) is newer than PyO3's maximum supported version ({})\n\ + = help: please check if an updated version of PyO3 is available. Current version: {}\n\ + = help: set PYO3_USE_ABI3_FORWARD_COMPATIBILITY=1 to suppress this check and build anyway using the stable ABI", + interpreter_config.version, + versions.max, + std::env::var("CARGO_PKG_VERSION").unwrap(), + ); + } + PythonImplementation::PyPy => { + let versions = SUPPORTED_VERSIONS_PYPY; + ensure!( + interpreter_config.version >= versions.min, + "the configured PyPy interpreter version ({}) is lower than PyO3's minimum supported version ({})", + interpreter_config.version, + versions.min, + ); + // PyO3 does not support abi3, so we cannot offer forward compatibility + ensure!( + interpreter_config.version <= versions.max, + "the configured PyPy interpreter version ({}) is newer than PyO3's maximum supported version ({})\n\ + = help: please check if an updated version of PyO3 is available. Current version: {}", + interpreter_config.version, + versions.max, + std::env::var("CARGO_PKG_VERSION").unwrap() + ); + } + PythonImplementation::GraalPy => { + let versions = SUPPORTED_VERSIONS_GRAALPY; + ensure!( + interpreter_config.version >= versions.min, + "the configured GraalPy interpreter version ({}) is lower than PyO3's minimum supported version ({})", + interpreter_config.version, + versions.min, + ); + // GraalPy does not support abi3, so we cannot offer forward compatibility + ensure!( + interpreter_config.version <= versions.max, + "the configured GraalPy interpreter version ({}) is newer than PyO3's maximum supported version ({})\n\ + = help: please check if an updated version of PyO3 is available. Current version: {}", + interpreter_config.version, + versions.max, + std::env::var("CARGO_PKG_VERSION").unwrap() + ); + } + } Ok(()) } @@ -102,7 +189,7 @@ fn configure_pyo3() -> Result<()> { println!("{}", line); } - // Emit cfgs like `thread_local_const_init` + // Emit cfgs like `invalid_from_utf8_lint` print_feature_cfgs(); Ok(()) @@ -118,6 +205,7 @@ fn print_config_and_exit(config: &InterpreterConfig) { } fn main() { + pyo3_build_config::print_expected_cfgs(); if let Err(e) = configure_pyo3() { eprintln!("error: {}", e.report()); std::process::exit(1) diff --git a/pyo3-ffi/src/abstract_.rs b/pyo3-ffi/src/abstract_.rs index 0b3b7dbb3c2..b5bf9cc3d35 100644 --- a/pyo3-ffi/src/abstract_.rs +++ b/pyo3-ffi/src/abstract_.rs @@ -23,6 +23,7 @@ pub unsafe fn PyObject_DelAttr(o: *mut PyObject, attr_name: *mut PyObject) -> c_ extern "C" { #[cfg(all( not(PyPy), + not(GraalPy), any(Py_3_10, all(not(Py_LIMITED_API), Py_3_9)) // Added to python in 3.9 but to limited API in 3.10 ))] #[cfg_attr(PyPy, link_name = "PyPyObject_CallNoArgs")] diff --git a/pyo3-ffi/src/boolobject.rs b/pyo3-ffi/src/boolobject.rs index 78972ff0835..10b5969fa4f 100644 --- a/pyo3-ffi/src/boolobject.rs +++ b/pyo3-ffi/src/boolobject.rs @@ -1,3 +1,4 @@ +#[cfg(not(GraalPy))] use crate::longobject::PyLongObject; use crate::object::*; use std::os::raw::{c_int, c_long}; @@ -16,20 +17,33 @@ pub unsafe fn PyBool_Check(op: *mut PyObject) -> c_int { #[cfg_attr(windows, link(name = "pythonXY"))] extern "C" { + #[cfg(not(GraalPy))] #[cfg_attr(PyPy, link_name = "_PyPy_FalseStruct")] static mut _Py_FalseStruct: PyLongObject; + #[cfg(not(GraalPy))] #[cfg_attr(PyPy, link_name = "_PyPy_TrueStruct")] static mut _Py_TrueStruct: PyLongObject; + + #[cfg(GraalPy)] + static mut _Py_FalseStructReference: *mut PyObject; + #[cfg(GraalPy)] + static mut _Py_TrueStructReference: *mut PyObject; } #[inline] pub unsafe fn Py_False() -> *mut PyObject { - addr_of_mut!(_Py_FalseStruct) as *mut PyObject + #[cfg(not(GraalPy))] + return addr_of_mut!(_Py_FalseStruct) as *mut PyObject; + #[cfg(GraalPy)] + return _Py_FalseStructReference; } #[inline] pub unsafe fn Py_True() -> *mut PyObject { - addr_of_mut!(_Py_TrueStruct) as *mut PyObject + #[cfg(not(GraalPy))] + return addr_of_mut!(_Py_TrueStruct) as *mut PyObject; + #[cfg(GraalPy)] + return _Py_TrueStructReference; } #[inline] diff --git a/pyo3-ffi/src/bytearrayobject.rs b/pyo3-ffi/src/bytearrayobject.rs index a37deb410f7..c09eac5b22c 100644 --- a/pyo3-ffi/src/bytearrayobject.rs +++ b/pyo3-ffi/src/bytearrayobject.rs @@ -3,7 +3,7 @@ use crate::pyport::Py_ssize_t; use std::os::raw::{c_char, c_int}; use std::ptr::addr_of_mut; -#[cfg(not(any(PyPy, Py_LIMITED_API)))] +#[cfg(not(any(PyPy, GraalPy, Py_LIMITED_API)))] #[repr(C)] #[derive(Copy, Clone)] pub struct PyByteArrayObject { @@ -17,7 +17,7 @@ pub struct PyByteArrayObject { pub ob_exports: c_int, } -#[cfg(any(PyPy, Py_LIMITED_API))] +#[cfg(any(PyPy, GraalPy, Py_LIMITED_API))] opaque_struct!(PyByteArrayObject); #[cfg_attr(windows, link(name = "pythonXY"))] diff --git a/pyo3-ffi/src/complexobject.rs b/pyo3-ffi/src/complexobject.rs index 339f5d8c81a..a03d9b00932 100644 --- a/pyo3-ffi/src/complexobject.rs +++ b/pyo3-ffi/src/complexobject.rs @@ -30,6 +30,7 @@ extern "C" { // non-limited pub struct PyComplexObject { pub ob_base: PyObject, + #[cfg(not(GraalPy))] pub cval: Py_complex, } diff --git a/pyo3-ffi/src/cpython/abstract_.rs b/pyo3-ffi/src/cpython/abstract_.rs index d2e3ca9d67a..ad28216cbff 100644 --- a/pyo3-ffi/src/cpython/abstract_.rs +++ b/pyo3-ffi/src/cpython/abstract_.rs @@ -4,9 +4,7 @@ use std::os::raw::{c_char, c_int}; #[cfg(not(Py_3_11))] use crate::Py_buffer; -#[cfg(Py_3_8)] -use crate::pyport::PY_SSIZE_T_MAX; -#[cfg(all(Py_3_8, not(PyPy)))] +#[cfg(all(Py_3_8, not(any(PyPy, GraalPy))))] use crate::{ vectorcallfunc, PyCallable_Check, PyThreadState, PyThreadState_GET, PyTuple_Check, PyType_HasFeature, Py_TPFLAGS_HAVE_VECTORCALL, @@ -15,15 +13,15 @@ use crate::{ use libc::size_t; extern "C" { - #[cfg(all(Py_3_8, not(PyPy)))] + #[cfg(all(Py_3_8, not(any(PyPy, GraalPy))))] pub fn _PyStack_AsDict(values: *const *mut PyObject, kwnames: *mut PyObject) -> *mut PyObject; } -#[cfg(all(Py_3_8, not(PyPy)))] +#[cfg(all(Py_3_8, not(any(PyPy, GraalPy))))] const _PY_FASTCALL_SMALL_STACK: size_t = 5; extern "C" { - #[cfg(all(Py_3_8, not(PyPy)))] + #[cfg(all(Py_3_8, not(any(PyPy, GraalPy))))] pub fn _Py_CheckFunctionResult( tstate: *mut PyThreadState, callable: *mut PyObject, @@ -31,7 +29,7 @@ extern "C" { where_: *const c_char, ) -> *mut PyObject; - #[cfg(all(Py_3_8, not(PyPy)))] + #[cfg(all(Py_3_8, not(any(PyPy, GraalPy))))] pub fn _PyObject_MakeTpCall( tstate: *mut PyThreadState, callable: *mut PyObject, @@ -42,17 +40,17 @@ extern "C" { } #[cfg(Py_3_8)] -const PY_VECTORCALL_ARGUMENTS_OFFSET: Py_ssize_t = - 1 << (8 * std::mem::size_of::() as Py_ssize_t - 1); +const PY_VECTORCALL_ARGUMENTS_OFFSET: size_t = + 1 << (8 * std::mem::size_of::() as size_t - 1); #[cfg(Py_3_8)] #[inline(always)] pub unsafe fn PyVectorcall_NARGS(n: size_t) -> Py_ssize_t { - assert!(n <= (PY_SSIZE_T_MAX as size_t)); - (n as Py_ssize_t) & !PY_VECTORCALL_ARGUMENTS_OFFSET + let n = n & !PY_VECTORCALL_ARGUMENTS_OFFSET; + n.try_into().expect("cannot fail due to mask") } -#[cfg(all(Py_3_8, not(PyPy)))] +#[cfg(all(Py_3_8, not(any(PyPy, GraalPy))))] #[inline(always)] pub unsafe fn PyVectorcall_Function(callable: *mut PyObject) -> Option { assert!(!callable.is_null()); @@ -67,7 +65,7 @@ pub unsafe fn PyVectorcall_Function(callable: *mut PyObject) -> Option *mut PyObject; #[cfg(Py_3_8)] - #[cfg_attr(all(not(PyPy), not(Py_3_9)), link_name = "_PyObject_VectorcallDict")] + #[cfg_attr( + all(not(any(PyPy, GraalPy)), not(Py_3_9)), + link_name = "_PyObject_VectorcallDict" + )] #[cfg_attr(all(PyPy, not(Py_3_9)), link_name = "_PyPyObject_VectorcallDict")] #[cfg_attr(all(PyPy, Py_3_9), link_name = "PyPyObject_VectorcallDict")] pub fn PyObject_VectorcallDict( @@ -134,7 +135,7 @@ extern "C" { ) -> *mut PyObject; } -#[cfg(all(Py_3_8, not(PyPy)))] +#[cfg(all(Py_3_8, not(any(PyPy, GraalPy))))] #[inline(always)] pub unsafe fn _PyObject_FastCallTstate( tstate: *mut PyThreadState, @@ -145,7 +146,7 @@ pub unsafe fn _PyObject_FastCallTstate( _PyObject_VectorcallTstate(tstate, func, args, nargs as size_t, std::ptr::null_mut()) } -#[cfg(all(Py_3_8, not(PyPy)))] +#[cfg(all(Py_3_8, not(any(PyPy, GraalPy))))] #[inline(always)] pub unsafe fn _PyObject_FastCall( func: *mut PyObject, @@ -155,7 +156,7 @@ pub unsafe fn _PyObject_FastCall( _PyObject_FastCallTstate(PyThreadState_GET(), func, args, nargs) } -#[cfg(all(Py_3_8, not(PyPy)))] +#[cfg(all(Py_3_8, not(any(PyPy, GraalPy))))] #[inline(always)] pub unsafe fn _PyObject_CallNoArg(func: *mut PyObject) -> *mut PyObject { _PyObject_VectorcallTstate( @@ -173,7 +174,7 @@ extern "C" { pub fn _PyObject_CallNoArg(func: *mut PyObject) -> *mut PyObject; } -#[cfg(all(Py_3_8, not(PyPy)))] +#[cfg(all(Py_3_8, not(any(PyPy, GraalPy))))] #[inline(always)] pub unsafe fn PyObject_CallOneArg(func: *mut PyObject, arg: *mut PyObject) -> *mut PyObject { assert!(!arg.is_null()); @@ -181,11 +182,11 @@ pub unsafe fn PyObject_CallOneArg(func: *mut PyObject, arg: *mut PyObject) -> *m let args = args_array.as_ptr().offset(1); // For PY_VECTORCALL_ARGUMENTS_OFFSET let tstate = PyThreadState_GET(); let nargsf = 1 | PY_VECTORCALL_ARGUMENTS_OFFSET; - _PyObject_VectorcallTstate(tstate, func, args, nargsf as size_t, std::ptr::null_mut()) + _PyObject_VectorcallTstate(tstate, func, args, nargsf, std::ptr::null_mut()) } extern "C" { - #[cfg(all(Py_3_9, not(PyPy)))] + #[cfg(all(Py_3_9, not(any(PyPy, GraalPy))))] pub fn PyObject_VectorcallMethod( name: *mut PyObject, args: *const *mut PyObject, @@ -194,7 +195,7 @@ extern "C" { ) -> *mut PyObject; } -#[cfg(all(Py_3_9, not(PyPy)))] +#[cfg(all(Py_3_9, not(any(PyPy, GraalPy))))] #[inline(always)] pub unsafe fn PyObject_CallMethodNoArgs( self_: *mut PyObject, @@ -203,12 +204,12 @@ pub unsafe fn PyObject_CallMethodNoArgs( PyObject_VectorcallMethod( name, &self_, - 1 | PY_VECTORCALL_ARGUMENTS_OFFSET as size_t, + 1 | PY_VECTORCALL_ARGUMENTS_OFFSET, std::ptr::null_mut(), ) } -#[cfg(all(Py_3_9, not(PyPy)))] +#[cfg(all(Py_3_9, not(any(PyPy, GraalPy))))] #[inline(always)] pub unsafe fn PyObject_CallMethodOneArg( self_: *mut PyObject, @@ -220,7 +221,7 @@ pub unsafe fn PyObject_CallMethodOneArg( PyObject_VectorcallMethod( name, args.as_ptr(), - 2 | PY_VECTORCALL_ARGUMENTS_OFFSET as size_t, + 2 | PY_VECTORCALL_ARGUMENTS_OFFSET, std::ptr::null_mut(), ) } @@ -236,7 +237,7 @@ extern "C" { pub fn PyObject_LengthHint(o: *mut PyObject, arg1: Py_ssize_t) -> Py_ssize_t; #[cfg(not(Py_3_11))] // moved to src/buffer.rs from 3.11 - #[cfg(all(Py_3_9, not(PyPy)))] + #[cfg(all(Py_3_9, not(any(PyPy, GraalPy))))] pub fn PyObject_CheckBuffer(obj: *mut PyObject) -> c_int; } @@ -308,7 +309,7 @@ pub const PY_ITERSEARCH_INDEX: c_int = 2; pub const PY_ITERSEARCH_CONTAINS: c_int = 3; extern "C" { - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] pub fn _PySequence_IterSearch( seq: *mut PyObject, obj: *mut PyObject, diff --git a/pyo3-ffi/src/cpython/bytesobject.rs b/pyo3-ffi/src/cpython/bytesobject.rs index 912fc0ac427..fb0b38cf1d8 100644 --- a/pyo3-ffi/src/cpython/bytesobject.rs +++ b/pyo3-ffi/src/cpython/bytesobject.rs @@ -1,10 +1,10 @@ use crate::object::*; use crate::Py_ssize_t; -#[cfg(not(any(PyPy, Py_LIMITED_API)))] +#[cfg(not(any(PyPy, GraalPy, Py_LIMITED_API)))] use std::os::raw::c_char; use std::os::raw::c_int; -#[cfg(not(any(PyPy, Py_LIMITED_API)))] +#[cfg(not(any(PyPy, GraalPy, Py_LIMITED_API)))] #[repr(C)] #[derive(Copy, Clone)] pub struct PyBytesObject { @@ -13,7 +13,7 @@ pub struct PyBytesObject { pub ob_sval: [c_char; 1], } -#[cfg(any(PyPy, Py_LIMITED_API))] +#[cfg(any(PyPy, GraalPy, Py_LIMITED_API))] opaque_struct!(PyBytesObject); extern "C" { diff --git a/pyo3-ffi/src/cpython/code.rs b/pyo3-ffi/src/cpython/code.rs index 05f21a137b5..86e862f21ab 100644 --- a/pyo3-ffi/src/cpython/code.rs +++ b/pyo3-ffi/src/cpython/code.rs @@ -3,10 +3,10 @@ use crate::pyport::Py_ssize_t; #[allow(unused_imports)] use std::os::raw::{c_char, c_int, c_short, c_uchar, c_void}; -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] use std::ptr::addr_of_mut; -#[cfg(all(Py_3_8, not(PyPy), not(Py_3_11)))] +#[cfg(all(Py_3_8, not(any(PyPy, GraalPy)), not(Py_3_11)))] opaque_struct!(_PyOpcache); #[cfg(Py_3_12)] @@ -20,7 +20,11 @@ pub const _PY_MONITORING_EVENTS: usize = 17; #[repr(C)] #[derive(Clone, Copy)] pub struct _Py_LocalMonitors { - pub tools: [u8; _PY_MONITORING_UNGROUPED_EVENTS], + pub tools: [u8; if cfg!(Py_3_13) { + _PY_MONITORING_LOCAL_EVENTS + } else { + _PY_MONITORING_UNGROUPED_EVENTS + }], } #[cfg(Py_3_12)] @@ -73,10 +77,10 @@ pub struct _PyCoMonitoringData { pub per_instruction_tools: *mut u8, } -#[cfg(all(not(PyPy), not(Py_3_7)))] +#[cfg(all(not(any(PyPy, GraalPy)), not(Py_3_7)))] opaque_struct!(PyCodeObject); -#[cfg(all(not(PyPy), Py_3_7, not(Py_3_8)))] +#[cfg(all(not(any(PyPy, GraalPy)), Py_3_7, not(Py_3_8)))] #[repr(C)] #[derive(Copy, Clone)] pub struct PyCodeObject { @@ -102,7 +106,10 @@ pub struct PyCodeObject { pub co_extra: *mut c_void, } -#[cfg(all(not(PyPy), Py_3_8, not(Py_3_11)))] +#[cfg(Py_3_13)] +opaque_struct!(_PyExecutorArray); + +#[cfg(all(not(any(PyPy, GraalPy)), Py_3_8, not(Py_3_11)))] #[repr(C)] #[derive(Copy, Clone)] pub struct PyCodeObject { @@ -136,7 +143,7 @@ pub struct PyCodeObject { pub co_opcache_size: c_uchar, } -#[cfg(all(not(PyPy), Py_3_11))] +#[cfg(all(not(any(PyPy, GraalPy)), Py_3_11))] #[repr(C)] #[derive(Copy, Clone)] pub struct PyCodeObject { @@ -176,6 +183,8 @@ pub struct PyCodeObject { pub _co_code: *mut PyObject, #[cfg(not(Py_3_12))] pub _co_linearray: *mut c_char, + #[cfg(Py_3_13)] + pub co_executors: *mut _PyExecutorArray, #[cfg(Py_3_12)] pub _co_cached: *mut _PyCoCached, #[cfg(Py_3_12)] @@ -230,25 +239,26 @@ pub const CO_FUTURE_GENERATOR_STOP: c_int = 0x8_0000; pub const CO_MAXBLOCKS: usize = 20; +#[cfg(not(any(PyPy, GraalPy)))] #[cfg_attr(windows, link(name = "pythonXY"))] extern "C" { pub static mut PyCode_Type: PyTypeObject; } #[inline] -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] pub unsafe fn PyCode_Check(op: *mut PyObject) -> c_int { (Py_TYPE(op) == addr_of_mut!(PyCode_Type)) as c_int } #[inline] -#[cfg(all(not(PyPy), Py_3_10, not(Py_3_11)))] +#[cfg(all(not(any(PyPy, GraalPy)), Py_3_10, not(Py_3_11)))] pub unsafe fn PyCode_GetNumFree(op: *mut PyCodeObject) -> Py_ssize_t { crate::PyTuple_GET_SIZE((*op).co_freevars) } #[inline] -#[cfg(all(not(Py_3_10), Py_3_11, not(PyPy)))] +#[cfg(all(not(Py_3_10), Py_3_11, not(any(PyPy, GraalPy))))] pub unsafe fn PyCode_GetNumFree(op: *mut PyCodeObject) -> c_int { (*op).co_nfreevars } @@ -264,6 +274,7 @@ extern "C" { } extern "C" { + #[cfg(not(GraalPy))] #[cfg_attr(PyPy, link_name = "PyPyCode_New")] pub fn PyCode_New( argcount: c_int, @@ -282,6 +293,7 @@ extern "C" { firstlineno: c_int, lnotab: *mut PyObject, ) -> *mut PyCodeObject; + #[cfg(not(GraalPy))] #[cfg(Py_3_8)] pub fn PyCode_NewWithPosOnlyArgs( argcount: c_int, @@ -301,12 +313,14 @@ extern "C" { firstlineno: c_int, lnotab: *mut PyObject, ) -> *mut PyCodeObject; + #[cfg(not(GraalPy))] #[cfg_attr(PyPy, link_name = "PyPyCode_NewEmpty")] pub fn PyCode_NewEmpty( filename: *const c_char, funcname: *const c_char, firstlineno: c_int, ) -> *mut PyCodeObject; + #[cfg(not(GraalPy))] pub fn PyCode_Addr2Line(arg1: *mut PyCodeObject, arg2: c_int) -> c_int; // skipped PyCodeAddressRange "for internal use only" // skipped _PyCode_CheckLineNumber diff --git a/pyo3-ffi/src/cpython/compile.rs b/pyo3-ffi/src/cpython/compile.rs index 71af81e83e5..79f06c92003 100644 --- a/pyo3-ffi/src/cpython/compile.rs +++ b/pyo3-ffi/src/cpython/compile.rs @@ -30,7 +30,7 @@ pub struct PyCompilerFlags { // skipped non-limited _PyCompilerFlags_INIT -#[cfg(all(Py_3_12, not(PyPy)))] +#[cfg(all(Py_3_12, not(any(Py_3_13, PyPy, GraalPy))))] #[repr(C)] #[derive(Copy, Clone)] pub struct _PyCompilerSrcLocation { @@ -42,7 +42,7 @@ pub struct _PyCompilerSrcLocation { // skipped SRC_LOCATION_FROM_AST -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy, Py_3_13)))] #[repr(C)] #[derive(Copy, Clone)] pub struct PyFutureFeatures { diff --git a/pyo3-ffi/src/cpython/dictobject.rs b/pyo3-ffi/src/cpython/dictobject.rs index 4af990a2d9a..74b970ebac2 100644 --- a/pyo3-ffi/src/cpython/dictobject.rs +++ b/pyo3-ffi/src/cpython/dictobject.rs @@ -7,6 +7,7 @@ opaque_struct!(PyDictKeysObject); #[cfg(Py_3_11)] opaque_struct!(PyDictValues); +#[cfg(not(GraalPy))] #[repr(C)] #[derive(Debug)] pub struct PyDictObject { diff --git a/pyo3-ffi/src/cpython/floatobject.rs b/pyo3-ffi/src/cpython/floatobject.rs index e33da0b91b9..8c7ee88543d 100644 --- a/pyo3-ffi/src/cpython/floatobject.rs +++ b/pyo3-ffi/src/cpython/floatobject.rs @@ -1,9 +1,12 @@ +#[cfg(GraalPy)] +use crate::PyFloat_AsDouble; use crate::{PyFloat_Check, PyObject}; use std::os::raw::c_double; #[repr(C)] pub struct PyFloatObject { pub ob_base: PyObject, + #[cfg(not(GraalPy))] pub ob_fval: c_double, } @@ -15,7 +18,10 @@ pub unsafe fn _PyFloat_CAST(op: *mut PyObject) -> *mut PyFloatObject { #[inline] pub unsafe fn PyFloat_AS_DOUBLE(op: *mut PyObject) -> c_double { - (*_PyFloat_CAST(op)).ob_fval + #[cfg(not(GraalPy))] + return (*_PyFloat_CAST(op)).ob_fval; + #[cfg(GraalPy)] + return PyFloat_AsDouble(op); } // skipped PyFloat_Pack2 diff --git a/pyo3-ffi/src/cpython/frameobject.rs b/pyo3-ffi/src/cpython/frameobject.rs index 7410000ef45..a85818ace0a 100644 --- a/pyo3-ffi/src/cpython/frameobject.rs +++ b/pyo3-ffi/src/cpython/frameobject.rs @@ -1,17 +1,19 @@ +#[cfg(not(GraalPy))] use crate::cpython::code::PyCodeObject; use crate::object::*; +#[cfg(not(GraalPy))] use crate::pystate::PyThreadState; -#[cfg(not(any(PyPy, Py_3_11)))] +#[cfg(not(any(PyPy, GraalPy, Py_3_11)))] use std::os::raw::c_char; use std::os::raw::c_int; use std::ptr::addr_of_mut; -#[cfg(not(any(PyPy, Py_3_11)))] +#[cfg(not(any(PyPy, GraalPy, Py_3_11)))] pub type PyFrameState = c_char; #[repr(C)] #[derive(Copy, Clone)] -#[cfg(not(any(PyPy, Py_3_11)))] +#[cfg(not(any(PyPy, GraalPy, Py_3_11)))] pub struct PyTryBlock { pub b_type: c_int, pub b_handler: c_int, @@ -20,7 +22,7 @@ pub struct PyTryBlock { #[repr(C)] #[derive(Copy, Clone)] -#[cfg(not(any(PyPy, Py_3_11)))] +#[cfg(not(any(PyPy, GraalPy, Py_3_11)))] pub struct PyFrameObject { pub ob_base: PyVarObject, pub f_back: *mut PyFrameObject, @@ -51,7 +53,7 @@ pub struct PyFrameObject { pub f_localsplus: [*mut PyObject; 1], } -#[cfg(any(PyPy, Py_3_11))] +#[cfg(any(PyPy, GraalPy, Py_3_11))] opaque_struct!(PyFrameObject); // skipped _PyFrame_IsRunnable @@ -69,6 +71,7 @@ pub unsafe fn PyFrame_Check(op: *mut PyObject) -> c_int { } extern "C" { + #[cfg(not(GraalPy))] #[cfg_attr(PyPy, link_name = "PyPyFrame_New")] pub fn PyFrame_New( tstate: *mut PyThreadState, @@ -79,7 +82,7 @@ extern "C" { // skipped _PyFrame_New_NoTrack pub fn PyFrame_BlockSetup(f: *mut PyFrameObject, _type: c_int, handler: c_int, level: c_int); - #[cfg(not(any(PyPy, Py_3_11)))] + #[cfg(not(any(PyPy, GraalPy, Py_3_11)))] pub fn PyFrame_BlockPop(f: *mut PyFrameObject) -> *mut PyTryBlock; pub fn PyFrame_LocalsToFast(f: *mut PyFrameObject, clear: c_int); diff --git a/pyo3-ffi/src/cpython/funcobject.rs b/pyo3-ffi/src/cpython/funcobject.rs index 1e9ee0cc18c..25de30d57f7 100644 --- a/pyo3-ffi/src/cpython/funcobject.rs +++ b/pyo3-ffi/src/cpython/funcobject.rs @@ -4,7 +4,7 @@ use std::ptr::addr_of_mut; use crate::PyObject; -#[cfg(all(not(PyPy), not(Py_3_10)))] +#[cfg(all(not(any(PyPy, GraalPy)), not(Py_3_10)))] #[repr(C)] pub struct PyFunctionObject { pub ob_base: PyObject, @@ -24,7 +24,7 @@ pub struct PyFunctionObject { pub vectorcall: Option, } -#[cfg(all(not(PyPy), Py_3_10))] +#[cfg(all(not(any(PyPy, GraalPy)), Py_3_10))] #[repr(C)] pub struct PyFunctionObject { pub ob_base: PyObject, @@ -55,6 +55,11 @@ pub struct PyFunctionObject { pub func_name: *mut PyObject, } +#[cfg(GraalPy)] +pub struct PyFunctionObject { + pub ob_base: PyObject, +} + #[cfg_attr(windows, link(name = "pythonXY"))] extern "C" { #[cfg(not(all(PyPy, not(Py_3_8))))] diff --git a/pyo3-ffi/src/cpython/genobject.rs b/pyo3-ffi/src/cpython/genobject.rs index aaa03f82eef..73ebdb491ff 100644 --- a/pyo3-ffi/src/cpython/genobject.rs +++ b/pyo3-ffi/src/cpython/genobject.rs @@ -1,13 +1,13 @@ use crate::object::*; use crate::PyFrameObject; -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] use crate::_PyErr_StackItem; #[cfg(Py_3_11)] use std::os::raw::c_char; use std::os::raw::c_int; use std::ptr::addr_of_mut; -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] #[repr(C)] #[derive(Copy, Clone)] pub struct PyGenObject { diff --git a/pyo3-ffi/src/cpython/import.rs b/pyo3-ffi/src/cpython/import.rs index aafd71a8355..697d68a419c 100644 --- a/pyo3-ffi/src/cpython/import.rs +++ b/pyo3-ffi/src/cpython/import.rs @@ -57,7 +57,7 @@ pub struct _frozen { pub size: c_int, #[cfg(Py_3_11)] pub is_package: c_int, - #[cfg(Py_3_11)] + #[cfg(all(Py_3_11, not(Py_3_13)))] pub get_code: Option *mut PyObject>, } diff --git a/pyo3-ffi/src/cpython/initconfig.rs b/pyo3-ffi/src/cpython/initconfig.rs index 17fe7559e1b..32931415888 100644 --- a/pyo3-ffi/src/cpython/initconfig.rs +++ b/pyo3-ffi/src/cpython/initconfig.rs @@ -141,6 +141,8 @@ pub struct PyConfig { pub safe_path: c_int, #[cfg(Py_3_12)] pub int_max_str_digits: c_int, + #[cfg(Py_3_13)] + pub cpu_count: c_int, pub pathconfig_warnings: c_int, #[cfg(Py_3_10)] pub program_name: *mut wchar_t, @@ -165,6 +167,8 @@ pub struct PyConfig { pub run_command: *mut wchar_t, pub run_module: *mut wchar_t, pub run_filename: *mut wchar_t, + #[cfg(Py_3_13)] + pub sys_path_0: *mut wchar_t, pub _install_importlib: c_int, pub _init_main: c_int, #[cfg(all(Py_3_9, not(Py_3_12)))] diff --git a/pyo3-ffi/src/cpython/listobject.rs b/pyo3-ffi/src/cpython/listobject.rs index 7fb2228fadd..ea15cfc1ff5 100644 --- a/pyo3-ffi/src/cpython/listobject.rs +++ b/pyo3-ffi/src/cpython/listobject.rs @@ -2,7 +2,7 @@ use crate::object::*; #[cfg(not(PyPy))] use crate::pyport::Py_ssize_t; -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] #[repr(C)] #[derive(Copy, Clone)] pub struct PyListObject { @@ -11,7 +11,7 @@ pub struct PyListObject { pub allocated: Py_ssize_t, } -#[cfg(PyPy)] +#[cfg(any(PyPy, GraalPy))] pub struct PyListObject { pub ob_base: PyObject, } @@ -22,14 +22,14 @@ pub struct PyListObject { /// Macro, trading safety for speed #[inline] -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] pub unsafe fn PyList_GET_ITEM(op: *mut PyObject, i: Py_ssize_t) -> *mut PyObject { *(*(op as *mut PyListObject)).ob_item.offset(i) } /// Macro, *only* to be used to fill in brand new lists #[inline] -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] pub unsafe fn PyList_SET_ITEM(op: *mut PyObject, i: Py_ssize_t, v: *mut PyObject) { *(*(op as *mut PyListObject)).ob_item.offset(i) = v; } diff --git a/pyo3-ffi/src/cpython/longobject.rs b/pyo3-ffi/src/cpython/longobject.rs new file mode 100644 index 00000000000..45acaae577d --- /dev/null +++ b/pyo3-ffi/src/cpython/longobject.rs @@ -0,0 +1,74 @@ +use crate::longobject::*; +use crate::object::*; +#[cfg(Py_3_13)] +use crate::pyport::Py_ssize_t; +use libc::size_t; +#[cfg(Py_3_13)] +use std::os::raw::c_void; +use std::os::raw::{c_int, c_uchar}; + +#[cfg(Py_3_13)] +extern "C" { + pub fn PyLong_FromUnicodeObject(u: *mut PyObject, base: c_int) -> *mut PyObject; +} + +#[cfg(Py_3_13)] +pub const Py_ASNATIVEBYTES_DEFAULTS: c_int = -1; +#[cfg(Py_3_13)] +pub const Py_ASNATIVEBYTES_BIG_ENDIAN: c_int = 0; +#[cfg(Py_3_13)] +pub const Py_ASNATIVEBYTES_LITTLE_ENDIAN: c_int = 1; +#[cfg(Py_3_13)] +pub const Py_ASNATIVEBYTES_NATIVE_ENDIAN: c_int = 3; +#[cfg(Py_3_13)] +pub const Py_ASNATIVEBYTES_UNSIGNED_BUFFER: c_int = 4; +#[cfg(Py_3_13)] +pub const Py_ASNATIVEBYTES_REJECT_NEGATIVE: c_int = 8; + +extern "C" { + // skipped _PyLong_Sign + + #[cfg(Py_3_13)] + pub fn PyLong_AsNativeBytes( + v: *mut PyObject, + buffer: *mut c_void, + n_bytes: Py_ssize_t, + flags: c_int, + ) -> Py_ssize_t; + + #[cfg(Py_3_13)] + pub fn PyLong_FromNativeBytes( + buffer: *const c_void, + n_bytes: size_t, + flags: c_int, + ) -> *mut PyObject; + + #[cfg(Py_3_13)] + pub fn PyLong_FromUnsignedNativeBytes( + buffer: *const c_void, + n_bytes: size_t, + flags: c_int, + ) -> *mut PyObject; + + // skipped PyUnstable_Long_IsCompact + // skipped PyUnstable_Long_CompactValue + + #[cfg_attr(PyPy, link_name = "_PyPyLong_FromByteArray")] + pub fn _PyLong_FromByteArray( + bytes: *const c_uchar, + n: size_t, + little_endian: c_int, + is_signed: c_int, + ) -> *mut PyObject; + + #[cfg_attr(PyPy, link_name = "_PyPyLong_AsByteArrayO")] + pub fn _PyLong_AsByteArray( + v: *mut PyLongObject, + bytes: *mut c_uchar, + n: size_t, + little_endian: c_int, + is_signed: c_int, + ) -> c_int; + + // skipped _PyLong_GCD +} diff --git a/pyo3-ffi/src/cpython/methodobject.rs b/pyo3-ffi/src/cpython/methodobject.rs index 7d9659785ba..97ad9ce35f0 100644 --- a/pyo3-ffi/src/cpython/methodobject.rs +++ b/pyo3-ffi/src/cpython/methodobject.rs @@ -1,8 +1,10 @@ use crate::object::*; +#[cfg(not(GraalPy))] use crate::{PyCFunctionObject, PyMethodDefPointer, METH_METHOD, METH_STATIC}; use std::os::raw::c_int; use std::ptr::addr_of_mut; +#[cfg(not(GraalPy))] pub struct PyCMethodObject { pub func: PyCFunctionObject, pub mm_class: *mut PyTypeObject, @@ -23,6 +25,7 @@ pub unsafe fn PyCMethod_Check(op: *mut PyObject) -> c_int { PyObject_TypeCheck(op, addr_of_mut!(PyCMethod_Type)) } +#[cfg(not(GraalPy))] #[inline] pub unsafe fn PyCFunction_GET_FUNCTION(func: *mut PyObject) -> PyMethodDefPointer { debug_assert_eq!(PyCMethod_Check(func), 1); @@ -31,6 +34,7 @@ pub unsafe fn PyCFunction_GET_FUNCTION(func: *mut PyObject) -> PyMethodDefPointe (*(*func).m_ml).ml_meth } +#[cfg(not(GraalPy))] #[inline] pub unsafe fn PyCFunction_GET_SELF(func: *mut PyObject) -> *mut PyObject { debug_assert_eq!(PyCMethod_Check(func), 1); @@ -43,6 +47,7 @@ pub unsafe fn PyCFunction_GET_SELF(func: *mut PyObject) -> *mut PyObject { } } +#[cfg(not(GraalPy))] #[inline] pub unsafe fn PyCFunction_GET_FLAGS(func: *mut PyObject) -> c_int { debug_assert_eq!(PyCMethod_Check(func), 1); @@ -51,6 +56,7 @@ pub unsafe fn PyCFunction_GET_FLAGS(func: *mut PyObject) -> c_int { (*(*func).m_ml).ml_flags } +#[cfg(not(GraalPy))] #[inline] pub unsafe fn PyCFunction_GET_CLASS(func: *mut PyObject) -> *mut PyTypeObject { debug_assert_eq!(PyCMethod_Check(func), 1); diff --git a/pyo3-ffi/src/cpython/mod.rs b/pyo3-ffi/src/cpython/mod.rs index 738ba37652e..1710dbc4122 100644 --- a/pyo3-ffi/src/cpython/mod.rs +++ b/pyo3-ffi/src/cpython/mod.rs @@ -18,6 +18,7 @@ pub(crate) mod import; pub(crate) mod initconfig; // skipped interpreteridobject.h pub(crate) mod listobject; +pub(crate) mod longobject; #[cfg(all(Py_3_9, not(PyPy)))] pub(crate) mod methodobject; pub(crate) mod object; @@ -53,6 +54,7 @@ pub use self::import::*; #[cfg(all(Py_3_8, not(PyPy)))] pub use self::initconfig::*; pub use self::listobject::*; +pub use self::longobject::*; #[cfg(all(Py_3_9, not(PyPy)))] pub use self::methodobject::*; pub use self::object::*; @@ -68,5 +70,5 @@ pub use self::pystate::*; pub use self::pythonrun::*; pub use self::tupleobject::*; pub use self::unicodeobject::*; -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] pub use self::weakrefobject::*; diff --git a/pyo3-ffi/src/cpython/object.rs b/pyo3-ffi/src/cpython/object.rs index 423af03a0e2..b4f6ce5a878 100644 --- a/pyo3-ffi/src/cpython/object.rs +++ b/pyo3-ffi/src/cpython/object.rs @@ -1,5 +1,7 @@ #[cfg(Py_3_8)] use crate::vectorcallfunc; +#[cfg(Py_3_11)] +use crate::PyModuleDef; use crate::{object, PyGetSetDef, PyMemberDef, PyMethodDef, PyObject, Py_ssize_t}; use std::mem; use std::os::raw::{c_char, c_int, c_uint, c_ulong, c_void}; @@ -20,9 +22,6 @@ mod bufferinfo { use std::os::raw::{c_char, c_int, c_void}; use std::ptr; - #[cfg(PyPy)] - const Py_MAX_NDIMS: usize = 36; - #[repr(C)] #[derive(Copy, Clone)] pub struct Py_buffer { @@ -41,12 +40,13 @@ mod bufferinfo { #[cfg(PyPy)] pub flags: c_int, #[cfg(PyPy)] - pub _strides: [Py_ssize_t; Py_MAX_NDIMS], + pub _strides: [Py_ssize_t; PyBUF_MAX_NDIM as usize], #[cfg(PyPy)] - pub _shape: [Py_ssize_t; Py_MAX_NDIMS], + pub _shape: [Py_ssize_t; PyBUF_MAX_NDIM as usize], } impl Py_buffer { + #[allow(clippy::new_without_default)] pub const fn new() -> Self { Py_buffer { buf: ptr::null_mut(), @@ -63,9 +63,9 @@ mod bufferinfo { #[cfg(PyPy)] flags: 0, #[cfg(PyPy)] - _strides: [0; Py_MAX_NDIMS], + _strides: [0; PyBUF_MAX_NDIM as usize], #[cfg(PyPy)] - _shape: [0; Py_MAX_NDIMS], + _shape: [0; PyBUF_MAX_NDIM as usize], } } } @@ -79,7 +79,7 @@ mod bufferinfo { unsafe extern "C" fn(arg1: *mut crate::PyObject, arg2: *mut Py_buffer); /// Maximum number of dimensions - pub const PyBUF_MAX_NDIM: c_int = 64; + pub const PyBUF_MAX_NDIM: c_int = if cfg!(PyPy) { 36 } else { 64 }; /* Flags for getting buffers */ pub const PyBUF_SIMPLE: c_int = 0; @@ -217,6 +217,8 @@ pub struct PyTypeObject { pub ob_size: Py_ssize_t, #[cfg(not(all(PyPy, not(Py_3_9))))] pub ob_base: object::PyVarObject, + #[cfg(GraalPy)] + pub ob_size: Py_ssize_t, pub tp_name: *const c_char, pub tp_basicsize: Py_ssize_t, pub tp_itemsize: Py_ssize_t, @@ -294,6 +296,8 @@ pub struct _specialization_cache { pub getitem: *mut PyObject, #[cfg(Py_3_12)] pub getitem_version: u32, + #[cfg(Py_3_13)] + pub init: *mut PyObject, } #[repr(C)] @@ -339,9 +343,12 @@ pub unsafe fn PyHeapType_GET_MEMBERS(etype: *mut PyHeapTypeObject) -> *mut PyMem // skipped _PyType_CalculateMetaclass // skipped _PyType_GetDocFromInternalDoc // skipped _PyType_GetTextSignatureFromInternalDoc -// skipped _PyType_GetModuleByDef extern "C" { + #[cfg(Py_3_11)] + #[cfg_attr(PyPy, link_name = "PyPyType_GetModuleByDef")] + pub fn PyType_GetModuleByDef(ty: *mut PyTypeObject, def: *mut PyModuleDef) -> *mut PyObject; + #[cfg(Py_3_12)] pub fn PyType_GetDict(o: *mut PyTypeObject) -> *mut PyObject; diff --git a/pyo3-ffi/src/cpython/objimpl.rs b/pyo3-ffi/src/cpython/objimpl.rs index 36a4380d122..3e0270ddc8f 100644 --- a/pyo3-ffi/src/cpython/objimpl.rs +++ b/pyo3-ffi/src/cpython/objimpl.rs @@ -1,7 +1,7 @@ use libc::size_t; use std::os::raw::c_int; -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] use std::os::raw::c_void; use crate::object::*; @@ -14,7 +14,7 @@ extern "C" { pub fn _Py_GetAllocatedBlocks() -> crate::Py_ssize_t; } -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] #[repr(C)] #[derive(Copy, Clone)] pub struct PyObjectArenaAllocator { @@ -23,7 +23,7 @@ pub struct PyObjectArenaAllocator { pub free: Option, } -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] impl Default for PyObjectArenaAllocator { #[inline] fn default() -> Self { @@ -32,9 +32,9 @@ impl Default for PyObjectArenaAllocator { } extern "C" { - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] pub fn PyObject_GetArenaAllocator(allocator: *mut PyObjectArenaAllocator); - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] pub fn PyObject_SetArenaAllocator(allocator: *mut PyObjectArenaAllocator); #[cfg(Py_3_9)] diff --git a/pyo3-ffi/src/cpython/pyerrors.rs b/pyo3-ffi/src/cpython/pyerrors.rs index fe7b4d4b045..6d17ebc8124 100644 --- a/pyo3-ffi/src/cpython/pyerrors.rs +++ b/pyo3-ffi/src/cpython/pyerrors.rs @@ -1,28 +1,28 @@ use crate::PyObject; -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] use crate::Py_ssize_t; #[repr(C)] #[derive(Debug)] pub struct PyBaseExceptionObject { pub ob_base: PyObject, - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] pub dict: *mut PyObject, - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] pub args: *mut PyObject, - #[cfg(all(Py_3_11, not(PyPy)))] + #[cfg(all(Py_3_11, not(any(PyPy, GraalPy))))] pub notes: *mut PyObject, - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] pub traceback: *mut PyObject, - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] pub context: *mut PyObject, - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] pub cause: *mut PyObject, - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] pub suppress_context: char, } -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] #[repr(C)] #[derive(Debug)] pub struct PySyntaxErrorObject { @@ -48,7 +48,7 @@ pub struct PySyntaxErrorObject { pub print_file_and_line: *mut PyObject, } -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] #[repr(C)] #[derive(Debug)] pub struct PyImportErrorObject { @@ -69,7 +69,7 @@ pub struct PyImportErrorObject { pub name_from: *mut PyObject, } -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] #[repr(C)] #[derive(Debug)] pub struct PyUnicodeErrorObject { @@ -90,7 +90,7 @@ pub struct PyUnicodeErrorObject { pub reason: *mut PyObject, } -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] #[repr(C)] #[derive(Debug)] pub struct PySystemExitObject { @@ -107,7 +107,7 @@ pub struct PySystemExitObject { pub code: *mut PyObject, } -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] #[repr(C)] #[derive(Debug)] pub struct PyOSErrorObject { @@ -134,26 +134,26 @@ pub struct PyOSErrorObject { #[derive(Debug)] pub struct PyStopIterationObject { pub ob_base: PyObject, - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] pub dict: *mut PyObject, - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] pub args: *mut PyObject, - #[cfg(all(Py_3_11, not(PyPy)))] + #[cfg(all(Py_3_11, not(any(PyPy, GraalPy))))] pub notes: *mut PyObject, - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] pub traceback: *mut PyObject, - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] pub context: *mut PyObject, - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] pub cause: *mut PyObject, - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] pub suppress_context: char, pub value: *mut PyObject, } extern "C" { - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] pub fn _PyErr_ChainExceptions(typ: *mut PyObject, val: *mut PyObject, tb: *mut PyObject); } diff --git a/pyo3-ffi/src/cpython/pymem.rs b/pyo3-ffi/src/cpython/pymem.rs index 2dfb3f3bcfa..c400fa30b05 100644 --- a/pyo3-ffi/src/cpython/pymem.rs +++ b/pyo3-ffi/src/cpython/pymem.rs @@ -26,7 +26,7 @@ pub enum PyMemAllocatorDomain { } // skipped PyMemAllocatorName -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] #[repr(C)] #[derive(Copy, Clone)] pub struct PyMemAllocatorEx { @@ -40,10 +40,10 @@ pub struct PyMemAllocatorEx { } extern "C" { - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] pub fn PyMem_GetAllocator(domain: PyMemAllocatorDomain, allocator: *mut PyMemAllocatorEx); - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] pub fn PyMem_SetAllocator(domain: PyMemAllocatorDomain, allocator: *mut PyMemAllocatorEx); - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] pub fn PyMem_SetupDebugHooks(); } diff --git a/pyo3-ffi/src/cpython/pythonrun.rs b/pyo3-ffi/src/cpython/pythonrun.rs index a92528b7fdc..94863166e11 100644 --- a/pyo3-ffi/src/cpython/pythonrun.rs +++ b/pyo3-ffi/src/cpython/pythonrun.rs @@ -1,8 +1,8 @@ use crate::object::*; -#[cfg(not(any(PyPy, Py_LIMITED_API, Py_3_10)))] +#[cfg(not(any(PyPy, GraalPy, Py_LIMITED_API, Py_3_10)))] use crate::pyarena::PyArena; use crate::PyCompilerFlags; -#[cfg(not(any(PyPy, Py_3_10)))] +#[cfg(not(any(PyPy, GraalPy, Py_3_10)))] use crate::{_mod, _node}; use libc::FILE; use std::os::raw::{c_char, c_int}; @@ -54,7 +54,7 @@ extern "C" { flags: *mut PyCompilerFlags, ) -> c_int; - #[cfg(not(any(PyPy, Py_3_10)))] + #[cfg(not(any(PyPy, GraalPy, Py_3_10)))] pub fn PyParser_ASTFromString( s: *const c_char, filename: *const c_char, @@ -62,7 +62,7 @@ extern "C" { flags: *mut PyCompilerFlags, arena: *mut PyArena, ) -> *mut _mod; - #[cfg(not(any(PyPy, Py_3_10)))] + #[cfg(not(any(PyPy, GraalPy, Py_3_10)))] pub fn PyParser_ASTFromStringObject( s: *const c_char, filename: *mut PyObject, @@ -70,7 +70,7 @@ extern "C" { flags: *mut PyCompilerFlags, arena: *mut PyArena, ) -> *mut _mod; - #[cfg(not(any(PyPy, Py_3_10)))] + #[cfg(not(any(PyPy, GraalPy, Py_3_10)))] pub fn PyParser_ASTFromFile( fp: *mut FILE, filename: *const c_char, @@ -82,7 +82,7 @@ extern "C" { errcode: *mut c_int, arena: *mut PyArena, ) -> *mut _mod; - #[cfg(not(any(PyPy, Py_3_10)))] + #[cfg(not(any(PyPy, GraalPy, Py_3_10)))] pub fn PyParser_ASTFromFileObject( fp: *mut FILE, filename: *mut PyObject, @@ -105,7 +105,7 @@ extern "C" { arg4: *mut PyObject, arg5: *mut PyCompilerFlags, ) -> *mut PyObject; - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] pub fn PyRun_FileExFlags( fp: *mut FILE, filename: *const c_char, @@ -116,7 +116,7 @@ extern "C" { flags: *mut PyCompilerFlags, ) -> *mut PyObject; - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] pub fn Py_CompileStringExFlags( str: *const c_char, filename: *const c_char, @@ -135,6 +135,7 @@ extern "C" { } #[inline] +#[cfg(not(GraalPy))] pub unsafe fn Py_CompileString(string: *const c_char, p: *const c_char, s: c_int) -> *mut PyObject { #[cfg(not(PyPy))] return Py_CompileStringExFlags(string, p, s, std::ptr::null_mut(), -1); @@ -144,7 +145,7 @@ pub unsafe fn Py_CompileString(string: *const c_char, p: *const c_char, s: c_int } #[inline] -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] pub unsafe fn Py_CompileStringFlags( string: *const c_char, p: *const c_char, @@ -164,11 +165,11 @@ extern "C" { g: *mut PyObject, l: *mut PyObject, ) -> *mut PyObject; - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] pub fn PyRun_AnyFile(fp: *mut FILE, name: *const c_char) -> c_int; - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] pub fn PyRun_AnyFileEx(fp: *mut FILE, name: *const c_char, closeit: c_int) -> c_int; - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] pub fn PyRun_AnyFileFlags( arg1: *mut FILE, arg2: *const c_char, @@ -176,13 +177,13 @@ extern "C" { ) -> c_int; #[cfg_attr(PyPy, link_name = "PyPyRun_SimpleString")] pub fn PyRun_SimpleString(s: *const c_char) -> c_int; - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] pub fn PyRun_SimpleFile(f: *mut FILE, p: *const c_char) -> c_int; - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] pub fn PyRun_SimpleFileEx(f: *mut FILE, p: *const c_char, c: c_int) -> c_int; - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] pub fn PyRun_InteractiveOne(f: *mut FILE, p: *const c_char) -> c_int; - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] pub fn PyRun_InteractiveLoop(f: *mut FILE, p: *const c_char) -> c_int; #[cfg_attr(PyPy, link_name = "PyPyRun_File")] pub fn PyRun_File( @@ -192,7 +193,7 @@ extern "C" { g: *mut PyObject, l: *mut PyObject, ) -> *mut PyObject; - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] pub fn PyRun_FileEx( fp: *mut FILE, p: *const c_char, @@ -201,7 +202,7 @@ extern "C" { l: *mut PyObject, c: c_int, ) -> *mut PyObject; - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] pub fn PyRun_FileFlags( fp: *mut FILE, p: *const c_char, @@ -218,14 +219,14 @@ extern "C" { // skipped macro PyRun_AnyFileFlags extern "C" { - #[cfg(not(any(PyPy, Py_3_10)))] + #[cfg(not(any(PyPy, GraalPy, Py_3_10)))] #[cfg_attr(Py_3_9, deprecated(note = "Python 3.9"))] pub fn PyParser_SimpleParseStringFlags( arg1: *const c_char, arg2: c_int, arg3: c_int, ) -> *mut _node; - #[cfg(not(any(PyPy, Py_3_10)))] + #[cfg(not(any(PyPy, GraalPy, Py_3_10)))] #[cfg_attr(Py_3_9, deprecated(note = "Python 3.9"))] pub fn PyParser_SimpleParseStringFlagsFilename( arg1: *const c_char, @@ -233,7 +234,7 @@ extern "C" { arg3: c_int, arg4: c_int, ) -> *mut _node; - #[cfg(not(any(PyPy, Py_3_10)))] + #[cfg(not(any(PyPy, GraalPy, Py_3_10)))] #[cfg_attr(Py_3_9, deprecated(note = "Python 3.9"))] pub fn PyParser_SimpleParseFileFlags( arg1: *mut FILE, diff --git a/pyo3-ffi/src/cpython/tupleobject.rs b/pyo3-ffi/src/cpython/tupleobject.rs index 24dde268526..4ed8520daf3 100644 --- a/pyo3-ffi/src/cpython/tupleobject.rs +++ b/pyo3-ffi/src/cpython/tupleobject.rs @@ -5,6 +5,7 @@ use crate::pyport::Py_ssize_t; #[repr(C)] pub struct PyTupleObject { pub ob_base: PyVarObject, + #[cfg(not(GraalPy))] pub ob_item: [*mut PyObject; 1], } @@ -22,14 +23,14 @@ pub unsafe fn PyTuple_GET_SIZE(op: *mut PyObject) -> Py_ssize_t { } #[inline] -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] pub unsafe fn PyTuple_GET_ITEM(op: *mut PyObject, i: Py_ssize_t) -> *mut PyObject { *(*(op as *mut PyTupleObject)).ob_item.as_ptr().offset(i) } /// Macro, *only* to be used to fill in brand new tuples #[inline] -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] pub unsafe fn PyTuple_SET_ITEM(op: *mut PyObject, i: Py_ssize_t, v: *mut PyObject) { *(*(op as *mut PyTupleObject)).ob_item.as_mut_ptr().offset(i) = v; } diff --git a/pyo3-ffi/src/cpython/unicodeobject.rs b/pyo3-ffi/src/cpython/unicodeobject.rs index f618ecf0a84..feb78cf0c82 100644 --- a/pyo3-ffi/src/cpython/unicodeobject.rs +++ b/pyo3-ffi/src/cpython/unicodeobject.rs @@ -1,7 +1,7 @@ -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] use crate::Py_hash_t; use crate::{PyObject, Py_UCS1, Py_UCS2, Py_UCS4, Py_UNICODE, Py_ssize_t}; -#[cfg(not(Py_3_12))] +#[cfg(not(any(Py_3_12, GraalPy)))] use libc::wchar_t; use std::os::raw::{c_char, c_int, c_uint, c_void}; @@ -44,6 +44,7 @@ impl BitfieldUnit { } } +#[cfg(not(GraalPy))] impl BitfieldUnit where Storage: AsRef<[u8]> + AsMut<[u8]>, @@ -117,23 +118,31 @@ where } } +#[cfg(not(GraalPy))] const STATE_INTERNED_INDEX: usize = 0; +#[cfg(not(GraalPy))] const STATE_INTERNED_WIDTH: u8 = 2; +#[cfg(not(GraalPy))] const STATE_KIND_INDEX: usize = STATE_INTERNED_WIDTH as usize; +#[cfg(not(GraalPy))] const STATE_KIND_WIDTH: u8 = 3; +#[cfg(not(GraalPy))] const STATE_COMPACT_INDEX: usize = (STATE_INTERNED_WIDTH + STATE_KIND_WIDTH) as usize; +#[cfg(not(GraalPy))] const STATE_COMPACT_WIDTH: u8 = 1; +#[cfg(not(GraalPy))] const STATE_ASCII_INDEX: usize = (STATE_INTERNED_WIDTH + STATE_KIND_WIDTH + STATE_COMPACT_WIDTH) as usize; +#[cfg(not(GraalPy))] const STATE_ASCII_WIDTH: u8 = 1; -#[cfg(not(Py_3_12))] +#[cfg(not(any(Py_3_12, GraalPy)))] const STATE_READY_INDEX: usize = (STATE_INTERNED_WIDTH + STATE_KIND_WIDTH + STATE_COMPACT_WIDTH + STATE_ASCII_WIDTH) as usize; -#[cfg(not(Py_3_12))] +#[cfg(not(any(Py_3_12, GraalPy)))] const STATE_READY_WIDTH: u8 = 1; // generated by bindgen v0.63.0 (with small adaptations) @@ -153,6 +162,7 @@ struct PyASCIIObjectState { } // c_uint and u32 are not necessarily the same type on all targets / architectures +#[cfg(not(GraalPy))] #[allow(clippy::useless_transmute)] impl PyASCIIObjectState { #[inline] @@ -241,8 +251,9 @@ impl From for u32 { #[repr(C)] pub struct PyASCIIObject { pub ob_base: PyObject, + #[cfg(not(GraalPy))] pub length: Py_ssize_t, - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] pub hash: Py_hash_t, /// A bit field with various properties. /// @@ -255,12 +266,14 @@ pub struct PyASCIIObject { /// unsigned int ascii:1; /// unsigned int ready:1; /// unsigned int :24; + #[cfg(not(GraalPy))] pub state: u32, - #[cfg(not(Py_3_12))] + #[cfg(not(any(Py_3_12, GraalPy)))] pub wstr: *mut wchar_t, } /// Interacting with the bitfield is not actually well-defined, so we mark these APIs unsafe. +#[cfg(not(GraalPy))] impl PyASCIIObject { #[cfg_attr(not(Py_3_12), allow(rustdoc::broken_intra_doc_links))] // SSTATE_INTERNED_IMMORTAL_STATIC requires 3.12 /// Get the `interned` field of the [`PyASCIIObject`] state bitfield. @@ -367,9 +380,11 @@ impl PyASCIIObject { #[repr(C)] pub struct PyCompactUnicodeObject { pub _base: PyASCIIObject, + #[cfg(not(GraalPy))] pub utf8_length: Py_ssize_t, + #[cfg(not(GraalPy))] pub utf8: *mut c_char, - #[cfg(not(Py_3_12))] + #[cfg(not(any(Py_3_12, GraalPy)))] pub wstr_length: Py_ssize_t, } @@ -384,11 +399,12 @@ pub union PyUnicodeObjectData { #[repr(C)] pub struct PyUnicodeObject { pub _base: PyCompactUnicodeObject, + #[cfg(not(GraalPy))] pub data: PyUnicodeObjectData, } extern "C" { - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] pub fn _PyUnicode_CheckConsistency(op: *mut PyObject, check_content: c_int) -> c_int; } @@ -403,6 +419,7 @@ pub const SSTATE_INTERNED_IMMORTAL: c_uint = 2; #[cfg(Py_3_12)] pub const SSTATE_INTERNED_IMMORTAL_STATIC: c_uint = 3; +#[cfg(not(GraalPy))] #[inline] pub unsafe fn PyUnicode_IS_ASCII(op: *mut PyObject) -> c_uint { debug_assert!(crate::PyUnicode_Check(op) != 0); @@ -412,11 +429,13 @@ pub unsafe fn PyUnicode_IS_ASCII(op: *mut PyObject) -> c_uint { (*(op as *mut PyASCIIObject)).ascii() } +#[cfg(not(GraalPy))] #[inline] pub unsafe fn PyUnicode_IS_COMPACT(op: *mut PyObject) -> c_uint { (*(op as *mut PyASCIIObject)).compact() } +#[cfg(not(GraalPy))] #[inline] pub unsafe fn PyUnicode_IS_COMPACT_ASCII(op: *mut PyObject) -> c_uint { ((*(op as *mut PyASCIIObject)).ascii() != 0 && PyUnicode_IS_COMPACT(op) != 0).into() @@ -430,21 +449,25 @@ pub const PyUnicode_1BYTE_KIND: c_uint = 1; pub const PyUnicode_2BYTE_KIND: c_uint = 2; pub const PyUnicode_4BYTE_KIND: c_uint = 4; +#[cfg(not(any(GraalPy, PyPy)))] #[inline] pub unsafe fn PyUnicode_1BYTE_DATA(op: *mut PyObject) -> *mut Py_UCS1 { PyUnicode_DATA(op) as *mut Py_UCS1 } +#[cfg(not(any(GraalPy, PyPy)))] #[inline] pub unsafe fn PyUnicode_2BYTE_DATA(op: *mut PyObject) -> *mut Py_UCS2 { PyUnicode_DATA(op) as *mut Py_UCS2 } +#[cfg(not(any(GraalPy, PyPy)))] #[inline] pub unsafe fn PyUnicode_4BYTE_DATA(op: *mut PyObject) -> *mut Py_UCS4 { PyUnicode_DATA(op) as *mut Py_UCS4 } +#[cfg(not(GraalPy))] #[inline] pub unsafe fn PyUnicode_KIND(op: *mut PyObject) -> c_uint { debug_assert!(crate::PyUnicode_Check(op) != 0); @@ -454,6 +477,7 @@ pub unsafe fn PyUnicode_KIND(op: *mut PyObject) -> c_uint { (*(op as *mut PyASCIIObject)).kind() } +#[cfg(not(GraalPy))] #[inline] pub unsafe fn _PyUnicode_COMPACT_DATA(op: *mut PyObject) -> *mut c_void { if PyUnicode_IS_ASCII(op) != 0 { @@ -463,6 +487,7 @@ pub unsafe fn _PyUnicode_COMPACT_DATA(op: *mut PyObject) -> *mut c_void { } } +#[cfg(not(any(GraalPy, PyPy)))] #[inline] pub unsafe fn _PyUnicode_NONCOMPACT_DATA(op: *mut PyObject) -> *mut c_void { debug_assert!(!(*(op as *mut PyUnicodeObject)).data.any.is_null()); @@ -470,6 +495,7 @@ pub unsafe fn _PyUnicode_NONCOMPACT_DATA(op: *mut PyObject) -> *mut c_void { (*(op as *mut PyUnicodeObject)).data.any } +#[cfg(not(any(GraalPy, PyPy)))] #[inline] pub unsafe fn PyUnicode_DATA(op: *mut PyObject) -> *mut c_void { debug_assert!(crate::PyUnicode_Check(op) != 0); @@ -485,6 +511,7 @@ pub unsafe fn PyUnicode_DATA(op: *mut PyObject) -> *mut c_void { // skipped PyUnicode_READ // skipped PyUnicode_READ_CHAR +#[cfg(not(GraalPy))] #[inline] pub unsafe fn PyUnicode_GET_LENGTH(op: *mut PyObject) -> Py_ssize_t { debug_assert!(crate::PyUnicode_Check(op) != 0); @@ -494,26 +521,26 @@ pub unsafe fn PyUnicode_GET_LENGTH(op: *mut PyObject) -> Py_ssize_t { (*(op as *mut PyASCIIObject)).length } -#[cfg(Py_3_12)] +#[cfg(any(Py_3_12, GraalPy))] #[inline] pub unsafe fn PyUnicode_IS_READY(_op: *mut PyObject) -> c_uint { // kept in CPython for backwards compatibility 1 } -#[cfg(not(Py_3_12))] +#[cfg(not(any(GraalPy, Py_3_12)))] #[inline] pub unsafe fn PyUnicode_IS_READY(op: *mut PyObject) -> c_uint { (*(op as *mut PyASCIIObject)).ready() } -#[cfg(Py_3_12)] +#[cfg(any(Py_3_12, GraalPy))] #[inline] pub unsafe fn PyUnicode_READY(_op: *mut PyObject) -> c_int { 0 } -#[cfg(not(Py_3_12))] +#[cfg(not(any(Py_3_12, GraalPy)))] #[inline] pub unsafe fn PyUnicode_READY(op: *mut PyObject) -> c_int { debug_assert!(crate::PyUnicode_Check(op) != 0); diff --git a/pyo3-ffi/src/cpython/weakrefobject.rs b/pyo3-ffi/src/cpython/weakrefobject.rs index 5a5f85c5f0c..3a232c7ed38 100644 --- a/pyo3-ffi/src/cpython/weakrefobject.rs +++ b/pyo3-ffi/src/cpython/weakrefobject.rs @@ -1,4 +1,4 @@ -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] pub struct _PyWeakReference { pub ob_base: crate::PyObject, pub wr_object: *mut crate::PyObject, diff --git a/pyo3-ffi/src/datetime.rs b/pyo3-ffi/src/datetime.rs index 7e5a250990f..a20b76aa91d 100644 --- a/pyo3-ffi/src/datetime.rs +++ b/pyo3-ffi/src/datetime.rs @@ -9,17 +9,16 @@ //! Support for `PyDateTime_CAPI` is limited as of PyPy 7.0.0. //! `DateTime_FromTimestamp` and `Date_FromTimestamp` are currently not supported. +#[cfg(GraalPy)] +use crate::{PyLong_AsLong, PyLong_Check, PyObject_GetAttrString, Py_DecRef}; use crate::{PyObject, PyObject_TypeCheck, PyTypeObject, Py_TYPE}; use std::cell::UnsafeCell; use std::os::raw::{c_char, c_int}; use std::ptr; #[cfg(not(PyPy))] -use { - crate::{PyCapsule_Import, Py_hash_t}, - std::ffi::CString, - std::os::raw::c_uchar, -}; - +use {crate::PyCapsule_Import, std::ffi::CString}; +#[cfg(not(any(PyPy, GraalPy)))] +use {crate::Py_hash_t, std::os::raw::c_uchar}; // Type struct wrappers const _PyDateTime_DATE_DATASIZE: usize = 4; const _PyDateTime_TIME_DATASIZE: usize = 6; @@ -30,26 +29,27 @@ const _PyDateTime_DATETIME_DATASIZE: usize = 10; /// Structure representing a `datetime.timedelta`. pub struct PyDateTime_Delta { pub ob_base: PyObject, - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] pub hashcode: Py_hash_t, + #[cfg(not(GraalPy))] pub days: c_int, + #[cfg(not(GraalPy))] pub seconds: c_int, + #[cfg(not(GraalPy))] pub microseconds: c_int, } // skipped non-limited PyDateTime_TZInfo // skipped non-limited _PyDateTime_BaseTZInfo -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] #[repr(C)] #[derive(Debug, Copy, Clone)] /// Structure representing a `datetime.time` without a `tzinfo` member. pub struct _PyDateTime_BaseTime { pub ob_base: PyObject, - #[cfg(not(PyPy))] pub hashcode: Py_hash_t, pub hastzinfo: c_char, - #[cfg(not(PyPy))] pub data: [c_uchar; _PyDateTime_TIME_DATASIZE], } @@ -58,17 +58,19 @@ pub struct _PyDateTime_BaseTime { /// Structure representing a `datetime.time`. pub struct PyDateTime_Time { pub ob_base: PyObject, - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] pub hashcode: Py_hash_t, + #[cfg(not(GraalPy))] pub hastzinfo: c_char, - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] pub data: [c_uchar; _PyDateTime_TIME_DATASIZE], - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] pub fold: c_uchar, /// # Safety /// /// Care should be taken when reading this field. If the time does not have a /// tzinfo then CPython may allocate as a `_PyDateTime_BaseTime` without this field. + #[cfg(not(GraalPy))] pub tzinfo: *mut PyObject, } @@ -77,24 +79,22 @@ pub struct PyDateTime_Time { /// Structure representing a `datetime.date` pub struct PyDateTime_Date { pub ob_base: PyObject, - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] pub hashcode: Py_hash_t, - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] pub hastzinfo: c_char, - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] pub data: [c_uchar; _PyDateTime_DATE_DATASIZE], } -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] #[repr(C)] #[derive(Debug, Copy, Clone)] /// Structure representing a `datetime.datetime` without a `tzinfo` member. pub struct _PyDateTime_BaseDateTime { pub ob_base: PyObject, - #[cfg(not(PyPy))] pub hashcode: Py_hash_t, pub hastzinfo: c_char, - #[cfg(not(PyPy))] pub data: [c_uchar; _PyDateTime_DATETIME_DATASIZE], } @@ -103,17 +103,19 @@ pub struct _PyDateTime_BaseDateTime { /// Structure representing a `datetime.datetime`. pub struct PyDateTime_DateTime { pub ob_base: PyObject, - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] pub hashcode: Py_hash_t, + #[cfg(not(GraalPy))] pub hastzinfo: c_char, - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] pub data: [c_uchar; _PyDateTime_DATETIME_DATASIZE], - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] pub fold: c_uchar, /// # Safety /// /// Care should be taken when reading this field. If the time does not have a /// tzinfo then CPython may allocate as a `_PyDateTime_BaseDateTime` without this field. + #[cfg(not(GraalPy))] pub tzinfo: *mut PyObject, } @@ -121,7 +123,7 @@ pub struct PyDateTime_DateTime { // Accessor functions for PyDateTime_Date and PyDateTime_DateTime #[inline] -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] /// Retrieve the year component of a `PyDateTime_Date` or `PyDateTime_DateTime`. /// Returns a signed integer greater than 0. pub unsafe fn PyDateTime_GET_YEAR(o: *mut PyObject) -> c_int { @@ -131,7 +133,7 @@ pub unsafe fn PyDateTime_GET_YEAR(o: *mut PyObject) -> c_int { } #[inline] -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] /// Retrieve the month component of a `PyDateTime_Date` or `PyDateTime_DateTime`. /// Returns a signed integer in the range `[1, 12]`. pub unsafe fn PyDateTime_GET_MONTH(o: *mut PyObject) -> c_int { @@ -140,7 +142,7 @@ pub unsafe fn PyDateTime_GET_MONTH(o: *mut PyObject) -> c_int { } #[inline] -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] /// Retrieve the day component of a `PyDateTime_Date` or `PyDateTime_DateTime`. /// Returns a signed integer in the interval `[1, 31]`. pub unsafe fn PyDateTime_GET_DAY(o: *mut PyObject) -> c_int { @@ -149,28 +151,28 @@ pub unsafe fn PyDateTime_GET_DAY(o: *mut PyObject) -> c_int { } // Accessor macros for times -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] macro_rules! _PyDateTime_GET_HOUR { ($o: expr, $offset:expr) => { c_int::from((*$o).data[$offset + 0]) }; } -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] macro_rules! _PyDateTime_GET_MINUTE { ($o: expr, $offset:expr) => { c_int::from((*$o).data[$offset + 1]) }; } -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] macro_rules! _PyDateTime_GET_SECOND { ($o: expr, $offset:expr) => { c_int::from((*$o).data[$offset + 2]) }; } -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] macro_rules! _PyDateTime_GET_MICROSECOND { ($o: expr, $offset:expr) => { (c_int::from((*$o).data[$offset + 3]) << 16) @@ -179,14 +181,14 @@ macro_rules! _PyDateTime_GET_MICROSECOND { }; } -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] macro_rules! _PyDateTime_GET_FOLD { ($o: expr) => { (*$o).fold }; } -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] macro_rules! _PyDateTime_GET_TZINFO { ($o: expr) => { if (*$o).hastzinfo != 0 { @@ -199,7 +201,7 @@ macro_rules! _PyDateTime_GET_TZINFO { // Accessor functions for DateTime #[inline] -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] /// Retrieve the hour component of a `PyDateTime_DateTime`. /// Returns a signed integer in the interval `[0, 23]` pub unsafe fn PyDateTime_DATE_GET_HOUR(o: *mut PyObject) -> c_int { @@ -207,7 +209,7 @@ pub unsafe fn PyDateTime_DATE_GET_HOUR(o: *mut PyObject) -> c_int { } #[inline] -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] /// Retrieve the minute component of a `PyDateTime_DateTime`. /// Returns a signed integer in the interval `[0, 59]` pub unsafe fn PyDateTime_DATE_GET_MINUTE(o: *mut PyObject) -> c_int { @@ -215,7 +217,7 @@ pub unsafe fn PyDateTime_DATE_GET_MINUTE(o: *mut PyObject) -> c_int { } #[inline] -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] /// Retrieve the second component of a `PyDateTime_DateTime`. /// Returns a signed integer in the interval `[0, 59]` pub unsafe fn PyDateTime_DATE_GET_SECOND(o: *mut PyObject) -> c_int { @@ -223,7 +225,7 @@ pub unsafe fn PyDateTime_DATE_GET_SECOND(o: *mut PyObject) -> c_int { } #[inline] -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] /// Retrieve the microsecond component of a `PyDateTime_DateTime`. /// Returns a signed integer in the interval `[0, 999999]` pub unsafe fn PyDateTime_DATE_GET_MICROSECOND(o: *mut PyObject) -> c_int { @@ -231,7 +233,7 @@ pub unsafe fn PyDateTime_DATE_GET_MICROSECOND(o: *mut PyObject) -> c_int { } #[inline] -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] /// Retrieve the fold component of a `PyDateTime_DateTime`. /// Returns a signed integer in the interval `[0, 1]` pub unsafe fn PyDateTime_DATE_GET_FOLD(o: *mut PyObject) -> c_uchar { @@ -239,7 +241,7 @@ pub unsafe fn PyDateTime_DATE_GET_FOLD(o: *mut PyObject) -> c_uchar { } #[inline] -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] /// Retrieve the tzinfo component of a `PyDateTime_DateTime`. /// Returns a pointer to a `PyObject` that should be either NULL or an instance /// of a `datetime.tzinfo` subclass. @@ -249,7 +251,7 @@ pub unsafe fn PyDateTime_DATE_GET_TZINFO(o: *mut PyObject) -> *mut PyObject { // Accessor functions for Time #[inline] -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] /// Retrieve the hour component of a `PyDateTime_Time`. /// Returns a signed integer in the interval `[0, 23]` pub unsafe fn PyDateTime_TIME_GET_HOUR(o: *mut PyObject) -> c_int { @@ -257,7 +259,7 @@ pub unsafe fn PyDateTime_TIME_GET_HOUR(o: *mut PyObject) -> c_int { } #[inline] -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] /// Retrieve the minute component of a `PyDateTime_Time`. /// Returns a signed integer in the interval `[0, 59]` pub unsafe fn PyDateTime_TIME_GET_MINUTE(o: *mut PyObject) -> c_int { @@ -265,7 +267,7 @@ pub unsafe fn PyDateTime_TIME_GET_MINUTE(o: *mut PyObject) -> c_int { } #[inline] -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] /// Retrieve the second component of a `PyDateTime_DateTime`. /// Returns a signed integer in the interval `[0, 59]` pub unsafe fn PyDateTime_TIME_GET_SECOND(o: *mut PyObject) -> c_int { @@ -273,14 +275,14 @@ pub unsafe fn PyDateTime_TIME_GET_SECOND(o: *mut PyObject) -> c_int { } #[inline] -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] /// Retrieve the microsecond component of a `PyDateTime_DateTime`. /// Returns a signed integer in the interval `[0, 999999]` pub unsafe fn PyDateTime_TIME_GET_MICROSECOND(o: *mut PyObject) -> c_int { _PyDateTime_GET_MICROSECOND!((o as *mut PyDateTime_Time), 0) } -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] #[inline] /// Retrieve the fold component of a `PyDateTime_Time`. /// Returns a signed integer in the interval `[0, 1]` @@ -289,7 +291,7 @@ pub unsafe fn PyDateTime_TIME_GET_FOLD(o: *mut PyObject) -> c_uchar { } #[inline] -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] /// Retrieve the tzinfo component of a `PyDateTime_Time`. /// Returns a pointer to a `PyObject` that should be either NULL or an instance /// of a `datetime.tzinfo` subclass. @@ -298,7 +300,7 @@ pub unsafe fn PyDateTime_TIME_GET_TZINFO(o: *mut PyObject) -> *mut PyObject { } // Accessor functions -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] macro_rules! _access_field { ($obj:expr, $type: ident, $field:ident) => { (*($obj as *mut $type)).$field @@ -306,7 +308,7 @@ macro_rules! _access_field { } // Accessor functions for PyDateTime_Delta -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] macro_rules! _access_delta_field { ($obj:expr, $field:ident) => { _access_field!($obj, PyDateTime_Delta, $field) @@ -314,7 +316,7 @@ macro_rules! _access_delta_field { } #[inline] -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] /// Retrieve the days component of a `PyDateTime_Delta`. /// /// Returns a signed integer in the interval [-999999999, 999999999]. @@ -326,7 +328,7 @@ pub unsafe fn PyDateTime_DELTA_GET_DAYS(o: *mut PyObject) -> c_int { } #[inline] -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] /// Retrieve the seconds component of a `PyDateTime_Delta`. /// /// Returns a signed integer in the interval [0, 86399]. @@ -338,7 +340,7 @@ pub unsafe fn PyDateTime_DELTA_GET_SECONDS(o: *mut PyObject) -> c_int { } #[inline] -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] /// Retrieve the seconds component of a `PyDateTime_Delta`. /// /// Returns a signed integer in the interval [0, 999999]. @@ -349,6 +351,132 @@ pub unsafe fn PyDateTime_DELTA_GET_MICROSECONDS(o: *mut PyObject) -> c_int { _access_delta_field!(o, microseconds) } +// Accessor functions for GraalPy. The macros on GraalPy work differently, +// but copying them seems suboptimal +#[inline] +#[cfg(GraalPy)] +pub unsafe fn _get_attr(obj: *mut PyObject, field: &str) -> c_int { + let result = PyObject_GetAttrString(obj, field.as_ptr() as *const c_char); + Py_DecRef(result); // the original macros are borrowing + if PyLong_Check(result) == 1 { + PyLong_AsLong(result) as c_int + } else { + 0 + } +} + +#[inline] +#[cfg(GraalPy)] +pub unsafe fn PyDateTime_GET_YEAR(o: *mut PyObject) -> c_int { + _get_attr(o, "year\0") +} + +#[inline] +#[cfg(GraalPy)] +pub unsafe fn PyDateTime_GET_MONTH(o: *mut PyObject) -> c_int { + _get_attr(o, "month\0") +} + +#[inline] +#[cfg(GraalPy)] +pub unsafe fn PyDateTime_GET_DAY(o: *mut PyObject) -> c_int { + _get_attr(o, "day\0") +} + +#[inline] +#[cfg(GraalPy)] +pub unsafe fn PyDateTime_DATE_GET_HOUR(o: *mut PyObject) -> c_int { + _get_attr(o, "hour\0") +} + +#[inline] +#[cfg(GraalPy)] +pub unsafe fn PyDateTime_DATE_GET_MINUTE(o: *mut PyObject) -> c_int { + _get_attr(o, "minute\0") +} + +#[inline] +#[cfg(GraalPy)] +pub unsafe fn PyDateTime_DATE_GET_SECOND(o: *mut PyObject) -> c_int { + _get_attr(o, "second\0") +} + +#[inline] +#[cfg(GraalPy)] +pub unsafe fn PyDateTime_DATE_GET_MICROSECOND(o: *mut PyObject) -> c_int { + _get_attr(o, "microsecond\0") +} + +#[inline] +#[cfg(GraalPy)] +pub unsafe fn PyDateTime_DATE_GET_FOLD(o: *mut PyObject) -> c_int { + _get_attr(o, "fold\0") +} + +#[inline] +#[cfg(GraalPy)] +pub unsafe fn PyDateTime_DATE_GET_TZINFO(o: *mut PyObject) -> *mut PyObject { + let res = PyObject_GetAttrString(o, "tzinfo\0".as_ptr() as *const c_char); + Py_DecRef(res); // the original macros are borrowing + res +} + +#[inline] +#[cfg(GraalPy)] +pub unsafe fn PyDateTime_TIME_GET_HOUR(o: *mut PyObject) -> c_int { + _get_attr(o, "hour\0") +} + +#[inline] +#[cfg(GraalPy)] +pub unsafe fn PyDateTime_TIME_GET_MINUTE(o: *mut PyObject) -> c_int { + _get_attr(o, "minute\0") +} + +#[inline] +#[cfg(GraalPy)] +pub unsafe fn PyDateTime_TIME_GET_SECOND(o: *mut PyObject) -> c_int { + _get_attr(o, "second\0") +} + +#[inline] +#[cfg(GraalPy)] +pub unsafe fn PyDateTime_TIME_GET_MICROSECOND(o: *mut PyObject) -> c_int { + _get_attr(o, "microsecond\0") +} + +#[inline] +#[cfg(GraalPy)] +pub unsafe fn PyDateTime_TIME_GET_FOLD(o: *mut PyObject) -> c_int { + _get_attr(o, "fold\0") +} + +#[inline] +#[cfg(GraalPy)] +pub unsafe fn PyDateTime_TIME_GET_TZINFO(o: *mut PyObject) -> *mut PyObject { + let res = PyObject_GetAttrString(o, "tzinfo\0".as_ptr() as *const c_char); + Py_DecRef(res); // the original macros are borrowing + res +} + +#[inline] +#[cfg(GraalPy)] +pub unsafe fn PyDateTime_DELTA_GET_DAYS(o: *mut PyObject) -> c_int { + _get_attr(o, "days\0") +} + +#[inline] +#[cfg(GraalPy)] +pub unsafe fn PyDateTime_DELTA_GET_SECONDS(o: *mut PyObject) -> c_int { + _get_attr(o, "seconds\0") +} + +#[inline] +#[cfg(GraalPy)] +pub unsafe fn PyDateTime_DELTA_GET_MICROSECONDS(o: *mut PyObject) -> c_int { + _get_attr(o, "microseconds\0") +} + #[cfg(PyPy)] extern "C" { // skipped _PyDateTime_HAS_TZINFO (not in PyPy) diff --git a/pyo3-ffi/src/dictobject.rs b/pyo3-ffi/src/dictobject.rs index 8d522df97e2..99fc56b246b 100644 --- a/pyo3-ffi/src/dictobject.rs +++ b/pyo3-ffi/src/dictobject.rs @@ -109,6 +109,6 @@ extern "C" { pub static mut PyDictRevIterItem_Type: PyTypeObject; } -#[cfg(any(PyPy, Py_LIMITED_API))] +#[cfg(any(PyPy, GraalPy, Py_LIMITED_API))] // TODO: remove (see https://github.com/PyO3/pyo3/pull/1341#issuecomment-751515985) opaque_struct!(PyDictObject); diff --git a/pyo3-ffi/src/lib.rs b/pyo3-ffi/src/lib.rs index 5e241fbb2c7..877d42dce33 100644 --- a/pyo3-ffi/src/lib.rs +++ b/pyo3-ffi/src/lib.rs @@ -51,7 +51,7 @@ //! //! PyO3 supports the following software versions: //! - Python 3.7 and up (CPython and PyPy) -//! - Rust 1.56 and up +//! - Rust 1.63 and up //! //! # Example: Building Python Native modules //! @@ -231,7 +231,7 @@ //! [`maturin`]: https://github.com/PyO3/maturin "Build and publish crates with pyo3, rust-cpython and cffi bindings as well as rust binaries as python packages" //! [`pyo3-build-config`]: https://docs.rs/pyo3-build-config //! [feature flags]: https://doc.rust-lang.org/cargo/reference/features.html "Features - The Cargo Book" -//! [manual_builds]: https://pyo3.rs/latest/building_and_distribution.html#manual-builds "Manual builds - Building and Distribution - PyO3 user guide" +//! [manual_builds]: https://pyo3.rs/latest/building-and-distribution.html#manual-builds "Manual builds - Building and Distribution - PyO3 user guide" //! [setuptools-rust]: https://github.com/PyO3/setuptools-rust "Setuptools plugin for Rust extensions" //! [PEP 384]: https://www.python.org/dev/peps/pep-0384 "PEP 384 -- Defining a Stable ABI" //! [Features chapter of the guide]: https://pyo3.rs/latest/features.html#features-reference "Features Reference - PyO3 user guide" diff --git a/pyo3-ffi/src/listobject.rs b/pyo3-ffi/src/listobject.rs index 32c6a2dc26a..a6f47eadf83 100644 --- a/pyo3-ffi/src/listobject.rs +++ b/pyo3-ffi/src/listobject.rs @@ -54,14 +54,16 @@ extern "C" { #[cfg_attr(PyPy, link_name = "PyPyList_AsTuple")] pub fn PyList_AsTuple(arg1: *mut PyObject) -> *mut PyObject; - // CPython macros exported as functions on PyPy - #[cfg(PyPy)] + // CPython macros exported as functions on PyPy or GraalPy + #[cfg(any(PyPy, GraalPy))] #[cfg_attr(PyPy, link_name = "PyPyList_GET_ITEM")] + #[cfg_attr(GraalPy, link_name = "PyList_GetItem")] pub fn PyList_GET_ITEM(arg1: *mut PyObject, arg2: Py_ssize_t) -> *mut PyObject; #[cfg(PyPy)] #[cfg_attr(PyPy, link_name = "PyPyList_GET_SIZE")] pub fn PyList_GET_SIZE(arg1: *mut PyObject) -> Py_ssize_t; - #[cfg(PyPy)] + #[cfg(any(PyPy, GraalPy))] #[cfg_attr(PyPy, link_name = "PyPyList_SET_ITEM")] + #[cfg_attr(GraalPy, link_name = "_PyList_SET_ITEM")] pub fn PyList_SET_ITEM(arg1: *mut PyObject, arg2: Py_ssize_t, arg3: *mut PyObject); } diff --git a/pyo3-ffi/src/longobject.rs b/pyo3-ffi/src/longobject.rs index 55ea8fa1462..35a2bc1b0ff 100644 --- a/pyo3-ffi/src/longobject.rs +++ b/pyo3-ffi/src/longobject.rs @@ -1,8 +1,6 @@ use crate::object::*; use crate::pyport::Py_ssize_t; use libc::size_t; -#[cfg(not(Py_LIMITED_API))] -use std::os::raw::c_uchar; use std::os::raw::{c_char, c_double, c_int, c_long, c_longlong, c_ulong, c_ulonglong, c_void}; use std::ptr::addr_of_mut; @@ -90,34 +88,12 @@ extern "C" { arg3: c_int, ) -> *mut PyObject; } -// skipped non-limited PyLong_FromUnicodeObject -// skipped non-limited _PyLong_FromBytes #[cfg(not(Py_LIMITED_API))] extern "C" { - // skipped non-limited _PyLong_Sign - #[cfg_attr(PyPy, link_name = "_PyPyLong_NumBits")] + #[cfg(not(Py_3_13))] pub fn _PyLong_NumBits(obj: *mut PyObject) -> size_t; - - // skipped _PyLong_DivmodNear - - #[cfg_attr(PyPy, link_name = "_PyPyLong_FromByteArray")] - pub fn _PyLong_FromByteArray( - bytes: *const c_uchar, - n: size_t, - little_endian: c_int, - is_signed: c_int, - ) -> *mut PyObject; - - #[cfg_attr(PyPy, link_name = "_PyPyLong_AsByteArrayO")] - pub fn _PyLong_AsByteArray( - v: *mut PyLongObject, - bytes: *mut c_uchar, - n: size_t, - little_endian: c_int, - is_signed: c_int, - ) -> c_int; } // skipped non-limited _PyLong_Format @@ -130,6 +106,5 @@ extern "C" { pub fn PyOS_strtol(arg1: *const c_char, arg2: *mut *mut c_char, arg3: c_int) -> c_long; } -// skipped non-limited _PyLong_GCD // skipped non-limited _PyLong_Rshift // skipped non-limited _PyLong_Lshift diff --git a/pyo3-ffi/src/methodobject.rs b/pyo3-ffi/src/methodobject.rs index e80d5668e78..3ed6b770e54 100644 --- a/pyo3-ffi/src/methodobject.rs +++ b/pyo3-ffi/src/methodobject.rs @@ -4,7 +4,7 @@ use crate::PyObject_TypeCheck; use std::os::raw::{c_char, c_int, c_void}; use std::{mem, ptr}; -#[cfg(all(Py_3_9, not(Py_LIMITED_API)))] +#[cfg(all(Py_3_9, not(Py_LIMITED_API), not(GraalPy)))] pub struct PyCFunctionObject { pub ob_base: PyObject, pub m_ml: *mut PyMethodDef, diff --git a/pyo3-ffi/src/object.rs b/pyo3-ffi/src/object.rs index 0e0243cd764..b33ee558a37 100644 --- a/pyo3-ffi/src/object.rs +++ b/pyo3-ffi/src/object.rs @@ -79,6 +79,7 @@ pub struct PyObject { #[derive(Debug, Copy, Clone)] pub struct PyVarObject { pub ob_base: PyObject, + #[cfg(not(GraalPy))] pub ob_size: Py_ssize_t, } @@ -98,12 +99,18 @@ pub unsafe fn Py_REFCNT(ob: *mut PyObject) -> Py_ssize_t { #[inline] #[cfg(not(Py_3_12))] pub unsafe fn Py_REFCNT(ob: *mut PyObject) -> Py_ssize_t { - (*ob).ob_refcnt + #[cfg(not(GraalPy))] + return (*ob).ob_refcnt; + #[cfg(GraalPy)] + return _Py_REFCNT(ob); } #[inline] pub unsafe fn Py_TYPE(ob: *mut PyObject) -> *mut PyTypeObject { - (*ob).ob_type + #[cfg(not(GraalPy))] + return (*ob).ob_type; + #[cfg(GraalPy)] + return _Py_TYPE(ob); } // PyLong_Type defined in longobject.rs @@ -111,9 +118,14 @@ pub unsafe fn Py_TYPE(ob: *mut PyObject) -> *mut PyTypeObject { #[inline] pub unsafe fn Py_SIZE(ob: *mut PyObject) -> Py_ssize_t { - debug_assert_ne!((*ob).ob_type, std::ptr::addr_of_mut!(crate::PyLong_Type)); - debug_assert_ne!((*ob).ob_type, std::ptr::addr_of_mut!(crate::PyBool_Type)); - (*ob.cast::()).ob_size + #[cfg(not(GraalPy))] + { + debug_assert_ne!((*ob).ob_type, std::ptr::addr_of_mut!(crate::PyLong_Type)); + debug_assert_ne!((*ob).ob_type, std::ptr::addr_of_mut!(crate::PyBool_Type)); + (*ob.cast::()).ob_size + } + #[cfg(GraalPy)] + _Py_SIZE(ob) } #[inline] @@ -464,8 +476,10 @@ extern "C" { pub fn _Py_Dealloc(arg1: *mut PyObject); #[cfg_attr(PyPy, link_name = "PyPy_IncRef")] + #[cfg_attr(GraalPy, link_name = "_Py_IncRef")] pub fn Py_IncRef(o: *mut PyObject); #[cfg_attr(PyPy, link_name = "PyPy_DecRef")] + #[cfg_attr(GraalPy, link_name = "_Py_DecRef")] pub fn Py_DecRef(o: *mut PyObject); #[cfg(Py_3_10)] @@ -474,11 +488,21 @@ extern "C" { #[cfg(Py_3_10)] #[cfg_attr(PyPy, link_name = "_PyPy_DecRef")] pub fn _Py_DecRef(o: *mut PyObject); + + #[cfg(GraalPy)] + pub fn _Py_REFCNT(arg1: *const PyObject) -> Py_ssize_t; + + #[cfg(GraalPy)] + pub fn _Py_TYPE(arg1: *const PyObject) -> *mut PyTypeObject; + + #[cfg(GraalPy)] + pub fn _Py_SIZE(arg1: *const PyObject) -> Py_ssize_t; } #[inline(always)] pub unsafe fn Py_INCREF(op: *mut PyObject) { #[cfg(any( + GraalPy, all(Py_LIMITED_API, Py_3_12), all( py_sys_config = "Py_REF_DEBUG", @@ -499,6 +523,7 @@ pub unsafe fn Py_INCREF(op: *mut PyObject) { all(Py_LIMITED_API, not(Py_3_12)), all( not(Py_LIMITED_API), + not(GraalPy), any( not(py_sys_config = "Py_REF_DEBUG"), all(py_sys_config = "Py_REF_DEBUG", Py_3_12), @@ -544,6 +569,7 @@ pub unsafe fn Py_INCREF(op: *mut PyObject) { )] pub unsafe fn Py_DECREF(op: *mut PyObject) { #[cfg(any( + GraalPy, all(Py_LIMITED_API, Py_3_12), all( py_sys_config = "Py_REF_DEBUG", @@ -564,6 +590,7 @@ pub unsafe fn Py_DECREF(op: *mut PyObject) { all(Py_LIMITED_API, not(Py_3_12)), all( not(Py_LIMITED_API), + not(GraalPy), any( not(py_sys_config = "Py_REF_DEBUG"), all(py_sys_config = "Py_REF_DEBUG", Py_3_12), @@ -669,13 +696,20 @@ pub unsafe fn Py_XNewRef(obj: *mut PyObject) -> *mut PyObject { #[cfg_attr(windows, link(name = "pythonXY"))] extern "C" { + #[cfg(not(GraalPy))] #[cfg_attr(PyPy, link_name = "_PyPy_NoneStruct")] static mut _Py_NoneStruct: PyObject; + + #[cfg(GraalPy)] + static mut _Py_NoneStructReference: *mut PyObject; } #[inline] pub unsafe fn Py_None() -> *mut PyObject { - ptr::addr_of_mut!(_Py_NoneStruct) + #[cfg(not(GraalPy))] + return ptr::addr_of_mut!(_Py_NoneStruct); + #[cfg(GraalPy)] + return _Py_NoneStructReference; } #[inline] @@ -687,13 +721,20 @@ pub unsafe fn Py_IsNone(x: *mut PyObject) -> c_int { #[cfg_attr(windows, link(name = "pythonXY"))] extern "C" { + #[cfg(not(GraalPy))] #[cfg_attr(PyPy, link_name = "_PyPy_NotImplementedStruct")] static mut _Py_NotImplementedStruct: PyObject; + + #[cfg(GraalPy)] + static mut _Py_NotImplementedStructReference: *mut PyObject; } #[inline] pub unsafe fn Py_NotImplemented() -> *mut PyObject { - ptr::addr_of_mut!(_Py_NotImplementedStruct) + #[cfg(not(GraalPy))] + return ptr::addr_of_mut!(_Py_NotImplementedStruct); + #[cfg(GraalPy)] + return _Py_NotImplementedStructReference; } // skipped Py_RETURN_NOTIMPLEMENTED diff --git a/pyo3-ffi/src/pybuffer.rs b/pyo3-ffi/src/pybuffer.rs index 20f92fb6d2b..50bf4e6109c 100644 --- a/pyo3-ffi/src/pybuffer.rs +++ b/pyo3-ffi/src/pybuffer.rs @@ -3,9 +3,6 @@ use crate::pyport::Py_ssize_t; use std::os::raw::{c_char, c_int, c_void}; use std::ptr; -#[cfg(PyPy)] -const Py_MAX_NDIMS: usize = 36; - #[repr(C)] #[derive(Copy, Clone)] pub struct Py_buffer { @@ -24,12 +21,13 @@ pub struct Py_buffer { #[cfg(PyPy)] pub flags: c_int, #[cfg(PyPy)] - pub _strides: [Py_ssize_t; Py_MAX_NDIMS], + pub _strides: [Py_ssize_t; PyBUF_MAX_NDIM], #[cfg(PyPy)] - pub _shape: [Py_ssize_t; Py_MAX_NDIMS], + pub _shape: [Py_ssize_t; PyBUF_MAX_NDIM], } impl Py_buffer { + #[allow(clippy::new_without_default)] pub const fn new() -> Self { Py_buffer { buf: ptr::null_mut(), @@ -46,9 +44,9 @@ impl Py_buffer { #[cfg(PyPy)] flags: 0, #[cfg(PyPy)] - _strides: [0; Py_MAX_NDIMS], + _strides: [0; PyBUF_MAX_NDIM], #[cfg(PyPy)] - _shape: [0; Py_MAX_NDIMS], + _shape: [0; PyBUF_MAX_NDIM], } } } @@ -105,7 +103,7 @@ extern "C" { } /// Maximum number of dimensions -pub const PyBUF_MAX_NDIM: c_int = 64; +pub const PyBUF_MAX_NDIM: c_int = if cfg!(PyPy) { 36 } else { 64 }; /* Flags for getting buffers */ pub const PyBUF_SIMPLE: c_int = 0; diff --git a/pyo3-ffi/src/pyframe.rs b/pyo3-ffi/src/pyframe.rs index 43a9d1f6777..4dd3d2b31a5 100644 --- a/pyo3-ffi/src/pyframe.rs +++ b/pyo3-ffi/src/pyframe.rs @@ -1,3 +1,4 @@ +#[cfg(not(GraalPy))] #[cfg(any(Py_3_10, all(Py_3_9, not(Py_LIMITED_API))))] use crate::PyCodeObject; #[cfg(not(Py_LIMITED_API))] @@ -9,6 +10,7 @@ opaque_struct!(PyFrameObject); extern "C" { pub fn PyFrame_GetLineNumber(f: *mut PyFrameObject) -> c_int; + #[cfg(not(GraalPy))] #[cfg(any(Py_3_10, all(Py_3_9, not(Py_LIMITED_API))))] pub fn PyFrame_GetCode(f: *mut PyFrameObject) -> *mut PyCodeObject; } diff --git a/pyo3-ffi/src/pyhash.rs b/pyo3-ffi/src/pyhash.rs index f8072d85108..f42f9730f1b 100644 --- a/pyo3-ffi/src/pyhash.rs +++ b/pyo3-ffi/src/pyhash.rs @@ -1,6 +1,6 @@ -#[cfg(not(any(Py_LIMITED_API, PyPy)))] +#[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))] use crate::pyport::{Py_hash_t, Py_ssize_t}; -#[cfg(not(any(Py_LIMITED_API, PyPy)))] +#[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))] use std::os::raw::{c_char, c_void}; use std::os::raw::{c_int, c_ulong}; @@ -10,7 +10,7 @@ extern "C" { // skipped non-limited _Py_HashPointer // skipped non-limited _Py_HashPointerRaw - #[cfg(not(any(Py_LIMITED_API, PyPy)))] + #[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))] pub fn _Py_HashBytes(src: *const c_void, len: Py_ssize_t) -> Py_hash_t; } @@ -20,7 +20,7 @@ pub const _PyHASH_MULTIPLIER: c_ulong = 1000003; // skipped non-limited _Py_HashSecret_t -#[cfg(not(any(Py_LIMITED_API, PyPy)))] +#[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))] #[repr(C)] #[derive(Copy, Clone)] pub struct PyHash_FuncDef { @@ -30,7 +30,7 @@ pub struct PyHash_FuncDef { pub seed_bits: c_int, } -#[cfg(not(any(Py_LIMITED_API, PyPy)))] +#[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))] impl Default for PyHash_FuncDef { #[inline] fn default() -> Self { @@ -39,7 +39,7 @@ impl Default for PyHash_FuncDef { } extern "C" { - #[cfg(not(any(Py_LIMITED_API, PyPy)))] + #[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))] pub fn PyHash_GetFuncDef() -> *mut PyHash_FuncDef; } diff --git a/pyo3-ffi/src/pyport.rs b/pyo3-ffi/src/pyport.rs index 741b0db7bf8..a144c67fb1b 100644 --- a/pyo3-ffi/src/pyport.rs +++ b/pyo3-ffi/src/pyport.rs @@ -11,8 +11,8 @@ pub type Py_ssize_t = ::libc::ssize_t; pub type Py_hash_t = Py_ssize_t; pub type Py_uhash_t = ::libc::size_t; -pub const PY_SSIZE_T_MIN: Py_ssize_t = std::isize::MIN as Py_ssize_t; -pub const PY_SSIZE_T_MAX: Py_ssize_t = std::isize::MAX as Py_ssize_t; +pub const PY_SSIZE_T_MIN: Py_ssize_t = isize::MIN as Py_ssize_t; +pub const PY_SSIZE_T_MAX: Py_ssize_t = isize::MAX as Py_ssize_t; #[cfg(target_endian = "big")] pub const PY_BIG_ENDIAN: usize = 1; diff --git a/pyo3-ffi/src/pythonrun.rs b/pyo3-ffi/src/pythonrun.rs index e5f20de0058..10985b6068c 100644 --- a/pyo3-ffi/src/pythonrun.rs +++ b/pyo3-ffi/src/pythonrun.rs @@ -1,12 +1,12 @@ use crate::object::*; #[cfg(not(any(PyPy, Py_LIMITED_API, Py_3_10)))] use libc::FILE; -#[cfg(all(not(PyPy), any(Py_LIMITED_API, not(Py_3_10))))] +#[cfg(all(not(PyPy), any(Py_LIMITED_API, not(Py_3_10), GraalPy)))] use std::os::raw::c_char; use std::os::raw::c_int; extern "C" { - #[cfg(all(Py_LIMITED_API, not(PyPy)))] + #[cfg(any(all(Py_LIMITED_API, not(PyPy)), GraalPy))] pub fn Py_CompileString(string: *const c_char, p: *const c_char, s: c_int) -> *mut PyObject; #[cfg_attr(PyPy, link_name = "PyPyErr_Print")] diff --git a/pyo3-ffi/src/setobject.rs b/pyo3-ffi/src/setobject.rs index 84a368a7f27..9d5351fc798 100644 --- a/pyo3-ffi/src/setobject.rs +++ b/pyo3-ffi/src/setobject.rs @@ -1,5 +1,5 @@ use crate::object::*; -#[cfg(not(any(Py_LIMITED_API, PyPy)))] +#[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))] use crate::pyport::Py_hash_t; use crate::pyport::Py_ssize_t; use std::os::raw::c_int; @@ -7,7 +7,7 @@ use std::ptr::addr_of_mut; pub const PySet_MINSIZE: usize = 8; -#[cfg(not(any(Py_LIMITED_API, PyPy)))] +#[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))] #[repr(C)] #[derive(Debug)] pub struct setentry { @@ -15,7 +15,7 @@ pub struct setentry { pub hash: Py_hash_t, } -#[cfg(not(any(Py_LIMITED_API, PyPy)))] +#[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))] #[repr(C)] #[derive(Debug)] pub struct PySetObject { @@ -32,7 +32,7 @@ pub struct PySetObject { // skipped #[inline] -#[cfg(all(not(PyPy), not(Py_LIMITED_API)))] +#[cfg(all(not(any(PyPy, GraalPy)), not(Py_LIMITED_API)))] pub unsafe fn PySet_GET_SIZE(so: *mut PyObject) -> Py_ssize_t { debug_assert_eq!(PyAnySet_Check(so), 1); let so = so.cast::(); @@ -92,7 +92,7 @@ extern "C" { } #[inline] -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] pub unsafe fn PyFrozenSet_CheckExact(ob: *mut PyObject) -> c_int { (Py_TYPE(ob) == addr_of_mut!(PyFrozenSet_Type)) as c_int } diff --git a/pyo3-ffi/src/sliceobject.rs b/pyo3-ffi/src/sliceobject.rs index 6f09906fcc4..a3ea153987c 100644 --- a/pyo3-ffi/src/sliceobject.rs +++ b/pyo3-ffi/src/sliceobject.rs @@ -5,21 +5,31 @@ use std::ptr::addr_of_mut; #[cfg_attr(windows, link(name = "pythonXY"))] extern "C" { + #[cfg(not(GraalPy))] #[cfg_attr(PyPy, link_name = "_PyPy_EllipsisObject")] static mut _Py_EllipsisObject: PyObject; + + #[cfg(GraalPy)] + static mut _Py_EllipsisObjectReference: *mut PyObject; } #[inline] pub unsafe fn Py_Ellipsis() -> *mut PyObject { - addr_of_mut!(_Py_EllipsisObject) + #[cfg(not(GraalPy))] + return addr_of_mut!(_Py_EllipsisObject); + #[cfg(GraalPy)] + return _Py_EllipsisObjectReference; } #[cfg(not(Py_LIMITED_API))] #[repr(C)] pub struct PySliceObject { pub ob_base: PyObject, + #[cfg(not(GraalPy))] pub start: *mut PyObject, + #[cfg(not(GraalPy))] pub stop: *mut PyObject, + #[cfg(not(GraalPy))] pub step: *mut PyObject, } diff --git a/pyo3-ffi/src/structseq.rs b/pyo3-ffi/src/structseq.rs index 6dfc1daf158..f8566787b51 100644 --- a/pyo3-ffi/src/structseq.rs +++ b/pyo3-ffi/src/structseq.rs @@ -42,13 +42,13 @@ extern "C" { #[cfg(not(Py_LIMITED_API))] pub type PyStructSequence = crate::PyTupleObject; -#[cfg(not(any(Py_LIMITED_API, PyPy)))] +#[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))] #[inline] pub unsafe fn PyStructSequence_SET_ITEM(op: *mut PyObject, i: Py_ssize_t, v: *mut PyObject) { crate::PyTuple_SET_ITEM(op, i, v) } -#[cfg(not(any(Py_LIMITED_API, PyPy)))] +#[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))] #[inline] pub unsafe fn PyStructSequence_GET_ITEM(op: *mut PyObject, i: Py_ssize_t) -> *mut PyObject { crate::PyTuple_GET_ITEM(op, i) diff --git a/pyo3-ffi/src/weakrefobject.rs b/pyo3-ffi/src/weakrefobject.rs index d065ae23e0f..7e11a9012e7 100644 --- a/pyo3-ffi/src/weakrefobject.rs +++ b/pyo3-ffi/src/weakrefobject.rs @@ -3,10 +3,10 @@ use std::os::raw::c_int; #[cfg(not(PyPy))] use std::ptr::addr_of_mut; -#[cfg(all(not(PyPy), Py_LIMITED_API))] +#[cfg(all(not(PyPy), Py_LIMITED_API, not(GraalPy)))] opaque_struct!(PyWeakReference); -#[cfg(all(not(PyPy), not(Py_LIMITED_API)))] +#[cfg(all(not(PyPy), not(Py_LIMITED_API), not(GraalPy)))] pub use crate::_PyWeakReference as PyWeakReference; #[cfg_attr(windows, link(name = "pythonXY"))] diff --git a/pyo3-macros-backend/Cargo.toml b/pyo3-macros-backend/Cargo.toml index 3263365dc80..7bc0f6a2da1 100644 --- a/pyo3-macros-backend/Cargo.toml +++ b/pyo3-macros-backend/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3-macros-backend" -version = "0.21.0-dev" +version = "0.22.0-dev" description = "Code generation for PyO3 package" authors = ["PyO3 Project and Contributors "] keywords = ["pyo3", "python", "cpython", "ffi"] @@ -14,17 +14,19 @@ edition = "2021" # not to depend on proc-macro itself. # See https://github.com/PyO3/pyo3/pull/810 for more. [dependencies] -quote = { version = "1", default-features = false } +heck = "0.5" proc-macro2 = { version = "1", default-features = false } -heck = "0.4" +pyo3-build-config = { path = "../pyo3-build-config", version = "=0.22.0-dev", features = ["resolve-config"] } +quote = { version = "1", default-features = false } [dependencies.syn] version = "2" default-features = false features = ["derive", "parsing", "printing", "clone-impls", "full", "extra-traits"] -[features] -abi3 = [] - [lints] workspace = true + +[features] +experimental-async = [] +gil-refs = [] diff --git a/pyo3-macros-backend/src/attributes.rs b/pyo3-macros-backend/src/attributes.rs index e91b3b8d9a2..d9c805aa3fa 100644 --- a/pyo3-macros-backend/src/attributes.rs +++ b/pyo3-macros-backend/src/attributes.rs @@ -12,6 +12,7 @@ pub mod kw { syn::custom_keyword!(annotation); syn::custom_keyword!(attribute); syn::custom_keyword!(cancel_handle); + syn::custom_keyword!(constructor); syn::custom_keyword!(dict); syn::custom_keyword!(extends); syn::custom_keyword!(freelist); diff --git a/pyo3-macros-backend/src/deprecations.rs b/pyo3-macros-backend/src/deprecations.rs index ea2922737b9..4db40cc86f7 100644 --- a/pyo3-macros-backend/src/deprecations.rs +++ b/pyo3-macros-backend/src/deprecations.rs @@ -1,3 +1,7 @@ +use crate::{ + method::{FnArg, FnSpec}, + utils::Ctx, +}; use proc_macro2::{Span, TokenStream}; use quote::{quote_spanned, ToTokens}; @@ -14,12 +18,11 @@ impl Deprecation { } } -#[derive(Default)] -pub struct Deprecations(Vec<(Deprecation, Span)>); +pub struct Deprecations<'ctx>(Vec<(Deprecation, Span)>, &'ctx Ctx); -impl Deprecations { - pub fn new() -> Self { - Deprecations(Vec::new()) +impl<'ctx> Deprecations<'ctx> { + pub fn new(ctx: &'ctx Ctx) -> Self { + Deprecations(Vec::new(), ctx) } pub fn push(&mut self, deprecation: Deprecation, span: Span) { @@ -27,18 +30,69 @@ impl Deprecations { } } -impl ToTokens for Deprecations { +impl<'ctx> ToTokens for Deprecations<'ctx> { fn to_tokens(&self, tokens: &mut TokenStream) { - for (deprecation, span) in &self.0 { + let Self(deprecations, Ctx { pyo3_path }) = self; + + for (deprecation, span) in deprecations { + let pyo3_path = pyo3_path.to_tokens_spanned(*span); let ident = deprecation.ident(*span); quote_spanned!( *span => #[allow(clippy::let_unit_value)] { - let _ = _pyo3::impl_::deprecations::#ident; + let _ = #pyo3_path::impl_::deprecations::#ident; } ) .to_tokens(tokens) } } } + +pub(crate) fn deprecate_trailing_option_default(spec: &FnSpec<'_>) -> TokenStream { + if spec.signature.attribute.is_none() + && spec.signature.arguments.iter().any(|arg| { + if let FnArg::Regular(arg) = arg { + arg.option_wrapped_type.is_some() + } else { + false + } + }) + { + use std::fmt::Write; + let mut deprecation_msg = String::from( + "This function has implicit defaults for the trailing `Option` arguments. \ + These implicit defaults are being phased out. Add `#[pyo3(signature = (", + ); + spec.signature.arguments.iter().for_each(|arg| { + match arg { + FnArg::Regular(arg) => { + if arg.option_wrapped_type.is_some() { + write!(deprecation_msg, "{}=None, ", arg.name) + } else { + write!(deprecation_msg, "{}, ", arg.name) + } + } + FnArg::VarArgs(arg) => write!(deprecation_msg, "{}, ", arg.name), + FnArg::KwArgs(arg) => write!(deprecation_msg, "{}, ", arg.name), + FnArg::Py(_) | FnArg::CancelHandle(_) => Ok(()), + } + .expect("writing to `String` should not fail"); + }); + + //remove trailing space and comma + deprecation_msg.pop(); + deprecation_msg.pop(); + + deprecation_msg + .push_str(")]` to this function to silence this warning and keep the current behavior"); + quote_spanned! { spec.name.span() => + #[deprecated(note = #deprecation_msg)] + #[allow(dead_code)] + const SIGNATURE: () = (); + const _: () = SIGNATURE; + } + } else { + TokenStream::new() + } +} diff --git a/pyo3-macros-backend/src/frompyobject.rs b/pyo3-macros-backend/src/frompyobject.rs index 2b527dc8a29..7f26e5b14fc 100644 --- a/pyo3-macros-backend/src/frompyobject.rs +++ b/pyo3-macros-backend/src/frompyobject.rs @@ -1,9 +1,7 @@ -use crate::{ - attributes::{self, get_pyo3_options, CrateAttribute, FromPyWithAttribute}, - utils::get_pyo3_crate, -}; +use crate::attributes::{self, get_pyo3_options, CrateAttribute, FromPyWithAttribute}; +use crate::utils::Ctx; use proc_macro2::TokenStream; -use quote::{format_ident, quote}; +use quote::{format_ident, quote, quote_spanned}; use syn::{ parenthesized, parse::{Parse, ParseStream}, @@ -46,14 +44,18 @@ impl<'a> Enum<'a> { } /// Build derivation body for enums. - fn build(&self) -> TokenStream { + fn build(&self, ctx: &Ctx) -> (TokenStream, TokenStream) { + let Ctx { pyo3_path } = ctx; let mut var_extracts = Vec::new(); let mut variant_names = Vec::new(); let mut error_names = Vec::new(); + + let mut deprecations = TokenStream::new(); for var in &self.variants { - let struct_derive = var.build(); + let (struct_derive, dep) = var.build(ctx); + deprecations.extend(dep); let ext = quote!({ - let maybe_ret = || -> _pyo3::PyResult { + let maybe_ret = || -> #pyo3_path::PyResult { #struct_derive }(); @@ -68,19 +70,22 @@ impl<'a> Enum<'a> { error_names.push(&var.err_name); } let ty_name = self.enum_ident.to_string(); - quote!( - let errors = [ - #(#var_extracts),* - ]; - ::std::result::Result::Err( - _pyo3::impl_::frompyobject::failed_to_extract_enum( - obj.py(), - #ty_name, - &[#(#variant_names),*], - &[#(#error_names),*], - &errors + ( + quote!( + let errors = [ + #(#var_extracts),* + ]; + ::std::result::Result::Err( + #pyo3_path::impl_::frompyobject::failed_to_extract_enum( + obj.py(), + #ty_name, + &[#(#variant_names),*], + &[#(#error_names),*], + &errors + ) ) - ) + ), + deprecations, ) } } @@ -239,16 +244,16 @@ impl<'a> Container<'a> { } /// Build derivation body for a struct. - fn build(&self) -> TokenStream { + fn build(&self, ctx: &Ctx) -> (TokenStream, TokenStream) { match &self.ty { ContainerType::StructNewtype(ident, from_py_with) => { - self.build_newtype_struct(Some(ident), from_py_with) + self.build_newtype_struct(Some(ident), from_py_with, ctx) } ContainerType::TupleNewtype(from_py_with) => { - self.build_newtype_struct(None, from_py_with) + self.build_newtype_struct(None, from_py_with, ctx) } - ContainerType::Tuple(tups) => self.build_tuple_struct(tups), - ContainerType::Struct(tups) => self.build_struct(tups), + ContainerType::Tuple(tups) => self.build_tuple_struct(tups, ctx), + ContainerType::Struct(tups) => self.build_struct(tups, ctx), } } @@ -256,40 +261,75 @@ impl<'a> Container<'a> { &self, field_ident: Option<&Ident>, from_py_with: &Option, - ) -> TokenStream { + ctx: &Ctx, + ) -> (TokenStream, TokenStream) { + let Ctx { pyo3_path } = ctx; let self_ty = &self.path; let struct_name = self.name(); if let Some(ident) = field_ident { let field_name = ident.to_string(); match from_py_with { - None => quote! { - Ok(#self_ty { - #ident: _pyo3::impl_::frompyobject::extract_struct_field(obj, #struct_name, #field_name)? - }) - }, + None => ( + quote! { + Ok(#self_ty { + #ident: #pyo3_path::impl_::frompyobject::extract_struct_field(obj, #struct_name, #field_name)? + }) + }, + TokenStream::new(), + ), Some(FromPyWithAttribute { value: expr_path, .. - }) => quote! { - Ok(#self_ty { - #ident: _pyo3::impl_::frompyobject::extract_struct_field_with(#expr_path, obj, #struct_name, #field_name)? - }) - }, + }) => ( + quote! { + Ok(#self_ty { + #ident: #pyo3_path::impl_::frompyobject::extract_struct_field_with(#expr_path as fn(_) -> _, obj, #struct_name, #field_name)? + }) + }, + quote_spanned! { expr_path.span() => + const _: () = { + fn check_from_py_with() { + let e = #pyo3_path::impl_::deprecations::GilRefs::new(); + #pyo3_path::impl_::deprecations::inspect_fn(#expr_path, &e); + e.from_py_with_arg(); + } + }; + }, + ), } } else { match from_py_with { - None => quote!( - _pyo3::impl_::frompyobject::extract_tuple_struct_field(obj, #struct_name, 0).map(#self_ty) + None => ( + quote!( + #pyo3_path::impl_::frompyobject::extract_tuple_struct_field(obj, #struct_name, 0).map(#self_ty) + ), + TokenStream::new(), ), Some(FromPyWithAttribute { value: expr_path, .. - }) => quote! ( - _pyo3::impl_::frompyobject::extract_tuple_struct_field_with(#expr_path, obj, #struct_name, 0).map(#self_ty) + }) => ( + quote! ( + #pyo3_path::impl_::frompyobject::extract_tuple_struct_field_with(#expr_path as fn(_) -> _, obj, #struct_name, 0).map(#self_ty) + ), + quote_spanned! { expr_path.span() => + const _: () = { + fn check_from_py_with() { + let e = #pyo3_path::impl_::deprecations::GilRefs::new(); + #pyo3_path::impl_::deprecations::inspect_fn(#expr_path, &e); + e.from_py_with_arg(); + } + }; + }, ), } } } - fn build_tuple_struct(&self, struct_fields: &[TupleStructField]) -> TokenStream { + fn build_tuple_struct( + &self, + struct_fields: &[TupleStructField], + ctx: &Ctx, + ) -> (TokenStream, TokenStream) { + let Ctx { pyo3_path } = ctx; let self_ty = &self.path; let struct_name = &self.name(); let field_idents: Vec<_> = (0..struct_fields.len()) @@ -298,24 +338,51 @@ impl<'a> Container<'a> { let fields = struct_fields.iter().zip(&field_idents).enumerate().map(|(index, (field, ident))| { match &field.from_py_with { None => quote!( - _pyo3::impl_::frompyobject::extract_tuple_struct_field(#ident, #struct_name, #index)? + #pyo3_path::impl_::frompyobject::extract_tuple_struct_field(&#ident, #struct_name, #index)? ), Some(FromPyWithAttribute { value: expr_path, .. }) => quote! ( - _pyo3::impl_::frompyobject::extract_tuple_struct_field_with(#expr_path, #ident, #struct_name, #index)? + #pyo3_path::impl_::frompyobject::extract_tuple_struct_field_with(#expr_path as fn(_) -> _, &#ident, #struct_name, #index)? ), } }); - quote!( - match obj.extract() { - ::std::result::Result::Ok((#(#field_idents),*)) => ::std::result::Result::Ok(#self_ty(#(#fields),*)), - ::std::result::Result::Err(err) => ::std::result::Result::Err(err), - } + + let deprecations = struct_fields + .iter() + .filter_map(|field| { + let FromPyWithAttribute { + value: expr_path, .. + } = field.from_py_with.as_ref()?; + Some(quote_spanned! { expr_path.span() => + const _: () = { + fn check_from_py_with() { + let e = #pyo3_path::impl_::deprecations::GilRefs::new(); + #pyo3_path::impl_::deprecations::inspect_fn(#expr_path, &e); + e.from_py_with_arg(); + } + }; + }) + }) + .collect::(); + + ( + quote!( + match #pyo3_path::types::PyAnyMethods::extract(obj) { + ::std::result::Result::Ok((#(#field_idents),*)) => ::std::result::Result::Ok(#self_ty(#(#fields),*)), + ::std::result::Result::Err(err) => ::std::result::Result::Err(err), + } + ), + deprecations, ) } - fn build_struct(&self, struct_fields: &[NamedStructField<'_>]) -> TokenStream { + fn build_struct( + &self, + struct_fields: &[NamedStructField<'_>], + ctx: &Ctx, + ) -> (TokenStream, TokenStream) { + let Ctx { pyo3_path } = ctx; let self_ty = &self.path; let struct_name = &self.name(); let mut fields: Punctuated = Punctuated::new(); @@ -324,33 +391,57 @@ impl<'a> Container<'a> { let field_name = ident.to_string(); let getter = match field.getter.as_ref().unwrap_or(&FieldGetter::GetAttr(None)) { FieldGetter::GetAttr(Some(name)) => { - quote!(getattr(_pyo3::intern!(obj.py(), #name))) + quote!(#pyo3_path::types::PyAnyMethods::getattr(obj, #pyo3_path::intern!(obj.py(), #name))) } FieldGetter::GetAttr(None) => { - quote!(getattr(_pyo3::intern!(obj.py(), #field_name))) + quote!(#pyo3_path::types::PyAnyMethods::getattr(obj, #pyo3_path::intern!(obj.py(), #field_name))) } FieldGetter::GetItem(Some(syn::Lit::Str(key))) => { - quote!(get_item(_pyo3::intern!(obj.py(), #key))) + quote!(#pyo3_path::types::PyAnyMethods::get_item(obj, #pyo3_path::intern!(obj.py(), #key))) + } + FieldGetter::GetItem(Some(key)) => { + quote!(#pyo3_path::types::PyAnyMethods::get_item(obj, #key)) } - FieldGetter::GetItem(Some(key)) => quote!(get_item(#key)), FieldGetter::GetItem(None) => { - quote!(get_item(_pyo3::intern!(obj.py(), #field_name))) + quote!(#pyo3_path::types::PyAnyMethods::get_item(obj, #pyo3_path::intern!(obj.py(), #field_name))) } }; let extractor = match &field.from_py_with { None => { - quote!(_pyo3::impl_::frompyobject::extract_struct_field(obj.#getter?, #struct_name, #field_name)?) + quote!(#pyo3_path::impl_::frompyobject::extract_struct_field(&#getter?, #struct_name, #field_name)?) } Some(FromPyWithAttribute { value: expr_path, .. }) => { - quote! (_pyo3::impl_::frompyobject::extract_struct_field_with(#expr_path, obj.#getter?, #struct_name, #field_name)?) + quote! (#pyo3_path::impl_::frompyobject::extract_struct_field_with(#expr_path as fn(_) -> _, &#getter?, #struct_name, #field_name)?) } }; fields.push(quote!(#ident: #extractor)); } - quote!(::std::result::Result::Ok(#self_ty{#fields})) + + let deprecations = struct_fields + .iter() + .filter_map(|field| { + let FromPyWithAttribute { + value: expr_path, .. + } = field.from_py_with.as_ref()?; + Some(quote_spanned! { expr_path.span() => + const _: () = { + fn check_from_py_with() { + let e = #pyo3_path::impl_::deprecations::GilRefs::new(); + #pyo3_path::impl_::deprecations::inspect_fn(#expr_path, &e); + e.from_py_with_arg(); + } + }; + }) + }) + .collect::(); + + ( + quote!(::std::result::Result::Ok(#self_ty{#fields})), + deprecations, + ) } } @@ -568,8 +659,8 @@ pub fn build_derive_from_pyobject(tokens: &DeriveInput) -> Result { let lt_param = if let Some(lt) = verify_and_get_lifetime(generics)? { lt.clone() } else { - trait_generics.params.push(parse_quote!('source)); - parse_quote!('source) + trait_generics.params.push(parse_quote!('py)); + parse_quote!('py) }; let mut where_clause: syn::WhereClause = parse_quote!(where); for param in generics.type_params() { @@ -579,15 +670,17 @@ pub fn build_derive_from_pyobject(tokens: &DeriveInput) -> Result { .push(parse_quote!(#gen_ident: FromPyObject<#lt_param>)) } let options = ContainerOptions::from_attrs(&tokens.attrs)?; - let krate = get_pyo3_crate(&options.krate); - let derives = match &tokens.data { + let ctx = &Ctx::new(&options.krate); + let Ctx { pyo3_path } = &ctx; + + let (derives, from_py_with_deprecations) = match &tokens.data { syn::Data::Enum(en) => { if options.transparent || options.annotation.is_some() { bail_spanned!(tokens.span() => "`transparent` or `annotation` is not supported \ at top level for enums"); } let en = Enum::new(en, &tokens.ident)?; - en.build() + en.build(ctx) } syn::Data::Struct(st) => { if let Some(lit_str) = &options.annotation { @@ -595,7 +688,7 @@ pub fn build_derive_from_pyobject(tokens: &DeriveInput) -> Result { } let ident = &tokens.ident; let st = Container::new(&st.fields, parse_quote!(#ident), options)?; - st.build() + st.build(ctx) } syn::Data::Union(_) => bail_spanned!( tokens.span() => "#[derive(FromPyObject)] is not supported for unions" @@ -604,15 +697,13 @@ pub fn build_derive_from_pyobject(tokens: &DeriveInput) -> Result { let ident = &tokens.ident; Ok(quote!( - const _: () = { - use #krate as _pyo3; - - #[automatically_derived] - impl #trait_generics _pyo3::FromPyObject<#lt_param> for #ident #generics #where_clause { - fn extract(obj: &#lt_param _pyo3::PyAny) -> _pyo3::PyResult { - #derives - } + #[automatically_derived] + impl #trait_generics #pyo3_path::FromPyObject<#lt_param> for #ident #generics #where_clause { + fn extract_bound(obj: &#pyo3_path::Bound<#lt_param, #pyo3_path::PyAny>) -> #pyo3_path::PyResult { + #derives } - }; + } + + #from_py_with_deprecations )) } diff --git a/pyo3-macros-backend/src/konst.rs b/pyo3-macros-backend/src/konst.rs index 935c9d4a302..9a41a2b7178 100644 --- a/pyo3-macros-backend/src/konst.rs +++ b/pyo3-macros-backend/src/konst.rs @@ -1,5 +1,6 @@ use std::borrow::Cow; +use crate::utils::Ctx; use crate::{ attributes::{self, get_pyo3_options, take_attributes, NameAttribute}, deprecations::Deprecations, @@ -13,12 +14,12 @@ use syn::{ Result, }; -pub struct ConstSpec { +pub struct ConstSpec<'ctx> { pub rust_ident: syn::Ident, - pub attributes: ConstAttributes, + pub attributes: ConstAttributes<'ctx>, } -impl ConstSpec { +impl ConstSpec<'_> { pub fn python_name(&self) -> Cow<'_, Ident> { if let Some(name) = &self.attributes.name { Cow::Borrowed(&name.value.0) @@ -34,10 +35,10 @@ impl ConstSpec { } } -pub struct ConstAttributes { +pub struct ConstAttributes<'ctx> { pub is_class_attr: bool, pub name: Option, - pub deprecations: Deprecations, + pub deprecations: Deprecations<'ctx>, } pub enum PyO3ConstAttribute { @@ -55,12 +56,12 @@ impl Parse for PyO3ConstAttribute { } } -impl ConstAttributes { - pub fn from_attrs(attrs: &mut Vec) -> syn::Result { +impl<'ctx> ConstAttributes<'ctx> { + pub fn from_attrs(attrs: &mut Vec, ctx: &'ctx Ctx) -> syn::Result { let mut attributes = ConstAttributes { is_class_attr: false, name: None, - deprecations: Deprecations::new(), + deprecations: Deprecations::new(ctx), }; take_attributes(attrs, |attr| { diff --git a/pyo3-macros-backend/src/lib.rs b/pyo3-macros-backend/src/lib.rs index 745a8471c2b..a9d75a2a6fe 100644 --- a/pyo3-macros-backend/src/lib.rs +++ b/pyo3-macros-backend/src/lib.rs @@ -22,7 +22,7 @@ mod pymethod; mod quotes; pub use frompyobject::build_derive_from_pyobject; -pub use module::{process_functions_in_module, pymodule_impl, PyModuleOptions}; +pub use module::{pymodule_function_impl, pymodule_module_impl, PyModuleOptions}; pub use pyclass::{build_py_class, build_py_enum, PyClassArgs}; pub use pyfunction::{build_py_function, PyFunctionOptions}; pub use pyimpl::{build_py_methods, PyClassMethodsType}; diff --git a/pyo3-macros-backend/src/method.rs b/pyo3-macros-backend/src/method.rs index 7050be23d5c..c0e38bf8416 100644 --- a/pyo3-macros-backend/src/method.rs +++ b/pyo3-macros-backend/src/method.rs @@ -1,34 +1,133 @@ +use std::borrow::Cow; use std::fmt::Display; use proc_macro2::{Span, TokenStream}; -use quote::{quote, quote_spanned, ToTokens}; +use quote::{format_ident, quote, quote_spanned, ToTokens}; use syn::{ext::IdentExt, spanned::Spanned, Ident, Result}; +use crate::deprecations::deprecate_trailing_option_default; +use crate::utils::Ctx; use crate::{ - attributes::{TextSignatureAttribute, TextSignatureAttributeValue}, + attributes::{FromPyWithAttribute, TextSignatureAttribute, TextSignatureAttributeValue}, deprecations::{Deprecation, Deprecations}, - params::impl_arg_params, + params::{impl_arg_params, Holders}, pyfunction::{ FunctionSignature, PyFunctionArgPyO3Attributes, PyFunctionOptions, SignatureAttribute, }, quotes, - utils::{self, PythonDoc}, + utils::{self, is_abi3, PythonDoc}, }; #[derive(Clone, Debug)] -pub struct FnArg<'a> { +pub struct RegularArg<'a> { + pub name: Cow<'a, syn::Ident>, + pub ty: &'a syn::Type, + pub from_py_with: Option, + pub default_value: Option, + pub option_wrapped_type: Option<&'a syn::Type>, +} + +/// Pythons *args argument +#[derive(Clone, Debug)] +pub struct VarargsArg<'a> { + pub name: Cow<'a, syn::Ident>, + pub ty: &'a syn::Type, +} + +/// Pythons **kwarg argument +#[derive(Clone, Debug)] +pub struct KwargsArg<'a> { + pub name: Cow<'a, syn::Ident>, + pub ty: &'a syn::Type, +} + +#[derive(Clone, Debug)] +pub struct CancelHandleArg<'a> { + pub name: &'a syn::Ident, + pub ty: &'a syn::Type, +} + +#[derive(Clone, Debug)] +pub struct PyArg<'a> { pub name: &'a syn::Ident, pub ty: &'a syn::Type, - pub optional: Option<&'a syn::Type>, - pub default: Option, - pub py: bool, - pub attrs: PyFunctionArgPyO3Attributes, - pub is_varargs: bool, - pub is_kwargs: bool, - pub is_cancel_handle: bool, +} + +#[derive(Clone, Debug)] +pub enum FnArg<'a> { + Regular(RegularArg<'a>), + VarArgs(VarargsArg<'a>), + KwArgs(KwargsArg<'a>), + Py(PyArg<'a>), + CancelHandle(CancelHandleArg<'a>), } impl<'a> FnArg<'a> { + pub fn name(&self) -> &syn::Ident { + match self { + FnArg::Regular(RegularArg { name, .. }) => name, + FnArg::VarArgs(VarargsArg { name, .. }) => name, + FnArg::KwArgs(KwargsArg { name, .. }) => name, + FnArg::Py(PyArg { name, .. }) => name, + FnArg::CancelHandle(CancelHandleArg { name, .. }) => name, + } + } + + pub fn ty(&self) -> &'a syn::Type { + match self { + FnArg::Regular(RegularArg { ty, .. }) => ty, + FnArg::VarArgs(VarargsArg { ty, .. }) => ty, + FnArg::KwArgs(KwargsArg { ty, .. }) => ty, + FnArg::Py(PyArg { ty, .. }) => ty, + FnArg::CancelHandle(CancelHandleArg { ty, .. }) => ty, + } + } + + #[allow(clippy::wrong_self_convention)] + pub fn from_py_with(&self) -> Option<&FromPyWithAttribute> { + if let FnArg::Regular(RegularArg { from_py_with, .. }) = self { + from_py_with.as_ref() + } else { + None + } + } + + pub fn to_varargs_mut(&mut self) -> Result<&mut Self> { + if let Self::Regular(RegularArg { + name, + ty, + option_wrapped_type: None, + .. + }) = self + { + *self = Self::VarArgs(VarargsArg { + name: name.clone(), + ty, + }); + Ok(self) + } else { + bail_spanned!(self.name().span() => "args cannot be optional") + } + } + + pub fn to_kwargs_mut(&mut self) -> Result<&mut Self> { + if let Self::Regular(RegularArg { + name, + ty, + option_wrapped_type: Some(..), + .. + }) = self + { + *self = Self::KwArgs(KwargsArg { + name: name.clone(), + ty, + }); + Ok(self) + } else { + bail_spanned!(self.name().span() => "kwargs must be Option<_>") + } + } + /// Transforms a rust fn arg parsed with syn into a method::FnArg pub fn parse(arg: &'a mut syn::FnArg) -> Result { match arg { @@ -40,25 +139,40 @@ impl<'a> FnArg<'a> { bail_spanned!(cap.ty.span() => IMPL_TRAIT_ERR); } - let arg_attrs = PyFunctionArgPyO3Attributes::from_attrs(&mut cap.attrs)?; + let PyFunctionArgPyO3Attributes { + from_py_with, + cancel_handle, + } = PyFunctionArgPyO3Attributes::from_attrs(&mut cap.attrs)?; let ident = match &*cap.pat { syn::Pat::Ident(syn::PatIdent { ident, .. }) => ident, other => return Err(handle_argument_error(other)), }; - let is_cancel_handle = arg_attrs.cancel_handle.is_some(); + if utils::is_python(&cap.ty) { + return Ok(Self::Py(PyArg { + name: ident, + ty: &cap.ty, + })); + } + + if cancel_handle.is_some() { + // `PyFunctionArgPyO3Attributes::from_attrs` validates that + // only compatible attributes are specified, either + // `cancel_handle` or `from_py_with`, dublicates and any + // combination of the two are already rejected. + return Ok(Self::CancelHandle(CancelHandleArg { + name: ident, + ty: &cap.ty, + })); + } - Ok(FnArg { - name: ident, + Ok(Self::Regular(RegularArg { + name: Cow::Borrowed(ident), ty: &cap.ty, - optional: utils::option_type_argument(&cap.ty), - default: None, - py: utils::is_python(&cap.ty), - attrs: arg_attrs, - is_varargs: false, - is_kwargs: false, - is_cancel_handle, - }) + from_py_with, + default_value: None, + option_wrapped_type: utils::option_type_argument(&cap.ty), + })) } } } @@ -107,14 +221,17 @@ impl FnType { &self, cls: Option<&syn::Type>, error_mode: ExtractErrorMode, - holders: &mut Vec, + holders: &mut Holders, + ctx: &Ctx, ) -> TokenStream { + let Ctx { pyo3_path } = ctx; match self { FnType::Getter(st) | FnType::Setter(st) | FnType::Fn(st) => { let mut receiver = st.receiver( cls.expect("no class given for Fn with a \"self\" receiver"), error_mode, holders, + ctx, ); syn::Token![,](Span::call_site()).to_tokens(&mut receiver); receiver @@ -124,16 +241,26 @@ impl FnType { } FnType::FnClass(span) | FnType::FnNewClass(span) => { let py = syn::Ident::new("py", Span::call_site()); - let slf: Ident = syn::Ident::new("_slf", Span::call_site()); + let slf: Ident = syn::Ident::new("_slf_ref", Span::call_site()); + let pyo3_path = pyo3_path.to_tokens_spanned(*span); quote_spanned! { *span => #[allow(clippy::useless_conversion)] - ::std::convert::Into::into(_pyo3::types::PyType::from_type_ptr(#py, #slf.cast())), + ::std::convert::Into::into( + #pyo3_path::impl_::pymethods::BoundRef::ref_from_ptr(#py, &*(#slf as *const _ as *const *mut _)) + .downcast_unchecked::<#pyo3_path::types::PyType>() + ), } } FnType::FnModule(span) => { + let py = syn::Ident::new("py", Span::call_site()); + let slf: Ident = syn::Ident::new("_slf_ref", Span::call_site()); + let pyo3_path = pyo3_path.to_tokens_spanned(*span); quote_spanned! { *span => #[allow(clippy::useless_conversion)] - ::std::convert::Into::into(py.from_borrowed_ptr::<_pyo3::types::PyModule>(_slf)), + ::std::convert::Into::into( + #pyo3_path::impl_::pymethods::BoundRef::ref_from_ptr(#py, &*(#slf as *const _ as *const *mut _)) + .downcast_unchecked::<#pyo3_path::types::PyModule>() + ), } } } @@ -143,7 +270,7 @@ impl FnType { #[derive(Clone, Debug)] pub enum SelfType { Receiver { mutable: bool, span: Span }, - TryFromPyCell(Span), + TryFromBoundRef(Span), } #[derive(Clone, Copy)] @@ -153,13 +280,14 @@ pub enum ExtractErrorMode { } impl ExtractErrorMode { - pub fn handle_error(self, extract: TokenStream) -> TokenStream { + pub fn handle_error(self, extract: TokenStream, ctx: &Ctx) -> TokenStream { + let Ctx { pyo3_path } = ctx; match self { ExtractErrorMode::Raise => quote! { #extract? }, ExtractErrorMode::NotImplemented => quote! { match #extract { ::std::result::Result::Ok(value) => value, - ::std::result::Result::Err(_) => { return _pyo3::callback::convert(py, py.NotImplemented()); }, + ::std::result::Result::Err(_) => { return #pyo3_path::callback::convert(py, py.NotImplemented()); }, } }, } @@ -171,12 +299,14 @@ impl SelfType { &self, cls: &syn::Type, error_mode: ExtractErrorMode, - holders: &mut Vec, + holders: &mut Holders, + ctx: &Ctx, ) -> TokenStream { // Due to use of quote_spanned in this function, need to bind these idents to the // main macro callsite. let py = syn::Ident::new("py", Span::call_site()); let slf = syn::Ident::new("_slf", Span::call_site()); + let Ctx { pyo3_path } = ctx; match self { SelfType::Receiver { span, mutable } => { let method = if *mutable { @@ -184,30 +314,31 @@ impl SelfType { } else { syn::Ident::new("extract_pyclass_ref", *span) }; - let holder = syn::Ident::new(&format!("holder_{}", holders.len()), *span); - holders.push(quote_spanned! { *span => - #[allow(clippy::let_unit_value)] - let mut #holder = _pyo3::impl_::extract_argument::FunctionArgumentHolder::INIT; - }); - error_mode.handle_error(quote_spanned! { *span => - _pyo3::impl_::extract_argument::#method::<#cls>( - #py.from_borrowed_ptr::<_pyo3::PyAny>(#slf), - &mut #holder, - ) - }) + let holder = holders.push_holder(*span); + let pyo3_path = pyo3_path.to_tokens_spanned(*span); + error_mode.handle_error( + quote_spanned! { *span => + #pyo3_path::impl_::extract_argument::#method::<#cls>( + #pyo3_path::impl_::pymethods::BoundRef::ref_from_ptr(#py, &#slf).0, + &mut #holder, + ) + }, + ctx, + ) } - SelfType::TryFromPyCell(span) => { + SelfType::TryFromBoundRef(span) => { + let pyo3_path = pyo3_path.to_tokens_spanned(*span); error_mode.handle_error( quote_spanned! { *span => - #py.from_borrowed_ptr::<_pyo3::PyAny>(#slf).downcast::<_pyo3::PyCell<#cls>>() - .map_err(::std::convert::Into::<_pyo3::PyErr>::into) + #pyo3_path::impl_::pymethods::BoundRef::ref_from_ptr(#py, &#slf).downcast::<#cls>() + .map_err(::std::convert::Into::<#pyo3_path::PyErr>::into) .and_then( - #[allow(clippy::useless_conversion)] // In case slf is PyCell #[allow(unknown_lints, clippy::unnecessary_fallible_conversions)] // In case slf is Py (unknown_lints can be removed when MSRV is 1.75+) - |cell| ::std::convert::TryFrom::try_from(cell).map_err(::std::convert::Into::into) + |bound| ::std::convert::TryFrom::try_from(bound).map_err(::std::convert::Into::into) ) - } + }, + ctx ) } } @@ -234,8 +365,8 @@ impl CallingConvention { } else if signature.python_signature.kwargs.is_some() { // for functions that accept **kwargs, always prefer varargs Self::Varargs - } else if cfg!(not(feature = "abi3")) { - // Not available in the Stable ABI as of Python 3.10 + } else if !is_abi3() { + // FIXME: available in the stable ABI since 3.10 Self::Fastcall } else { Self::Varargs @@ -251,19 +382,11 @@ pub struct FnSpec<'a> { // r# can be removed by syn::ext::IdentExt::unraw() pub python_name: syn::Ident, pub signature: FunctionSignature<'a>, - pub output: syn::Type, pub convention: CallingConvention, pub text_signature: Option, pub asyncness: Option, pub unsafety: Option, - pub deprecations: Deprecations, -} - -pub fn get_return_info(output: &syn::ReturnType) -> syn::Type { - match output { - syn::ReturnType::Default => syn::Type::Infer(syn::parse_quote! {_}), - syn::ReturnType::Type(_, ty) => *ty.clone(), - } + pub deprecations: Deprecations<'a>, } pub fn parse_method_receiver(arg: &syn::FnArg) -> Result { @@ -283,7 +406,7 @@ pub fn parse_method_receiver(arg: &syn::FnArg) -> Result { if let syn::Type::ImplTrait(_) = &**ty { bail_spanned!(ty.span() => IMPL_TRAIT_ERR); } - Ok(SelfType::TryFromPyCell(ty.span())) + Ok(SelfType::TryFromBoundRef(ty.span())) } } } @@ -295,6 +418,7 @@ impl<'a> FnSpec<'a> { sig: &'a mut syn::Signature, meth_attrs: &mut Vec, options: PyFunctionOptions, + ctx: &'a Ctx, ) -> Result> { let PyFunctionOptions { text_signature, @@ -304,13 +428,12 @@ impl<'a> FnSpec<'a> { } = options; let mut python_name = name.map(|name| name.value.0); - let mut deprecations = Deprecations::new(); + let mut deprecations = Deprecations::new(ctx); let fn_type = Self::parse_fn_type(sig, meth_attrs, &mut python_name, &mut deprecations)?; ensure_signatures_on_valid_method(&fn_type, signature.as_ref(), text_signature.as_ref())?; let name = &sig.ident; - let ty = get_return_info(&sig.output); let python_name = python_name.as_ref().unwrap_or(name).unraw(); let arguments: Vec<_> = sig @@ -342,7 +465,6 @@ impl<'a> FnSpec<'a> { convention, python_name, signature, - output: ty, text_signature, asyncness: sig.asyncness, unsafety: sig.unsafety, @@ -358,7 +480,7 @@ impl<'a> FnSpec<'a> { sig: &syn::Signature, meth_attrs: &mut Vec, python_name: &mut Option, - deprecations: &mut Deprecations, + deprecations: &mut Deprecations<'_>, ) -> Result { let mut method_attributes = parse_method_attributes(meth_attrs, deprecations)?; @@ -409,7 +531,7 @@ impl<'a> FnSpec<'a> { // will error on incorrect type. Some(syn::FnArg::Typed(first_arg)) => first_arg.ty.span(), Some(syn::FnArg::Receiver(_)) | None => bail_spanned!( - sig.paren_token.span.join() => "Expected `&PyType` or `Py` as the first argument to `#[classmethod]`" + sig.paren_token.span.join() => "Expected `&Bound` or `Py` as the first argument to `#[classmethod]`" ), }; FnType::FnClass(span) @@ -472,22 +594,33 @@ impl<'a> FnSpec<'a> { &self, ident: &proc_macro2::Ident, cls: Option<&syn::Type>, + ctx: &Ctx, ) -> Result { + let Ctx { pyo3_path } = ctx; let mut cancel_handle_iter = self .signature .arguments .iter() - .filter(|arg| arg.is_cancel_handle); + .filter(|arg| matches!(arg, FnArg::CancelHandle(..))); let cancel_handle = cancel_handle_iter.next(); - if let Some(arg) = cancel_handle { - ensure_spanned!(self.asyncness.is_some(), arg.name.span() => "`cancel_handle` attribute can only be used with `async fn`"); - if let Some(arg2) = cancel_handle_iter.next() { - bail_spanned!(arg2.name.span() => "`cancel_handle` may only be specified once"); + if let Some(FnArg::CancelHandle(CancelHandleArg { name, .. })) = cancel_handle { + ensure_spanned!(self.asyncness.is_some(), name.span() => "`cancel_handle` attribute can only be used with `async fn`"); + if let Some(FnArg::CancelHandle(CancelHandleArg { name, .. })) = + cancel_handle_iter.next() + { + bail_spanned!(name.span() => "`cancel_handle` may only be specified once"); } } - let rust_call = |args: Vec, holders: &mut Vec| { - let self_arg = self.tp.self_arg(cls, ExtractErrorMode::Raise, holders); + if self.asyncness.is_some() { + ensure_spanned!( + cfg!(feature = "experimental-async"), + self.asyncness.span() => "async functions are only supported with the `experimental-async` feature" + ); + } + + let rust_call = |args: Vec, holders: &mut Holders| { + let mut self_arg = || self.tp.self_arg(cls, ExtractErrorMode::Raise, holders, ctx); let call = if self.asyncness.is_some() { let throw_callback = if cancel_handle.is_some() { @@ -497,49 +630,76 @@ impl<'a> FnSpec<'a> { }; let python_name = &self.python_name; let qualname_prefix = match cls { - Some(cls) => quote!(Some(<#cls as _pyo3::PyTypeInfo>::NAME)), + Some(cls) => quote!(Some(<#cls as #pyo3_path::PyTypeInfo>::NAME)), None => quote!(None), }; + let arg_names = (0..args.len()) + .map(|i| format_ident!("arg_{}", i)) + .collect::>(); let future = match self.tp { FnType::Fn(SelfType::Receiver { mutable: false, .. }) => { - holders.pop().unwrap(); // does not actually use holder created by `self_arg` - quote! {{ - let __guard = _pyo3::impl_::coroutine::RefGuard::<#cls>::new(py.from_borrowed_ptr::<_pyo3::types::PyAny>(_slf))?; - async move { function(&__guard, #(#args),*).await } + #(let #arg_names = #args;)* + let __guard = #pyo3_path::impl_::coroutine::RefGuard::<#cls>::new(&#pyo3_path::impl_::pymethods::BoundRef::ref_from_ptr(py, &_slf))?; + async move { function(&__guard, #(#arg_names),*).await } }} } FnType::Fn(SelfType::Receiver { mutable: true, .. }) => { - holders.pop().unwrap(); // does not actually use holder created by `self_arg` - quote! {{ - let mut __guard = _pyo3::impl_::coroutine::RefMutGuard::<#cls>::new(py.from_borrowed_ptr::<_pyo3::types::PyAny>(_slf))?; - async move { function(&mut __guard, #(#args),*).await } + #(let #arg_names = #args;)* + let mut __guard = #pyo3_path::impl_::coroutine::RefMutGuard::<#cls>::new(&#pyo3_path::impl_::pymethods::BoundRef::ref_from_ptr(py, &_slf))?; + async move { function(&mut __guard, #(#arg_names),*).await } }} } - _ => quote! { function(#self_arg #(#args),*) }, + _ => { + let self_arg = self_arg(); + if self_arg.is_empty() { + quote! { function(#(#args),*) } + } else { + let self_checker = holders.push_gil_refs_checker(self_arg.span()); + quote! { + function( + // NB #self_arg includes a comma, so none inserted here + #pyo3_path::impl_::deprecations::inspect_type(#self_arg &#self_checker), + #(#args),* + ) + } + } + } }; let mut call = quote! {{ let future = #future; - _pyo3::impl_::coroutine::new_coroutine( - _pyo3::intern!(py, stringify!(#python_name)), + #pyo3_path::impl_::coroutine::new_coroutine( + #pyo3_path::intern!(py, stringify!(#python_name)), #qualname_prefix, #throw_callback, - async move { _pyo3::impl_::wrap::OkWrap::wrap(future.await) }, + async move { #pyo3_path::impl_::wrap::OkWrap::wrap(future.await) }, ) }}; if cancel_handle.is_some() { call = quote! {{ - let __cancel_handle = _pyo3::coroutine::CancelHandle::new(); + let __cancel_handle = #pyo3_path::coroutine::CancelHandle::new(); let __throw_callback = __cancel_handle.throw_callback(); #call }}; } call } else { - quote! { function(#self_arg #(#args),*) } + let self_arg = self_arg(); + if self_arg.is_empty() { + quote! { function(#(#args),*) } + } else { + let self_checker = holders.push_gil_refs_checker(self_arg.span()); + quote! { + function( + // NB #self_arg includes a comma, so none inserted here + #pyo3_path::impl_::deprecations::inspect_type(#self_arg &#self_checker), + #(#args),* + ) + } + } }; - quotes::map_result_into_ptr(quotes::ok_wrap(call)) + quotes::map_result_into_ptr(quotes::ok_wrap(call, ctx), ctx) }; let func_name = &self.name; @@ -549,93 +709,117 @@ impl<'a> FnSpec<'a> { quote!(#func_name) }; + let deprecation = deprecate_trailing_option_default(self); + Ok(match self.convention { CallingConvention::Noargs => { - let mut holders = Vec::new(); + let mut holders = Holders::new(); let args = self .signature .arguments .iter() - .map(|arg| { - if arg.py { - quote!(py) - } else if arg.is_cancel_handle { - quote!(__cancel_handle) - } else { - unreachable!() - } + .map(|arg| match arg { + FnArg::Py(..) => quote!(py), + FnArg::CancelHandle(..) => quote!(__cancel_handle), + _ => unreachable!("`CallingConvention::Noargs` should not contain any arguments (reaching Python) except for `self`, which is handled below."), }) .collect(); let call = rust_call(args, &mut holders); + let check_gil_refs = holders.check_gil_refs(); + let init_holders = holders.init_holders(ctx); quote! { unsafe fn #ident<'py>( - py: _pyo3::Python<'py>, - _slf: *mut _pyo3::ffi::PyObject, - ) -> _pyo3::PyResult<*mut _pyo3::ffi::PyObject> { + py: #pyo3_path::Python<'py>, + _slf: *mut #pyo3_path::ffi::PyObject, + ) -> #pyo3_path::PyResult<*mut #pyo3_path::ffi::PyObject> { + #deprecation + let _slf_ref = &_slf; let function = #rust_name; // Shadow the function name to avoid #3017 - #( #holders )* - #call + #init_holders + let result = #call; + #check_gil_refs + result } } } CallingConvention::Fastcall => { - let mut holders = Vec::new(); - let (arg_convert, args) = impl_arg_params(self, cls, true, &mut holders)?; + let mut holders = Holders::new(); + let (arg_convert, args) = impl_arg_params(self, cls, true, &mut holders, ctx); let call = rust_call(args, &mut holders); + let init_holders = holders.init_holders(ctx); + let check_gil_refs = holders.check_gil_refs(); + quote! { unsafe fn #ident<'py>( - py: _pyo3::Python<'py>, - _slf: *mut _pyo3::ffi::PyObject, - _args: *const *mut _pyo3::ffi::PyObject, - _nargs: _pyo3::ffi::Py_ssize_t, - _kwnames: *mut _pyo3::ffi::PyObject - ) -> _pyo3::PyResult<*mut _pyo3::ffi::PyObject> { + py: #pyo3_path::Python<'py>, + _slf: *mut #pyo3_path::ffi::PyObject, + _args: *const *mut #pyo3_path::ffi::PyObject, + _nargs: #pyo3_path::ffi::Py_ssize_t, + _kwnames: *mut #pyo3_path::ffi::PyObject + ) -> #pyo3_path::PyResult<*mut #pyo3_path::ffi::PyObject> { + #deprecation + let _slf_ref = &_slf; let function = #rust_name; // Shadow the function name to avoid #3017 #arg_convert - #( #holders )* - #call + #init_holders + let result = #call; + #check_gil_refs + result } } } CallingConvention::Varargs => { - let mut holders = Vec::new(); - let (arg_convert, args) = impl_arg_params(self, cls, false, &mut holders)?; + let mut holders = Holders::new(); + let (arg_convert, args) = impl_arg_params(self, cls, false, &mut holders, ctx); let call = rust_call(args, &mut holders); + let init_holders = holders.init_holders(ctx); + let check_gil_refs = holders.check_gil_refs(); + quote! { unsafe fn #ident<'py>( - py: _pyo3::Python<'py>, - _slf: *mut _pyo3::ffi::PyObject, - _args: *mut _pyo3::ffi::PyObject, - _kwargs: *mut _pyo3::ffi::PyObject - ) -> _pyo3::PyResult<*mut _pyo3::ffi::PyObject> { + py: #pyo3_path::Python<'py>, + _slf: *mut #pyo3_path::ffi::PyObject, + _args: *mut #pyo3_path::ffi::PyObject, + _kwargs: *mut #pyo3_path::ffi::PyObject + ) -> #pyo3_path::PyResult<*mut #pyo3_path::ffi::PyObject> { + #deprecation + let _slf_ref = &_slf; let function = #rust_name; // Shadow the function name to avoid #3017 #arg_convert - #( #holders )* - #call + #init_holders + let result = #call; + #check_gil_refs + result } } } CallingConvention::TpNew => { - let mut holders = Vec::new(); - let (arg_convert, args) = impl_arg_params(self, cls, false, &mut holders)?; - let self_arg = self.tp.self_arg(cls, ExtractErrorMode::Raise, &mut holders); + let mut holders = Holders::new(); + let (arg_convert, args) = impl_arg_params(self, cls, false, &mut holders, ctx); + let self_arg = self + .tp + .self_arg(cls, ExtractErrorMode::Raise, &mut holders, ctx); let call = quote! { #rust_name(#self_arg #(#args),*) }; + let init_holders = holders.init_holders(ctx); + let check_gil_refs = holders.check_gil_refs(); quote! { unsafe fn #ident( - py: _pyo3::Python<'_>, - _slf: *mut _pyo3::ffi::PyTypeObject, - _args: *mut _pyo3::ffi::PyObject, - _kwargs: *mut _pyo3::ffi::PyObject - ) -> _pyo3::PyResult<*mut _pyo3::ffi::PyObject> { - use _pyo3::callback::IntoPyCallbackOutput; + py: #pyo3_path::Python<'_>, + _slf: *mut #pyo3_path::ffi::PyTypeObject, + _args: *mut #pyo3_path::ffi::PyObject, + _kwargs: *mut #pyo3_path::ffi::PyObject + ) -> #pyo3_path::PyResult<*mut #pyo3_path::ffi::PyObject> { + use #pyo3_path::callback::IntoPyCallbackOutput; + #deprecation + let _slf_ref = &_slf; let function = #rust_name; // Shadow the function name to avoid #3017 #arg_convert - #( #holders )* + #init_holders let result = #call; - let initializer: _pyo3::PyClassInitializer::<#cls> = result.convert(py)?; - let cell = initializer.create_cell_from_subtype(py, _slf)?; - ::std::result::Result::Ok(cell as *mut _pyo3::ffi::PyObject) + let initializer: #pyo3_path::PyClassInitializer::<#cls> = result.convert(py)?; + #check_gil_refs + #pyo3_path::impl_::pymethods::tp_new_impl(py, initializer, _slf) } } } @@ -644,41 +828,42 @@ impl<'a> FnSpec<'a> { /// Return a `PyMethodDef` constructor for this function, matching the selected /// calling convention. - pub fn get_methoddef(&self, wrapper: impl ToTokens, doc: &PythonDoc) -> TokenStream { + pub fn get_methoddef(&self, wrapper: impl ToTokens, doc: &PythonDoc, ctx: &Ctx) -> TokenStream { + let Ctx { pyo3_path } = ctx; let python_name = self.null_terminated_python_name(); match self.convention { CallingConvention::Noargs => quote! { - _pyo3::impl_::pymethods::PyMethodDef::noargs( + #pyo3_path::impl_::pymethods::PyMethodDef::noargs( #python_name, - _pyo3::impl_::pymethods::PyCFunction({ + { unsafe extern "C" fn trampoline( - _slf: *mut _pyo3::ffi::PyObject, - _args: *mut _pyo3::ffi::PyObject, - ) -> *mut _pyo3::ffi::PyObject + _slf: *mut #pyo3_path::ffi::PyObject, + _args: *mut #pyo3_path::ffi::PyObject, + ) -> *mut #pyo3_path::ffi::PyObject { - _pyo3::impl_::trampoline::noargs( + #pyo3_path::impl_::trampoline::noargs( _slf, _args, #wrapper ) } trampoline - }), + }, #doc, ) }, CallingConvention::Fastcall => quote! { - _pyo3::impl_::pymethods::PyMethodDef::fastcall_cfunction_with_keywords( + #pyo3_path::impl_::pymethods::PyMethodDef::fastcall_cfunction_with_keywords( #python_name, - _pyo3::impl_::pymethods::PyCFunctionFastWithKeywords({ + { unsafe extern "C" fn trampoline( - _slf: *mut _pyo3::ffi::PyObject, - _args: *const *mut _pyo3::ffi::PyObject, - _nargs: _pyo3::ffi::Py_ssize_t, - _kwnames: *mut _pyo3::ffi::PyObject - ) -> *mut _pyo3::ffi::PyObject + _slf: *mut #pyo3_path::ffi::PyObject, + _args: *const *mut #pyo3_path::ffi::PyObject, + _nargs: #pyo3_path::ffi::Py_ssize_t, + _kwnames: *mut #pyo3_path::ffi::PyObject + ) -> *mut #pyo3_path::ffi::PyObject { - _pyo3::impl_::trampoline::fastcall_with_keywords( + #pyo3_path::impl_::trampoline::fastcall_with_keywords( _slf, _args, _nargs, @@ -687,21 +872,21 @@ impl<'a> FnSpec<'a> { ) } trampoline - }), + }, #doc, ) }, CallingConvention::Varargs => quote! { - _pyo3::impl_::pymethods::PyMethodDef::cfunction_with_keywords( + #pyo3_path::impl_::pymethods::PyMethodDef::cfunction_with_keywords( #python_name, - _pyo3::impl_::pymethods::PyCFunctionWithKeywords({ + { unsafe extern "C" fn trampoline( - _slf: *mut _pyo3::ffi::PyObject, - _args: *mut _pyo3::ffi::PyObject, - _kwargs: *mut _pyo3::ffi::PyObject, - ) -> *mut _pyo3::ffi::PyObject + _slf: *mut #pyo3_path::ffi::PyObject, + _args: *mut #pyo3_path::ffi::PyObject, + _kwargs: *mut #pyo3_path::ffi::PyObject, + ) -> *mut #pyo3_path::ffi::PyObject { - _pyo3::impl_::trampoline::cfunction_with_keywords( + #pyo3_path::impl_::trampoline::cfunction_with_keywords( _slf, _args, _kwargs, @@ -709,7 +894,7 @@ impl<'a> FnSpec<'a> { ) } trampoline - }), + }, #doc, ) }, @@ -773,7 +958,7 @@ impl MethodTypeAttribute { /// Otherwise will either return a parse error or the attribute. fn parse_if_matching_attribute( attr: &syn::Attribute, - deprecations: &mut Deprecations, + deprecations: &mut Deprecations<'_>, ) -> Result> { fn ensure_no_arguments(meta: &syn::Meta, ident: &str) -> syn::Result<()> { match meta { @@ -859,7 +1044,7 @@ impl Display for MethodTypeAttribute { fn parse_method_attributes( attrs: &mut Vec, - deprecations: &mut Deprecations, + deprecations: &mut Deprecations<'_>, ) -> Result> { let mut new_attrs = Vec::new(); let mut found_attrs = Vec::new(); diff --git a/pyo3-macros-backend/src/module.rs b/pyo3-macros-backend/src/module.rs index ccd84bb363a..756037263e3 100644 --- a/pyo3-macros-backend/src/module.rs +++ b/pyo3-macros-backend/src/module.rs @@ -1,18 +1,20 @@ //! Code generation for the function that initializes a python module and adds classes and function. +use crate::utils::Ctx; use crate::{ attributes::{self, take_attributes, take_pyo3_options, CrateAttribute, NameAttribute}, + get_doc, pyfunction::{impl_wrap_pyfunction, PyFunctionOptions}, - utils::{get_pyo3_crate, PythonDoc}, }; use proc_macro2::TokenStream; use quote::quote; use syn::{ ext::IdentExt, parse::{Parse, ParseStream}, + parse_quote, parse_quote_spanned, spanned::Spanned, token::Comma, - Ident, Path, Result, Visibility, + Item, Path, Result, }; #[derive(Default)] @@ -56,70 +58,350 @@ impl PyModuleOptions { } } +pub fn pymodule_module_impl(mut module: syn::ItemMod) -> Result { + let syn::ItemMod { + attrs, + vis, + unsafety: _, + ident, + mod_token: _, + content, + semi: _, + } = &mut module; + let items = if let Some((_, items)) = content { + items + } else { + bail_spanned!(module.span() => "`#[pymodule]` can only be used on inline modules") + }; + let options = PyModuleOptions::from_attrs(attrs)?; + let ctx = &Ctx::new(&options.krate); + let Ctx { pyo3_path } = ctx; + let doc = get_doc(attrs, None); + + let mut module_items = Vec::new(); + let mut module_items_cfg_attrs = Vec::new(); + + fn extract_use_items( + source: &syn::UseTree, + cfg_attrs: &[syn::Attribute], + target_items: &mut Vec, + target_cfg_attrs: &mut Vec>, + ) -> Result<()> { + match source { + syn::UseTree::Name(name) => { + target_items.push(name.ident.clone()); + target_cfg_attrs.push(cfg_attrs.to_vec()); + } + syn::UseTree::Path(path) => { + extract_use_items(&path.tree, cfg_attrs, target_items, target_cfg_attrs)? + } + syn::UseTree::Group(group) => { + for tree in &group.items { + extract_use_items(tree, cfg_attrs, target_items, target_cfg_attrs)? + } + } + syn::UseTree::Glob(glob) => { + bail_spanned!(glob.span() => "#[pymodule] cannot import glob statements") + } + syn::UseTree::Rename(rename) => { + target_items.push(rename.rename.clone()); + target_cfg_attrs.push(cfg_attrs.to_vec()); + } + } + Ok(()) + } + + let mut pymodule_init = None; + + for item in &mut *items { + match item { + Item::Use(item_use) => { + let is_pymodule_export = + find_and_remove_attribute(&mut item_use.attrs, "pymodule_export"); + if is_pymodule_export { + let cfg_attrs = get_cfg_attributes(&item_use.attrs); + extract_use_items( + &item_use.tree, + &cfg_attrs, + &mut module_items, + &mut module_items_cfg_attrs, + )?; + } + } + Item::Fn(item_fn) => { + ensure_spanned!( + !has_attribute(&item_fn.attrs, "pymodule_export"), + item.span() => "`#[pymodule_export]` may only be used on `use` statements" + ); + let is_pymodule_init = + find_and_remove_attribute(&mut item_fn.attrs, "pymodule_init"); + let ident = &item_fn.sig.ident; + if is_pymodule_init { + ensure_spanned!( + !has_attribute(&item_fn.attrs, "pyfunction"), + item_fn.span() => "`#[pyfunction]` cannot be used alongside `#[pymodule_init]`" + ); + ensure_spanned!(pymodule_init.is_none(), item_fn.span() => "only one `#[pymodule_init]` may be specified"); + pymodule_init = Some(quote! { #ident(module)?; }); + } else if has_attribute(&item_fn.attrs, "pyfunction") { + module_items.push(ident.clone()); + module_items_cfg_attrs.push(get_cfg_attributes(&item_fn.attrs)); + } + } + Item::Struct(item_struct) => { + ensure_spanned!( + !has_attribute(&item_struct.attrs, "pymodule_export"), + item.span() => "`#[pymodule_export]` may only be used on `use` statements" + ); + if has_attribute(&item_struct.attrs, "pyclass") { + module_items.push(item_struct.ident.clone()); + module_items_cfg_attrs.push(get_cfg_attributes(&item_struct.attrs)); + } + } + Item::Enum(item_enum) => { + ensure_spanned!( + !has_attribute(&item_enum.attrs, "pymodule_export"), + item.span() => "`#[pymodule_export]` may only be used on `use` statements" + ); + if has_attribute(&item_enum.attrs, "pyclass") { + module_items.push(item_enum.ident.clone()); + module_items_cfg_attrs.push(get_cfg_attributes(&item_enum.attrs)); + } + } + Item::Mod(item_mod) => { + ensure_spanned!( + !has_attribute(&item_mod.attrs, "pymodule_export"), + item.span() => "`#[pymodule_export]` may only be used on `use` statements" + ); + if has_attribute(&item_mod.attrs, "pymodule") { + module_items.push(item_mod.ident.clone()); + module_items_cfg_attrs.push(get_cfg_attributes(&item_mod.attrs)); + } + } + Item::ForeignMod(item) => { + ensure_spanned!( + !has_attribute(&item.attrs, "pymodule_export"), + item.span() => "`#[pymodule_export]` may only be used on `use` statements" + ); + } + Item::Trait(item) => { + ensure_spanned!( + !has_attribute(&item.attrs, "pymodule_export"), + item.span() => "`#[pymodule_export]` may only be used on `use` statements" + ); + } + Item::Const(item) => { + ensure_spanned!( + !has_attribute(&item.attrs, "pymodule_export"), + item.span() => "`#[pymodule_export]` may only be used on `use` statements" + ); + } + Item::Static(item) => { + ensure_spanned!( + !has_attribute(&item.attrs, "pymodule_export"), + item.span() => "`#[pymodule_export]` may only be used on `use` statements" + ); + } + Item::Macro(item) => { + ensure_spanned!( + !has_attribute(&item.attrs, "pymodule_export"), + item.span() => "`#[pymodule_export]` may only be used on `use` statements" + ); + } + Item::ExternCrate(item) => { + ensure_spanned!( + !has_attribute(&item.attrs, "pymodule_export"), + item.span() => "`#[pymodule_export]` may only be used on `use` statements" + ); + } + Item::Impl(item) => { + ensure_spanned!( + !has_attribute(&item.attrs, "pymodule_export"), + item.span() => "`#[pymodule_export]` may only be used on `use` statements" + ); + } + Item::TraitAlias(item) => { + ensure_spanned!( + !has_attribute(&item.attrs, "pymodule_export"), + item.span() => "`#[pymodule_export]` may only be used on `use` statements" + ); + } + Item::Type(item) => { + ensure_spanned!( + !has_attribute(&item.attrs, "pymodule_export"), + item.span() => "`#[pymodule_export]` may only be used on `use` statements" + ); + } + Item::Union(item) => { + ensure_spanned!( + !has_attribute(&item.attrs, "pymodule_export"), + item.span() => "`#[pymodule_export]` may only be used on `use` statements" + ); + } + _ => (), + } + } + + let initialization = module_initialization(options, ident); + Ok(quote!( + #vis mod #ident { + #(#items)* + + #initialization + + #[allow(unknown_lints, non_local_definitions)] + impl MakeDef { + const fn make_def() -> #pyo3_path::impl_::pymodule::ModuleDef { + use #pyo3_path::impl_::pymodule as impl_; + const INITIALIZER: impl_::ModuleInitializer = impl_::ModuleInitializer(__pyo3_pymodule); + unsafe { + impl_::ModuleDef::new( + __PYO3_NAME, + #doc, + INITIALIZER + ) + } + } + } + + fn __pyo3_pymodule(module: &#pyo3_path::Bound<'_, #pyo3_path::types::PyModule>) -> #pyo3_path::PyResult<()> { + use #pyo3_path::impl_::pymodule::PyAddToModule; + #( + #(#module_items_cfg_attrs)* + #module_items::_PYO3_DEF.add_to_module(module)?; + )* + #pymodule_init + Ok(()) + } + } + )) +} + /// Generates the function that is called by the python interpreter to initialize the native /// module -pub fn pymodule_impl( - fnname: &Ident, - options: PyModuleOptions, - doc: PythonDoc, - visibility: &Visibility, -) -> TokenStream { - let name = options.name.unwrap_or_else(|| fnname.unraw()); - let krate = get_pyo3_crate(&options.krate); - let pyinit_symbol = format!("PyInit_{}", name); +pub fn pymodule_function_impl(mut function: syn::ItemFn) -> Result { + let options = PyModuleOptions::from_attrs(&mut function.attrs)?; + process_functions_in_module(&options, &mut function)?; + let ctx = &Ctx::new(&options.krate); + let stmts = std::mem::take(&mut function.block.stmts); + let Ctx { pyo3_path } = ctx; + let ident = &function.sig.ident; + let vis = &function.vis; + let doc = get_doc(&function.attrs, None); - quote! { - // Create a module with the same name as the `#[pymodule]` - this way `use ` - // will actually bring both the module and the function into scope. - #[doc(hidden)] - #visibility mod #fnname { - pub(crate) struct MakeDef; - pub static DEF: #krate::impl_::pymodule::ModuleDef = MakeDef::make_def(); - pub const NAME: &'static str = concat!(stringify!(#name), "\0"); + let initialization = module_initialization(options, ident); - /// This autogenerated function is called by the python interpreter when importing - /// the module. - #[export_name = #pyinit_symbol] - pub unsafe extern "C" fn init() -> *mut #krate::ffi::PyObject { - #krate::impl_::trampoline::module_init(|py| DEF.make_module(py)) + // Module function called with optional Python<'_> marker as first arg, followed by the module. + let mut module_args = Vec::new(); + if function.sig.inputs.len() == 2 { + module_args.push(quote!(module.py())); + } + module_args + .push(quote!(::std::convert::Into::into(#pyo3_path::impl_::pymethods::BoundRef(module)))); + + let extractors = function + .sig + .inputs + .iter() + .filter_map(|param| { + if let syn::FnArg::Typed(pat_type) = param { + if let syn::Pat::Ident(pat_ident) = &*pat_type.pat { + let ident: &syn::Ident = &pat_ident.ident; + return Some([ + parse_quote!{ let check_gil_refs = #pyo3_path::impl_::deprecations::GilRefs::new(); }, + parse_quote! { let #ident = #pyo3_path::impl_::deprecations::inspect_type(#ident, &check_gil_refs); }, + parse_quote_spanned! { pat_type.span() => check_gil_refs.function_arg(); }, + ]); + } } + None + }) + .flatten(); + + function.block.stmts = extractors.chain(stmts).collect(); + function + .attrs + .push(parse_quote!(#[allow(clippy::used_underscore_binding)])); + + Ok(quote! { + #function + #[doc(hidden)] + #vis mod #ident { + #initialization } // Generate the definition inside an anonymous function in the same scope as the original function - // this avoids complications around the fact that the generated module has a different scope // (and `super` doesn't always refer to the outer scope, e.g. if the `#[pymodule] is // inside a function body) - const _: () = { - use #krate::impl_::pymodule as impl_; - impl #fnname::MakeDef { - const fn make_def() -> impl_::ModuleDef { - const INITIALIZER: impl_::ModuleInitializer = impl_::ModuleInitializer(#fnname); - unsafe { - impl_::ModuleDef::new(#fnname::NAME, #doc, INITIALIZER) - } + #[allow(unknown_lints, non_local_definitions)] + impl #ident::MakeDef { + const fn make_def() -> #pyo3_path::impl_::pymodule::ModuleDef { + fn __pyo3_pymodule(module: &#pyo3_path::Bound<'_, #pyo3_path::types::PyModule>) -> #pyo3_path::PyResult<()> { + #ident(#(#module_args),*) + } + + const INITIALIZER: #pyo3_path::impl_::pymodule::ModuleInitializer = #pyo3_path::impl_::pymodule::ModuleInitializer(__pyo3_pymodule); + unsafe { + #pyo3_path::impl_::pymodule::ModuleDef::new( + #ident::__PYO3_NAME, + #doc, + INITIALIZER + ) } } - }; + } + }) +} + +fn module_initialization(options: PyModuleOptions, ident: &syn::Ident) -> TokenStream { + let name = options.name.unwrap_or_else(|| ident.unraw()); + let ctx = &Ctx::new(&options.krate); + let Ctx { pyo3_path } = ctx; + let pyinit_symbol = format!("PyInit_{}", name); + + quote! { + #[doc(hidden)] + pub const __PYO3_NAME: &'static str = concat!(stringify!(#name), "\0"); + + pub(super) struct MakeDef; + #[doc(hidden)] + pub static _PYO3_DEF: #pyo3_path::impl_::pymodule::ModuleDef = MakeDef::make_def(); + + /// This autogenerated function is called by the python interpreter when importing + /// the module. + #[doc(hidden)] + #[export_name = #pyinit_symbol] + pub unsafe extern "C" fn __pyo3_init() -> *mut #pyo3_path::ffi::PyObject { + #pyo3_path::impl_::trampoline::module_init(|py| _PYO3_DEF.make_module(py)) + } } } /// Finds and takes care of the #[pyfn(...)] in `#[pymodule]` -pub fn process_functions_in_module( - options: &PyModuleOptions, - func: &mut syn::ItemFn, -) -> syn::Result<()> { +fn process_functions_in_module(options: &PyModuleOptions, func: &mut syn::ItemFn) -> Result<()> { + let ctx = &Ctx::new(&options.krate); + let Ctx { pyo3_path } = ctx; let mut stmts: Vec = Vec::new(); - let krate = get_pyo3_crate(&options.krate); + + #[cfg(feature = "gil-refs")] + let imports = quote!(use #pyo3_path::{PyNativeType, types::PyModuleMethods};); + #[cfg(not(feature = "gil-refs"))] + let imports = quote!(use #pyo3_path::types::PyModuleMethods;); for mut stmt in func.block.stmts.drain(..) { - if let syn::Stmt::Item(syn::Item::Fn(func)) = &mut stmt { + if let syn::Stmt::Item(Item::Fn(func)) = &mut stmt { if let Some(pyfn_args) = get_pyfn_attr(&mut func.attrs)? { let module_name = pyfn_args.modname; let wrapped_function = impl_wrap_pyfunction(func, pyfn_args.options)?; let name = &func.sig.ident; let statements: Vec = syn::parse_quote! { #wrapped_function - #module_name.add_function(#krate::impl_::pyfunction::_wrap_pyfunction(&#name::DEF, #module_name)?)?; + { + #[allow(unknown_lints, unused_imports, redundant_imports)] + #imports + #module_name.as_borrowed().add_function(#pyo3_path::wrap_pyfunction!(#name, #module_name.as_borrowed())?)?; + } }; stmts.extend(statements); } @@ -184,6 +466,31 @@ fn get_pyfn_attr(attrs: &mut Vec) -> syn::Result Vec { + attrs + .iter() + .filter(|attr| attr.path().is_ident("cfg")) + .cloned() + .collect() +} + +fn find_and_remove_attribute(attrs: &mut Vec, ident: &str) -> bool { + let mut found = false; + attrs.retain(|attr| { + if attr.path().is_ident(ident) { + found = true; + false + } else { + true + } + }); + found +} + +fn has_attribute(attrs: &[syn::Attribute], ident: &str) -> bool { + attrs.iter().any(|attr| attr.path().is_ident(ident)) +} + enum PyModulePyO3Option { Crate(CrateAttribute), Name(NameAttribute), diff --git a/pyo3-macros-backend/src/params.rs b/pyo3-macros-backend/src/params.rs index 1ef31867406..cab9d2a7d29 100644 --- a/pyo3-macros-backend/src/params.rs +++ b/pyo3-macros-backend/src/params.rs @@ -1,37 +1,130 @@ +use crate::utils::Ctx; use crate::{ - method::{FnArg, FnSpec}, + method::{FnArg, FnSpec, RegularArg}, pyfunction::FunctionSignature, quotes::some_wrap, }; use proc_macro2::{Span, TokenStream}; -use quote::{quote, quote_spanned}; +use quote::{format_ident, quote, quote_spanned}; use syn::spanned::Spanned; -use syn::Result; + +pub struct Holders { + holders: Vec, + gil_refs_checkers: Vec, +} + +impl Holders { + pub fn new() -> Self { + Holders { + holders: Vec::new(), + gil_refs_checkers: Vec::new(), + } + } + + pub fn push_holder(&mut self, span: Span) -> syn::Ident { + let holder = syn::Ident::new(&format!("holder_{}", self.holders.len()), span); + self.holders.push(holder.clone()); + holder + } + + pub fn push_gil_refs_checker(&mut self, span: Span) -> syn::Ident { + let gil_refs_checker = syn::Ident::new( + &format!("gil_refs_checker_{}", self.gil_refs_checkers.len()), + span, + ); + self.gil_refs_checkers + .push(GilRefChecker::FunctionArg(gil_refs_checker.clone())); + gil_refs_checker + } + + pub fn push_from_py_with_checker(&mut self, span: Span) -> syn::Ident { + let gil_refs_checker = syn::Ident::new( + &format!("gil_refs_checker_{}", self.gil_refs_checkers.len()), + span, + ); + self.gil_refs_checkers + .push(GilRefChecker::FromPyWith(gil_refs_checker.clone())); + gil_refs_checker + } + + pub fn init_holders(&self, ctx: &Ctx) -> TokenStream { + let Ctx { pyo3_path } = ctx; + let holders = &self.holders; + let gil_refs_checkers = self.gil_refs_checkers.iter().map(|checker| match checker { + GilRefChecker::FunctionArg(ident) => ident, + GilRefChecker::FromPyWith(ident) => ident, + }); + quote! { + #[allow(clippy::let_unit_value)] + #(let mut #holders = #pyo3_path::impl_::extract_argument::FunctionArgumentHolder::INIT;)* + #(let #gil_refs_checkers = #pyo3_path::impl_::deprecations::GilRefs::new();)* + } + } + + pub fn check_gil_refs(&self) -> TokenStream { + self.gil_refs_checkers + .iter() + .map(|checker| match checker { + GilRefChecker::FunctionArg(ident) => { + quote_spanned! { ident.span() => #ident.function_arg(); } + } + GilRefChecker::FromPyWith(ident) => { + quote_spanned! { ident.span() => #ident.from_py_with_arg(); } + } + }) + .collect() + } +} + +enum GilRefChecker { + FunctionArg(syn::Ident), + FromPyWith(syn::Ident), +} /// Return true if the argument list is simply (*args, **kwds). pub fn is_forwarded_args(signature: &FunctionSignature<'_>) -> bool { matches!( signature.arguments.as_slice(), - [ - FnArg { - is_varargs: true, - .. - }, - FnArg { - is_kwargs: true, - .. - }, - ] + [FnArg::VarArgs(..), FnArg::KwArgs(..),] ) } +pub(crate) fn check_arg_for_gil_refs( + tokens: TokenStream, + gil_refs_checker: syn::Ident, + ctx: &Ctx, +) -> TokenStream { + let Ctx { pyo3_path } = ctx; + quote! { + #pyo3_path::impl_::deprecations::inspect_type(#tokens, &#gil_refs_checker) + } +} + pub fn impl_arg_params( spec: &FnSpec<'_>, self_: Option<&syn::Type>, fastcall: bool, - holders: &mut Vec, -) -> Result<(TokenStream, Vec)> { + holders: &mut Holders, + ctx: &Ctx, +) -> (TokenStream, Vec) { let args_array = syn::Ident::new("output", Span::call_site()); + let Ctx { pyo3_path } = ctx; + + let from_py_with = spec + .signature + .arguments + .iter() + .enumerate() + .filter_map(|(i, arg)| { + let from_py_with = &arg.from_py_with()?.value; + let from_py_with_holder = format_ident!("from_py_with_{}", i); + Some(quote_spanned! { from_py_with.span() => + let e = #pyo3_path::impl_::deprecations::GilRefs::new(); + let #from_py_with_holder = #pyo3_path::impl_::deprecations::inspect_fn(#from_py_with, &e); + e.from_py_with_arg(); + }) + }) + .collect::(); if !fastcall && is_forwarded_args(&spec.signature) { // In the varargs convention, we can just pass though if the signature @@ -40,15 +133,17 @@ pub fn impl_arg_params( .signature .arguments .iter() - .map(|arg| impl_arg_param(arg, &mut 0, &args_array, holders)) - .collect::>()?; - return Ok(( + .enumerate() + .map(|(i, arg)| impl_arg_param(arg, i, &mut 0, holders, ctx)) + .collect(); + return ( quote! { - let _args = py.from_borrowed_ptr::<_pyo3::types::PyTuple>(_args); - let _kwargs: ::std::option::Option<&_pyo3::types::PyDict> = py.from_borrowed_ptr_or_opt(_kwargs); + let _args = #pyo3_path::impl_::pymethods::BoundRef::ref_from_ptr(py, &_args); + let _kwargs = #pyo3_path::impl_::pymethods::BoundRef::ref_from_ptr_or_opt(py, &_kwargs); + #from_py_with }, arg_convert, - )); + ); }; let positional_parameter_names = &spec.signature.python_signature.positional_parameters; @@ -64,7 +159,7 @@ pub fn impl_arg_params( .iter() .map(|(name, required)| { quote! { - _pyo3::impl_::extract_argument::KeywordOnlyParameterDescription { + #pyo3_path::impl_::extract_argument::KeywordOnlyParameterDescription { name: #name, required: #required, } @@ -73,27 +168,28 @@ pub fn impl_arg_params( let num_params = positional_parameter_names.len() + keyword_only_parameters.len(); - let mut option_pos = 0; + let mut option_pos = 0usize; let param_conversion = spec .signature .arguments .iter() - .map(|arg| impl_arg_param(arg, &mut option_pos, &args_array, holders)) - .collect::>()?; + .enumerate() + .map(|(i, arg)| impl_arg_param(arg, i, &mut option_pos, holders, ctx)) + .collect(); let args_handler = if spec.signature.python_signature.varargs.is_some() { - quote! { _pyo3::impl_::extract_argument::TupleVarargs } + quote! { #pyo3_path::impl_::extract_argument::TupleVarargs } } else { - quote! { _pyo3::impl_::extract_argument::NoVarargs } + quote! { #pyo3_path::impl_::extract_argument::NoVarargs } }; let kwargs_handler = if spec.signature.python_signature.kwargs.is_some() { - quote! { _pyo3::impl_::extract_argument::DictVarkeywords } + quote! { #pyo3_path::impl_::extract_argument::DictVarkeywords } } else { - quote! { _pyo3::impl_::extract_argument::NoVarkeywords } + quote! { #pyo3_path::impl_::extract_argument::NoVarkeywords } }; let cls_name = if let Some(cls) = self_ { - quote! { ::std::option::Option::Some(<#cls as _pyo3::type_object::PyTypeInfo>::NAME) } + quote! { ::std::option::Option::Some(<#cls as #pyo3_path::type_object::PyTypeInfo>::NAME) } } else { quote! { ::std::option::Option::None } }; @@ -121,9 +217,9 @@ pub fn impl_arg_params( }; // create array of arguments, and then parse - Ok(( + ( quote! { - const DESCRIPTION: _pyo3::impl_::extract_argument::FunctionDescription = _pyo3::impl_::extract_argument::FunctionDescription { + const DESCRIPTION: #pyo3_path::impl_::extract_argument::FunctionDescription = #pyo3_path::impl_::extract_argument::FunctionDescription { cls_name: #cls_name, func_name: stringify!(#python_name), positional_parameter_names: &[#(#positional_parameter_names),*], @@ -133,136 +229,144 @@ pub fn impl_arg_params( }; let mut #args_array = [::std::option::Option::None; #num_params]; let (_args, _kwargs) = #extract_expression; + #from_py_with }, param_conversion, - )) + ) } -/// Re option_pos: The option slice doesn't contain the py: Python argument, so the argument -/// index and the index in option diverge when using py: Python fn impl_arg_param( arg: &FnArg<'_>, + pos: usize, option_pos: &mut usize, - args_array: &syn::Ident, - holders: &mut Vec, -) -> Result { - // Use this macro inside this function, to ensure that all code generated here is associated - // with the function argument - macro_rules! quote_arg_span { - ($($tokens:tt)*) => { quote_spanned!(arg.ty.span() => $($tokens)*) } - } - - if arg.py { - return Ok(quote! { py }); - } + holders: &mut Holders, + ctx: &Ctx, +) -> TokenStream { + let Ctx { pyo3_path } = ctx; + let args_array = syn::Ident::new("output", Span::call_site()); - if arg.is_cancel_handle { - return Ok(quote! { __cancel_handle }); + match arg { + FnArg::Regular(arg) => { + let from_py_with = format_ident!("from_py_with_{}", pos); + let arg_value = quote!(#args_array[#option_pos].as_deref()); + *option_pos += 1; + let tokens = impl_regular_arg_param(arg, from_py_with, arg_value, holders, ctx); + check_arg_for_gil_refs(tokens, holders.push_gil_refs_checker(arg.ty.span()), ctx) + } + FnArg::VarArgs(arg) => { + let holder = holders.push_holder(arg.name.span()); + let name_str = arg.name.to_string(); + quote_spanned! { arg.name.span() => + #pyo3_path::impl_::extract_argument::extract_argument( + &_args, + &mut #holder, + #name_str + )? + } + } + FnArg::KwArgs(arg) => { + let holder = holders.push_holder(arg.name.span()); + let name_str = arg.name.to_string(); + quote_spanned! { arg.name.span() => + #pyo3_path::impl_::extract_argument::extract_optional_argument( + _kwargs.as_deref(), + &mut #holder, + #name_str, + || ::std::option::Option::None + )? + } + } + FnArg::Py(..) => quote! { py }, + FnArg::CancelHandle(..) => quote! { __cancel_handle }, } +} - let name = arg.name; - let name_str = name.to_string(); - - let mut push_holder = || { - let holder = syn::Ident::new(&format!("holder_{}", holders.len()), arg.ty.span()); - holders.push(quote_arg_span! { - #[allow(clippy::let_unit_value)] - let mut #holder = _pyo3::impl_::extract_argument::FunctionArgumentHolder::INIT; - }); - holder - }; +/// Re option_pos: The option slice doesn't contain the py: Python argument, so the argument +/// index and the index in option diverge when using py: Python +pub(crate) fn impl_regular_arg_param( + arg: &RegularArg<'_>, + from_py_with: syn::Ident, + arg_value: TokenStream, // expected type: Option<&'a Bound<'py, PyAny>> + holders: &mut Holders, + ctx: &Ctx, +) -> TokenStream { + let Ctx { pyo3_path } = ctx; + let pyo3_path = pyo3_path.to_tokens_spanned(arg.ty.span()); - if arg.is_varargs { - ensure_spanned!( - arg.optional.is_none(), - arg.name.span() => "args cannot be optional" - ); - let holder = push_holder(); - return Ok(quote_arg_span! { - _pyo3::impl_::extract_argument::extract_argument( - _args, - &mut #holder, - #name_str - )? - }); - } else if arg.is_kwargs { - ensure_spanned!( - arg.optional.is_some(), - arg.name.span() => "kwargs must be Option<_>" - ); - let holder = push_holder(); - return Ok(quote_arg_span! { - _pyo3::impl_::extract_argument::extract_optional_argument( - _kwargs.map(::std::convert::AsRef::as_ref), - &mut #holder, - #name_str, - || ::std::option::Option::None - )? - }); + // Use this macro inside this function, to ensure that all code generated here is associated + // with the function argument + macro_rules! quote_arg_span { + ($($tokens:tt)*) => { quote_spanned!(arg.ty.span() => $($tokens)*) } } - let arg_value = quote_arg_span!(#args_array[#option_pos]); - *option_pos += 1; - - let mut default = arg.default.as_ref().map(|expr| quote!(#expr)); + let name_str = arg.name.to_string(); + let mut default = arg.default_value.as_ref().map(|expr| quote!(#expr)); // Option arguments have special treatment: the default should be specified _without_ the // Some() wrapper. Maybe this should be changed in future?! - if arg.optional.is_some() { - default = Some(default.map_or_else(|| quote!(::std::option::Option::None), some_wrap)); + if arg.option_wrapped_type.is_some() { + default = Some(default.map_or_else( + || quote!(::std::option::Option::None), + |tokens| some_wrap(tokens, ctx), + )); } - let tokens = if let Some(expr_path) = arg.attrs.from_py_with.as_ref().map(|attr| &attr.value) { + if arg.from_py_with.is_some() { if let Some(default) = default { quote_arg_span! { - #[allow(clippy::redundant_closure)] - _pyo3::impl_::extract_argument::from_py_with_with_default( + #pyo3_path::impl_::extract_argument::from_py_with_with_default( #arg_value, #name_str, - #expr_path, - || #default + #from_py_with as fn(_) -> _, + #[allow(clippy::redundant_closure)] + { + || #default + } )? } } else { quote_arg_span! { - _pyo3::impl_::extract_argument::from_py_with( - _pyo3::impl_::extract_argument::unwrap_required_argument(#arg_value), + #pyo3_path::impl_::extract_argument::from_py_with( + #pyo3_path::impl_::extract_argument::unwrap_required_argument(#arg_value), #name_str, - #expr_path, + #from_py_with as fn(_) -> _, )? } } - } else if arg.optional.is_some() { - let holder = push_holder(); + } else if arg.option_wrapped_type.is_some() { + let holder = holders.push_holder(arg.name.span()); quote_arg_span! { - #[allow(clippy::redundant_closure)] - _pyo3::impl_::extract_argument::extract_optional_argument( + #pyo3_path::impl_::extract_argument::extract_optional_argument( #arg_value, &mut #holder, #name_str, - || #default + #[allow(clippy::redundant_closure)] + { + || #default + } )? } } else if let Some(default) = default { - let holder = push_holder(); + let holder = holders.push_holder(arg.name.span()); quote_arg_span! { - #[allow(clippy::redundant_closure)] - _pyo3::impl_::extract_argument::extract_argument_with_default( + #pyo3_path::impl_::extract_argument::extract_argument_with_default( #arg_value, &mut #holder, #name_str, - || #default + #[allow(clippy::redundant_closure)] + { + || #default + } )? } } else { - let holder = push_holder(); + let holder = holders.push_holder(arg.name.span()); quote_arg_span! { - _pyo3::impl_::extract_argument::extract_argument( - _pyo3::impl_::extract_argument::unwrap_required_argument(#arg_value), + #pyo3_path::impl_::extract_argument::extract_argument( + #pyo3_path::impl_::extract_argument::unwrap_required_argument(#arg_value), &mut #holder, #name_str )? } - }; - Ok(tokens) + } } diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index 239514ae38a..47c52c84518 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -3,21 +3,22 @@ use std::borrow::Cow; use crate::attributes::kw::frozen; use crate::attributes::{ self, kw, take_pyo3_options, CrateAttribute, ExtendsAttribute, FreelistAttribute, - ModuleAttribute, NameAttribute, NameLitStr, RenameAllAttribute, TextSignatureAttribute, - TextSignatureAttributeValue, + ModuleAttribute, NameAttribute, NameLitStr, RenameAllAttribute, }; use crate::deprecations::Deprecations; use crate::konst::{ConstAttributes, ConstSpec}; -use crate::method::FnSpec; +use crate::method::{FnArg, FnSpec, PyArg, RegularArg}; +use crate::pyfunction::ConstructorAttribute; use crate::pyimpl::{gen_py_const, PyClassMethodsType}; use crate::pymethod::{ impl_py_getter_def, impl_py_setter_def, MethodAndMethodDef, MethodAndSlotDef, PropertyType, - SlotDef, __INT__, __REPR__, __RICHCMP__, + SlotDef, __GETITEM__, __INT__, __LEN__, __REPR__, __RICHCMP__, }; -use crate::utils::{self, apply_renaming_rule, get_pyo3_crate, PythonDoc}; +use crate::utils::Ctx; +use crate::utils::{self, apply_renaming_rule, PythonDoc}; use crate::PyFunctionOptions; use proc_macro2::{Ident, Span, TokenStream}; -use quote::quote; +use quote::{format_ident, quote}; use syn::ext::IdentExt; use syn::parse::{Parse, ParseStream}; use syn::punctuated::Punctuated; @@ -31,6 +32,7 @@ pub enum PyClassKind { } /// The parsed arguments of the pyclass macro +#[derive(Clone)] pub struct PyClassArgs { pub class_kind: PyClassKind, pub options: PyClassPyO3Options, @@ -53,7 +55,7 @@ impl PyClassArgs { } } -#[derive(Default)] +#[derive(Clone, Default)] pub struct PyClassPyO3Options { pub krate: Option, pub dict: Option, @@ -68,7 +70,6 @@ pub struct PyClassPyO3Options { pub sequence: Option, pub set_all: Option, pub subclass: Option, - pub text_signature: Option, pub unsendable: Option, pub weakref: Option, } @@ -130,7 +131,7 @@ impl Parse for PyClassPyO3Option { } } -impl PyClassPyO3Options { +impl Parse for PyClassPyO3Options { fn parse(input: ParseStream<'_>) -> syn::Result { let mut options: PyClassPyO3Options = Default::default(); @@ -140,7 +141,9 @@ impl PyClassPyO3Options { Ok(options) } +} +impl PyClassPyO3Options { pub fn take_pyo3_options(&mut self, attrs: &mut Vec) -> syn::Result<()> { take_pyo3_options(attrs)? .into_iter() @@ -188,7 +191,8 @@ pub fn build_py_class( ) -> syn::Result { args.options.take_pyo3_options(&mut class.attrs)?; let doc = utils::get_doc(&class.attrs, None); - let krate = get_pyo3_crate(&args.options.krate); + + let ctx = &Ctx::new(&args.options.krate); if let Some(lt) = class.generics.lifetimes().next() { bail_spanned!( @@ -250,7 +254,7 @@ pub fn build_py_class( } } - impl_class(&class.ident, &args, doc, field_options, methods_type, krate) + impl_class(&class.ident, &args, doc, field_options, methods_type, ctx) } enum Annotated { @@ -341,9 +345,10 @@ fn impl_class( doc: PythonDoc, field_options: Vec<(&syn::Field, FieldPyO3Options)>, methods_type: PyClassMethodsType, - krate: syn::Path, + ctx: &Ctx, ) -> syn::Result { - let pytypeinfo_impl = impl_pytypeinfo(cls, args, None); + let Ctx { pyo3_path } = ctx; + let pytypeinfo_impl = impl_pytypeinfo(cls, args, None, ctx); let py_class_impl = PyClassImplsBuilder::new( cls, @@ -354,55 +359,74 @@ fn impl_class( args.options.rename_all.as_ref(), args.options.frozen, field_options, + ctx, )?, vec![], ) .doc(doc) - .impl_all()?; + .impl_all(ctx)?; Ok(quote! { - const _: () = { - use #krate as _pyo3; + impl #pyo3_path::types::DerefToPyAny for #cls {} - #pytypeinfo_impl + #pytypeinfo_impl - #py_class_impl - }; + #py_class_impl }) } -struct PyClassEnumVariant<'a> { - ident: &'a syn::Ident, - options: EnumVariantPyO3Options, +enum PyClassEnum<'a> { + Simple(PyClassSimpleEnum<'a>), + Complex(PyClassComplexEnum<'a>), } -impl<'a> PyClassEnumVariant<'a> { - fn python_name(&self, args: &PyClassArgs) -> Cow<'_, syn::Ident> { - self.options - .name - .as_ref() - .map(|name_attr| Cow::Borrowed(&name_attr.value.0)) - .unwrap_or_else(|| { - let name = self.ident.unraw(); - if let Some(attr) = &args.options.rename_all { - let new_name = apply_renaming_rule(attr.value.rule, &name.to_string()); - Cow::Owned(Ident::new(&new_name, Span::call_site())) - } else { - Cow::Owned(name) - } - }) +impl<'a> PyClassEnum<'a> { + fn new(enum_: &'a mut syn::ItemEnum) -> syn::Result { + let has_only_unit_variants = enum_ + .variants + .iter() + .all(|variant| matches!(variant.fields, syn::Fields::Unit)); + + Ok(if has_only_unit_variants { + let simple_enum = PyClassSimpleEnum::new(enum_)?; + Self::Simple(simple_enum) + } else { + let complex_enum = PyClassComplexEnum::new(enum_)?; + Self::Complex(complex_enum) + }) + } +} + +pub fn build_py_enum( + enum_: &mut syn::ItemEnum, + mut args: PyClassArgs, + method_type: PyClassMethodsType, +) -> syn::Result { + args.options.take_pyo3_options(&mut enum_.attrs)?; + + let ctx = &Ctx::new(&args.options.krate); + if let Some(extends) = &args.options.extends { + bail_spanned!(extends.span() => "enums can't extend from other classes"); + } else if let Some(subclass) = &args.options.subclass { + bail_spanned!(subclass.span() => "enums can't be inherited by other classes"); + } else if enum_.variants.is_empty() { + bail_spanned!(enum_.brace_token.span.join() => "#[pyclass] can't be used on enums without any variants"); } + + let doc = utils::get_doc(&enum_.attrs, None); + let enum_ = PyClassEnum::new(enum_)?; + impl_enum(enum_, &args, doc, method_type, ctx) } -struct PyClassEnum<'a> { +struct PyClassSimpleEnum<'a> { ident: &'a syn::Ident, // The underlying #[repr] of the enum, used to implement __int__ and __richcmp__. // This matters when the underlying representation may not fit in `isize`. repr_type: syn::Ident, - variants: Vec>, + variants: Vec>, } -impl<'a> PyClassEnum<'a> { +impl<'a> PyClassSimpleEnum<'a> { fn new(enum_: &'a mut syn::ItemEnum) -> syn::Result { fn is_numeric_type(t: &syn::Ident) -> bool { [ @@ -412,7 +436,21 @@ impl<'a> PyClassEnum<'a> { .iter() .any(|&s| t == s) } + + fn extract_unit_variant_data( + variant: &mut syn::Variant, + ) -> syn::Result> { + use syn::Fields; + let ident = match &variant.fields { + Fields::Unit => &variant.ident, + _ => bail_spanned!(variant.span() => "Must be a unit variant."), + }; + let options = EnumVariantPyO3Options::take_pyo3_options(&mut variant.attrs)?; + Ok(PyClassEnumUnitVariant { ident, options }) + } + let ident = &enum_.ident; + // According to the [reference](https://doc.rust-lang.org/reference/items/enumerations.html), // "Under the default representation, the specified discriminant is interpreted as an isize // value", so `isize` should be enough by default. @@ -429,10 +467,10 @@ impl<'a> PyClassEnum<'a> { } } - let variants = enum_ + let variants: Vec<_> = enum_ .variants .iter_mut() - .map(extract_variant_data) + .map(extract_unit_variant_data) .collect::>()?; Ok(Self { ident, @@ -442,33 +480,178 @@ impl<'a> PyClassEnum<'a> { } } -pub fn build_py_enum( - enum_: &mut syn::ItemEnum, - mut args: PyClassArgs, - method_type: PyClassMethodsType, -) -> syn::Result { - args.options.take_pyo3_options(&mut enum_.attrs)?; +struct PyClassComplexEnum<'a> { + ident: &'a syn::Ident, + variants: Vec>, +} - if let Some(extends) = &args.options.extends { - bail_spanned!(extends.span() => "enums can't extend from other classes"); - } else if let Some(subclass) = &args.options.subclass { - bail_spanned!(subclass.span() => "enums can't be inherited by other classes"); - } else if enum_.variants.is_empty() { - bail_spanned!(enum_.brace_token.span.join() => "#[pyclass] can't be used on enums without any variants"); +impl<'a> PyClassComplexEnum<'a> { + fn new(enum_: &'a mut syn::ItemEnum) -> syn::Result { + let witness = enum_ + .variants + .iter() + .find(|variant| !matches!(variant.fields, syn::Fields::Unit)) + .expect("complex enum has a non-unit variant") + .ident + .to_owned(); + + let extract_variant_data = + |variant: &'a mut syn::Variant| -> syn::Result> { + use syn::Fields; + let ident = &variant.ident; + let options = EnumVariantPyO3Options::take_pyo3_options(&mut variant.attrs)?; + + let variant = match &variant.fields { + Fields::Unit => { + bail_spanned!(variant.span() => format!( + "Unit variant `{ident}` is not yet supported in a complex enum\n\ + = help: change to an empty tuple variant instead: `{ident}()`\n\ + = note: the enum is complex because of non-unit variant `{witness}`", + ident=ident, witness=witness)) + } + Fields::Named(fields) => { + let fields = fields + .named + .iter() + .map(|field| PyClassEnumVariantNamedField { + ident: field.ident.as_ref().expect("named field has an identifier"), + ty: &field.ty, + span: field.span(), + }) + .collect(); + + PyClassEnumVariant::Struct(PyClassEnumStructVariant { + ident, + fields, + options, + }) + } + Fields::Unnamed(types) => { + let fields = types + .unnamed + .iter() + .map(|field| PyClassEnumVariantUnnamedField { + ty: &field.ty, + span: field.span(), + }) + .collect(); + + PyClassEnumVariant::Tuple(PyClassEnumTupleVariant { + ident, + fields, + options, + }) + } + }; + + Ok(variant) + }; + + let ident = &enum_.ident; + + let variants: Vec<_> = enum_ + .variants + .iter_mut() + .map(extract_variant_data) + .collect::>()?; + + Ok(Self { ident, variants }) } +} - let doc = utils::get_doc(&enum_.attrs, None); - let enum_ = PyClassEnum::new(enum_)?; - impl_enum(enum_, &args, doc, method_type) +enum PyClassEnumVariant<'a> { + // TODO(mkovaxx): Unit(PyClassEnumUnitVariant<'a>), + Struct(PyClassEnumStructVariant<'a>), + Tuple(PyClassEnumTupleVariant<'a>), +} + +trait EnumVariant { + fn get_ident(&self) -> &syn::Ident; + fn get_options(&self) -> &EnumVariantPyO3Options; + + fn get_python_name(&self, args: &PyClassArgs) -> Cow<'_, syn::Ident> { + self.get_options() + .name + .as_ref() + .map(|name_attr| Cow::Borrowed(&name_attr.value.0)) + .unwrap_or_else(|| { + let name = self.get_ident().unraw(); + if let Some(attr) = &args.options.rename_all { + let new_name = apply_renaming_rule(attr.value.rule, &name.to_string()); + Cow::Owned(Ident::new(&new_name, Span::call_site())) + } else { + Cow::Owned(name) + } + }) + } +} + +impl<'a> EnumVariant for PyClassEnumVariant<'a> { + fn get_ident(&self) -> &syn::Ident { + match self { + PyClassEnumVariant::Struct(struct_variant) => struct_variant.ident, + PyClassEnumVariant::Tuple(tuple_variant) => tuple_variant.ident, + } + } + + fn get_options(&self) -> &EnumVariantPyO3Options { + match self { + PyClassEnumVariant::Struct(struct_variant) => &struct_variant.options, + PyClassEnumVariant::Tuple(tuple_variant) => &tuple_variant.options, + } + } +} + +/// A unit variant has no fields +struct PyClassEnumUnitVariant<'a> { + ident: &'a syn::Ident, + options: EnumVariantPyO3Options, +} + +impl<'a> EnumVariant for PyClassEnumUnitVariant<'a> { + fn get_ident(&self) -> &syn::Ident { + self.ident + } + + fn get_options(&self) -> &EnumVariantPyO3Options { + &self.options + } +} + +/// A struct variant has named fields +struct PyClassEnumStructVariant<'a> { + ident: &'a syn::Ident, + fields: Vec>, + options: EnumVariantPyO3Options, +} + +struct PyClassEnumTupleVariant<'a> { + ident: &'a syn::Ident, + fields: Vec>, + options: EnumVariantPyO3Options, +} + +struct PyClassEnumVariantNamedField<'a> { + ident: &'a syn::Ident, + ty: &'a syn::Type, + span: Span, +} + +struct PyClassEnumVariantUnnamedField<'a> { + ty: &'a syn::Type, + span: Span, } /// `#[pyo3()]` options for pyclass enum variants +#[derive(Default)] struct EnumVariantPyO3Options { name: Option, + constructor: Option, } enum EnumVariantPyO3Option { Name(NameAttribute), + Constructor(ConstructorAttribute), } impl Parse for EnumVariantPyO3Option { @@ -476,6 +659,8 @@ impl Parse for EnumVariantPyO3Option { let lookahead = input.lookahead1(); if lookahead.peek(attributes::kw::name) { input.parse().map(EnumVariantPyO3Option::Name) + } else if lookahead.peek(attributes::kw::constructor) { + input.parse().map(EnumVariantPyO3Option::Constructor) } else { Err(lookahead.error()) } @@ -484,21 +669,33 @@ impl Parse for EnumVariantPyO3Option { impl EnumVariantPyO3Options { fn take_pyo3_options(attrs: &mut Vec) -> Result { - let mut options = EnumVariantPyO3Options { name: None }; + let mut options = EnumVariantPyO3Options::default(); - for option in take_pyo3_options(attrs)? { - match option { - EnumVariantPyO3Option::Name(name) => { + take_pyo3_options(attrs)? + .into_iter() + .try_for_each(|option| options.set_option(option))?; + + Ok(options) + } + + fn set_option(&mut self, option: EnumVariantPyO3Option) -> syn::Result<()> { + macro_rules! set_option { + ($key:ident) => { + { ensure_spanned!( - options.name.is_none(), - name.span() => "`name` may only be specified once" + self.$key.is_none(), + $key.span() => concat!("`", stringify!($key), "` may only be specified once") ); - options.name = Some(name); + self.$key = Some($key); } - } + }; } - Ok(options) + match option { + EnumVariantPyO3Option::Constructor(constructor) => set_option!(constructor), + EnumVariantPyO3Option::Name(name) => set_option!(name), + } + Ok(()) } } @@ -507,12 +704,34 @@ fn impl_enum( args: &PyClassArgs, doc: PythonDoc, methods_type: PyClassMethodsType, + ctx: &Ctx, +) -> Result { + match enum_ { + PyClassEnum::Simple(simple_enum) => { + impl_simple_enum(simple_enum, args, doc, methods_type, ctx) + } + PyClassEnum::Complex(complex_enum) => { + impl_complex_enum(complex_enum, args, doc, methods_type, ctx) + } + } +} + +fn impl_simple_enum( + simple_enum: PyClassSimpleEnum<'_>, + args: &PyClassArgs, + doc: PythonDoc, + methods_type: PyClassMethodsType, + ctx: &Ctx, ) -> Result { - let krate = get_pyo3_crate(&args.options.krate); - let cls = enum_.ident; + let Ctx { pyo3_path } = ctx; + let cls = simple_enum.ident; let ty: syn::Type = syn::parse_quote!(#cls); - let variants = enum_.variants; - let pytypeinfo = impl_pytypeinfo(cls, args, None); + let variants = simple_enum.variants; + let pytypeinfo = impl_pytypeinfo(cls, args, None, ctx); + + for variant in &variants { + ensure_spanned!(variant.options.constructor.is_none(), variant.options.constructor.span() => "`constructor` can't be used on a simple enum variant"); + } let (default_repr, default_repr_slot) = { let variants_repr = variants.iter().map(|variant| { @@ -521,7 +740,7 @@ fn impl_enum( let repr = format!( "{}.{}", get_class_python_name(cls, args), - variant.python_name(args), + variant.get_python_name(args), ); quote! { #cls::#variant_name => #repr, } }); @@ -532,11 +751,12 @@ fn impl_enum( } } }; - let repr_slot = generate_default_protocol_slot(&ty, &mut repr_impl, &__REPR__).unwrap(); + let repr_slot = + generate_default_protocol_slot(&ty, &mut repr_impl, &__REPR__, ctx).unwrap(); (repr_impl, repr_slot) }; - let repr_type = &enum_.repr_type; + let repr_type = &simple_enum.repr_type; let (default_int, default_int_slot) = { // This implementation allows us to convert &T to #repr_type without implementing `Copy` @@ -551,7 +771,7 @@ fn impl_enum( } } }; - let int_slot = generate_default_protocol_slot(&ty, &mut int_impl, &__INT__).unwrap(); + let int_slot = generate_default_protocol_slot(&ty, &mut int_impl, &__INT__, ctx).unwrap(); (int_impl, int_slot) }; @@ -559,41 +779,42 @@ fn impl_enum( let mut richcmp_impl: syn::ImplItemFn = syn::parse_quote! { fn __pyo3__richcmp__( &self, - py: _pyo3::Python, - other: &_pyo3::PyAny, - op: _pyo3::basic::CompareOp - ) -> _pyo3::PyResult<_pyo3::PyObject> { - use _pyo3::conversion::ToPyObject; + py: #pyo3_path::Python, + other: &#pyo3_path::Bound<'_, #pyo3_path::PyAny>, + op: #pyo3_path::basic::CompareOp + ) -> #pyo3_path::PyResult<#pyo3_path::PyObject> { + use #pyo3_path::conversion::ToPyObject; + use #pyo3_path::types::PyAnyMethods; use ::core::result::Result::*; match op { - _pyo3::basic::CompareOp::Eq => { + #pyo3_path::basic::CompareOp::Eq => { let self_val = self.__pyo3__int__(); if let Ok(i) = other.extract::<#repr_type>() { return Ok((self_val == i).to_object(py)); } - if let Ok(other) = other.extract::<_pyo3::PyRef>() { + if let Ok(other) = other.extract::<#pyo3_path::PyRef>() { return Ok((self_val == other.__pyo3__int__()).to_object(py)); } - return Ok(::std::convert::Into::into(py.NotImplemented())); + return Ok(py.NotImplemented()); } - _pyo3::basic::CompareOp::Ne => { + #pyo3_path::basic::CompareOp::Ne => { let self_val = self.__pyo3__int__(); if let Ok(i) = other.extract::<#repr_type>() { return Ok((self_val != i).to_object(py)); } - if let Ok(other) = other.extract::<_pyo3::PyRef>() { + if let Ok(other) = other.extract::<#pyo3_path::PyRef>() { return Ok((self_val != other.__pyo3__int__()).to_object(py)); } - return Ok(::std::convert::Into::into(py.NotImplemented())); + return Ok(py.NotImplemented()); } - _ => Ok(::std::convert::Into::into(py.NotImplemented())), + _ => Ok(py.NotImplemented()), } } }; let richcmp_slot = - generate_default_protocol_slot(&ty, &mut richcmp_impl, &__RICHCMP__).unwrap(); + generate_default_protocol_slot(&ty, &mut richcmp_impl, &__RICHCMP__, ctx).unwrap(); (richcmp_impl, richcmp_slot) }; @@ -603,40 +824,469 @@ fn impl_enum( cls, args, methods_type, - enum_default_methods(cls, variants.iter().map(|v| (v.ident, v.python_name(args)))), + simple_enum_default_methods( + cls, + variants.iter().map(|v| (v.ident, v.get_python_name(args))), + ctx, + ), default_slots, ) .doc(doc) - .impl_all()?; + .impl_all(ctx)?; Ok(quote! { - const _: () = { - use #krate as _pyo3; + #pytypeinfo - #pytypeinfo + #pyclass_impls - #pyclass_impls + #[doc(hidden)] + #[allow(non_snake_case)] + impl #cls { + #default_repr + #default_int + #default_richcmp + } + }) +} - #[doc(hidden)] - #[allow(non_snake_case)] - impl #cls { - #default_repr - #default_int - #default_richcmp +fn impl_complex_enum( + complex_enum: PyClassComplexEnum<'_>, + args: &PyClassArgs, + doc: PythonDoc, + methods_type: PyClassMethodsType, + ctx: &Ctx, +) -> Result { + let Ctx { pyo3_path } = ctx; + + // Need to rig the enum PyClass options + let args = { + let mut rigged_args = args.clone(); + // Needs to be frozen to disallow `&mut self` methods, which could break a runtime invariant + rigged_args.options.frozen = parse_quote!(frozen); + // Needs to be subclassable by the variant PyClasses + rigged_args.options.subclass = parse_quote!(subclass); + rigged_args + }; + + let ctx = &Ctx::new(&args.options.krate); + let cls = complex_enum.ident; + let variants = complex_enum.variants; + let pytypeinfo = impl_pytypeinfo(cls, &args, None, ctx); + + let default_slots = vec![]; + + let impl_builder = PyClassImplsBuilder::new( + cls, + &args, + methods_type, + complex_enum_default_methods( + cls, + variants + .iter() + .map(|v| (v.get_ident(), v.get_python_name(&args))), + ctx, + ), + default_slots, + ) + .doc(doc); + + // Need to customize the into_py impl so that it returns the variant PyClass + let enum_into_py_impl = { + let match_arms: Vec = variants + .iter() + .map(|variant| { + let variant_ident = variant.get_ident(); + let variant_cls = gen_complex_enum_variant_class_ident(cls, variant.get_ident()); + quote! { + #cls::#variant_ident { .. } => { + let pyclass_init = #pyo3_path::PyClassInitializer::from(self).add_subclass(#variant_cls); + let variant_value = #pyo3_path::Py::new(py, pyclass_init).unwrap(); + #pyo3_path::IntoPy::into_py(variant_value, py) + } + } + }) + .collect(); + + quote! { + impl #pyo3_path::IntoPy<#pyo3_path::PyObject> for #cls { + fn into_py(self, py: #pyo3_path::Python) -> #pyo3_path::PyObject { + match self { + #(#match_arms)* + } + } } + } + }; + + let pyclass_impls: TokenStream = [ + impl_builder.impl_pyclass(ctx), + impl_builder.impl_extractext(ctx), + enum_into_py_impl, + impl_builder.impl_pyclassimpl(ctx)?, + impl_builder.impl_add_to_module(ctx), + impl_builder.impl_freelist(ctx), + ] + .into_iter() + .collect(); + + let mut variant_cls_zsts = vec![]; + let mut variant_cls_pytypeinfos = vec![]; + let mut variant_cls_pyclass_impls = vec![]; + let mut variant_cls_impls = vec![]; + for variant in variants { + let variant_cls = gen_complex_enum_variant_class_ident(cls, variant.get_ident()); + + let variant_cls_zst = quote! { + #[doc(hidden)] + #[allow(non_camel_case_types)] + struct #variant_cls; + }; + variant_cls_zsts.push(variant_cls_zst); + + let variant_args = PyClassArgs { + class_kind: PyClassKind::Struct, + // TODO(mkovaxx): propagate variant.options + options: parse_quote!(extends = #cls, frozen), }; + + let variant_cls_pytypeinfo = impl_pytypeinfo(&variant_cls, &variant_args, None, ctx); + variant_cls_pytypeinfos.push(variant_cls_pytypeinfo); + + let (variant_cls_impl, field_getters, mut slots) = + impl_complex_enum_variant_cls(cls, &variant, ctx)?; + variant_cls_impls.push(variant_cls_impl); + + let variant_new = complex_enum_variant_new(cls, variant, ctx)?; + slots.push(variant_new); + + let pyclass_impl = PyClassImplsBuilder::new( + &variant_cls, + &variant_args, + methods_type, + field_getters, + slots, + ) + .impl_all(ctx)?; + + variant_cls_pyclass_impls.push(pyclass_impl); + } + + Ok(quote! { + #pytypeinfo + + #pyclass_impls + + #[doc(hidden)] + #[allow(non_snake_case)] + impl #cls {} + + #(#variant_cls_zsts)* + + #(#variant_cls_pytypeinfos)* + + #(#variant_cls_pyclass_impls)* + + #(#variant_cls_impls)* }) } +fn impl_complex_enum_variant_cls( + enum_name: &syn::Ident, + variant: &PyClassEnumVariant<'_>, + ctx: &Ctx, +) -> Result<(TokenStream, Vec, Vec)> { + match variant { + PyClassEnumVariant::Struct(struct_variant) => { + impl_complex_enum_struct_variant_cls(enum_name, struct_variant, ctx) + } + PyClassEnumVariant::Tuple(tuple_variant) => { + impl_complex_enum_tuple_variant_cls(enum_name, tuple_variant, ctx) + } + } +} + +fn impl_complex_enum_variant_match_args( + ctx: &Ctx, + variant_cls_type: &syn::Type, + field_names: &mut Vec, +) -> (MethodAndMethodDef, syn::ImplItemConst) { + let match_args_const_impl: syn::ImplItemConst = { + let args_tp = field_names.iter().map(|_| { + quote! { &'static str } + }); + parse_quote! { + const __match_args__: ( #(#args_tp,)* ) = ( + #(stringify!(#field_names),)* + ); + } + }; + + let spec = ConstSpec { + rust_ident: format_ident!("__match_args__"), + attributes: ConstAttributes { + is_class_attr: true, + name: None, + deprecations: Deprecations::new(ctx), + }, + }; + + let variant_match_args = gen_py_const(variant_cls_type, &spec, ctx); + + (variant_match_args, match_args_const_impl) +} + +fn impl_complex_enum_struct_variant_cls( + enum_name: &syn::Ident, + variant: &PyClassEnumStructVariant<'_>, + ctx: &Ctx, +) -> Result<(TokenStream, Vec, Vec)> { + let Ctx { pyo3_path } = ctx; + let variant_ident = &variant.ident; + let variant_cls = gen_complex_enum_variant_class_ident(enum_name, variant.ident); + let variant_cls_type = parse_quote!(#variant_cls); + + let mut field_names: Vec = vec![]; + let mut fields_with_types: Vec = vec![]; + let mut field_getters = vec![]; + let mut field_getter_impls: Vec = vec![]; + for field in &variant.fields { + let field_name = field.ident; + let field_type = field.ty; + let field_with_type = quote! { #field_name: #field_type }; + + let field_getter = + complex_enum_variant_field_getter(&variant_cls_type, field_name, field.span, ctx)?; + + let field_getter_impl = quote! { + fn #field_name(slf: #pyo3_path::PyRef) -> #pyo3_path::PyResult<#field_type> { + match &*slf.into_super() { + #enum_name::#variant_ident { #field_name, .. } => Ok(#field_name.clone()), + _ => unreachable!("Wrong complex enum variant found in variant wrapper PyClass"), + } + } + }; + + field_names.push(field_name.clone()); + fields_with_types.push(field_with_type); + field_getters.push(field_getter); + field_getter_impls.push(field_getter_impl); + } + + let (variant_match_args, match_args_const_impl) = + impl_complex_enum_variant_match_args(ctx, &variant_cls_type, &mut field_names); + + field_getters.push(variant_match_args); + + let cls_impl = quote! { + #[doc(hidden)] + #[allow(non_snake_case)] + impl #variant_cls { + fn __pymethod_constructor__(py: #pyo3_path::Python<'_>, #(#fields_with_types,)*) -> #pyo3_path::PyClassInitializer<#variant_cls> { + let base_value = #enum_name::#variant_ident { #(#field_names,)* }; + #pyo3_path::PyClassInitializer::from(base_value).add_subclass(#variant_cls) + } + + #match_args_const_impl + + #(#field_getter_impls)* + } + }; + + Ok((cls_impl, field_getters, Vec::new())) +} + +fn impl_complex_enum_tuple_variant_field_getters( + ctx: &Ctx, + variant: &PyClassEnumTupleVariant<'_>, + enum_name: &syn::Ident, + variant_cls_type: &syn::Type, + variant_ident: &&Ident, + field_names: &mut Vec, + fields_types: &mut Vec, +) -> Result<(Vec, Vec)> { + let Ctx { pyo3_path } = ctx; + + let mut field_getters = vec![]; + let mut field_getter_impls = vec![]; + + for (index, field) in variant.fields.iter().enumerate() { + let field_name = format_ident!("_{}", index); + let field_type = field.ty; + + let field_getter = + complex_enum_variant_field_getter(variant_cls_type, &field_name, field.span, ctx)?; + + // Generate the match arms needed to destructure the tuple and access the specific field + let field_access_tokens: Vec<_> = (0..variant.fields.len()) + .map(|i| { + if i == index { + quote! { val } + } else { + quote! { _ } + } + }) + .collect(); + + let field_getter_impl: syn::ImplItemFn = parse_quote! { + fn #field_name(slf: #pyo3_path::PyRef) -> #pyo3_path::PyResult<#field_type> { + match &*slf.into_super() { + #enum_name::#variant_ident ( #(#field_access_tokens), *) => Ok(val.clone()), + _ => unreachable!("Wrong complex enum variant found in variant wrapper PyClass"), + } + } + }; + + field_names.push(field_name); + fields_types.push(field_type.clone()); + field_getters.push(field_getter); + field_getter_impls.push(field_getter_impl); + } + + Ok((field_getters, field_getter_impls)) +} + +fn impl_complex_enum_tuple_variant_len( + ctx: &Ctx, + + variant_cls_type: &syn::Type, + num_fields: usize, +) -> Result<(MethodAndSlotDef, syn::ImplItemFn)> { + let Ctx { pyo3_path } = ctx; + + let mut len_method_impl: syn::ImplItemFn = parse_quote! { + fn __len__(slf: #pyo3_path::PyRef) -> #pyo3_path::PyResult { + Ok(#num_fields) + } + }; + + let variant_len = + generate_default_protocol_slot(variant_cls_type, &mut len_method_impl, &__LEN__, ctx)?; + + Ok((variant_len, len_method_impl)) +} + +fn impl_complex_enum_tuple_variant_getitem( + ctx: &Ctx, + variant_cls: &syn::Ident, + variant_cls_type: &syn::Type, + num_fields: usize, +) -> Result<(MethodAndSlotDef, syn::ImplItemFn)> { + let Ctx { pyo3_path } = ctx; + + let match_arms: Vec<_> = (0..num_fields) + .map(|i| { + let field_access = format_ident!("_{}", i); + quote! { + #i => Ok( + #pyo3_path::IntoPy::into_py( + #variant_cls::#field_access(slf)? + , py) + ) + + } + }) + .collect(); + + let mut get_item_method_impl: syn::ImplItemFn = parse_quote! { + fn __getitem__(slf: #pyo3_path::PyRef, idx: usize) -> #pyo3_path::PyResult< #pyo3_path::PyObject> { + let py = slf.py(); + match idx { + #( #match_arms, )* + _ => Err(pyo3::exceptions::PyIndexError::new_err("tuple index out of range")), + } + } + }; + + let variant_getitem = generate_default_protocol_slot( + variant_cls_type, + &mut get_item_method_impl, + &__GETITEM__, + ctx, + )?; + + Ok((variant_getitem, get_item_method_impl)) +} + +fn impl_complex_enum_tuple_variant_cls( + enum_name: &syn::Ident, + variant: &PyClassEnumTupleVariant<'_>, + ctx: &Ctx, +) -> Result<(TokenStream, Vec, Vec)> { + let Ctx { pyo3_path } = ctx; + let variant_ident = &variant.ident; + let variant_cls = gen_complex_enum_variant_class_ident(enum_name, variant.ident); + let variant_cls_type = parse_quote!(#variant_cls); + + let mut slots = vec![]; + + // represents the index of the field + let mut field_names: Vec = vec![]; + let mut field_types: Vec = vec![]; + + let (mut field_getters, field_getter_impls) = impl_complex_enum_tuple_variant_field_getters( + ctx, + variant, + enum_name, + &variant_cls_type, + variant_ident, + &mut field_names, + &mut field_types, + )?; + + let num_fields = variant.fields.len(); + + let (variant_len, len_method_impl) = + impl_complex_enum_tuple_variant_len(ctx, &variant_cls_type, num_fields)?; + + slots.push(variant_len); + + let (variant_getitem, getitem_method_impl) = + impl_complex_enum_tuple_variant_getitem(ctx, &variant_cls, &variant_cls_type, num_fields)?; + + slots.push(variant_getitem); + + let (variant_match_args, match_args_method_impl) = + impl_complex_enum_variant_match_args(ctx, &variant_cls_type, &mut field_names); + + field_getters.push(variant_match_args); + + let cls_impl = quote! { + #[doc(hidden)] + #[allow(non_snake_case)] + impl #variant_cls { + fn __pymethod_constructor__(py: #pyo3_path::Python<'_>, #(#field_names : #field_types,)*) -> #pyo3_path::PyClassInitializer<#variant_cls> { + let base_value = #enum_name::#variant_ident ( #(#field_names,)* ); + #pyo3_path::PyClassInitializer::from(base_value).add_subclass(#variant_cls) + } + + #len_method_impl + + #getitem_method_impl + + #match_args_method_impl + + #(#field_getter_impls)* + } + }; + + Ok((cls_impl, field_getters, slots)) +} + +fn gen_complex_enum_variant_class_ident(enum_: &syn::Ident, variant: &syn::Ident) -> syn::Ident { + format_ident!("{}_{}", enum_, variant) +} + fn generate_default_protocol_slot( cls: &syn::Type, method: &mut syn::ImplItemFn, slot: &SlotDef, + ctx: &Ctx, ) -> syn::Result { let spec = FnSpec::parse( &mut method.sig, &mut Vec::new(), PyFunctionOptions::default(), + ctx, ) .unwrap(); let name = spec.name.to_string(); @@ -644,12 +1294,14 @@ fn generate_default_protocol_slot( &syn::parse_quote!(#cls), &spec, &format!("__default_{}__", name), + ctx, ) } -fn enum_default_methods<'a>( +fn simple_enum_default_methods<'a>( cls: &'a syn::Ident, unit_variant_names: impl IntoIterator)>, + ctx: &Ctx, ) -> Vec { let cls_type = syn::parse_quote!(#cls); let variant_to_attribute = |var_ident: &syn::Ident, py_ident: &syn::Ident| ConstSpec { @@ -660,23 +1312,232 @@ fn enum_default_methods<'a>( kw: syn::parse_quote! { name }, value: NameLitStr(py_ident.clone()), }), - deprecations: Default::default(), + deprecations: Deprecations::new(ctx), }, }; unit_variant_names .into_iter() - .map(|(var, py_name)| gen_py_const(&cls_type, &variant_to_attribute(var, &py_name))) + .map(|(var, py_name)| gen_py_const(&cls_type, &variant_to_attribute(var, &py_name), ctx)) .collect() } -fn extract_variant_data(variant: &mut syn::Variant) -> syn::Result> { - use syn::Fields; - let ident = match variant.fields { - Fields::Unit => &variant.ident, - _ => bail_spanned!(variant.span() => "Currently only support unit variants."), +fn complex_enum_default_methods<'a>( + cls: &'a syn::Ident, + variant_names: impl IntoIterator)>, + ctx: &Ctx, +) -> Vec { + let cls_type = syn::parse_quote!(#cls); + let variant_to_attribute = |var_ident: &syn::Ident, py_ident: &syn::Ident| ConstSpec { + rust_ident: var_ident.clone(), + attributes: ConstAttributes { + is_class_attr: true, + name: Some(NameAttribute { + kw: syn::parse_quote! { name }, + value: NameLitStr(py_ident.clone()), + }), + deprecations: Deprecations::new(ctx), + }, + }; + variant_names + .into_iter() + .map(|(var, py_name)| { + gen_complex_enum_variant_attr(cls, &cls_type, &variant_to_attribute(var, &py_name), ctx) + }) + .collect() +} + +pub fn gen_complex_enum_variant_attr( + cls: &syn::Ident, + cls_type: &syn::Type, + spec: &ConstSpec<'_>, + ctx: &Ctx, +) -> MethodAndMethodDef { + let Ctx { pyo3_path } = ctx; + let member = &spec.rust_ident; + let wrapper_ident = format_ident!("__pymethod_variant_cls_{}__", member); + let deprecations = &spec.attributes.deprecations; + let python_name = &spec.null_terminated_python_name(); + + let variant_cls = format_ident!("{}_{}", cls, member); + let associated_method = quote! { + fn #wrapper_ident(py: #pyo3_path::Python<'_>) -> #pyo3_path::PyResult<#pyo3_path::PyObject> { + #deprecations + ::std::result::Result::Ok(py.get_type_bound::<#variant_cls>().into_any().unbind()) + } + }; + + let method_def = quote! { + #pyo3_path::class::PyMethodDefType::ClassAttribute({ + #pyo3_path::class::PyClassAttributeDef::new( + #python_name, + #cls_type::#wrapper_ident + ) + }) }; - let options = EnumVariantPyO3Options::take_pyo3_options(&mut variant.attrs)?; - Ok(PyClassEnumVariant { ident, options }) + + MethodAndMethodDef { + associated_method, + method_def, + } +} + +fn complex_enum_variant_new<'a>( + cls: &'a syn::Ident, + variant: PyClassEnumVariant<'a>, + ctx: &Ctx, +) -> Result { + match variant { + PyClassEnumVariant::Struct(struct_variant) => { + complex_enum_struct_variant_new(cls, struct_variant, ctx) + } + PyClassEnumVariant::Tuple(tuple_variant) => { + complex_enum_tuple_variant_new(cls, tuple_variant, ctx) + } + } +} + +fn complex_enum_struct_variant_new<'a>( + cls: &'a syn::Ident, + variant: PyClassEnumStructVariant<'a>, + ctx: &Ctx, +) -> Result { + let Ctx { pyo3_path } = ctx; + let variant_cls = format_ident!("{}_{}", cls, variant.ident); + let variant_cls_type: syn::Type = parse_quote!(#variant_cls); + + let arg_py_ident: syn::Ident = parse_quote!(py); + let arg_py_type: syn::Type = parse_quote!(#pyo3_path::Python<'_>); + + let args = { + let mut args = vec![ + // py: Python<'_> + FnArg::Py(PyArg { + name: &arg_py_ident, + ty: &arg_py_type, + }), + ]; + + for field in &variant.fields { + args.push(FnArg::Regular(RegularArg { + name: Cow::Borrowed(field.ident), + ty: field.ty, + from_py_with: None, + default_value: None, + option_wrapped_type: None, + })); + } + args + }; + + let signature = if let Some(constructor) = variant.options.constructor { + crate::pyfunction::FunctionSignature::from_arguments_and_attribute( + args, + constructor.into_signature(), + )? + } else { + crate::pyfunction::FunctionSignature::from_arguments(args)? + }; + + let spec = FnSpec { + tp: crate::method::FnType::FnNew, + name: &format_ident!("__pymethod_constructor__"), + python_name: format_ident!("__new__"), + signature, + convention: crate::method::CallingConvention::TpNew, + text_signature: None, + asyncness: None, + unsafety: None, + deprecations: Deprecations::new(ctx), + }; + + crate::pymethod::impl_py_method_def_new(&variant_cls_type, &spec, ctx) +} + +fn complex_enum_tuple_variant_new<'a>( + cls: &'a syn::Ident, + variant: PyClassEnumTupleVariant<'a>, + ctx: &Ctx, +) -> Result { + let Ctx { pyo3_path } = ctx; + + let variant_cls: Ident = format_ident!("{}_{}", cls, variant.ident); + let variant_cls_type: syn::Type = parse_quote!(#variant_cls); + + let arg_py_ident: syn::Ident = parse_quote!(py); + let arg_py_type: syn::Type = parse_quote!(#pyo3_path::Python<'_>); + + let args = { + let mut args = vec![FnArg::Py(PyArg { + name: &arg_py_ident, + ty: &arg_py_type, + })]; + + for (i, field) in variant.fields.iter().enumerate() { + args.push(FnArg::Regular(RegularArg { + name: std::borrow::Cow::Owned(format_ident!("_{}", i)), + ty: field.ty, + from_py_with: None, + default_value: None, + option_wrapped_type: None, + })); + } + args + }; + + let signature = if let Some(constructor) = variant.options.constructor { + crate::pyfunction::FunctionSignature::from_arguments_and_attribute( + args, + constructor.into_signature(), + )? + } else { + crate::pyfunction::FunctionSignature::from_arguments(args)? + }; + + let spec = FnSpec { + tp: crate::method::FnType::FnNew, + name: &format_ident!("__pymethod_constructor__"), + python_name: format_ident!("__new__"), + signature, + convention: crate::method::CallingConvention::TpNew, + text_signature: None, + asyncness: None, + unsafety: None, + deprecations: Deprecations::new(ctx), + }; + + crate::pymethod::impl_py_method_def_new(&variant_cls_type, &spec, ctx) +} + +fn complex_enum_variant_field_getter<'a>( + variant_cls_type: &'a syn::Type, + field_name: &'a syn::Ident, + field_span: Span, + ctx: &Ctx, +) -> Result { + let signature = crate::pyfunction::FunctionSignature::from_arguments(vec![])?; + + let self_type = crate::method::SelfType::TryFromBoundRef(field_span); + + let spec = FnSpec { + tp: crate::method::FnType::Getter(self_type.clone()), + name: field_name, + python_name: field_name.clone(), + signature, + convention: crate::method::CallingConvention::Noargs, + text_signature: None, + asyncness: None, + unsafety: None, + deprecations: Deprecations::new(ctx), + }; + + let property_type = crate::pymethod::PropertyType::Function { + self_type: &self_type, + spec: &spec, + doc: crate::get_doc(&[], None), + }; + + let getter = crate::pymethod::impl_py_getter_def(variant_cls_type, property_type, ctx)?; + Ok(getter) } fn descriptors_to_items( @@ -684,6 +1545,7 @@ fn descriptors_to_items( rename_all: Option<&RenameAllAttribute>, frozen: Option, field_options: Vec<(&syn::Field, FieldPyO3Options)>, + ctx: &Ctx, ) -> syn::Result> { let ty = syn::parse_quote!(#cls); let mut items = Vec::new(); @@ -706,6 +1568,7 @@ fn descriptors_to_items( python_name: options.name.as_ref(), renaming_rule: rename_all.map(|rename_all| rename_all.value.rule), }, + ctx, )?; items.push(getter); } @@ -720,6 +1583,7 @@ fn descriptors_to_items( python_name: options.name.as_ref(), renaming_rule: rename_all.map(|rename_all| rename_all.value.rule), }, + ctx, )?; items.push(setter); }; @@ -730,8 +1594,10 @@ fn descriptors_to_items( fn impl_pytypeinfo( cls: &syn::Ident, attr: &PyClassArgs, - deprecations: Option<&Deprecations>, + deprecations: Option<&Deprecations<'_>>, + ctx: &Ctx, ) -> TokenStream { + let Ctx { pyo3_path } = ctx; let cls_name = get_class_python_name(cls, attr).to_string(); let module = if let Some(ModuleAttribute { value, .. }) = &attr.options.module { @@ -740,20 +1606,30 @@ fn impl_pytypeinfo( quote! { ::core::option::Option::None } }; - quote! { - unsafe impl _pyo3::type_object::HasPyGilRef for #cls { - type AsRefTarget = _pyo3::PyCell; + #[cfg(feature = "gil-refs")] + let has_py_gil_ref = quote! { + #[allow(deprecated)] + unsafe impl #pyo3_path::type_object::HasPyGilRef for #cls { + type AsRefTarget = #pyo3_path::PyCell; } + }; + + #[cfg(not(feature = "gil-refs"))] + let has_py_gil_ref = TokenStream::new(); - unsafe impl _pyo3::type_object::PyTypeInfo for #cls { + quote! { + #has_py_gil_ref + + unsafe impl #pyo3_path::type_object::PyTypeInfo for #cls { const NAME: &'static str = #cls_name; const MODULE: ::std::option::Option<&'static str> = #module; #[inline] - fn type_object_raw(py: _pyo3::Python<'_>) -> *mut _pyo3::ffi::PyTypeObject { + fn type_object_raw(py: #pyo3_path::Python<'_>) -> *mut #pyo3_path::ffi::PyTypeObject { + use #pyo3_path::prelude::PyTypeMethods; #deprecations - <#cls as _pyo3::impl_::pyclass::PyClassImpl>::lazy_type_object() + <#cls as #pyo3_path::impl_::pyclass::PyClassImpl>::lazy_type_object() .get_or_init(py) .as_type_ptr() } @@ -800,82 +1676,86 @@ impl<'a> PyClassImplsBuilder<'a> { } } - fn impl_all(&self) -> Result { - let tokens = vec![ - self.impl_pyclass(), - self.impl_extractext(), - self.impl_into_py(), - self.impl_pyclassimpl()?, - self.impl_freelist(), + fn impl_all(&self, ctx: &Ctx) -> Result { + let tokens = [ + self.impl_pyclass(ctx), + self.impl_extractext(ctx), + self.impl_into_py(ctx), + self.impl_pyclassimpl(ctx)?, + self.impl_add_to_module(ctx), + self.impl_freelist(ctx), ] .into_iter() .collect(); Ok(tokens) } - fn impl_pyclass(&self) -> TokenStream { + fn impl_pyclass(&self, ctx: &Ctx) -> TokenStream { + let Ctx { pyo3_path } = ctx; let cls = self.cls; let frozen = if self.attr.options.frozen.is_some() { - quote! { _pyo3::pyclass::boolean_struct::True } + quote! { #pyo3_path::pyclass::boolean_struct::True } } else { - quote! { _pyo3::pyclass::boolean_struct::False } + quote! { #pyo3_path::pyclass::boolean_struct::False } }; quote! { - impl _pyo3::PyClass for #cls { + impl #pyo3_path::PyClass for #cls { type Frozen = #frozen; } } } - fn impl_extractext(&self) -> TokenStream { + fn impl_extractext(&self, ctx: &Ctx) -> TokenStream { + let Ctx { pyo3_path } = ctx; let cls = self.cls; if self.attr.options.frozen.is_some() { quote! { - impl<'a, 'py> _pyo3::impl_::extract_argument::PyFunctionArgument<'a, 'py> for &'a #cls + impl<'a, 'py> #pyo3_path::impl_::extract_argument::PyFunctionArgument<'a, 'py> for &'a #cls { - type Holder = ::std::option::Option<_pyo3::PyRef<'py, #cls>>; + type Holder = ::std::option::Option<#pyo3_path::PyRef<'py, #cls>>; #[inline] - fn extract(obj: &'py _pyo3::PyAny, holder: &'a mut Self::Holder) -> _pyo3::PyResult { - _pyo3::impl_::extract_argument::extract_pyclass_ref(obj, holder) + fn extract(obj: &'a #pyo3_path::Bound<'py, #pyo3_path::PyAny>, holder: &'a mut Self::Holder) -> #pyo3_path::PyResult { + #pyo3_path::impl_::extract_argument::extract_pyclass_ref(obj, holder) } } } } else { quote! { - impl<'a, 'py> _pyo3::impl_::extract_argument::PyFunctionArgument<'a, 'py> for &'a #cls + impl<'a, 'py> #pyo3_path::impl_::extract_argument::PyFunctionArgument<'a, 'py> for &'a #cls { - type Holder = ::std::option::Option<_pyo3::PyRef<'py, #cls>>; + type Holder = ::std::option::Option<#pyo3_path::PyRef<'py, #cls>>; #[inline] - fn extract(obj: &'py _pyo3::PyAny, holder: &'a mut Self::Holder) -> _pyo3::PyResult { - _pyo3::impl_::extract_argument::extract_pyclass_ref(obj, holder) + fn extract(obj: &'a #pyo3_path::Bound<'py, #pyo3_path::PyAny>, holder: &'a mut Self::Holder) -> #pyo3_path::PyResult { + #pyo3_path::impl_::extract_argument::extract_pyclass_ref(obj, holder) } } - impl<'a, 'py> _pyo3::impl_::extract_argument::PyFunctionArgument<'a, 'py> for &'a mut #cls + impl<'a, 'py> #pyo3_path::impl_::extract_argument::PyFunctionArgument<'a, 'py> for &'a mut #cls { - type Holder = ::std::option::Option<_pyo3::PyRefMut<'py, #cls>>; + type Holder = ::std::option::Option<#pyo3_path::PyRefMut<'py, #cls>>; #[inline] - fn extract(obj: &'py _pyo3::PyAny, holder: &'a mut Self::Holder) -> _pyo3::PyResult { - _pyo3::impl_::extract_argument::extract_pyclass_ref_mut(obj, holder) + fn extract(obj: &'a #pyo3_path::Bound<'py, #pyo3_path::PyAny>, holder: &'a mut Self::Holder) -> #pyo3_path::PyResult { + #pyo3_path::impl_::extract_argument::extract_pyclass_ref_mut(obj, holder) } } } } } - fn impl_into_py(&self) -> TokenStream { + fn impl_into_py(&self, ctx: &Ctx) -> TokenStream { + let Ctx { pyo3_path } = ctx; let cls = self.cls; let attr = self.attr; // If #cls is not extended type, we allow Self->PyObject conversion if attr.options.extends.is_none() { quote! { - impl _pyo3::IntoPy<_pyo3::PyObject> for #cls { - fn into_py(self, py: _pyo3::Python) -> _pyo3::PyObject { - _pyo3::IntoPy::into_py(_pyo3::Py::new(py, self).unwrap(), py) + impl #pyo3_path::IntoPy<#pyo3_path::PyObject> for #cls { + fn into_py(self, py: #pyo3_path::Python) -> #pyo3_path::PyObject { + #pyo3_path::IntoPy::into_py(#pyo3_path::Py::new(py, self).unwrap(), py) } } } @@ -883,29 +1763,15 @@ impl<'a> PyClassImplsBuilder<'a> { quote! {} } } - fn impl_pyclassimpl(&self) -> Result { + fn impl_pyclassimpl(&self, ctx: &Ctx) -> Result { + let Ctx { pyo3_path } = ctx; let cls = self.cls; let doc = self.doc.as_ref().map_or(quote! {"\0"}, |doc| quote! {#doc}); - let deprecated_text_signature = match self - .attr - .options - .text_signature - .as_ref() - .map(|attr| &attr.value) - { - Some(TextSignatureAttributeValue::Str(s)) => quote!(::std::option::Option::Some(#s)), - Some(TextSignatureAttributeValue::Disabled(_)) | None => { - quote!(::std::option::Option::None) - } - }; let is_basetype = self.attr.options.subclass.is_some(); - let base = self - .attr - .options - .extends - .as_ref() - .map(|extends_attr| extends_attr.value.clone()) - .unwrap_or_else(|| parse_quote! { _pyo3::PyAny }); + let base = match &self.attr.options.extends { + Some(extends_attr) => extends_attr.value.clone(), + None => parse_quote! { #pyo3_path::PyAny }, + }; let is_subclass = self.attr.options.extends.is_some(); let is_mapping: bool = self.attr.options.mapping.is_some(); let is_sequence: bool = self.attr.options.sequence.is_some(); @@ -917,8 +1783,8 @@ impl<'a> PyClassImplsBuilder<'a> { let dict_offset = if self.attr.options.dict.is_some() { quote! { - fn dict_offset() -> ::std::option::Option<_pyo3::ffi::Py_ssize_t> { - ::std::option::Option::Some(_pyo3::impl_::pyclass::dict_offset::()) + fn dict_offset() -> ::std::option::Option<#pyo3_path::ffi::Py_ssize_t> { + ::std::option::Option::Some(#pyo3_path::impl_::pyclass::dict_offset::()) } } } else { @@ -928,8 +1794,8 @@ impl<'a> PyClassImplsBuilder<'a> { // insert space for weak ref let weaklist_offset = if self.attr.options.weakref.is_some() { quote! { - fn weaklist_offset() -> ::std::option::Option<_pyo3::ffi::Py_ssize_t> { - ::std::option::Option::Some(_pyo3::impl_::pyclass::weaklist_offset::()) + fn weaklist_offset() -> ::std::option::Option<#pyo3_path::ffi::Py_ssize_t> { + ::std::option::Option::Some(#pyo3_path::impl_::pyclass::weaklist_offset::()) } } } else { @@ -937,9 +1803,9 @@ impl<'a> PyClassImplsBuilder<'a> { }; let thread_checker = if self.attr.options.unsendable.is_some() { - quote! { _pyo3::impl_::pyclass::ThreadCheckerImpl } + quote! { #pyo3_path::impl_::pyclass::ThreadCheckerImpl } } else { - quote! { _pyo3::impl_::pyclass::SendablePyClass<#cls> } + quote! { #pyo3_path::impl_::pyclass::SendablePyClass<#cls> } }; let (pymethods_items, inventory, inventory_class) = match self.methods_type { @@ -954,13 +1820,13 @@ impl<'a> PyClassImplsBuilder<'a> { quote! { ::std::boxed::Box::new( ::std::iter::Iterator::map( - _pyo3::inventory::iter::<::Inventory>(), - _pyo3::impl_::pyclass::PyClassInventory::items + #pyo3_path::inventory::iter::<::Inventory>(), + #pyo3_path::impl_::pyclass::PyClassInventory::items ) ) }, Some(quote! { type Inventory = #inventory_class_name; }), - Some(define_inventory_class(&inventory_class_name)), + Some(define_inventory_class(&inventory_class_name, ctx)), ) } }; @@ -977,7 +1843,7 @@ impl<'a> PyClassImplsBuilder<'a> { let default_method_defs = self.default_methods.iter().map(|meth| &meth.method_def); let default_slot_defs = self.default_slots.iter().map(|slot| &slot.slot_def); - let freelist_slots = self.freelist_slots(); + let freelist_slots = self.freelist_slots(ctx); let class_mutability = if self.attr.options.frozen.is_some() { quote! { @@ -992,26 +1858,26 @@ impl<'a> PyClassImplsBuilder<'a> { let cls = self.cls; let attr = self.attr; let dict = if attr.options.dict.is_some() { - quote! { _pyo3::impl_::pyclass::PyClassDictSlot } + quote! { #pyo3_path::impl_::pyclass::PyClassDictSlot } } else { - quote! { _pyo3::impl_::pyclass::PyClassDummySlot } + quote! { #pyo3_path::impl_::pyclass::PyClassDummySlot } }; // insert space for weak ref let weakref = if attr.options.weakref.is_some() { - quote! { _pyo3::impl_::pyclass::PyClassWeakRefSlot } + quote! { #pyo3_path::impl_::pyclass::PyClassWeakRefSlot } } else { - quote! { _pyo3::impl_::pyclass::PyClassDummySlot } + quote! { #pyo3_path::impl_::pyclass::PyClassDummySlot } }; let base_nativetype = if attr.options.extends.is_some() { - quote! { ::BaseNativeType } + quote! { ::BaseNativeType } } else { - quote! { _pyo3::PyAny } + quote! { #pyo3_path::PyAny } }; Ok(quote! { - impl _pyo3::impl_::pyclass::PyClassImpl for #cls { + impl #pyo3_path::impl_::pyclass::PyClassImpl for #cls { const IS_BASETYPE: bool = #is_basetype; const IS_SUBCLASS: bool = #is_subclass; const IS_MAPPING: bool = #is_mapping; @@ -1020,13 +1886,13 @@ impl<'a> PyClassImplsBuilder<'a> { type BaseType = #base; type ThreadChecker = #thread_checker; #inventory - type PyClassMutability = <<#base as _pyo3::impl_::pyclass::PyClassBaseType>::PyClassMutability as _pyo3::impl_::pycell::PyClassMutability>::#class_mutability; + type PyClassMutability = <<#base as #pyo3_path::impl_::pyclass::PyClassBaseType>::PyClassMutability as #pyo3_path::impl_::pycell::PyClassMutability>::#class_mutability; type Dict = #dict; type WeakRef = #weakref; type BaseNativeType = #base_nativetype; - fn items_iter() -> _pyo3::impl_::pyclass::PyClassItemsIter { - use _pyo3::impl_::pyclass::*; + fn items_iter() -> #pyo3_path::impl_::pyclass::PyClassItemsIter { + use #pyo3_path::impl_::pyclass::*; let collector = PyClassImplCollector::::new(); static INTRINSIC_ITEMS: PyClassItems = PyClassItems { methods: &[#(#default_method_defs),*], @@ -1035,12 +1901,12 @@ impl<'a> PyClassImplsBuilder<'a> { PyClassItemsIter::new(&INTRINSIC_ITEMS, #pymethods_items) } - fn doc(py: _pyo3::Python<'_>) -> _pyo3::PyResult<&'static ::std::ffi::CStr> { - use _pyo3::impl_::pyclass::*; - static DOC: _pyo3::sync::GILOnceCell<::std::borrow::Cow<'static, ::std::ffi::CStr>> = _pyo3::sync::GILOnceCell::new(); + fn doc(py: #pyo3_path::Python<'_>) -> #pyo3_path::PyResult<&'static ::std::ffi::CStr> { + use #pyo3_path::impl_::pyclass::*; + static DOC: #pyo3_path::sync::GILOnceCell<::std::borrow::Cow<'static, ::std::ffi::CStr>> = #pyo3_path::sync::GILOnceCell::new(); DOC.get_or_try_init(py, || { let collector = PyClassImplCollector::::new(); - build_pyclass_doc(<#cls as _pyo3::PyTypeInfo>::NAME, #doc, #deprecated_text_signature.or_else(|| collector.new_text_signature())) + build_pyclass_doc(<#cls as #pyo3_path::PyTypeInfo>::NAME, #doc, collector.new_text_signature()) }).map(::std::ops::Deref::deref) } @@ -1048,8 +1914,8 @@ impl<'a> PyClassImplsBuilder<'a> { #weaklist_offset - fn lazy_type_object() -> &'static _pyo3::impl_::pyclass::LazyTypeObject { - use _pyo3::impl_::pyclass::LazyTypeObject; + fn lazy_type_object() -> &'static #pyo3_path::impl_::pyclass::LazyTypeObject { + use #pyo3_path::impl_::pyclass::LazyTypeObject; static TYPE_OBJECT: LazyTypeObject<#cls> = LazyTypeObject::new(); &TYPE_OBJECT } @@ -1065,20 +1931,32 @@ impl<'a> PyClassImplsBuilder<'a> { }) } - fn impl_freelist(&self) -> TokenStream { + fn impl_add_to_module(&self, ctx: &Ctx) -> TokenStream { + let Ctx { pyo3_path } = ctx; + let cls = self.cls; + quote! { + impl #cls { + #[doc(hidden)] + pub const _PYO3_DEF: #pyo3_path::impl_::pymodule::AddClassToModule = #pyo3_path::impl_::pymodule::AddClassToModule::new(); + } + } + } + + fn impl_freelist(&self, ctx: &Ctx) -> TokenStream { let cls = self.cls; + let Ctx { pyo3_path } = ctx; self.attr.options.freelist.as_ref().map_or(quote!{}, |freelist| { let freelist = &freelist.value; quote! { - impl _pyo3::impl_::pyclass::PyClassWithFreeList for #cls { + impl #pyo3_path::impl_::pyclass::PyClassWithFreeList for #cls { #[inline] - fn get_free_list(py: _pyo3::Python<'_>) -> &mut _pyo3::impl_::freelist::FreeList<*mut _pyo3::ffi::PyObject> { - static mut FREELIST: *mut _pyo3::impl_::freelist::FreeList<*mut _pyo3::ffi::PyObject> = 0 as *mut _; + fn get_free_list(py: #pyo3_path::Python<'_>) -> &mut #pyo3_path::impl_::freelist::FreeList<*mut #pyo3_path::ffi::PyObject> { + static mut FREELIST: *mut #pyo3_path::impl_::freelist::FreeList<*mut #pyo3_path::ffi::PyObject> = 0 as *mut _; unsafe { if FREELIST.is_null() { FREELIST = ::std::boxed::Box::into_raw(::std::boxed::Box::new( - _pyo3::impl_::freelist::FreeList::with_capacity(#freelist))); + #pyo3_path::impl_::freelist::FreeList::with_capacity(#freelist))); } &mut *FREELIST } @@ -1088,21 +1966,22 @@ impl<'a> PyClassImplsBuilder<'a> { }) } - fn freelist_slots(&self) -> Vec { + fn freelist_slots(&self, ctx: &Ctx) -> Vec { + let Ctx { pyo3_path } = ctx; let cls = self.cls; if self.attr.options.freelist.is_some() { vec![ quote! { - _pyo3::ffi::PyType_Slot { - slot: _pyo3::ffi::Py_tp_alloc, - pfunc: _pyo3::impl_::pyclass::alloc_with_freelist::<#cls> as *mut _, + #pyo3_path::ffi::PyType_Slot { + slot: #pyo3_path::ffi::Py_tp_alloc, + pfunc: #pyo3_path::impl_::pyclass::alloc_with_freelist::<#cls> as *mut _, } }, quote! { - _pyo3::ffi::PyType_Slot { - slot: _pyo3::ffi::Py_tp_free, - pfunc: _pyo3::impl_::pyclass::free_with_freelist::<#cls> as *mut _, + #pyo3_path::ffi::PyType_Slot { + slot: #pyo3_path::ffi::Py_tp_free, + pfunc: #pyo3_path::impl_::pyclass::free_with_freelist::<#cls> as *mut _, } }, ] @@ -1112,25 +1991,26 @@ impl<'a> PyClassImplsBuilder<'a> { } } -fn define_inventory_class(inventory_class_name: &syn::Ident) -> TokenStream { +fn define_inventory_class(inventory_class_name: &syn::Ident, ctx: &Ctx) -> TokenStream { + let Ctx { pyo3_path } = ctx; quote! { #[doc(hidden)] pub struct #inventory_class_name { - items: _pyo3::impl_::pyclass::PyClassItems, + items: #pyo3_path::impl_::pyclass::PyClassItems, } impl #inventory_class_name { - pub const fn new(items: _pyo3::impl_::pyclass::PyClassItems) -> Self { + pub const fn new(items: #pyo3_path::impl_::pyclass::PyClassItems) -> Self { Self { items } } } - impl _pyo3::impl_::pyclass::PyClassInventory for #inventory_class_name { - fn items(&self) -> &_pyo3::impl_::pyclass::PyClassItems { + impl #pyo3_path::impl_::pyclass::PyClassInventory for #inventory_class_name { + fn items(&self) -> &#pyo3_path::impl_::pyclass::PyClassItems { &self.items } } - _pyo3::inventory::collect!(#inventory_class_name); + #pyo3_path::inventory::collect!(#inventory_class_name); } } diff --git a/pyo3-macros-backend/src/pyfunction.rs b/pyo3-macros-backend/src/pyfunction.rs index b265a34d39f..e259f0e2c1e 100644 --- a/pyo3-macros-backend/src/pyfunction.rs +++ b/pyo3-macros-backend/src/pyfunction.rs @@ -1,3 +1,4 @@ +use crate::utils::Ctx; use crate::{ attributes::{ self, get_pyo3_options, take_attributes, take_pyo3_options, CrateAttribute, @@ -6,7 +7,6 @@ use crate::{ deprecations::Deprecations, method::{self, CallingConvention, FnArg}, pymethod::check_generic, - utils::get_pyo3_crate, }; use proc_macro2::TokenStream; use quote::{format_ident, quote}; @@ -18,7 +18,7 @@ use syn::{ mod signature; -pub use self::signature::{FunctionSignature, SignatureAttribute}; +pub use self::signature::{ConstructorAttribute, FunctionSignature, SignatureAttribute}; #[derive(Clone, Debug)] pub struct PyFunctionArgPyO3Attributes { @@ -205,6 +205,9 @@ pub fn impl_wrap_pyfunction( krate, } = options; + let ctx = &Ctx::new(&krate); + let Ctx { pyo3_path } = &ctx; + let python_name = name.map_or_else(|| func.sig.ident.unraw(), |name| name.value.0); let tp = if pass_module.is_some() { @@ -237,29 +240,24 @@ pub fn impl_wrap_pyfunction( FunctionSignature::from_arguments(arguments)? }; - let ty = method::get_return_info(&func.sig.output); - let spec = method::FnSpec { tp, name: &func.sig.ident, convention: CallingConvention::from_signature(&signature), python_name, signature, - output: ty, text_signature, asyncness: func.sig.asyncness, unsafety: func.sig.unsafety, - deprecations: Deprecations::new(), + deprecations: Deprecations::new(ctx), }; - let krate = get_pyo3_crate(&krate); - let vis = &func.vis; let name = &func.sig.ident; let wrapper_ident = format_ident!("__pyfunction_{}", spec.name); - let wrapper = spec.get_wrapper_function(&wrapper_ident, None)?; - let methoddef = spec.get_methoddef(wrapper_ident, &spec.get_doc(&func.attrs)); + let wrapper = spec.get_wrapper_function(&wrapper_ident, None, ctx)?; + let methoddef = spec.get_methoddef(wrapper_ident, &spec.get_doc(&func.attrs), ctx); let wrapped_pyfunction = quote! { @@ -268,22 +266,20 @@ pub fn impl_wrap_pyfunction( #[doc(hidden)] #vis mod #name { pub(crate) struct MakeDef; - pub const DEF: #krate::impl_::pyfunction::PyMethodDef = MakeDef::DEF; + pub const _PYO3_DEF: #pyo3_path::impl_::pymethods::PyMethodDef = MakeDef::_PYO3_DEF; } // Generate the definition inside an anonymous function in the same scope as the original function - // this avoids complications around the fact that the generated module has a different scope // (and `super` doesn't always refer to the outer scope, e.g. if the `#[pyfunction] is // inside a function body) - const _: () = { - use #krate as _pyo3; - impl #name::MakeDef { - const DEF: #krate::impl_::pyfunction::PyMethodDef = #methoddef; - } + #[allow(unknown_lints, non_local_definitions)] + impl #name::MakeDef { + const _PYO3_DEF: #pyo3_path::impl_::pymethods::PyMethodDef = #methoddef; + } - #[allow(non_snake_case)] - #wrapper - }; + #[allow(non_snake_case)] + #wrapper }; Ok(wrapped_pyfunction) } diff --git a/pyo3-macros-backend/src/pyfunction/signature.rs b/pyo3-macros-backend/src/pyfunction/signature.rs index baf01285658..b73b96a3d59 100644 --- a/pyo3-macros-backend/src/pyfunction/signature.rs +++ b/pyo3-macros-backend/src/pyfunction/signature.rs @@ -10,7 +10,7 @@ use syn::{ use crate::{ attributes::{kw, KeywordAttribute}, - method::FnArg, + method::{FnArg, RegularArg}, }; pub struct Signature { @@ -195,6 +195,16 @@ impl ToTokens for SignatureItemPosargsSep { } pub type SignatureAttribute = KeywordAttribute; +pub type ConstructorAttribute = KeywordAttribute; + +impl ConstructorAttribute { + pub fn into_signature(self) -> SignatureAttribute { + SignatureAttribute { + kw: kw::signature(self.kw.span), + value: self.value, + } + } +} #[derive(Default)] pub struct PythonSignature { @@ -351,36 +361,39 @@ impl<'a> FunctionSignature<'a> { let mut next_non_py_argument_checked = |name: &syn::Ident| { for fn_arg in args_iter.by_ref() { - if fn_arg.py { - // If the user incorrectly tried to include py: Python in the - // signature, give a useful error as a hint. - ensure_spanned!( - name != fn_arg.name, - name.span() => "arguments of type `Python` must not be part of the signature" - ); - // Otherwise try next argument. - continue; - } - if fn_arg.is_cancel_handle { - // If the user incorrectly tried to include cancel: CoroutineCancel in the - // signature, give a useful error as a hint. - ensure_spanned!( - name != fn_arg.name, - name.span() => "`cancel_handle` argument must not be part of the signature" - ); - // Otherwise try next argument. - continue; + match fn_arg { + crate::method::FnArg::Py(..) => { + // If the user incorrectly tried to include py: Python in the + // signature, give a useful error as a hint. + ensure_spanned!( + name != fn_arg.name(), + name.span() => "arguments of type `Python` must not be part of the signature" + ); + // Otherwise try next argument. + continue; + } + crate::method::FnArg::CancelHandle(..) => { + // If the user incorrectly tried to include cancel: CoroutineCancel in the + // signature, give a useful error as a hint. + ensure_spanned!( + name != fn_arg.name(), + name.span() => "`cancel_handle` argument must not be part of the signature" + ); + // Otherwise try next argument. + continue; + } + _ => { + ensure_spanned!( + name == fn_arg.name(), + name.span() => format!( + "expected argument from function definition `{}` but got argument `{}`", + fn_arg.name().unraw(), + name.unraw(), + ) + ); + return Ok(fn_arg); + } } - - ensure_spanned!( - name == fn_arg.name, - name.span() => format!( - "expected argument from function definition `{}` but got argument `{}`", - fn_arg.name.unraw(), - name.unraw(), - ) - ); - return Ok(fn_arg); } bail_spanned!( name.span() => "signature entry does not have a corresponding function argument" @@ -398,7 +411,15 @@ impl<'a> FunctionSignature<'a> { arg.span(), )?; if let Some((_, default)) = &arg.eq_and_default { - fn_arg.default = Some(default.clone()); + if let FnArg::Regular(arg) = fn_arg { + arg.default_value = Some(default.clone()); + } else { + unreachable!( + "`Python` and `CancelHandle` are already handled above and `*args`/`**kwargs` are \ + parsed and transformed below. Because the have to come last and are only allowed \ + once, this has to be a regular argument." + ); + } } } SignatureItem::VarargsSep(sep) => { @@ -406,12 +427,12 @@ impl<'a> FunctionSignature<'a> { } SignatureItem::Varargs(varargs) => { let fn_arg = next_non_py_argument_checked(&varargs.ident)?; - fn_arg.is_varargs = true; + fn_arg.to_varargs_mut()?; parse_state.add_varargs(&mut python_signature, varargs)?; } SignatureItem::Kwargs(kwargs) => { let fn_arg = next_non_py_argument_checked(&kwargs.ident)?; - fn_arg.is_kwargs = true; + fn_arg.to_kwargs_mut()?; parse_state.add_kwargs(&mut python_signature, kwargs)?; } SignatureItem::PosargsSep(sep) => { @@ -421,9 +442,11 @@ impl<'a> FunctionSignature<'a> { } // Ensure no non-py arguments remain - if let Some(arg) = args_iter.find(|arg| !arg.py && !arg.is_cancel_handle) { + if let Some(arg) = + args_iter.find(|arg| !matches!(arg, FnArg::Py(..) | FnArg::CancelHandle(..))) + { bail_spanned!( - attribute.kw.span() => format!("missing signature entry for argument `{}`", arg.name) + attribute.kw.span() => format!("missing signature entry for argument `{}`", arg.name()) ); } @@ -439,15 +462,20 @@ impl<'a> FunctionSignature<'a> { let mut python_signature = PythonSignature::default(); for arg in &arguments { // Python<'_> arguments don't show in Python signature - if arg.py || arg.is_cancel_handle { + if matches!(arg, FnArg::Py(..) | FnArg::CancelHandle(..)) { continue; } - if arg.optional.is_none() { + if let FnArg::Regular(RegularArg { + ty, + option_wrapped_type: None, + .. + }) = arg + { // This argument is required, all previous arguments must also have been required ensure_spanned!( python_signature.required_positional_parameters == python_signature.positional_parameters.len(), - arg.ty.span() => "required arguments after an `Option<_>` argument are ambiguous\n\ + ty.span() => "required arguments after an `Option<_>` argument are ambiguous\n\ = help: add a `#[pyo3(signature)]` annotation on this function to unambiguously specify the default values for all optional parameters" ); @@ -457,7 +485,7 @@ impl<'a> FunctionSignature<'a> { python_signature .positional_parameters - .push(arg.name.unraw().to_string()); + .push(arg.name().unraw().to_string()); } Ok(Self { @@ -469,8 +497,12 @@ impl<'a> FunctionSignature<'a> { fn default_value_for_parameter(&self, parameter: &str) -> String { let mut default = "...".to_string(); - if let Some(fn_arg) = self.arguments.iter().find(|arg| arg.name == parameter) { - if let Some(arg_default) = fn_arg.default.as_ref() { + if let Some(fn_arg) = self.arguments.iter().find(|arg| arg.name() == parameter) { + if let FnArg::Regular(RegularArg { + default_value: Some(arg_default), + .. + }) = fn_arg + { match arg_default { // literal values syn::Expr::Lit(syn::ExprLit { lit, .. }) => match lit { @@ -496,7 +528,11 @@ impl<'a> FunctionSignature<'a> { // others, unsupported yet so defaults to `...` _ => {} } - } else if fn_arg.optional.is_some() { + } else if let FnArg::Regular(RegularArg { + option_wrapped_type: Some(..), + .. + }) = fn_arg + { // functions without a `#[pyo3(signature = (...))]` option // will treat trailing `Option` arguments as having a default of `None` default = "None".to_string(); diff --git a/pyo3-macros-backend/src/pyimpl.rs b/pyo3-macros-backend/src/pyimpl.rs index f5ae111bf48..0cb7631a4df 100644 --- a/pyo3-macros-backend/src/pyimpl.rs +++ b/pyo3-macros-backend/src/pyimpl.rs @@ -1,11 +1,11 @@ use std::collections::HashSet; +use crate::utils::Ctx; use crate::{ attributes::{take_pyo3_options, CrateAttribute}, konst::{ConstAttributes, ConstSpec}, pyfunction::PyFunctionOptions, pymethod::{self, is_proto_method, MethodAndMethodDef, MethodAndSlotDef}, - utils::get_pyo3_crate, }; use proc_macro2::TokenStream; use pymethod::GeneratedPyMethod; @@ -90,6 +90,7 @@ pub fn impl_methods( methods_type: PyClassMethodsType, options: PyImplOptions, ) -> syn::Result { + let ctx = &Ctx::new(&options.krate); let mut trait_impls = Vec::new(); let mut proto_impls = Vec::new(); let mut methods = Vec::new(); @@ -102,7 +103,8 @@ pub fn impl_methods( syn::ImplItem::Fn(meth) => { let mut fun_options = PyFunctionOptions::from_attrs(&mut meth.attrs)?; fun_options.krate = fun_options.krate.or_else(|| options.krate.clone()); - match pymethod::gen_py_method(ty, &mut meth.sig, &mut meth.attrs, fun_options)? { + match pymethod::gen_py_method(ty, &mut meth.sig, &mut meth.attrs, fun_options, ctx)? + { GeneratedPyMethod::Method(MethodAndMethodDef { associated_method, method_def, @@ -127,7 +129,7 @@ pub fn impl_methods( } } syn::ImplItem::Const(konst) => { - let attributes = ConstAttributes::from_attrs(&mut konst.attrs)?; + let attributes = ConstAttributes::from_attrs(&mut konst.attrs, ctx)?; if attributes.is_class_attr { let spec = ConstSpec { rust_ident: konst.ident.clone(), @@ -137,7 +139,7 @@ pub fn impl_methods( let MethodAndMethodDef { associated_method, method_def, - } = gen_py_const(ty, &spec); + } = gen_py_const(ty, &spec, ctx); methods.push(quote!(#(#attrs)* #method_def)); associated_methods.push(quote!(#(#attrs)* #associated_method)); if is_proto_method(&spec.python_name().to_string()) { @@ -158,50 +160,47 @@ pub fn impl_methods( } } - add_shared_proto_slots(ty, &mut proto_impls, implemented_proto_fragments); + add_shared_proto_slots(ty, &mut proto_impls, implemented_proto_fragments, ctx); - let krate = get_pyo3_crate(&options.krate); + let ctx = &Ctx::new(&options.krate); let items = match methods_type { - PyClassMethodsType::Specialization => impl_py_methods(ty, methods, proto_impls), - PyClassMethodsType::Inventory => submit_methods_inventory(ty, methods, proto_impls), + PyClassMethodsType::Specialization => impl_py_methods(ty, methods, proto_impls, ctx), + PyClassMethodsType::Inventory => submit_methods_inventory(ty, methods, proto_impls, ctx), }; Ok(quote! { - const _: () = { - use #krate as _pyo3; + #(#trait_impls)* - #(#trait_impls)* + #items - #items - - #[doc(hidden)] - #[allow(non_snake_case)] - impl #ty { - #(#associated_methods)* - } - }; + #[doc(hidden)] + #[allow(non_snake_case)] + impl #ty { + #(#associated_methods)* + } }) } -pub fn gen_py_const(cls: &syn::Type, spec: &ConstSpec) -> MethodAndMethodDef { +pub fn gen_py_const(cls: &syn::Type, spec: &ConstSpec<'_>, ctx: &Ctx) -> MethodAndMethodDef { let member = &spec.rust_ident; let wrapper_ident = format_ident!("__pymethod_{}__", member); let deprecations = &spec.attributes.deprecations; let python_name = &spec.null_terminated_python_name(); + let Ctx { pyo3_path } = ctx; let associated_method = quote! { - fn #wrapper_ident(py: _pyo3::Python<'_>) -> _pyo3::PyResult<_pyo3::PyObject> { + fn #wrapper_ident(py: #pyo3_path::Python<'_>) -> #pyo3_path::PyResult<#pyo3_path::PyObject> { #deprecations - ::std::result::Result::Ok(_pyo3::IntoPy::into_py(#cls::#member, py)) + ::std::result::Result::Ok(#pyo3_path::IntoPy::into_py(#cls::#member, py)) } }; let method_def = quote! { - _pyo3::class::PyMethodDefType::ClassAttribute({ - _pyo3::class::PyClassAttributeDef::new( + #pyo3_path::class::PyMethodDefType::ClassAttribute({ + #pyo3_path::class::PyClassAttributeDef::new( #python_name, - _pyo3::impl_::pymethods::PyClassAttributeFactory(#cls::#wrapper_ident) + #cls::#wrapper_ident ) }) }; @@ -216,13 +215,15 @@ fn impl_py_methods( ty: &syn::Type, methods: Vec, proto_impls: Vec, + ctx: &Ctx, ) -> TokenStream { + let Ctx { pyo3_path } = ctx; quote! { - impl _pyo3::impl_::pyclass::PyMethods<#ty> - for _pyo3::impl_::pyclass::PyClassImplCollector<#ty> + impl #pyo3_path::impl_::pyclass::PyMethods<#ty> + for #pyo3_path::impl_::pyclass::PyClassImplCollector<#ty> { - fn py_methods(self) -> &'static _pyo3::impl_::pyclass::PyClassItems { - static ITEMS: _pyo3::impl_::pyclass::PyClassItems = _pyo3::impl_::pyclass::PyClassItems { + fn py_methods(self) -> &'static #pyo3_path::impl_::pyclass::PyClassItems { + static ITEMS: #pyo3_path::impl_::pyclass::PyClassItems = #pyo3_path::impl_::pyclass::PyClassItems { methods: &[#(#methods),*], slots: &[#(#proto_impls),*] }; @@ -236,13 +237,15 @@ fn add_shared_proto_slots( ty: &syn::Type, proto_impls: &mut Vec, mut implemented_proto_fragments: HashSet, + ctx: &Ctx, ) { + let Ctx { pyo3_path } = ctx; macro_rules! try_add_shared_slot { ($slot:ident, $($fragments:literal),*) => {{ let mut implemented = false; $(implemented |= implemented_proto_fragments.remove($fragments));*; if implemented { - proto_impls.push(quote! { _pyo3::impl_::pyclass::$slot!(#ty) }) + proto_impls.push(quote! { #pyo3_path::impl_::pyclass::$slot!(#ty) }) } }}; } @@ -292,11 +295,13 @@ fn submit_methods_inventory( ty: &syn::Type, methods: Vec, proto_impls: Vec, + ctx: &Ctx, ) -> TokenStream { + let Ctx { pyo3_path } = ctx; quote! { - _pyo3::inventory::submit! { - type Inventory = <#ty as _pyo3::impl_::pyclass::PyClassImpl>::Inventory; - Inventory::new(_pyo3::impl_::pyclass::PyClassItems { methods: &[#(#methods),*], slots: &[#(#proto_impls),*] }) + #pyo3_path::inventory::submit! { + type Inventory = <#ty as #pyo3_path::impl_::pyclass::PyClassImpl>::Inventory; + Inventory::new(#pyo3_path::impl_::pyclass::PyClassItems { methods: &[#(#methods),*], slots: &[#(#proto_impls),*] }) } } } diff --git a/pyo3-macros-backend/src/pymethod.rs b/pyo3-macros-backend/src/pymethod.rs index 8a97c712cac..f5b11af3c27 100644 --- a/pyo3-macros-backend/src/pymethod.rs +++ b/pyo3-macros-backend/src/pymethod.rs @@ -1,7 +1,10 @@ use std::borrow::Cow; use crate::attributes::{NameAttribute, RenamingRule}; -use crate::method::{CallingConvention, ExtractErrorMode}; +use crate::deprecations::deprecate_trailing_option_default; +use crate::method::{CallingConvention, ExtractErrorMode, PyArg}; +use crate::params::{check_arg_for_gil_refs, impl_regular_arg_param, Holders}; +use crate::utils::Ctx; use crate::utils::PythonDoc; use crate::{ method::{FnArg, FnSpec, FnType, SelfType}, @@ -9,7 +12,7 @@ use crate::{ }; use crate::{quotes, utils}; use proc_macro2::{Span, TokenStream}; -use quote::{format_ident, quote, ToTokens}; +use quote::{format_ident, quote, quote_spanned, ToTokens}; use syn::{ext::IdentExt, spanned::Spanned, Result}; /// Generated code for a single pymethod item. @@ -160,8 +163,9 @@ impl<'a> PyMethod<'a> { sig: &'a mut syn::Signature, meth_attrs: &mut Vec, options: PyFunctionOptions, + ctx: &'a Ctx, ) -> Result { - let spec = FnSpec::parse(sig, meth_attrs, options)?; + let spec = FnSpec::parse(sig, meth_attrs, options, ctx)?; let method_name = spec.python_name.to_string(); let kind = PyMethodKind::from_name(&method_name); @@ -186,33 +190,35 @@ pub fn gen_py_method( sig: &mut syn::Signature, meth_attrs: &mut Vec, options: PyFunctionOptions, + ctx: &Ctx, ) -> Result { check_generic(sig)?; ensure_function_options_valid(&options)?; - let method = PyMethod::parse(sig, meth_attrs, options)?; + let method = PyMethod::parse(sig, meth_attrs, options, ctx)?; let spec = &method.spec; + let Ctx { pyo3_path } = ctx; Ok(match (method.kind, &spec.tp) { // Class attributes go before protos so that class attributes can be used to set proto // method to None. (_, FnType::ClassAttribute) => { - GeneratedPyMethod::Method(impl_py_class_attribute(cls, spec)?) + GeneratedPyMethod::Method(impl_py_class_attribute(cls, spec, ctx)?) } (PyMethodKind::Proto(proto_kind), _) => { ensure_no_forbidden_protocol_attributes(&proto_kind, spec, &method.method_name)?; match proto_kind { PyMethodProtoKind::Slot(slot_def) => { - let slot = slot_def.generate_type_slot(cls, spec, &method.method_name)?; + let slot = slot_def.generate_type_slot(cls, spec, &method.method_name, ctx)?; GeneratedPyMethod::Proto(slot) } PyMethodProtoKind::Call => { - GeneratedPyMethod::Proto(impl_call_slot(cls, method.spec)?) + GeneratedPyMethod::Proto(impl_call_slot(cls, method.spec, ctx)?) } PyMethodProtoKind::Traverse => { - GeneratedPyMethod::Proto(impl_traverse_slot(cls, spec)?) + GeneratedPyMethod::Proto(impl_traverse_slot(cls, spec, ctx)?) } PyMethodProtoKind::SlotFragment(slot_fragment_def) => { - let proto = slot_fragment_def.generate_pyproto_fragment(cls, spec)?; + let proto = slot_fragment_def.generate_pyproto_fragment(cls, spec, ctx)?; GeneratedPyMethod::SlotTraitImpl(method.method_name, proto) } } @@ -223,22 +229,25 @@ pub fn gen_py_method( spec, &spec.get_doc(meth_attrs), None, + ctx, )?), (_, FnType::FnClass(_)) => GeneratedPyMethod::Method(impl_py_method_def( cls, spec, &spec.get_doc(meth_attrs), - Some(quote!(_pyo3::ffi::METH_CLASS)), + Some(quote!(#pyo3_path::ffi::METH_CLASS)), + ctx, )?), (_, FnType::FnStatic) => GeneratedPyMethod::Method(impl_py_method_def( cls, spec, &spec.get_doc(meth_attrs), - Some(quote!(_pyo3::ffi::METH_STATIC)), + Some(quote!(#pyo3_path::ffi::METH_STATIC)), + ctx, )?), // special prototypes (_, FnType::FnNew) | (_, FnType::FnNewClass(_)) => { - GeneratedPyMethod::Proto(impl_py_method_def_new(cls, spec)?) + GeneratedPyMethod::Proto(impl_py_method_def_new(cls, spec, ctx)?) } (_, FnType::Getter(self_type)) => GeneratedPyMethod::Method(impl_py_getter_def( @@ -248,6 +257,7 @@ pub fn gen_py_method( spec, doc: spec.get_doc(meth_attrs), }, + ctx, )?), (_, FnType::Setter(self_type)) => GeneratedPyMethod::Method(impl_py_setter_def( cls, @@ -256,6 +266,7 @@ pub fn gen_py_method( spec, doc: spec.get_doc(meth_attrs), }, + ctx, )?), (_, FnType::FnModule(_)) => { unreachable!("methods cannot be FnModule") @@ -305,18 +316,20 @@ pub fn impl_py_method_def( spec: &FnSpec<'_>, doc: &PythonDoc, flags: Option, + ctx: &Ctx, ) -> Result { + let Ctx { pyo3_path } = ctx; let wrapper_ident = format_ident!("__pymethod_{}__", spec.python_name); - let associated_method = spec.get_wrapper_function(&wrapper_ident, Some(cls))?; + let associated_method = spec.get_wrapper_function(&wrapper_ident, Some(cls), ctx)?; let add_flags = flags.map(|flags| quote!(.flags(#flags))); let methoddef_type = match spec.tp { FnType::FnStatic => quote!(Static), FnType::FnClass(_) => quote!(Class), _ => quote!(Method), }; - let methoddef = spec.get_methoddef(quote! { #cls::#wrapper_ident }, doc); + let methoddef = spec.get_methoddef(quote! { #cls::#wrapper_ident }, doc, ctx); let method_def = quote! { - _pyo3::class::PyMethodDefType::#methoddef_type(#methoddef #add_flags) + #pyo3_path::class::PyMethodDefType::#methoddef_type(#methoddef #add_flags) }; Ok(MethodAndMethodDef { associated_method, @@ -324,9 +337,15 @@ pub fn impl_py_method_def( }) } -fn impl_py_method_def_new(cls: &syn::Type, spec: &FnSpec<'_>) -> Result { +/// Also used by pyclass. +pub fn impl_py_method_def_new( + cls: &syn::Type, + spec: &FnSpec<'_>, + ctx: &Ctx, +) -> Result { + let Ctx { pyo3_path } = ctx; let wrapper_ident = syn::Ident::new("__pymethod___new____", Span::call_site()); - let associated_method = spec.get_wrapper_function(&wrapper_ident, Some(cls))?; + let associated_method = spec.get_wrapper_function(&wrapper_ident, Some(cls), ctx)?; // Use just the text_signature_call_signature() because the class' Python name // isn't known to `#[pymethods]` - that has to be attached at runtime from the PyClassImpl // trait implementation created by `#[pyclass]`. @@ -336,18 +355,19 @@ fn impl_py_method_def_new(cls: &syn::Type, spec: &FnSpec<'_>) -> Result *mut _pyo3::ffi::PyObject + subtype: *mut #pyo3_path::ffi::PyTypeObject, + args: *mut #pyo3_path::ffi::PyObject, + kwargs: *mut #pyo3_path::ffi::PyObject, + ) -> *mut #pyo3_path::ffi::PyObject { #deprecations - use _pyo3::impl_::pyclass::*; + use #pyo3_path::impl_::pyclass::*; + #[allow(unknown_lints, non_local_definitions)] impl PyClassNewTextSignature<#cls> for PyClassImplCollector<#cls> { #[inline] fn new_text_signature(self) -> ::std::option::Option<&'static str> { @@ -355,7 +375,7 @@ fn impl_py_method_def_new(cls: &syn::Type, spec: &FnSpec<'_>) -> Result) -> Result) -> Result) -> Result { +fn impl_call_slot(cls: &syn::Type, mut spec: FnSpec<'_>, ctx: &Ctx) -> Result { + let Ctx { pyo3_path } = ctx; + // HACK: __call__ proto slot must always use varargs calling convention, so change the spec. // Probably indicates there's a refactoring opportunity somewhere. spec.convention = CallingConvention::Varargs; let wrapper_ident = syn::Ident::new("__pymethod___call____", Span::call_site()); - let associated_method = spec.get_wrapper_function(&wrapper_ident, Some(cls))?; + let associated_method = spec.get_wrapper_function(&wrapper_ident, Some(cls), ctx)?; let slot_def = quote! { - _pyo3::ffi::PyType_Slot { - slot: _pyo3::ffi::Py_tp_call, + #pyo3_path::ffi::PyType_Slot { + slot: #pyo3_path::ffi::Py_tp_call, pfunc: { unsafe extern "C" fn trampoline( - slf: *mut _pyo3::ffi::PyObject, - args: *mut _pyo3::ffi::PyObject, - kwargs: *mut _pyo3::ffi::PyObject, - ) -> *mut _pyo3::ffi::PyObject + slf: *mut #pyo3_path::ffi::PyObject, + args: *mut #pyo3_path::ffi::PyObject, + kwargs: *mut #pyo3_path::ffi::PyObject, + ) -> *mut #pyo3_path::ffi::PyObject { - _pyo3::impl_::trampoline::ternaryfunc( + #pyo3_path::impl_::trampoline::ternaryfunc( slf, args, kwargs, @@ -397,7 +419,7 @@ fn impl_call_slot(cls: &syn::Type, mut spec: FnSpec<'_>) -> Result) -> Result) -> syn::Result { +fn impl_traverse_slot( + cls: &syn::Type, + spec: &FnSpec<'_>, + ctx: &Ctx, +) -> syn::Result { + let Ctx { pyo3_path } = ctx; if let (Some(py_arg), _) = split_off_python_arg(&spec.signature.arguments) { return Err(syn::Error::new_spanned(py_arg.ty, "__traverse__ may not take `Python`. \ - Usually, an implementation of `__traverse__` should do nothing but calls to `visit.call`. \ - Most importantly, safe access to the GIL is prohibited inside implementations of `__traverse__`, \ - i.e. `Python::with_gil` will panic.")); + Usually, an implementation of `__traverse__(&self, visit: PyVisit<'_>) -> Result<(), PyTraverseError>` \ + should do nothing but calls to `visit.call`. Most importantly, safe access to the GIL is prohibited \ + inside implementations of `__traverse__`, i.e. `Python::with_gil` will panic.")); + } + + // check that the receiver does not try to smuggle an (implicit) `Python` token into here + if let FnType::Fn(SelfType::TryFromBoundRef(span)) + | FnType::Fn(SelfType::Receiver { + mutable: true, + span, + }) = spec.tp + { + bail_spanned! { span => + "__traverse__ may not take a receiver other than `&self`. Usually, an implementation of \ + `__traverse__(&self, visit: PyVisit<'_>) -> Result<(), PyTraverseError>` \ + should do nothing but calls to `visit.call`. Most importantly, safe access to the GIL is prohibited \ + inside implementations of `__traverse__`, i.e. `Python::with_gil` will panic." + } } let rust_fn_ident = spec.name; let associated_method = quote! { pub unsafe extern "C" fn __pymethod_traverse__( - slf: *mut _pyo3::ffi::PyObject, - visit: _pyo3::ffi::visitproc, + slf: *mut #pyo3_path::ffi::PyObject, + visit: #pyo3_path::ffi::visitproc, arg: *mut ::std::os::raw::c_void, ) -> ::std::os::raw::c_int { - _pyo3::impl_::pymethods::_call_traverse::<#cls>(slf, #cls::#rust_fn_ident, visit, arg) + #pyo3_path::impl_::pymethods::_call_traverse::<#cls>(slf, #cls::#rust_fn_ident, visit, arg) } }; let slot_def = quote! { - _pyo3::ffi::PyType_Slot { - slot: _pyo3::ffi::Py_tp_traverse, - pfunc: #cls::__pymethod_traverse__ as _pyo3::ffi::traverseproc as _ + #pyo3_path::ffi::PyType_Slot { + slot: #pyo3_path::ffi::Py_tp_traverse, + pfunc: #cls::__pymethod_traverse__ as #pyo3_path::ffi::traverseproc as _ } }; Ok(MethodAndSlotDef { @@ -437,11 +479,16 @@ fn impl_traverse_slot(cls: &syn::Type, spec: &FnSpec<'_>) -> syn::Result) -> syn::Result { +fn impl_py_class_attribute( + cls: &syn::Type, + spec: &FnSpec<'_>, + ctx: &Ctx, +) -> syn::Result { + let Ctx { pyo3_path } = ctx; let (py_arg, args) = split_off_python_arg(&spec.signature.arguments); ensure_spanned!( args.is_empty(), - args[0].ty.span() => "#[classattr] can only have one argument (of type pyo3::Python)" + args[0].ty().span() => "#[classattr] can only have one argument (of type pyo3::Python)" ); let name = &spec.name; @@ -453,20 +500,20 @@ fn impl_py_class_attribute(cls: &syn::Type, spec: &FnSpec<'_>) -> syn::Result) -> _pyo3::PyResult<_pyo3::PyObject> { + fn #wrapper_ident(py: #pyo3_path::Python<'_>) -> #pyo3_path::PyResult<#pyo3_path::PyObject> { let function = #cls::#name; // Shadow the method name to avoid #3017 - _pyo3::impl_::wrap::map_result_into_py(py, #body) + #pyo3_path::impl_::wrap::map_result_into_py(py, #body) } }; let method_def = quote! { - _pyo3::class::PyMethodDefType::ClassAttribute({ - _pyo3::class::PyClassAttributeDef::new( + #pyo3_path::class::PyMethodDefType::ClassAttribute({ + #pyo3_path::class::PyClassAttributeDef::new( #python_name, - _pyo3::impl_::pymethods::PyClassAttributeFactory(#cls::#wrapper_ident) + #cls::#wrapper_ident ) }) }; @@ -481,16 +528,17 @@ fn impl_call_setter( cls: &syn::Type, spec: &FnSpec<'_>, self_type: &SelfType, - holders: &mut Vec, + holders: &mut Holders, + ctx: &Ctx, ) -> syn::Result { let (py_arg, args) = split_off_python_arg(&spec.signature.arguments); - let slf = self_type.receiver(cls, ExtractErrorMode::Raise, holders); + let slf = self_type.receiver(cls, ExtractErrorMode::Raise, holders, ctx); if args.is_empty() { bail_spanned!(spec.name.span() => "setter function expected to have one argument"); } else if args.len() > 1 { bail_spanned!( - args[1].ty.span() => + args[1].ty().span() => "setter function can have at most two arguments ([pyo3::Python,] and value)" ); } @@ -509,10 +557,12 @@ fn impl_call_setter( pub fn impl_py_setter_def( cls: &syn::Type, property_type: PropertyType<'_>, + ctx: &Ctx, ) -> Result { + let Ctx { pyo3_path } = ctx; let python_name = property_type.null_terminated_python_name()?; let doc = property_type.doc(); - let mut holders = Vec::new(); + let mut holders = Holders::new(); let setter_impl = match property_type { PropertyType::Descriptor { field_index, field, .. @@ -521,7 +571,7 @@ pub fn impl_py_setter_def( mutable: true, span: Span::call_site(), } - .receiver(cls, ExtractErrorMode::Raise, &mut holders); + .receiver(cls, ExtractErrorMode::Raise, &mut holders, ctx); if let Some(ident) = &field.ident { // named struct field quote!({ #slf.#ident = _val; }) @@ -533,7 +583,7 @@ pub fn impl_py_setter_def( } PropertyType::Function { spec, self_type, .. - } => impl_call_setter(cls, spec, self_type, &mut holders)?, + } => impl_call_setter(cls, spec, self_type, &mut holders, ctx)?, }; let wrapper_ident = match property_type { @@ -553,6 +603,68 @@ pub fn impl_py_setter_def( } }; + let extract = match &property_type { + PropertyType::Function { spec, .. } => { + let (_, args) = split_off_python_arg(&spec.signature.arguments); + let value_arg = &args[0]; + let (from_py_with, ident) = if let Some(from_py_with) = + &value_arg.from_py_with().as_ref().map(|f| &f.value) + { + let ident = syn::Ident::new("from_py_with", from_py_with.span()); + ( + quote_spanned! { from_py_with.span() => + let e = #pyo3_path::impl_::deprecations::GilRefs::new(); + let #ident = #pyo3_path::impl_::deprecations::inspect_fn(#from_py_with, &e); + e.from_py_with_arg(); + }, + ident, + ) + } else { + (quote!(), syn::Ident::new("dummy", Span::call_site())) + }; + + let arg = if let FnArg::Regular(arg) = &value_arg { + arg + } else { + bail_spanned!(value_arg.name().span() => "The #[setter] value argument can't be *args, **kwargs or `cancel_handle`."); + }; + + let tokens = impl_regular_arg_param( + arg, + ident, + quote!(::std::option::Option::Some(_value.into())), + &mut holders, + ctx, + ); + let extract = + check_arg_for_gil_refs(tokens, holders.push_gil_refs_checker(arg.ty.span()), ctx); + + let deprecation = deprecate_trailing_option_default(spec); + quote! { + #deprecation + #from_py_with + let _val = #extract; + } + } + PropertyType::Descriptor { field, .. } => { + let span = field.ty.span(); + let name = field + .ident + .as_ref() + .map(|i| i.to_string()) + .unwrap_or_default(); + + let holder = holders.push_holder(span); + let gil_refs_checker = holders.push_gil_refs_checker(span); + quote! { + let _val = #pyo3_path::impl_::deprecations::inspect_type( + #pyo3_path::impl_::extract_argument::extract_argument(_value.into(), &mut #holder, #name)?, + &#gil_refs_checker + ); + } + } + }; + let mut cfg_attrs = TokenStream::new(); if let PropertyType::Descriptor { field, .. } = &property_type { for attr in field @@ -564,30 +676,34 @@ pub fn impl_py_setter_def( } } + let init_holders = holders.init_holders(ctx); + let check_gil_refs = holders.check_gil_refs(); let associated_method = quote! { #cfg_attrs unsafe fn #wrapper_ident( - py: _pyo3::Python<'_>, - _slf: *mut _pyo3::ffi::PyObject, - _value: *mut _pyo3::ffi::PyObject, - ) -> _pyo3::PyResult<::std::os::raw::c_int> { - let _value = py - .from_borrowed_ptr_or_opt(_value) + py: #pyo3_path::Python<'_>, + _slf: *mut #pyo3_path::ffi::PyObject, + _value: *mut #pyo3_path::ffi::PyObject, + ) -> #pyo3_path::PyResult<::std::os::raw::c_int> { + use ::std::convert::Into; + let _value = #pyo3_path::impl_::pymethods::BoundRef::ref_from_ptr_or_opt(py, &_value) .ok_or_else(|| { - _pyo3::exceptions::PyAttributeError::new_err("can't delete attribute") + #pyo3_path::exceptions::PyAttributeError::new_err("can't delete attribute") })?; - let _val = _pyo3::FromPyObject::extract(_value)?; - #( #holders )* - _pyo3::callback::convert(py, #setter_impl) + #init_holders + #extract + let result = #setter_impl; + #check_gil_refs + #pyo3_path::callback::convert(py, result) } }; let method_def = quote! { #cfg_attrs - _pyo3::class::PyMethodDefType::Setter( - _pyo3::class::PySetterDef::new( + #pyo3_path::class::PyMethodDefType::Setter( + #pyo3_path::class::PySetterDef::new( #python_name, - _pyo3::impl_::pymethods::PySetter(#cls::#wrapper_ident), + #cls::#wrapper_ident, #doc ) ) @@ -603,13 +719,14 @@ fn impl_call_getter( cls: &syn::Type, spec: &FnSpec<'_>, self_type: &SelfType, - holders: &mut Vec, + holders: &mut Holders, + ctx: &Ctx, ) -> syn::Result { let (py_arg, args) = split_off_python_arg(&spec.signature.arguments); - let slf = self_type.receiver(cls, ExtractErrorMode::Raise, holders); + let slf = self_type.receiver(cls, ExtractErrorMode::Raise, holders, ctx); ensure_spanned!( args.is_empty(), - args[0].ty.span() => "getter function can only have one argument (of type pyo3::Python)" + args[0].ty().span() => "getter function can only have one argument (of type pyo3::Python)" ); let name = &spec.name; @@ -626,11 +743,13 @@ fn impl_call_getter( pub fn impl_py_getter_def( cls: &syn::Type, property_type: PropertyType<'_>, + ctx: &Ctx, ) -> Result { + let Ctx { pyo3_path } = ctx; let python_name = property_type.null_terminated_python_name()?; let doc = property_type.doc(); - let mut holders = Vec::new(); + let mut holders = Holders::new(); let body = match property_type { PropertyType::Descriptor { field_index, field, .. @@ -639,7 +758,7 @@ pub fn impl_py_getter_def( mutable: false, span: Span::call_site(), } - .receiver(cls, ExtractErrorMode::Raise, &mut holders); + .receiver(cls, ExtractErrorMode::Raise, &mut holders, ctx); let field_token = if let Some(ident) = &field.ident { // named struct field ident.to_token_stream() @@ -647,17 +766,23 @@ pub fn impl_py_getter_def( // tuple struct field syn::Index::from(field_index).to_token_stream() }; - quotes::map_result_into_ptr(quotes::ok_wrap(quote! { - ::std::clone::Clone::clone(&(#slf.#field_token)) - })) + quotes::map_result_into_ptr( + quotes::ok_wrap( + quote! { + ::std::clone::Clone::clone(&(#slf.#field_token)) + }, + ctx, + ), + ctx, + ) } // Forward to `IntoPyCallbackOutput`, to handle `#[getter]`s returning results. PropertyType::Function { spec, self_type, .. } => { - let call = impl_call_getter(cls, spec, self_type, &mut holders)?; + let call = impl_call_getter(cls, spec, self_type, &mut holders, ctx)?; quote! { - _pyo3::callback::convert(py, #call) + #pyo3_path::callback::convert(py, #call) } } }; @@ -690,23 +815,27 @@ pub fn impl_py_getter_def( } } + let init_holders = holders.init_holders(ctx); + let check_gil_refs = holders.check_gil_refs(); let associated_method = quote! { #cfg_attrs unsafe fn #wrapper_ident( - py: _pyo3::Python<'_>, - _slf: *mut _pyo3::ffi::PyObject - ) -> _pyo3::PyResult<*mut _pyo3::ffi::PyObject> { - #( #holders )* - #body + py: #pyo3_path::Python<'_>, + _slf: *mut #pyo3_path::ffi::PyObject + ) -> #pyo3_path::PyResult<*mut #pyo3_path::ffi::PyObject> { + #init_holders + let result = #body; + #check_gil_refs + result } }; let method_def = quote! { #cfg_attrs - _pyo3::class::PyMethodDefType::Getter( - _pyo3::class::PyGetterDef::new( + #pyo3_path::class::PyMethodDefType::Getter( + #pyo3_path::class::PyGetterDef::new( #python_name, - _pyo3::impl_::pymethods::PyGetter(#cls::#wrapper_ident), + #cls::#wrapper_ident, #doc ) ) @@ -719,9 +848,9 @@ pub fn impl_py_getter_def( } /// Split an argument of pyo3::Python from the front of the arg list, if present -fn split_off_python_arg<'a>(args: &'a [FnArg<'a>]) -> (Option<&FnArg<'_>>, &[FnArg<'_>]) { +fn split_off_python_arg<'a>(args: &'a [FnArg<'a>]) -> (Option<&PyArg<'_>>, &[FnArg<'_>]) { match args { - [py, args @ ..] if utils::is_python(py.ty) => (Some(py), args), + [FnArg::Py(py), args @ ..] => (Some(py), args), args => (None, args), } } @@ -784,7 +913,7 @@ pub const __REPR__: SlotDef = SlotDef::new("Py_tp_repr", "reprfunc"); const __HASH__: SlotDef = SlotDef::new("Py_tp_hash", "hashfunc") .ret_ty(Ty::PyHashT) .return_conversion(TokenGenerator( - || quote! { _pyo3::callback::HashCallbackOutput }, + |Ctx { pyo3_path }: &Ctx| quote! { #pyo3_path::callback::HashCallbackOutput }, )); pub const __RICHCMP__: SlotDef = SlotDef::new("Py_tp_richcompare", "richcmpfunc") .extract_error_mode(ExtractErrorMode::NotImplemented) @@ -794,16 +923,18 @@ const __GET__: SlotDef = SlotDef::new("Py_tp_descr_get", "descrgetfunc") const __ITER__: SlotDef = SlotDef::new("Py_tp_iter", "getiterfunc"); const __NEXT__: SlotDef = SlotDef::new("Py_tp_iternext", "iternextfunc") .return_specialized_conversion( - TokenGenerator(|| quote! { IterBaseKind, IterOptionKind, IterResultOptionKind }), - TokenGenerator(|| quote! { iter_tag }), + TokenGenerator(|_| quote! { IterBaseKind, IterOptionKind, IterResultOptionKind }), + TokenGenerator(|_| quote! { iter_tag }), ); const __AWAIT__: SlotDef = SlotDef::new("Py_am_await", "unaryfunc"); const __AITER__: SlotDef = SlotDef::new("Py_am_aiter", "unaryfunc"); const __ANEXT__: SlotDef = SlotDef::new("Py_am_anext", "unaryfunc").return_specialized_conversion( - TokenGenerator(|| quote! { AsyncIterBaseKind, AsyncIterOptionKind, AsyncIterResultOptionKind }), - TokenGenerator(|| quote! { async_iter_tag }), + TokenGenerator( + |_| quote! { AsyncIterBaseKind, AsyncIterOptionKind, AsyncIterResultOptionKind }, + ), + TokenGenerator(|_| quote! { async_iter_tag }), ); -const __LEN__: SlotDef = SlotDef::new("Py_mp_length", "lenfunc").ret_ty(Ty::PySsizeT); +pub const __LEN__: SlotDef = SlotDef::new("Py_mp_length", "lenfunc").ret_ty(Ty::PySsizeT); const __CONTAINS__: SlotDef = SlotDef::new("Py_sq_contains", "objobjproc") .arguments(&[Ty::Object]) .ret_ty(Ty::Int); @@ -813,7 +944,8 @@ const __INPLACE_CONCAT__: SlotDef = SlotDef::new("Py_sq_concat", "binaryfunc").arguments(&[Ty::Object]); const __INPLACE_REPEAT__: SlotDef = SlotDef::new("Py_sq_repeat", "ssizeargfunc").arguments(&[Ty::PySsizeT]); -const __GETITEM__: SlotDef = SlotDef::new("Py_mp_subscript", "binaryfunc").arguments(&[Ty::Object]); +pub const __GETITEM__: SlotDef = + SlotDef::new("Py_mp_subscript", "binaryfunc").arguments(&[Ty::Object]); const __POS__: SlotDef = SlotDef::new("Py_nb_positive", "unaryfunc"); const __NEG__: SlotDef = SlotDef::new("Py_nb_negative", "unaryfunc"); @@ -903,16 +1035,17 @@ enum Ty { } impl Ty { - fn ffi_type(self) -> TokenStream { + fn ffi_type(self, ctx: &Ctx) -> TokenStream { + let Ctx { pyo3_path } = ctx; match self { - Ty::Object | Ty::MaybeNullObject => quote! { *mut _pyo3::ffi::PyObject }, - Ty::NonNullObject => quote! { ::std::ptr::NonNull<_pyo3::ffi::PyObject> }, - Ty::IPowModulo => quote! { _pyo3::impl_::pymethods::IPowModulo }, + Ty::Object | Ty::MaybeNullObject => quote! { *mut #pyo3_path::ffi::PyObject }, + Ty::NonNullObject => quote! { ::std::ptr::NonNull<#pyo3_path::ffi::PyObject> }, + Ty::IPowModulo => quote! { #pyo3_path::impl_::pymethods::IPowModulo }, Ty::Int | Ty::CompareOp => quote! { ::std::os::raw::c_int }, - Ty::PyHashT => quote! { _pyo3::ffi::Py_hash_t }, - Ty::PySsizeT => quote! { _pyo3::ffi::Py_ssize_t }, + Ty::PyHashT => quote! { #pyo3_path::ffi::Py_hash_t }, + Ty::PySsizeT => quote! { #pyo3_path::ffi::Py_ssize_t }, Ty::Void => quote! { () }, - Ty::PyBuffer => quote! { *mut _pyo3::ffi::Py_buffer }, + Ty::PyBuffer => quote! { *mut #pyo3_path::ffi::Py_buffer }, } } @@ -921,60 +1054,59 @@ impl Ty { ident: &syn::Ident, arg: &FnArg<'_>, extract_error_mode: ExtractErrorMode, - holders: &mut Vec, + holders: &mut Holders, + ctx: &Ctx, ) -> TokenStream { - let name_str = arg.name.unraw().to_string(); + let Ctx { pyo3_path } = ctx; match self { Ty::Object => extract_object( extract_error_mode, holders, - &name_str, - quote! { - py.from_borrowed_ptr::<_pyo3::PyAny>(#ident) - }, + arg, + quote! { #ident }, + ctx ), Ty::MaybeNullObject => extract_object( extract_error_mode, holders, - &name_str, + arg, quote! { - py.from_borrowed_ptr::<_pyo3::PyAny>( - if #ident.is_null() { - _pyo3::ffi::Py_None() - } else { - #ident - } - ) + if #ident.is_null() { + #pyo3_path::ffi::Py_None() + } else { + #ident + } }, + ctx ), Ty::NonNullObject => extract_object( extract_error_mode, holders, - &name_str, - quote! { - py.from_borrowed_ptr::<_pyo3::PyAny>(#ident.as_ptr()) - }, + arg, + quote! { #ident.as_ptr() }, + ctx ), Ty::IPowModulo => extract_object( extract_error_mode, holders, - &name_str, - quote! { - #ident.to_borrowed_any(py) - }, + arg, + quote! { #ident.as_ptr() }, + ctx ), Ty::CompareOp => extract_error_mode.handle_error( quote! { - _pyo3::class::basic::CompareOp::from_raw(#ident) - .ok_or_else(|| _pyo3::exceptions::PyValueError::new_err("invalid comparison operator")) + #pyo3_path::class::basic::CompareOp::from_raw(#ident) + .ok_or_else(|| #pyo3_path::exceptions::PyValueError::new_err("invalid comparison operator")) }, + ctx ), Ty::PySsizeT => { - let ty = arg.ty; + let ty = arg.ty(); extract_error_mode.handle_error( quote! { - ::std::convert::TryInto::<#ty>::try_into(#ident).map_err(|e| _pyo3::exceptions::PyValueError::new_err(e.to_string())) + ::std::convert::TryInto::<#ty>::try_into(#ident).map_err(|e| #pyo3_path::exceptions::PyValueError::new_err(e.to_string())) }, + ctx ) } // Just pass other types through unmodified @@ -985,22 +1117,41 @@ impl Ty { fn extract_object( extract_error_mode: ExtractErrorMode, - holders: &mut Vec, - name: &str, - source: TokenStream, + holders: &mut Holders, + arg: &FnArg<'_>, + source_ptr: TokenStream, + ctx: &Ctx, ) -> TokenStream { - let holder = syn::Ident::new(&format!("holder_{}", holders.len()), Span::call_site()); - holders.push(quote! { - #[allow(clippy::let_unit_value)] - let mut #holder = _pyo3::impl_::extract_argument::FunctionArgumentHolder::INIT; - }); - extract_error_mode.handle_error(quote! { - _pyo3::impl_::extract_argument::extract_argument( - #source, - &mut #holder, - #name - ) - }) + let Ctx { pyo3_path } = ctx; + let gil_refs_checker = holders.push_gil_refs_checker(arg.ty().span()); + let name = arg.name().unraw().to_string(); + + let extract = if let Some(from_py_with) = + arg.from_py_with().map(|from_py_with| &from_py_with.value) + { + let from_py_with_checker = holders.push_from_py_with_checker(from_py_with.span()); + quote! { + #pyo3_path::impl_::extract_argument::from_py_with( + #pyo3_path::impl_::pymethods::BoundRef::ref_from_ptr(py, &#source_ptr).0, + #name, + #pyo3_path::impl_::deprecations::inspect_fn(#from_py_with, &#from_py_with_checker) as fn(_) -> _, + ) + } + } else { + let holder = holders.push_holder(Span::call_site()); + quote! { + #pyo3_path::impl_::extract_argument::extract_argument( + #pyo3_path::impl_::pymethods::BoundRef::ref_from_ptr(py, &#source_ptr).0, + &mut #holder, + #name + ) + } + }; + + let extracted = extract_error_mode.handle_error(extract, ctx); + quote! { + #pyo3_path::impl_::deprecations::inspect_type(#extracted, &#gil_refs_checker) + } } enum ReturnMode { @@ -1010,21 +1161,33 @@ enum ReturnMode { } impl ReturnMode { - fn return_call_output(&self, call: TokenStream) -> TokenStream { + fn return_call_output(&self, call: TokenStream, ctx: &Ctx, holders: &Holders) -> TokenStream { + let Ctx { pyo3_path } = ctx; + let check_gil_refs = holders.check_gil_refs(); match self { - ReturnMode::Conversion(conversion) => quote! { - let _result: _pyo3::PyResult<#conversion> = _pyo3::callback::convert(py, #call); - _pyo3::callback::convert(py, _result) - }, - ReturnMode::SpecializedConversion(traits, tag) => quote! { - let _result = #call; - use _pyo3::impl_::pymethods::{#traits}; - (&_result).#tag().convert(py, _result) - }, + ReturnMode::Conversion(conversion) => { + let conversion = TokenGeneratorCtx(*conversion, ctx); + quote! { + let _result: #pyo3_path::PyResult<#conversion> = #pyo3_path::callback::convert(py, #call); + #check_gil_refs + #pyo3_path::callback::convert(py, _result) + } + } + ReturnMode::SpecializedConversion(traits, tag) => { + let traits = TokenGeneratorCtx(*traits, ctx); + let tag = TokenGeneratorCtx(*tag, ctx); + quote! { + let _result = #call; + use #pyo3_path::impl_::pymethods::{#traits}; + #check_gil_refs + (&_result).#tag().convert(py, _result) + } + } ReturnMode::ReturnSelf => quote! { - let _result: _pyo3::PyResult<()> = _pyo3::callback::convert(py, #call); + let _result: #pyo3_path::PyResult<()> = #pyo3_path::callback::convert(py, #call); _result?; - _pyo3::ffi::Py_XINCREF(_raw_slf); + #check_gil_refs + #pyo3_path::ffi::Py_XINCREF(_raw_slf); ::std::result::Result::Ok(_raw_slf) }, } @@ -1100,7 +1263,9 @@ impl SlotDef { cls: &syn::Type, spec: &FnSpec<'_>, method_name: &str, + ctx: &Ctx, ) -> Result { + let Ctx { pyo3_path } = ctx; let SlotDef { slot, func_ty, @@ -1116,13 +1281,13 @@ impl SlotDef { spec.name.span() => format!("`{}` must be `unsafe fn`", method_name) ); } - let arg_types: &Vec<_> = &arguments.iter().map(|arg| arg.ffi_type()).collect(); + let arg_types: &Vec<_> = &arguments.iter().map(|arg| arg.ffi_type(ctx)).collect(); let arg_idents: &Vec<_> = &(0..arguments.len()) .map(|i| format_ident!("arg{}", i)) .collect(); let wrapper_ident = format_ident!("__pymethod_{}__", method_name); - let ret_ty = ret_ty.ffi_type(); - let mut holders = Vec::new(); + let ret_ty = ret_ty.ffi_type(ctx); + let mut holders = Holders::new(); let body = generate_method_body( cls, spec, @@ -1130,36 +1295,38 @@ impl SlotDef { *extract_error_mode, &mut holders, return_mode.as_ref(), + ctx, )?; let name = spec.name; + let holders = holders.init_holders(ctx); let associated_method = quote! { unsafe fn #wrapper_ident( - py: _pyo3::Python<'_>, - _raw_slf: *mut _pyo3::ffi::PyObject, + py: #pyo3_path::Python<'_>, + _raw_slf: *mut #pyo3_path::ffi::PyObject, #(#arg_idents: #arg_types),* - ) -> _pyo3::PyResult<#ret_ty> { + ) -> #pyo3_path::PyResult<#ret_ty> { let function = #cls::#name; // Shadow the method name to avoid #3017 let _slf = _raw_slf; - #( #holders )* + #holders #body } }; let slot_def = quote! {{ unsafe extern "C" fn trampoline( - _slf: *mut _pyo3::ffi::PyObject, + _slf: *mut #pyo3_path::ffi::PyObject, #(#arg_idents: #arg_types),* ) -> #ret_ty { - _pyo3::impl_::trampoline:: #func_ty ( + #pyo3_path::impl_::trampoline:: #func_ty ( _slf, #(#arg_idents,)* #cls::#wrapper_ident ) } - _pyo3::ffi::PyType_Slot { - slot: _pyo3::ffi::#slot, - pfunc: trampoline as _pyo3::ffi::#func_ty as _ + #pyo3_path::ffi::PyType_Slot { + slot: #pyo3_path::ffi::#slot, + pfunc: trampoline as #pyo3_path::ffi::#func_ty as _ } }}; Ok(MethodAndSlotDef { @@ -1174,17 +1341,26 @@ fn generate_method_body( spec: &FnSpec<'_>, arguments: &[Ty], extract_error_mode: ExtractErrorMode, - holders: &mut Vec, + holders: &mut Holders, return_mode: Option<&ReturnMode>, + ctx: &Ctx, ) -> Result { - let self_arg = spec.tp.self_arg(Some(cls), extract_error_mode, holders); + let Ctx { pyo3_path } = ctx; + let self_arg = spec + .tp + .self_arg(Some(cls), extract_error_mode, holders, ctx); let rust_name = spec.name; - let args = extract_proto_arguments(spec, arguments, extract_error_mode, holders)?; + let args = extract_proto_arguments(spec, arguments, extract_error_mode, holders, ctx)?; let call = quote! { #cls::#rust_name(#self_arg #(#args),*) }; Ok(if let Some(return_mode) = return_mode { - return_mode.return_call_output(call) + return_mode.return_call_output(call, ctx, holders) } else { - quote! { _pyo3::callback::convert(py, #call) } + let check_gil_refs = holders.check_gil_refs(); + quote! { + let result = #call; + #check_gil_refs; + #pyo3_path::callback::convert(py, result) + } }) } @@ -1215,7 +1391,13 @@ impl SlotFragmentDef { self } - fn generate_pyproto_fragment(&self, cls: &syn::Type, spec: &FnSpec<'_>) -> Result { + fn generate_pyproto_fragment( + &self, + cls: &syn::Type, + spec: &FnSpec<'_>, + ctx: &Ctx, + ) -> Result { + let Ctx { pyo3_path } = ctx; let SlotFragmentDef { fragment, arguments, @@ -1225,11 +1407,11 @@ impl SlotFragmentDef { let fragment_trait = format_ident!("PyClass{}SlotFragment", fragment); let method = syn::Ident::new(fragment, Span::call_site()); let wrapper_ident = format_ident!("__pymethod_{}__", fragment); - let arg_types: &Vec<_> = &arguments.iter().map(|arg| arg.ffi_type()).collect(); + let arg_types: &Vec<_> = &arguments.iter().map(|arg| arg.ffi_type(ctx)).collect(); let arg_idents: &Vec<_> = &(0..arguments.len()) .map(|i| format_ident!("arg{}", i)) .collect(); - let mut holders = Vec::new(); + let mut holders = Holders::new(); let body = generate_method_body( cls, spec, @@ -1237,29 +1419,32 @@ impl SlotFragmentDef { *extract_error_mode, &mut holders, None, + ctx, )?; - let ret_ty = ret_ty.ffi_type(); + let ret_ty = ret_ty.ffi_type(ctx); + let holders = holders.init_holders(ctx); Ok(quote! { - impl _pyo3::impl_::pyclass::#fragment_trait<#cls> for _pyo3::impl_::pyclass::PyClassImplCollector<#cls> { + impl #cls { + unsafe fn #wrapper_ident( + py: #pyo3_path::Python, + _raw_slf: *mut #pyo3_path::ffi::PyObject, + #(#arg_idents: #arg_types),* + ) -> #pyo3_path::PyResult<#ret_ty> { + let _slf = _raw_slf; + #holders + #body + } + } + + impl #pyo3_path::impl_::pyclass::#fragment_trait<#cls> for #pyo3_path::impl_::pyclass::PyClassImplCollector<#cls> { #[inline] unsafe fn #method( self, - py: _pyo3::Python, - _raw_slf: *mut _pyo3::ffi::PyObject, + py: #pyo3_path::Python, + _raw_slf: *mut #pyo3_path::ffi::PyObject, #(#arg_idents: #arg_types),* - ) -> _pyo3::PyResult<#ret_ty> { - impl #cls { - unsafe fn #wrapper_ident( - py: _pyo3::Python, - _raw_slf: *mut _pyo3::ffi::PyObject, - #(#arg_idents: #arg_types),* - ) -> _pyo3::PyResult<#ret_ty> { - let _slf = _raw_slf; - #( #holders )* - #body - } - } + ) -> #pyo3_path::PyResult<#ret_ty> { #cls::#wrapper_ident(py, _raw_slf, #(#arg_idents),*) } } @@ -1345,19 +1530,20 @@ fn extract_proto_arguments( spec: &FnSpec<'_>, proto_args: &[Ty], extract_error_mode: ExtractErrorMode, - holders: &mut Vec, + holders: &mut Holders, + ctx: &Ctx, ) -> Result> { let mut args = Vec::with_capacity(spec.signature.arguments.len()); let mut non_python_args = 0; for arg in &spec.signature.arguments { - if arg.py { + if let FnArg::Py(..) = arg { args.push(quote! { py }); } else { let ident = syn::Ident::new(&format!("arg{}", non_python_args), Span::call_site()); let conversions = proto_args.get(non_python_args) - .ok_or_else(|| err_spanned!(arg.ty.span() => format!("Expected at most {} non-python arguments", proto_args.len())))? - .extract(&ident, arg, extract_error_mode, holders); + .ok_or_else(|| err_spanned!(arg.ty().span() => format!("Expected at most {} non-python arguments", proto_args.len())))? + .extract(&ident, arg, extract_error_mode, holders, ctx); non_python_args += 1; args.push(conversions); } @@ -1377,10 +1563,14 @@ impl ToTokens for StaticIdent { } } -struct TokenGenerator(fn() -> TokenStream); +#[derive(Clone, Copy)] +struct TokenGenerator(fn(&Ctx) -> TokenStream); + +struct TokenGeneratorCtx<'ctx>(TokenGenerator, &'ctx Ctx); -impl ToTokens for TokenGenerator { +impl ToTokens for TokenGeneratorCtx<'_> { fn to_tokens(&self, tokens: &mut TokenStream) { - self.0().to_tokens(tokens) + let Self(TokenGenerator(gen), ctx) = self; + (gen)(ctx).to_tokens(tokens) } } diff --git a/pyo3-macros-backend/src/quotes.rs b/pyo3-macros-backend/src/quotes.rs index 239036ef3ca..ceef23fb034 100644 --- a/pyo3-macros-backend/src/quotes.rs +++ b/pyo3-macros-backend/src/quotes.rs @@ -1,21 +1,23 @@ +use crate::utils::Ctx; use proc_macro2::TokenStream; use quote::quote; -pub(crate) fn some_wrap(obj: TokenStream) -> TokenStream { +pub(crate) fn some_wrap(obj: TokenStream, ctx: &Ctx) -> TokenStream { + let Ctx { pyo3_path } = ctx; quote! { - _pyo3::impl_::wrap::SomeWrap::wrap(#obj) + #pyo3_path::impl_::wrap::SomeWrap::wrap(#obj) } } -pub(crate) fn ok_wrap(obj: TokenStream) -> TokenStream { +pub(crate) fn ok_wrap(obj: TokenStream, ctx: &Ctx) -> TokenStream { + let Ctx { pyo3_path } = ctx; quote! { - _pyo3::impl_::wrap::OkWrap::wrap(#obj) - .map_err(::core::convert::Into::<_pyo3::PyErr>::into) + #pyo3_path::impl_::wrap::OkWrap::wrap(#obj) + .map_err(::core::convert::Into::<#pyo3_path::PyErr>::into) } } -pub(crate) fn map_result_into_ptr(result: TokenStream) -> TokenStream { - quote! { - _pyo3::impl_::wrap::map_result_into_ptr(py, #result) - } +pub(crate) fn map_result_into_ptr(result: TokenStream, ctx: &Ctx) -> TokenStream { + let Ctx { pyo3_path } = ctx; + quote! { #pyo3_path::impl_::wrap::map_result_into_ptr(py, #result) } } diff --git a/pyo3-macros-backend/src/utils.rs b/pyo3-macros-backend/src/utils.rs index 360b1ec2341..ca32abb42b3 100644 --- a/pyo3-macros-backend/src/utils.rs +++ b/pyo3-macros-backend/src/utils.rs @@ -144,11 +144,42 @@ pub fn unwrap_ty_group(mut ty: &syn::Type) -> &syn::Type { ty } -/// Extract the path to the pyo3 crate, or use the default (`::pyo3`). -pub(crate) fn get_pyo3_crate(attr: &Option) -> syn::Path { - attr.as_ref() - .map(|p| p.value.0.clone()) - .unwrap_or_else(|| syn::parse_str("::pyo3").unwrap()) +pub struct Ctx { + pub pyo3_path: PyO3CratePath, +} + +impl Ctx { + pub(crate) fn new(attr: &Option) -> Self { + let pyo3_path = match attr { + Some(attr) => PyO3CratePath::Given(attr.value.0.clone()), + None => PyO3CratePath::Default, + }; + + Self { pyo3_path } + } +} + +pub enum PyO3CratePath { + Given(syn::Path), + Default, +} + +impl PyO3CratePath { + pub fn to_tokens_spanned(&self, span: Span) -> TokenStream { + match self { + Self::Given(path) => quote::quote_spanned! { span => #path }, + Self::Default => quote::quote_spanned! { span => ::pyo3 }, + } + } +} + +impl quote::ToTokens for PyO3CratePath { + fn to_tokens(&self, tokens: &mut TokenStream) { + match self { + Self::Given(path) => path.to_tokens(tokens), + Self::Default => quote::quote! { ::pyo3 }.to_tokens(tokens), + } + } } pub fn apply_renaming_rule(rule: RenamingRule, name: &str) -> String { @@ -165,3 +196,7 @@ pub fn apply_renaming_rule(rule: RenamingRule, name: &str) -> String { RenamingRule::Uppercase => name.to_uppercase(), } } + +pub(crate) fn is_abi3() -> bool { + pyo3_build_config::get().abi3 +} diff --git a/pyo3-macros/Cargo.toml b/pyo3-macros/Cargo.toml index f34d483dd04..e4b550cfb8e 100644 --- a/pyo3-macros/Cargo.toml +++ b/pyo3-macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3-macros" -version = "0.21.0-dev" +version = "0.22.0-dev" description = "Proc macros for PyO3 package" authors = ["PyO3 Project and Contributors "] keywords = ["pyo3", "python", "cpython", "ffi"] @@ -15,14 +15,15 @@ proc-macro = true [features] multiple-pymethods = [] - -abi3 = ["pyo3-macros-backend/abi3"] +experimental-async = ["pyo3-macros-backend/experimental-async"] +experimental-declarative-modules = [] +gil-refs = ["pyo3-macros-backend/gil-refs"] [dependencies] proc-macro2 = { version = "1", default-features = false } quote = "1" syn = { version = "2", features = ["full", "extra-traits"] } -pyo3-macros-backend = { path = "../pyo3-macros-backend", version = "=0.21.0-dev" } +pyo3-macros-backend = { path = "../pyo3-macros-backend", version = "=0.22.0-dev" } [lints] workspace = true diff --git a/pyo3-macros/src/lib.rs b/pyo3-macros/src/lib.rs index d00ede89143..64756a1c73b 100644 --- a/pyo3-macros/src/lib.rs +++ b/pyo3-macros/src/lib.rs @@ -6,11 +6,11 @@ use proc_macro::TokenStream; use proc_macro2::TokenStream as TokenStream2; use pyo3_macros_backend::{ build_derive_from_pyobject, build_py_class, build_py_enum, build_py_function, build_py_methods, - get_doc, process_functions_in_module, pymodule_impl, PyClassArgs, PyClassMethodsType, - PyFunctionOptions, PyModuleOptions, + pymodule_function_impl, pymodule_module_impl, PyClassArgs, PyClassMethodsType, + PyFunctionOptions, }; use quote::quote; -use syn::{parse::Nothing, parse_macro_input}; +use syn::{parse::Nothing, parse_macro_input, Item}; /// A proc macro used to implement Python modules. /// @@ -36,31 +36,27 @@ use syn::{parse::Nothing, parse_macro_input}; #[proc_macro_attribute] pub fn pymodule(args: TokenStream, input: TokenStream) -> TokenStream { parse_macro_input!(args as Nothing); - - let mut ast = parse_macro_input!(input as syn::ItemFn); - let options = match PyModuleOptions::from_attrs(&mut ast.attrs) { - Ok(options) => options, - Err(e) => return e.into_compile_error().into(), - }; - - if let Err(err) = process_functions_in_module(&options, &mut ast) { - return err.into_compile_error().into(); + match parse_macro_input!(input as Item) { + Item::Mod(module) => if cfg!(feature = "experimental-declarative-modules") { + pymodule_module_impl(module) + } else { + Err(syn::Error::new_spanned( + module, + "#[pymodule] requires the 'experimental-declarative-modules' feature to be used on Rust modules.", + )) + }, + Item::Fn(function) => pymodule_function_impl(function), + unsupported => Err(syn::Error::new_spanned( + unsupported, + "#[pymodule] only supports modules and functions.", + )), } - - let doc = get_doc(&ast.attrs, None); - - let expanded = pymodule_impl(&ast.sig.ident, options, doc, &ast.vis); - - quote!( - #ast - #expanded - ) + .unwrap_or_compile_error() .into() } #[proc_macro_attribute] pub fn pyclass(attr: TokenStream, input: TokenStream) -> TokenStream { - use syn::Item; let item = parse_macro_input!(input as Item); match item { Item::Struct(struct_) => pyclass_impl(attr, struct_, methods_type()), diff --git a/pyproject.toml b/pyproject.toml index 866645d2ffc..a007ee6dc7a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ [tool.towncrier] filename = "CHANGELOG.md" -version = "0.21.0-dev" +version = "0.22.0-dev" start_string = "\n" template = ".towncrier.template.md" title_format = "## [{version}] - {project_date}" diff --git a/pytests/noxfile.py b/pytests/noxfile.py index 57d9d63a044..af2eb0d3a75 100644 --- a/pytests/noxfile.py +++ b/pytests/noxfile.py @@ -1,4 +1,5 @@ import nox +import sys from nox.command import CommandFailed nox.options.sessions = ["test"] @@ -8,12 +9,22 @@ def test(session: nox.Session): session.env["MATURIN_PEP517_ARGS"] = "--profile=dev" session.run_always("python", "-m", "pip", "install", "-v", ".[dev]") - try: - session.install("--only-binary=numpy", "numpy>=1.16") - except CommandFailed: - # No binary wheel for numpy available on this platform - pass - session.run("pytest", *session.posargs) + + def try_install_binary(package: str, constraint: str): + try: + session.install(f"--only-binary={package}", f"{package}{constraint}") + except CommandFailed: + # No binary wheel available on this platform + pass + + try_install_binary("numpy", ">=1.16") + try_install_binary("gevent", ">=22.10.2") + ignored_paths = [] + if sys.version_info < (3, 10): + # Match syntax is only available in Python >= 3.10 + ignored_paths.append("tests/test_enums_match.py") + ignore_args = [f"--ignore={path}" for path in ignored_paths] + session.run("pytest", *ignore_args, *session.posargs) @nox.session diff --git a/pytests/pyproject.toml b/pytests/pyproject.toml index fb1ac3dbdff..5f78a573124 100644 --- a/pytests/pyproject.toml +++ b/pytests/pyproject.toml @@ -23,6 +23,7 @@ dev = [ "hypothesis>=3.55", "pytest-asyncio>=0.21", "pytest-benchmark>=3.4", - "pytest>=6.0", + # pinned < 8.1 because https://github.com/CodSpeedHQ/pytest-codspeed/issues/27 + "pytest>=7,<8.1", "typing_extensions>=4.0.0" ] diff --git a/pytests/src/awaitable.rs b/pytests/src/awaitable.rs index 0cc173334b9..5e3b98e14ea 100644 --- a/pytests/src/awaitable.rs +++ b/pytests/src/awaitable.rs @@ -37,7 +37,7 @@ impl IterAwaitable { Ok(v) => Err(PyStopIteration::new_err(v)), Err(err) => Err(err), }, - _ => Ok(py.None().into()), + _ => Ok(py.None()), } } } @@ -79,7 +79,7 @@ impl FutureAwaitable { } #[pymodule] -pub fn awaitable(_py: Python<'_>, m: &PyModule) -> PyResult<()> { +pub fn awaitable(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; Ok(()) diff --git a/pytests/src/buf_and_str.rs b/pytests/src/buf_and_str.rs index 7cddc686c03..879d76af883 100644 --- a/pytests/src/buf_and_str.rs +++ b/pytests/src/buf_and_str.rs @@ -17,39 +17,38 @@ impl BytesExtractor { } #[staticmethod] - pub fn from_bytes(bytes: &PyBytes) -> PyResult { + pub fn from_bytes(bytes: &Bound<'_, PyBytes>) -> PyResult { let byte_vec: Vec = bytes.extract()?; Ok(byte_vec.len()) } #[staticmethod] - pub fn from_str(string: &PyString) -> PyResult { + pub fn from_str(string: &Bound<'_, PyString>) -> PyResult { let rust_string: String = string.extract()?; Ok(rust_string.len()) } #[staticmethod] - pub fn from_str_lossy(string: &PyString) -> usize { + pub fn from_str_lossy(string: &Bound<'_, PyString>) -> usize { let rust_string_lossy: String = string.to_string_lossy().to_string(); rust_string_lossy.len() } #[staticmethod] - pub fn from_buffer(buf: &PyAny) -> PyResult { - let buf = PyBuffer::::get(buf)?; + pub fn from_buffer(buf: &Bound<'_, PyAny>) -> PyResult { + let buf = PyBuffer::::get_bound(buf)?; Ok(buf.item_count()) } } #[pyfunction] -fn return_memoryview(py: Python<'_>) -> PyResult<&PyMemoryView> { - let bytes: &PyAny = PyBytes::new(py, b"hello world").into(); - let memoryview = TryInto::try_into(bytes)?; - Ok(memoryview) +fn return_memoryview(py: Python<'_>) -> PyResult> { + let bytes = PyBytes::new_bound(py, b"hello world"); + PyMemoryView::from_bound(&bytes) } #[pymodule] -pub fn buf_and_str(_py: Python<'_>, m: &PyModule) -> PyResult<()> { +pub fn buf_and_str(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_function(wrap_pyfunction!(return_memoryview, m)?)?; Ok(()) diff --git a/pytests/src/comparisons.rs b/pytests/src/comparisons.rs index d8c2f5a6a52..fa35acf8e1a 100644 --- a/pytests/src/comparisons.rs +++ b/pytests/src/comparisons.rs @@ -1,5 +1,4 @@ use pyo3::prelude::*; -use pyo3::{types::PyModule, Python}; #[pyclass] struct Eq(i64); @@ -102,7 +101,7 @@ impl OrderedDefaultNe { } #[pymodule] -pub fn comparisons(_py: Python<'_>, m: &PyModule) -> PyResult<()> { +pub fn comparisons(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; diff --git a/pytests/src/datetime.rs b/pytests/src/datetime.rs index 7f0492da75e..e26782d04f7 100644 --- a/pytests/src/datetime.rs +++ b/pytests/src/datetime.rs @@ -7,50 +7,54 @@ use pyo3::types::{ }; #[pyfunction] -fn make_date(py: Python<'_>, year: i32, month: u8, day: u8) -> PyResult<&PyDate> { - PyDate::new(py, year, month, day) +fn make_date(py: Python<'_>, year: i32, month: u8, day: u8) -> PyResult> { + PyDate::new_bound(py, year, month, day) } #[pyfunction] -fn get_date_tuple<'p>(py: Python<'p>, d: &PyDate) -> &'p PyTuple { - PyTuple::new(py, [d.get_year(), d.get_month() as i32, d.get_day() as i32]) +fn get_date_tuple<'py>(d: &Bound<'py, PyDate>) -> Bound<'py, PyTuple> { + PyTuple::new_bound( + d.py(), + [d.get_year(), d.get_month() as i32, d.get_day() as i32], + ) } #[pyfunction] -fn date_from_timestamp(py: Python<'_>, timestamp: i64) -> PyResult<&PyDate> { - PyDate::from_timestamp(py, timestamp) +fn date_from_timestamp(py: Python<'_>, timestamp: i64) -> PyResult> { + PyDate::from_timestamp_bound(py, timestamp) } #[pyfunction] -fn make_time<'p>( - py: Python<'p>, +#[pyo3(signature=(hour, minute, second, microsecond, tzinfo=None))] +fn make_time<'py>( + py: Python<'py>, hour: u8, minute: u8, second: u8, microsecond: u32, - tzinfo: Option<&PyTzInfo>, -) -> PyResult<&'p PyTime> { - PyTime::new(py, hour, minute, second, microsecond, tzinfo) + tzinfo: Option<&Bound<'py, PyTzInfo>>, +) -> PyResult> { + PyTime::new_bound(py, hour, minute, second, microsecond, tzinfo) } #[pyfunction] #[pyo3(signature = (hour, minute, second, microsecond, tzinfo, fold))] -fn time_with_fold<'p>( - py: Python<'p>, +fn time_with_fold<'py>( + py: Python<'py>, hour: u8, minute: u8, second: u8, microsecond: u32, - tzinfo: Option<&PyTzInfo>, + tzinfo: Option<&Bound<'py, PyTzInfo>>, fold: bool, -) -> PyResult<&'p PyTime> { - PyTime::new_with_fold(py, hour, minute, second, microsecond, tzinfo, fold) +) -> PyResult> { + PyTime::new_bound_with_fold(py, hour, minute, second, microsecond, tzinfo, fold) } #[pyfunction] -fn get_time_tuple<'p>(py: Python<'p>, dt: &PyTime) -> &'p PyTuple { - PyTuple::new( - py, +fn get_time_tuple<'py>(dt: &Bound<'py, PyTime>) -> Bound<'py, PyTuple> { + PyTuple::new_bound( + dt.py(), [ dt.get_hour() as u32, dt.get_minute() as u32, @@ -61,9 +65,9 @@ fn get_time_tuple<'p>(py: Python<'p>, dt: &PyTime) -> &'p PyTuple { } #[pyfunction] -fn get_time_tuple_fold<'p>(py: Python<'p>, dt: &PyTime) -> &'p PyTuple { - PyTuple::new( - py, +fn get_time_tuple_fold<'py>(dt: &Bound<'py, PyTime>) -> Bound<'py, PyTuple> { + PyTuple::new_bound( + dt.py(), [ dt.get_hour() as u32, dt.get_minute() as u32, @@ -75,14 +79,19 @@ fn get_time_tuple_fold<'p>(py: Python<'p>, dt: &PyTime) -> &'p PyTuple { } #[pyfunction] -fn make_delta(py: Python<'_>, days: i32, seconds: i32, microseconds: i32) -> PyResult<&PyDelta> { - PyDelta::new(py, days, seconds, microseconds, true) +fn make_delta( + py: Python<'_>, + days: i32, + seconds: i32, + microseconds: i32, +) -> PyResult> { + PyDelta::new_bound(py, days, seconds, microseconds, true) } #[pyfunction] -fn get_delta_tuple<'p>(py: Python<'p>, delta: &PyDelta) -> &'p PyTuple { - PyTuple::new( - py, +fn get_delta_tuple<'py>(delta: &Bound<'py, PyDelta>) -> Bound<'py, PyTuple> { + PyTuple::new_bound( + delta.py(), [ delta.get_days(), delta.get_seconds(), @@ -93,8 +102,9 @@ fn get_delta_tuple<'p>(py: Python<'p>, delta: &PyDelta) -> &'p PyTuple { #[allow(clippy::too_many_arguments)] #[pyfunction] -fn make_datetime<'p>( - py: Python<'p>, +#[pyo3(signature=(year, month, day, hour, minute, second, microsecond, tzinfo=None))] +fn make_datetime<'py>( + py: Python<'py>, year: i32, month: u8, day: u8, @@ -102,9 +112,9 @@ fn make_datetime<'p>( minute: u8, second: u8, microsecond: u32, - tzinfo: Option<&PyTzInfo>, -) -> PyResult<&'p PyDateTime> { - PyDateTime::new( + tzinfo: Option<&Bound<'py, PyTzInfo>>, +) -> PyResult> { + PyDateTime::new_bound( py, year, month, @@ -118,9 +128,9 @@ fn make_datetime<'p>( } #[pyfunction] -fn get_datetime_tuple<'p>(py: Python<'p>, dt: &PyDateTime) -> &'p PyTuple { - PyTuple::new( - py, +fn get_datetime_tuple<'py>(dt: &Bound<'py, PyDateTime>) -> Bound<'py, PyTuple> { + PyTuple::new_bound( + dt.py(), [ dt.get_year(), dt.get_month() as i32, @@ -134,9 +144,9 @@ fn get_datetime_tuple<'p>(py: Python<'p>, dt: &PyDateTime) -> &'p PyTuple { } #[pyfunction] -fn get_datetime_tuple_fold<'p>(py: Python<'p>, dt: &PyDateTime) -> &'p PyTuple { - PyTuple::new( - py, +fn get_datetime_tuple_fold<'py>(dt: &Bound<'py, PyDateTime>) -> Bound<'py, PyTuple> { + PyTuple::new_bound( + dt.py(), [ dt.get_year(), dt.get_month() as i32, @@ -151,22 +161,23 @@ fn get_datetime_tuple_fold<'p>(py: Python<'p>, dt: &PyDateTime) -> &'p PyTuple { } #[pyfunction] -fn datetime_from_timestamp<'p>( - py: Python<'p>, +#[pyo3(signature=(ts, tz=None))] +fn datetime_from_timestamp<'py>( + py: Python<'py>, ts: f64, - tz: Option<&PyTzInfo>, -) -> PyResult<&'p PyDateTime> { - PyDateTime::from_timestamp(py, ts, tz) + tz: Option<&Bound<'py, PyTzInfo>>, +) -> PyResult> { + PyDateTime::from_timestamp_bound(py, ts, tz) } #[pyfunction] -fn get_datetime_tzinfo(dt: &PyDateTime) -> Option> { - dt.get_tzinfo() +fn get_datetime_tzinfo<'py>(dt: &Bound<'py, PyDateTime>) -> Option> { + dt.get_tzinfo_bound() } #[pyfunction] -fn get_time_tzinfo(dt: &PyTime) -> Option> { - dt.get_tzinfo() +fn get_time_tzinfo<'py>(dt: &Bound<'py, PyTime>) -> Option> { + dt.get_tzinfo_bound() } #[pyclass(extends=PyTzInfo)] @@ -179,21 +190,21 @@ impl TzClass { TzClass {} } - fn utcoffset<'p>(&self, py: Python<'p>, _dt: &PyDateTime) -> PyResult<&'p PyDelta> { - PyDelta::new(py, 0, 3600, 0, true) + fn utcoffset<'py>(&self, dt: &Bound<'py, PyDateTime>) -> PyResult> { + PyDelta::new_bound(dt.py(), 0, 3600, 0, true) } - fn tzname(&self, _py: Python<'_>, _dt: &PyDateTime) -> String { + fn tzname(&self, _dt: &Bound<'_, PyDateTime>) -> String { String::from("+01:00") } - fn dst(&self, _py: Python<'_>, _dt: &PyDateTime) -> Option<&PyDelta> { + fn dst<'py>(&self, _dt: &Bound<'py, PyDateTime>) -> Option> { None } } #[pymodule] -pub fn datetime(_py: Python<'_>, m: &PyModule) -> PyResult<()> { +pub fn datetime(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(make_date, m)?)?; m.add_function(wrap_pyfunction!(get_date_tuple, m)?)?; m.add_function(wrap_pyfunction!(date_from_timestamp, m)?)?; diff --git a/pytests/src/dict_iter.rs b/pytests/src/dict_iter.rs index 5f5992b6efc..c312fbb5f83 100644 --- a/pytests/src/dict_iter.rs +++ b/pytests/src/dict_iter.rs @@ -3,7 +3,7 @@ use pyo3::prelude::*; use pyo3::types::PyDict; #[pymodule] -pub fn dict_iter(_py: Python<'_>, m: &PyModule) -> PyResult<()> { +pub fn dict_iter(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; Ok(()) } @@ -20,7 +20,7 @@ impl DictSize { DictSize { expected } } - fn iter_dict(&mut self, _py: Python<'_>, dict: &PyDict) -> PyResult { + fn iter_dict(&mut self, _py: Python<'_>, dict: &Bound<'_, PyDict>) -> PyResult { let mut seen = 0u32; for (sym, values) in dict { seen += 1; diff --git a/pytests/src/enums.rs b/pytests/src/enums.rs new file mode 100644 index 00000000000..964f0d431c3 --- /dev/null +++ b/pytests/src/enums.rs @@ -0,0 +1,123 @@ +use pyo3::{ + pyclass, pyfunction, pymodule, + types::{PyModule, PyModuleMethods}, + wrap_pyfunction_bound, Bound, PyResult, +}; + +#[pymodule] +pub fn enums(m: &Bound<'_, PyModule>) -> PyResult<()> { + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_wrapped(wrap_pyfunction_bound!(do_simple_stuff))?; + m.add_wrapped(wrap_pyfunction_bound!(do_complex_stuff))?; + m.add_wrapped(wrap_pyfunction_bound!(do_tuple_stuff))?; + m.add_wrapped(wrap_pyfunction_bound!(do_mixed_complex_stuff))?; + Ok(()) +} + +#[pyclass] +pub enum SimpleEnum { + Sunday, + Monday, + Tuesday, + Wednesday, + Thursday, + Friday, + Saturday, +} + +#[pyfunction] +pub fn do_simple_stuff(thing: &SimpleEnum) -> SimpleEnum { + match thing { + SimpleEnum::Sunday => SimpleEnum::Monday, + SimpleEnum::Monday => SimpleEnum::Tuesday, + SimpleEnum::Tuesday => SimpleEnum::Wednesday, + SimpleEnum::Wednesday => SimpleEnum::Thursday, + SimpleEnum::Thursday => SimpleEnum::Friday, + SimpleEnum::Friday => SimpleEnum::Saturday, + SimpleEnum::Saturday => SimpleEnum::Sunday, + } +} + +#[pyclass] +pub enum ComplexEnum { + Int { + i: i32, + }, + Float { + f: f64, + }, + Str { + s: String, + }, + EmptyStruct {}, + MultiFieldStruct { + a: i32, + b: f64, + c: bool, + }, + #[pyo3(constructor = (a = 42, b = None))] + VariantWithDefault { + a: i32, + b: Option, + }, +} + +#[pyfunction] +pub fn do_complex_stuff(thing: &ComplexEnum) -> ComplexEnum { + match thing { + ComplexEnum::Int { i } => ComplexEnum::Str { s: i.to_string() }, + ComplexEnum::Float { f } => ComplexEnum::Float { f: f * f }, + ComplexEnum::Str { s } => ComplexEnum::Int { i: s.len() as i32 }, + ComplexEnum::EmptyStruct {} => ComplexEnum::EmptyStruct {}, + ComplexEnum::MultiFieldStruct { a, b, c } => ComplexEnum::MultiFieldStruct { + a: *a, + b: *b, + c: *c, + }, + ComplexEnum::VariantWithDefault { a, b } => ComplexEnum::VariantWithDefault { + a: 2 * a, + b: b.as_ref().map(|s| s.to_uppercase()), + }, + } +} + +#[pyclass] +enum SimpleTupleEnum { + Int(i32), + Str(String), +} + +#[pyclass] +pub enum TupleEnum { + #[pyo3(constructor = (_0 = 1, _1 = 1.0, _2 = true))] + FullWithDefault(i32, f64, bool), + Full(i32, f64, bool), + EmptyTuple(), +} + +#[pyfunction] +pub fn do_tuple_stuff(thing: &TupleEnum) -> TupleEnum { + match thing { + TupleEnum::FullWithDefault(a, b, c) => TupleEnum::FullWithDefault(*a, *b, *c), + TupleEnum::Full(a, b, c) => TupleEnum::Full(*a, *b, *c), + TupleEnum::EmptyTuple() => TupleEnum::EmptyTuple(), + } +} + +#[pyclass] +pub enum MixedComplexEnum { + Nothing {}, + Empty(), +} + +#[pyfunction] +pub fn do_mixed_complex_stuff(thing: &MixedComplexEnum) -> MixedComplexEnum { + match thing { + MixedComplexEnum::Nothing {} => MixedComplexEnum::Empty(), + MixedComplexEnum::Empty() => MixedComplexEnum::Nothing {}, + } +} diff --git a/pytests/src/lib.rs b/pytests/src/lib.rs index dbcd3ca4113..cbd65c8012c 100644 --- a/pytests/src/lib.rs +++ b/pytests/src/lib.rs @@ -7,6 +7,7 @@ pub mod buf_and_str; pub mod comparisons; pub mod datetime; pub mod dict_iter; +pub mod enums; pub mod misc; pub mod objstore; pub mod othermod; @@ -17,7 +18,7 @@ pub mod sequence; pub mod subclassing; #[pymodule] -fn pyo3_pytests(py: Python<'_>, m: &PyModule) -> PyResult<()> { +fn pyo3_pytests(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_wrapped(wrap_pymodule!(awaitable::awaitable))?; #[cfg(not(Py_LIMITED_API))] m.add_wrapped(wrap_pymodule!(buf_and_str::buf_and_str))?; @@ -25,6 +26,7 @@ fn pyo3_pytests(py: Python<'_>, m: &PyModule) -> PyResult<()> { #[cfg(not(Py_LIMITED_API))] m.add_wrapped(wrap_pymodule!(datetime::datetime))?; m.add_wrapped(wrap_pymodule!(dict_iter::dict_iter))?; + m.add_wrapped(wrap_pymodule!(enums::enums))?; m.add_wrapped(wrap_pymodule!(misc::misc))?; m.add_wrapped(wrap_pymodule!(objstore::objstore))?; m.add_wrapped(wrap_pymodule!(othermod::othermod))?; @@ -37,13 +39,14 @@ fn pyo3_pytests(py: Python<'_>, m: &PyModule) -> PyResult<()> { // Inserting to sys.modules allows importing submodules nicely from Python // e.g. import pyo3_pytests.buf_and_str as bas - let sys = PyModule::import(py, "sys")?; - let sys_modules: &PyDict = sys.getattr("modules")?.downcast()?; + let sys = PyModule::import_bound(py, "sys")?; + let sys_modules = sys.getattr("modules")?.downcast_into::()?; sys_modules.set_item("pyo3_pytests.awaitable", m.getattr("awaitable")?)?; sys_modules.set_item("pyo3_pytests.buf_and_str", m.getattr("buf_and_str")?)?; sys_modules.set_item("pyo3_pytests.comparisons", m.getattr("comparisons")?)?; sys_modules.set_item("pyo3_pytests.datetime", m.getattr("datetime")?)?; sys_modules.set_item("pyo3_pytests.dict_iter", m.getattr("dict_iter")?)?; + sys_modules.set_item("pyo3_pytests.enums", m.getattr("enums")?)?; sys_modules.set_item("pyo3_pytests.misc", m.getattr("misc")?)?; sys_modules.set_item("pyo3_pytests.objstore", m.getattr("objstore")?)?; sys_modules.set_item("pyo3_pytests.othermod", m.getattr("othermod")?)?; diff --git a/pytests/src/misc.rs b/pytests/src/misc.rs index 029e8b16528..7704098bd5b 100644 --- a/pytests/src/misc.rs +++ b/pytests/src/misc.rs @@ -1,4 +1,4 @@ -use pyo3::prelude::*; +use pyo3::{prelude::*, types::PyDict}; use std::borrow::Cow; #[pyfunction] @@ -8,8 +8,8 @@ fn issue_219() { } #[pyfunction] -fn get_type_full_name(obj: &PyAny) -> PyResult> { - obj.get_type().name() +fn get_type_full_name(obj: &Bound<'_, PyAny>) -> PyResult { + obj.get_type().name().map(Cow::into_owned) } #[pyfunction] @@ -17,10 +17,24 @@ fn accepts_bool(val: bool) -> bool { val } +#[pyfunction] +fn get_item_and_run_callback(dict: Bound<'_, PyDict>, callback: Bound<'_, PyAny>) -> PyResult<()> { + // This function gives the opportunity to run a pure-Python callback so that + // gevent can instigate a context switch. This had problematic interactions + // with PyO3's removed "GIL Pool". + // For context, see https://github.com/PyO3/pyo3/issues/3668 + let item = dict.get_item("key")?.expect("key not found in dict"); + let string = item.to_string(); + callback.call0()?; + assert_eq!(item.to_string(), string); + Ok(()) +} + #[pymodule] -pub fn misc(_py: Python<'_>, m: &PyModule) -> PyResult<()> { +pub fn misc(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(issue_219, m)?)?; m.add_function(wrap_pyfunction!(get_type_full_name, m)?)?; m.add_function(wrap_pyfunction!(accepts_bool, m)?)?; + m.add_function(wrap_pyfunction!(get_item_and_run_callback, m)?)?; Ok(()) } diff --git a/pytests/src/objstore.rs b/pytests/src/objstore.rs index f7fc66edb84..9a005c0ec97 100644 --- a/pytests/src/objstore.rs +++ b/pytests/src/objstore.rs @@ -13,12 +13,12 @@ impl ObjStore { ObjStore::default() } - fn push(&mut self, py: Python<'_>, obj: &PyAny) { + fn push(&mut self, py: Python<'_>, obj: &Bound<'_, PyAny>) { self.obj.push(obj.to_object(py)); } } #[pymodule] -pub fn objstore(_py: Python<'_>, m: &PyModule) -> PyResult<()> { +pub fn objstore(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::() } diff --git a/pytests/src/othermod.rs b/pytests/src/othermod.rs index 763d38a878f..36ad4b5e23e 100644 --- a/pytests/src/othermod.rs +++ b/pytests/src/othermod.rs @@ -29,13 +29,13 @@ fn double(x: i32) -> i32 { } #[pymodule] -pub fn othermod(_py: Python<'_>, m: &PyModule) -> PyResult<()> { +pub fn othermod(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(double, m)?)?; m.add_class::()?; - m.add("USIZE_MIN", usize::min_value())?; - m.add("USIZE_MAX", usize::max_value())?; + m.add("USIZE_MIN", usize::MIN)?; + m.add("USIZE_MAX", usize::MAX)?; Ok(()) } diff --git a/pytests/src/path.rs b/pytests/src/path.rs index b3e8f92bacf..0675e56d13a 100644 --- a/pytests/src/path.rs +++ b/pytests/src/path.rs @@ -12,7 +12,7 @@ fn take_pathbuf(path: PathBuf) -> PathBuf { } #[pymodule] -pub fn path(_py: Python<'_>, m: &PyModule) -> PyResult<()> { +pub fn path(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(make_path, m)?)?; m.add_function(wrap_pyfunction!(take_pathbuf, m)?)?; diff --git a/pytests/src/pyclasses.rs b/pytests/src/pyclasses.rs index 326893d123d..6338596b481 100644 --- a/pytests/src/pyclasses.rs +++ b/pytests/src/pyclasses.rs @@ -11,6 +11,12 @@ impl EmptyClass { fn new() -> Self { EmptyClass {} } + + fn method(&self) {} + + fn __len__(&self) -> usize { + 0 + } } /// This is for demonstrating how to return a value from __next__ @@ -46,8 +52,8 @@ struct AssertingBaseClass; impl AssertingBaseClass { #[new] #[classmethod] - fn new(cls: &PyType, expected_type: &PyType) -> PyResult { - if !cls.is(expected_type) { + fn new(cls: &Bound<'_, PyType>, expected_type: Bound<'_, PyType>) -> PyResult { + if !cls.is(&expected_type) { return Err(PyValueError::new_err(format!( "{:?} != {:?}", cls, expected_type @@ -61,10 +67,11 @@ impl AssertingBaseClass { struct ClassWithoutConstructor; #[pymodule] -pub fn pyclasses(_py: Python<'_>, m: &PyModule) -> PyResult<()> { +pub fn pyclasses(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; m.add_class::()?; + Ok(()) } diff --git a/pytests/src/pyfunctions.rs b/pytests/src/pyfunctions.rs index 1eef970430e..77496198bb9 100644 --- a/pytests/src/pyfunctions.rs +++ b/pytests/src/pyfunctions.rs @@ -4,67 +4,71 @@ use pyo3::types::{PyDict, PyTuple}; #[pyfunction(signature = ())] fn none() {} +type Any<'py> = Bound<'py, PyAny>; +type Dict<'py> = Bound<'py, PyDict>; +type Tuple<'py> = Bound<'py, PyTuple>; + #[pyfunction(signature = (a, b = None, *, c = None))] -fn simple<'a>( - a: &'a PyAny, - b: Option<&'a PyAny>, - c: Option<&'a PyAny>, -) -> (&'a PyAny, Option<&'a PyAny>, Option<&'a PyAny>) { +fn simple<'py>( + a: Any<'py>, + b: Option>, + c: Option>, +) -> (Any<'py>, Option>, Option>) { (a, b, c) } #[pyfunction(signature = (a, b = None, *args, c = None))] -fn simple_args<'a>( - a: &'a PyAny, - b: Option<&'a PyAny>, - args: &'a PyTuple, - c: Option<&'a PyAny>, -) -> (&'a PyAny, Option<&'a PyAny>, &'a PyTuple, Option<&'a PyAny>) { +fn simple_args<'py>( + a: Any<'py>, + b: Option>, + args: Tuple<'py>, + c: Option>, +) -> (Any<'py>, Option>, Tuple<'py>, Option>) { (a, b, args, c) } #[pyfunction(signature = (a, b = None, c = None, **kwargs))] -fn simple_kwargs<'a>( - a: &'a PyAny, - b: Option<&'a PyAny>, - c: Option<&'a PyAny>, - kwargs: Option<&'a PyDict>, +fn simple_kwargs<'py>( + a: Any<'py>, + b: Option>, + c: Option>, + kwargs: Option>, ) -> ( - &'a PyAny, - Option<&'a PyAny>, - Option<&'a PyAny>, - Option<&'a PyDict>, + Any<'py>, + Option>, + Option>, + Option>, ) { (a, b, c, kwargs) } #[pyfunction(signature = (a, b = None, *args, c = None, **kwargs))] -fn simple_args_kwargs<'a>( - a: &'a PyAny, - b: Option<&'a PyAny>, - args: &'a PyTuple, - c: Option<&'a PyAny>, - kwargs: Option<&'a PyDict>, +fn simple_args_kwargs<'py>( + a: Any<'py>, + b: Option>, + args: Tuple<'py>, + c: Option>, + kwargs: Option>, ) -> ( - &'a PyAny, - Option<&'a PyAny>, - &'a PyTuple, - Option<&'a PyAny>, - Option<&'a PyDict>, + Any<'py>, + Option>, + Tuple<'py>, + Option>, + Option>, ) { (a, b, args, c, kwargs) } #[pyfunction(signature = (*args, **kwargs))] -fn args_kwargs<'a>( - args: &'a PyTuple, - kwargs: Option<&'a PyDict>, -) -> (&'a PyTuple, Option<&'a PyDict>) { +fn args_kwargs<'py>( + args: Tuple<'py>, + kwargs: Option>, +) -> (Tuple<'py>, Option>) { (args, kwargs) } #[pymodule] -pub fn pyfunctions(_py: Python<'_>, m: &PyModule) -> PyResult<()> { +pub fn pyfunctions(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(none, m)?)?; m.add_function(wrap_pyfunction!(simple, m)?)?; m.add_function(wrap_pyfunction!(simple_args, m)?)?; diff --git a/pytests/src/sequence.rs b/pytests/src/sequence.rs index 5916414ee8f..f552b4048b8 100644 --- a/pytests/src/sequence.rs +++ b/pytests/src/sequence.rs @@ -12,12 +12,12 @@ fn array_to_array_i32(arr: [i32; 3]) -> [i32; 3] { } #[pyfunction] -fn vec_to_vec_pystring(vec: Vec<&PyString>) -> Vec<&PyString> { +fn vec_to_vec_pystring(vec: Vec>) -> Vec> { vec } #[pymodule] -pub fn sequence(_py: Python<'_>, m: &PyModule) -> PyResult<()> { +pub fn sequence(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(vec_to_vec_i32, m)?)?; m.add_function(wrap_pyfunction!(array_to_array_i32, m)?)?; m.add_function(wrap_pyfunction!(vec_to_vec_pystring, m)?)?; diff --git a/pytests/src/subclassing.rs b/pytests/src/subclassing.rs index 0033114ccea..8e451cd9183 100644 --- a/pytests/src/subclassing.rs +++ b/pytests/src/subclassing.rs @@ -18,7 +18,7 @@ impl Subclassable { } #[pymodule] -pub fn subclassing(_py: Python<'_>, m: &PyModule) -> PyResult<()> { +pub fn subclassing(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; Ok(()) } diff --git a/pytests/tests/test_awaitable.py b/pytests/tests/test_awaitable.py index 2bada317517..40f1e1cc01d 100644 --- a/pytests/tests/test_awaitable.py +++ b/pytests/tests/test_awaitable.py @@ -1,13 +1,22 @@ import pytest +import sys from pyo3_pytests.awaitable import IterAwaitable, FutureAwaitable +@pytest.mark.skipif( + sys.implementation.name == "graalpy", + reason="GraalPy's asyncio module has a bug with native classes, see oracle/graalpython#365", +) @pytest.mark.asyncio async def test_iter_awaitable(): assert await IterAwaitable(5) == 5 +@pytest.mark.skipif( + sys.implementation.name == "graalpy", + reason="GraalPy's asyncio module has a bug with native classes, see oracle/graalpython#365", +) @pytest.mark.asyncio async def test_future_awaitable(): assert await FutureAwaitable(5) == 5 diff --git a/pytests/tests/test_enums.py b/pytests/tests/test_enums.py new file mode 100644 index 00000000000..cd4f7e124c9 --- /dev/null +++ b/pytests/tests/test_enums.py @@ -0,0 +1,203 @@ +import pytest +from pyo3_pytests import enums + + +def test_complex_enum_variant_constructors(): + int_variant = enums.ComplexEnum.Int(42) + assert isinstance(int_variant, enums.ComplexEnum.Int) + + float_variant = enums.ComplexEnum.Float(3.14) + assert isinstance(float_variant, enums.ComplexEnum.Float) + + str_variant = enums.ComplexEnum.Str("hello") + assert isinstance(str_variant, enums.ComplexEnum.Str) + + empty_struct_variant = enums.ComplexEnum.EmptyStruct() + assert isinstance(empty_struct_variant, enums.ComplexEnum.EmptyStruct) + + multi_field_struct_variant = enums.ComplexEnum.MultiFieldStruct(42, 3.14, True) + assert isinstance(multi_field_struct_variant, enums.ComplexEnum.MultiFieldStruct) + + variant_with_default_1 = enums.ComplexEnum.VariantWithDefault() + assert isinstance(variant_with_default_1, enums.ComplexEnum.VariantWithDefault) + + variant_with_default_2 = enums.ComplexEnum.VariantWithDefault(25, "Hello") + assert isinstance(variant_with_default_2, enums.ComplexEnum.VariantWithDefault) + + +@pytest.mark.parametrize( + "variant", + [ + enums.ComplexEnum.Int(42), + enums.ComplexEnum.Float(3.14), + enums.ComplexEnum.Str("hello"), + enums.ComplexEnum.EmptyStruct(), + enums.ComplexEnum.MultiFieldStruct(42, 3.14, True), + enums.ComplexEnum.VariantWithDefault(), + ], +) +def test_complex_enum_variant_subclasses(variant: enums.ComplexEnum): + assert isinstance(variant, enums.ComplexEnum) + + +def test_complex_enum_field_getters(): + int_variant = enums.ComplexEnum.Int(42) + assert int_variant.i == 42 + + float_variant = enums.ComplexEnum.Float(3.14) + assert float_variant.f == 3.14 + + str_variant = enums.ComplexEnum.Str("hello") + assert str_variant.s == "hello" + + multi_field_struct_variant = enums.ComplexEnum.MultiFieldStruct(42, 3.14, True) + assert multi_field_struct_variant.a == 42 + assert multi_field_struct_variant.b == 3.14 + assert multi_field_struct_variant.c is True + + variant_with_default = enums.ComplexEnum.VariantWithDefault() + assert variant_with_default.a == 42 + assert variant_with_default.b is None + + +@pytest.mark.parametrize( + "variant", + [ + enums.ComplexEnum.Int(42), + enums.ComplexEnum.Float(3.14), + enums.ComplexEnum.Str("hello"), + enums.ComplexEnum.EmptyStruct(), + enums.ComplexEnum.MultiFieldStruct(42, 3.14, True), + enums.ComplexEnum.VariantWithDefault(), + ], +) +def test_complex_enum_desugared_match(variant: enums.ComplexEnum): + if isinstance(variant, enums.ComplexEnum.Int): + x = variant.i + assert x == 42 + elif isinstance(variant, enums.ComplexEnum.Float): + x = variant.f + assert x == 3.14 + elif isinstance(variant, enums.ComplexEnum.Str): + x = variant.s + assert x == "hello" + elif isinstance(variant, enums.ComplexEnum.EmptyStruct): + assert True + elif isinstance(variant, enums.ComplexEnum.MultiFieldStruct): + x = variant.a + y = variant.b + z = variant.c + assert x == 42 + assert y == 3.14 + assert z is True + elif isinstance(variant, enums.ComplexEnum.VariantWithDefault): + x = variant.a + y = variant.b + assert x == 42 + assert y is None + else: + assert False + + +@pytest.mark.parametrize( + "variant", + [ + enums.ComplexEnum.Int(42), + enums.ComplexEnum.Float(3.14), + enums.ComplexEnum.Str("hello"), + enums.ComplexEnum.EmptyStruct(), + enums.ComplexEnum.MultiFieldStruct(42, 3.14, True), + enums.ComplexEnum.VariantWithDefault(b="hello"), + ], +) +def test_complex_enum_pyfunction_in_out_desugared_match(variant: enums.ComplexEnum): + variant = enums.do_complex_stuff(variant) + if isinstance(variant, enums.ComplexEnum.Int): + x = variant.i + assert x == 5 + elif isinstance(variant, enums.ComplexEnum.Float): + x = variant.f + assert x == 9.8596 + elif isinstance(variant, enums.ComplexEnum.Str): + x = variant.s + assert x == "42" + elif isinstance(variant, enums.ComplexEnum.EmptyStruct): + assert True + elif isinstance(variant, enums.ComplexEnum.MultiFieldStruct): + x = variant.a + y = variant.b + z = variant.c + assert x == 42 + assert y == 3.14 + assert z is True + elif isinstance(variant, enums.ComplexEnum.VariantWithDefault): + x = variant.a + y = variant.b + assert x == 84 + assert y == "HELLO" + else: + assert False + + +def test_tuple_enum_variant_constructors(): + tuple_variant = enums.TupleEnum.Full(42, 3.14, False) + assert isinstance(tuple_variant, enums.TupleEnum.Full) + + empty_tuple_variant = enums.TupleEnum.EmptyTuple() + assert isinstance(empty_tuple_variant, enums.TupleEnum.EmptyTuple) + + +@pytest.mark.parametrize( + "variant", + [ + enums.TupleEnum.FullWithDefault(), + enums.TupleEnum.Full(42, 3.14, False), + enums.TupleEnum.EmptyTuple(), + ], +) +def test_tuple_enum_variant_subclasses(variant: enums.TupleEnum): + assert isinstance(variant, enums.TupleEnum) + + +def test_tuple_enum_defaults(): + variant = enums.TupleEnum.FullWithDefault() + assert variant._0 == 1 + assert variant._1 == 1.0 + assert variant._2 is True + + +def test_tuple_enum_field_getters(): + tuple_variant = enums.TupleEnum.Full(42, 3.14, False) + assert tuple_variant._0 == 42 + assert tuple_variant._1 == 3.14 + assert tuple_variant._2 is False + + +def test_tuple_enum_index_getter(): + tuple_variant = enums.TupleEnum.Full(42, 3.14, False) + assert len(tuple_variant) == 3 + assert tuple_variant[0] == 42 + + +@pytest.mark.parametrize( + "variant", + [enums.MixedComplexEnum.Nothing()], +) +def test_mixed_complex_enum_pyfunction_instance_nothing( + variant: enums.MixedComplexEnum, +): + assert isinstance(variant, enums.MixedComplexEnum.Nothing) + assert isinstance( + enums.do_mixed_complex_stuff(variant), enums.MixedComplexEnum.Empty + ) + + +@pytest.mark.parametrize( + "variant", + [enums.MixedComplexEnum.Empty()], +) +def test_mixed_complex_enum_pyfunction_instance_empty(variant: enums.MixedComplexEnum): + assert isinstance(variant, enums.MixedComplexEnum.Empty) + assert isinstance( + enums.do_mixed_complex_stuff(variant), enums.MixedComplexEnum.Nothing + ) diff --git a/pytests/tests/test_enums_match.py b/pytests/tests/test_enums_match.py new file mode 100644 index 00000000000..6c4b5f6aa07 --- /dev/null +++ b/pytests/tests/test_enums_match.py @@ -0,0 +1,158 @@ +# This file is only collected when Python >= 3.10, because it tests match syntax. +import pytest +from pyo3_pytests import enums + + +@pytest.mark.parametrize( + "variant", + [ + enums.ComplexEnum.Int(42), + enums.ComplexEnum.Float(3.14), + enums.ComplexEnum.Str("hello"), + enums.ComplexEnum.EmptyStruct(), + enums.ComplexEnum.MultiFieldStruct(42, 3.14, True), + ], +) +def test_complex_enum_match_statement(variant: enums.ComplexEnum): + match variant: + case enums.ComplexEnum.Int(i=x): + assert x == 42 + case enums.ComplexEnum.Float(f=x): + assert x == 3.14 + case enums.ComplexEnum.Str(s=x): + assert x == "hello" + case enums.ComplexEnum.EmptyStruct(): + assert True + case enums.ComplexEnum.MultiFieldStruct(a=x, b=y, c=z): + assert x == 42 + assert y == 3.14 + assert z is True + case _: + assert False + + +@pytest.mark.parametrize( + "variant", + [ + enums.ComplexEnum.Int(42), + enums.ComplexEnum.Float(3.14), + enums.ComplexEnum.Str("hello"), + enums.ComplexEnum.EmptyStruct(), + enums.ComplexEnum.MultiFieldStruct(42, 3.14, True), + ], +) +def test_complex_enum_pyfunction_in_out(variant: enums.ComplexEnum): + match enums.do_complex_stuff(variant): + case enums.ComplexEnum.Int(i=x): + assert x == 5 + case enums.ComplexEnum.Float(f=x): + assert x == 9.8596 + case enums.ComplexEnum.Str(s=x): + assert x == "42" + case enums.ComplexEnum.EmptyStruct(): + assert True + case enums.ComplexEnum.MultiFieldStruct(a=x, b=y, c=z): + assert x == 42 + assert y == 3.14 + assert z is True + case _: + assert False + + +@pytest.mark.parametrize( + "variant", + [ + enums.ComplexEnum.MultiFieldStruct(42, 3.14, True), + ], +) +def test_complex_enum_partial_match(variant: enums.ComplexEnum): + match variant: + case enums.ComplexEnum.MultiFieldStruct(a): + assert a == 42 + case _: + assert False + + +@pytest.mark.parametrize( + "variant", + [ + enums.TupleEnum.Full(42, 3.14, True), + enums.TupleEnum.EmptyTuple(), + ], +) +def test_tuple_enum_match_statement(variant: enums.TupleEnum): + match variant: + case enums.TupleEnum.Full(_0=x, _1=y, _2=z): + assert x == 42 + assert y == 3.14 + assert z is True + case enums.TupleEnum.EmptyTuple(): + assert True + case _: + print(variant) + assert False + + +@pytest.mark.parametrize( + "variant", + [ + enums.SimpleTupleEnum.Int(42), + enums.SimpleTupleEnum.Str("hello"), + ], +) +def test_simple_tuple_enum_match_statement(variant: enums.SimpleTupleEnum): + match variant: + case enums.SimpleTupleEnum.Int(x): + assert x == 42 + case enums.SimpleTupleEnum.Str(x): + assert x == "hello" + case _: + assert False + + +@pytest.mark.parametrize( + "variant", + [ + enums.TupleEnum.Full(42, 3.14, True), + ], +) +def test_tuple_enum_match_match_args(variant: enums.TupleEnum): + match variant: + case enums.TupleEnum.Full(x, y, z): + assert x == 42 + assert y == 3.14 + assert z is True + assert True + case _: + assert False + + +@pytest.mark.parametrize( + "variant", + [ + enums.TupleEnum.Full(42, 3.14, True), + ], +) +def test_tuple_enum_partial_match(variant: enums.TupleEnum): + match variant: + case enums.TupleEnum.Full(a): + assert a == 42 + case _: + assert False + + +@pytest.mark.parametrize( + "variant", + [ + enums.MixedComplexEnum.Nothing(), + enums.MixedComplexEnum.Empty(), + ], +) +def test_mixed_complex_enum_match_statement(variant: enums.MixedComplexEnum): + match variant: + case enums.MixedComplexEnum.Nothing(): + assert True + case enums.MixedComplexEnum.Empty(): + assert True + case _: + assert False diff --git a/pytests/tests/test_misc.py b/pytests/tests/test_misc.py index 06b2ce73e10..88af735e861 100644 --- a/pytests/tests/test_misc.py +++ b/pytests/tests/test_misc.py @@ -5,6 +5,11 @@ import pyo3_pytests.misc import pytest +if sys.version_info >= (3, 13): + subinterpreters = pytest.importorskip("subinterpreters") +else: + subinterpreters = pytest.importorskip("_xxsubinterpreters") + def test_issue_219(): # Should not deadlock @@ -27,27 +32,23 @@ def test_multiple_imports_same_interpreter_ok(): reason="Cannot identify subinterpreters on Python older than 3.9", ) @pytest.mark.skipif( - platform.python_implementation() == "PyPy", - reason="PyPy does not support subinterpreters", + platform.python_implementation() in ("PyPy", "GraalVM"), + reason="PyPy and GraalPy do not support subinterpreters", ) def test_import_in_subinterpreter_forbidden(): - import _xxsubinterpreters - if sys.version_info < (3, 12): expected_error = "PyO3 modules do not yet support subinterpreters, see https://github.com/PyO3/pyo3/issues/576" else: expected_error = "module pyo3_pytests.pyo3_pytests does not support loading in subinterpreters" - sub_interpreter = _xxsubinterpreters.create() + sub_interpreter = subinterpreters.create() with pytest.raises( - _xxsubinterpreters.RunFailedError, + subinterpreters.RunFailedError, match=expected_error, ): - _xxsubinterpreters.run_string( - sub_interpreter, "import pyo3_pytests.pyo3_pytests" - ) + subinterpreters.run_string(sub_interpreter, "import pyo3_pytests.pyo3_pytests") - _xxsubinterpreters.destroy(sub_interpreter) + subinterpreters.destroy(sub_interpreter) def test_type_full_name_includes_module(): @@ -64,3 +65,41 @@ def test_accepts_numpy_bool(): assert pyo3_pytests.misc.accepts_bool(False) is False assert pyo3_pytests.misc.accepts_bool(numpy.bool_(True)) is True assert pyo3_pytests.misc.accepts_bool(numpy.bool_(False)) is False + + +class ArbitraryClass: + worker_id: int + iteration: int + + def __init__(self, worker_id: int, iteration: int): + self.worker_id = worker_id + self.iteration = iteration + + def __repr__(self): + return f"ArbitraryClass({self.worker_id}, {self.iteration})" + + def __del__(self): + print("del", self.worker_id, self.iteration) + + +def test_gevent(): + gevent = pytest.importorskip("gevent") + + def worker(worker_id: int) -> None: + for iteration in range(2): + d = {"key": ArbitraryClass(worker_id, iteration)} + + def arbitrary_python_code(): + # remove the dictionary entry so that the class value can be + # garbage collected + del d["key"] + print("gevent sleep", worker_id, iteration) + gevent.sleep(0) + print("after gevent sleep", worker_id, iteration) + + print("start", worker_id, iteration) + pyo3_pytests.misc.get_item_and_run_callback(d, arbitrary_python_code) + print("end", worker_id, iteration) + + workers = [gevent.spawn(worker, i) for i in range(2)] + gevent.joinall(workers) diff --git a/pytests/tests/test_objstore.py b/pytests/tests/test_objstore.py index bfd8bad84df..3f0d23fa97c 100644 --- a/pytests/tests/test_objstore.py +++ b/pytests/tests/test_objstore.py @@ -12,6 +12,11 @@ def test_objstore_doesnot_leak_memory(): # check refcount on PyPy getrefcount = getattr(sys, "getrefcount", lambda obj: 0) + if sys.implementation.name == "graalpy": + # GraalPy has an incomplete sys.getrefcount implementation + def getrefcount(obj): + return 0 + before = getrefcount(message) store = ObjStore() for _ in range(N): diff --git a/pytests/tests/test_pyclasses.py b/pytests/tests/test_pyclasses.py index 5482862811a..efef178d489 100644 --- a/pytests/tests/test_pyclasses.py +++ b/pytests/tests/test_pyclasses.py @@ -8,14 +8,38 @@ def test_empty_class_init(benchmark): benchmark(pyclasses.EmptyClass) +def test_method_call(benchmark): + obj = pyclasses.EmptyClass() + assert benchmark(obj.method) is None + + +def test_proto_call(benchmark): + obj = pyclasses.EmptyClass() + assert benchmark(len, obj) == 0 + + class EmptyClassPy: - pass + def method(self): + pass + + def __len__(self) -> int: + return 0 def test_empty_class_init_py(benchmark): benchmark(EmptyClassPy) +def test_method_call_py(benchmark): + obj = EmptyClassPy() + assert benchmark(obj.method) == pyclasses.EmptyClass().method() + + +def test_proto_call_py(benchmark): + obj = EmptyClassPy() + assert benchmark(len, obj) == len(pyclasses.EmptyClass()) + + def test_iter(): i = pyclasses.PyClassIter() assert next(i) == 1 diff --git a/src/buffer.rs b/src/buffer.rs index 4ff2c1d97eb..74ac7fe8e53 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -18,6 +18,9 @@ // DEALINGS IN THE SOFTWARE. //! `PyBuffer` implementation +use crate::Bound; +#[cfg(feature = "gil-refs")] +use crate::PyNativeType; use crate::{err, exceptions::PyBufferError, ffi, FromPyObject, PyAny, PyResult, Python}; use std::marker::PhantomData; use std::os::raw; @@ -181,15 +184,25 @@ pub unsafe trait Element: Copy { fn is_compatible_format(format: &CStr) -> bool; } -impl<'source, T: Element> FromPyObject<'source> for PyBuffer { - fn extract(obj: &PyAny) -> PyResult> { - Self::get(obj) +impl<'py, T: Element> FromPyObject<'py> for PyBuffer { + fn extract_bound(obj: &Bound<'_, PyAny>) -> PyResult> { + Self::get_bound(obj) } } impl PyBuffer { - /// Gets the underlying buffer from the specified python object. + /// Deprecated form of [`PyBuffer::get_bound`] + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`PyBuffer::get` will be replaced by `PyBuffer::get_bound` in a future PyO3 version" + )] pub fn get(obj: &PyAny) -> PyResult> { + Self::get_bound(&obj.as_borrowed()) + } + + /// Gets the underlying buffer from the specified python object. + pub fn get_bound(obj: &Bound<'_, PyAny>) -> PyResult> { // TODO: use nightly API Box::new_uninit() once stable let mut buf = Box::new(mem::MaybeUninit::uninit()); let buf: Box = { @@ -688,13 +701,14 @@ impl_element!(f64, Float); mod tests { use super::PyBuffer; use crate::ffi; + use crate::types::any::PyAnyMethods; use crate::Python; #[test] fn test_debug() { Python::with_gil(|py| { - let bytes = py.eval("b'abcde'", None, None).unwrap(); - let buffer: PyBuffer = PyBuffer::get(bytes).unwrap(); + let bytes = py.eval_bound("b'abcde'", None, None).unwrap(); + let buffer: PyBuffer = PyBuffer::get_bound(&bytes).unwrap(); let expected = format!( concat!( "PyBuffer {{ buf: {:?}, obj: {:?}, ", @@ -856,8 +870,8 @@ mod tests { #[test] fn test_bytes_buffer() { Python::with_gil(|py| { - let bytes = py.eval("b'abcde'", None, None).unwrap(); - let buffer = PyBuffer::get(bytes).unwrap(); + let bytes = py.eval_bound("b'abcde'", None, None).unwrap(); + let buffer = PyBuffer::get_bound(&bytes).unwrap(); assert_eq!(buffer.dimensions(), 1); assert_eq!(buffer.item_count(), 5); assert_eq!(buffer.format().to_str().unwrap(), "B"); @@ -889,11 +903,11 @@ mod tests { fn test_array_buffer() { Python::with_gil(|py| { let array = py - .import("array") + .import_bound("array") .unwrap() .call_method("array", ("f", (1.0, 1.5, 2.0, 2.5)), None) .unwrap(); - let buffer = PyBuffer::get(array).unwrap(); + let buffer = PyBuffer::get_bound(&array).unwrap(); assert_eq!(buffer.dimensions(), 1); assert_eq!(buffer.item_count(), 4); assert_eq!(buffer.format().to_str().unwrap(), "f"); @@ -923,7 +937,7 @@ mod tests { assert_eq!(buffer.to_vec(py).unwrap(), [10.0, 11.0, 12.0, 13.0]); // F-contiguous fns - let buffer = PyBuffer::get(array).unwrap(); + let buffer = PyBuffer::get_bound(&array).unwrap(); let slice = buffer.as_fortran_slice(py).unwrap(); assert_eq!(slice.len(), 4); assert_eq!(slice[1].get(), 11.0); diff --git a/src/callback.rs b/src/callback.rs index a56b268aa1e..1e446039904 100644 --- a/src/callback.rs +++ b/src/callback.rs @@ -4,7 +4,6 @@ use crate::err::{PyErr, PyResult}; use crate::exceptions::PyOverflowError; use crate::ffi::{self, Py_hash_t}; use crate::{IntoPy, PyObject, Python}; -use std::isize; use std::os::raw::c_int; /// A type which can be the return type of a python C-API callback @@ -85,11 +84,7 @@ impl IntoPyCallbackOutput<()> for () { impl IntoPyCallbackOutput for usize { #[inline] fn convert(self, _py: Python<'_>) -> PyResult { - if self <= (isize::MAX as usize) { - Ok(self as isize) - } else { - Err(PyOverflowError::new_err(())) - } + self.try_into().map_err(|_err| PyOverflowError::new_err(())) } } diff --git a/src/conversion.rs b/src/conversion.rs index 0a842f9a419..44dbc3c7eed 100644 --- a/src/conversion.rs +++ b/src/conversion.rs @@ -1,15 +1,19 @@ //! Defines conversions between Rust and Python types. -use crate::err::{self, PyDowncastError, PyResult}; +use crate::err::PyResult; #[cfg(feature = "experimental-inspect")] use crate::inspect::types::TypeInfo; use crate::pyclass::boolean_struct::False; -use crate::type_object::PyTypeInfo; +use crate::types::any::PyAnyMethods; use crate::types::PyTuple; -use crate::{ - ffi, gil, Py, PyAny, PyCell, PyClass, PyNativeType, PyObject, PyRef, PyRefMut, Python, +use crate::{ffi, Borrowed, Bound, Py, PyAny, PyClass, PyObject, PyRef, PyRefMut, Python}; +#[cfg(feature = "gil-refs")] +use { + crate::{ + err::{self, PyDowncastError}, + gil, PyNativeType, + }, + std::ptr::NonNull, }; -use std::cell::Cell; -use std::ptr::NonNull; /// Returns a borrowed pointer to a Python object. /// @@ -63,18 +67,6 @@ pub unsafe trait AsPyPointer { fn as_ptr(&self) -> *mut ffi::PyObject; } -/// Convert `None` into a null pointer. -unsafe impl AsPyPointer for Option -where - T: AsPyPointer, -{ - #[inline] - fn as_ptr(&self) -> *mut ffi::PyObject { - self.as_ref() - .map_or_else(std::ptr::null_mut, |t| t.as_ptr()) - } -} - /// Conversion trait that allows various objects to be converted into `PyObject`. pub trait ToPyObject { /// Converts self into a Python object. @@ -110,6 +102,7 @@ pub trait ToPyObject { /// ```rust /// use pyo3::prelude::*; /// +/// # #[allow(dead_code)] /// struct Number { /// value: i32, /// } @@ -141,7 +134,7 @@ pub trait ToPyObject { /// match self { /// Self::Integer(val) => val.into_py(py), /// Self::String(val) => val.into_py(py), -/// Self::None => py.None().into(), +/// Self::None => py.None(), /// } /// } /// } @@ -180,7 +173,7 @@ pub trait IntoPy: Sized { /// Extract a type from a Python object. /// /// -/// Normal usage is through the `extract` methods on [`Py`] and [`PyAny`], which forward to this trait. +/// Normal usage is through the `extract` methods on [`Bound`] and [`Py`], which forward to this trait. /// /// # Examples /// @@ -190,36 +183,52 @@ pub trait IntoPy: Sized { /// /// # fn main() -> PyResult<()> { /// Python::with_gil(|py| { -/// let obj: Py = PyString::new(py, "blah").into(); -/// -/// // Straight from an owned reference -/// let s: &str = obj.extract(py)?; +/// // Calling `.extract()` on a `Bound` smart pointer +/// let obj: Bound<'_, PyString> = PyString::new_bound(py, "blah"); +/// let s: String = obj.extract()?; /// # assert_eq!(s, "blah"); /// -/// // Or from a borrowed reference -/// let obj: &PyString = obj.as_ref(py); -/// let s: &str = obj.extract()?; +/// // Calling `.extract(py)` on a `Py` smart pointer +/// let obj: Py = obj.unbind(); +/// let s: String = obj.extract(py)?; /// # assert_eq!(s, "blah"); /// # Ok(()) /// }) /// # } /// ``` /// -/// Note: depending on the implementation, the lifetime of the extracted result may -/// depend on the lifetime of the `obj` or the `prepared` variable. -/// -/// For example, when extracting `&str` from a Python byte string, the resulting string slice will -/// point to the existing string data (lifetime: `'source`). -/// On the other hand, when extracting `&str` from a Python Unicode string, the preparation step -/// will convert the string to UTF-8, and the resulting string slice will have lifetime `'prepared`. -/// Since which case applies depends on the runtime type of the Python object, -/// both the `obj` and `prepared` variables must outlive the resulting string slice. -/// -/// The trait's conversion method takes a `&PyAny` argument but is called -/// `FromPyObject` for historical reasons. -pub trait FromPyObject<'source>: Sized { - /// Extracts `Self` from the source `PyObject`. - fn extract(ob: &'source PyAny) -> PyResult; +// /// FIXME: until `FromPyObject` can pick up a second lifetime, the below commentary is no longer +// /// true. Update and restore this documentation at that time. +// /// +// /// Note: depending on the implementation, the lifetime of the extracted result may +// /// depend on the lifetime of the `obj` or the `prepared` variable. +// /// +// /// For example, when extracting `&str` from a Python byte string, the resulting string slice will +// /// point to the existing string data (lifetime: `'py`). +// /// On the other hand, when extracting `&str` from a Python Unicode string, the preparation step +// /// will convert the string to UTF-8, and the resulting string slice will have lifetime `'prepared`. +// /// Since which case applies depends on the runtime type of the Python object, +// /// both the `obj` and `prepared` variables must outlive the resulting string slice. +/// +/// During the migration of PyO3 from the "GIL Refs" API to the `Bound` smart pointer, this trait +/// has two methods `extract` and `extract_bound` which are defaulted to call each other. To avoid +/// infinite recursion, implementors must implement at least one of these methods. The recommendation +/// is to implement `extract_bound` and leave `extract` as the default implementation. +pub trait FromPyObject<'py>: Sized { + /// Extracts `Self` from the source GIL Ref `obj`. + /// + /// Implementors are encouraged to implement `extract_bound` and leave this method as the + /// default implementation, which will forward calls to `extract_bound`. + #[cfg(feature = "gil-refs")] + fn extract(ob: &'py PyAny) -> PyResult { + Self::extract_bound(&ob.as_borrowed()) + } + + /// Extracts `Self` from the bound smart pointer `obj`. + /// + /// Implementors are encouraged to implement this method and leave `extract` defaulted, as + /// this will be most compatible with PyO3's future API. + fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult; /// Extracts the type hint information for this type when it appears as an argument. /// @@ -234,33 +243,91 @@ pub trait FromPyObject<'source>: Sized { } } -/// Identity conversion: allows using existing `PyObject` instances where -/// `T: ToPyObject` is expected. -impl ToPyObject for &'_ T { - #[inline] - fn to_object(&self, py: Python<'_>) -> PyObject { - ::to_object(*self, py) +mod from_py_object_bound_sealed { + /// Private seal for the `FromPyObjectBound` trait. + /// + /// This prevents downstream types from implementing the trait before + /// PyO3 is ready to declare the trait as public API. + pub trait Sealed {} + + // This generic implementation is why the seal is separate from + // `crate::sealed::Sealed`. + impl<'py, T> Sealed for T where T: super::FromPyObject<'py> {} + #[cfg(not(feature = "gil-refs"))] + impl Sealed for &'_ str {} + #[cfg(not(feature = "gil-refs"))] + impl Sealed for std::borrow::Cow<'_, str> {} + #[cfg(not(feature = "gil-refs"))] + impl Sealed for &'_ [u8] {} + #[cfg(not(feature = "gil-refs"))] + impl Sealed for std::borrow::Cow<'_, [u8]> {} +} + +/// Expected form of [`FromPyObject`] to be used in a future PyO3 release. +/// +/// The difference between this and `FromPyObject` is that this trait takes an +/// additional lifetime `'a`, which is the lifetime of the input `Bound`. +/// +/// This allows implementations for `&'a str` and `&'a [u8]`, which could not +/// be expressed by the existing `FromPyObject` trait once the GIL Refs API was +/// removed. +/// +/// # Usage +/// +/// Users are prevented from implementing this trait, instead they should implement +/// the normal `FromPyObject` trait. This trait has a blanket implementation +/// for `T: FromPyObject`. +/// +/// The only case where this trait may have a use case to be implemented is when the +/// lifetime of the extracted value is tied to the lifetime `'a` of the input `Bound` +/// instead of the GIL lifetime `py`, as is the case for the `&'a str` implementation. +/// +/// Please contact the PyO3 maintainers if you believe you have a use case for implementing +/// this trait before PyO3 is ready to change the main `FromPyObject` trait to take an +/// additional lifetime. +/// +/// Similarly, users should typically not call these trait methods and should instead +/// use this via the `extract` method on `Bound` and `Py`. +pub trait FromPyObjectBound<'a, 'py>: Sized + from_py_object_bound_sealed::Sealed { + /// Extracts `Self` from the bound smart pointer `obj`. + /// + /// Users are advised against calling this method directly: instead, use this via + /// [`Bound<'_, PyAny>::extract`] or [`Py::extract`]. + fn from_py_object_bound(ob: Borrowed<'a, 'py, PyAny>) -> PyResult; + + /// Extracts the type hint information for this type when it appears as an argument. + /// + /// For example, `Vec` would return `Sequence[int]`. + /// The default implementation returns `Any`, which is correct for any type. + /// + /// For most types, the return value for this method will be identical to that of [`IntoPy::type_output`]. + /// It may be different for some types, such as `Dict`, to allow duck-typing: functions return `Dict` but take `Mapping` as argument. + #[cfg(feature = "experimental-inspect")] + fn type_input() -> TypeInfo { + TypeInfo::Any } } -/// `Option::Some` is converted like `T`. -/// `Option::None` is converted to Python `None`. -impl ToPyObject for Option +impl<'py, T> FromPyObjectBound<'_, 'py> for T where - T: ToPyObject, + T: FromPyObject<'py>, { - fn to_object(&self, py: Python<'_>) -> PyObject { - self.as_ref() - .map_or_else(|| py.None().into(), |val| val.to_object(py)) + fn from_py_object_bound(ob: Borrowed<'_, 'py, PyAny>) -> PyResult { + Self::extract_bound(&ob) + } + + #[cfg(feature = "experimental-inspect")] + fn type_input() -> TypeInfo { + ::type_input() } } -impl IntoPy for Option -where - T: IntoPy, -{ - fn into_py(self, py: Python<'_>) -> PyObject { - self.map_or_else(|| py.None().into(), |val| val.into_py(py)) +/// Identity conversion: allows using existing `PyObject` instances where +/// `T: ToPyObject` is expected. +impl ToPyObject for &'_ T { + #[inline] + fn to_object(&self, py: Python<'_>) -> PyObject { + ::to_object(*self, py) } } @@ -281,73 +348,42 @@ where } } -impl ToPyObject for Cell { - fn to_object(&self, py: Python<'_>) -> PyObject { - self.get().to_object(py) - } -} - -impl> IntoPy for Cell { - fn into_py(self, py: Python<'_>) -> PyObject { - self.get().into_py(py) - } -} - -impl<'a, T: FromPyObject<'a>> FromPyObject<'a> for Cell { - fn extract(ob: &'a PyAny) -> PyResult { - T::extract(ob).map(Cell::new) - } -} - -impl<'a, T> FromPyObject<'a> for &'a PyCell +#[allow(deprecated)] +#[cfg(feature = "gil-refs")] +impl<'py, T> FromPyObject<'py> for &'py crate::PyCell where T: PyClass, { - fn extract(obj: &'a PyAny) -> PyResult { - obj.downcast().map_err(Into::into) + fn extract_bound(obj: &Bound<'py, PyAny>) -> PyResult { + obj.clone().into_gil_ref().downcast().map_err(Into::into) } } -impl<'a, T> FromPyObject<'a> for T +impl FromPyObject<'_> for T where T: PyClass + Clone, { - fn extract(obj: &'a PyAny) -> PyResult { - let cell: &PyCell = obj.downcast()?; - Ok(unsafe { cell.try_borrow_unguarded()?.clone() }) + fn extract_bound(obj: &Bound<'_, PyAny>) -> PyResult { + let bound = obj.downcast::()?; + Ok(bound.try_borrow()?.clone()) } } -impl<'a, T> FromPyObject<'a> for PyRef<'a, T> +impl<'py, T> FromPyObject<'py> for PyRef<'py, T> where T: PyClass, { - fn extract(obj: &'a PyAny) -> PyResult { - let cell: &PyCell = obj.downcast()?; - cell.try_borrow().map_err(Into::into) + fn extract_bound(obj: &Bound<'py, PyAny>) -> PyResult { + obj.downcast::()?.try_borrow().map_err(Into::into) } } -impl<'a, T> FromPyObject<'a> for PyRefMut<'a, T> +impl<'py, T> FromPyObject<'py> for PyRefMut<'py, T> where T: PyClass, { - fn extract(obj: &'a PyAny) -> PyResult { - let cell: &PyCell = obj.downcast()?; - cell.try_borrow_mut().map_err(Into::into) - } -} - -impl<'a, T> FromPyObject<'a> for Option -where - T: FromPyObject<'a>, -{ - fn extract(obj: &'a PyAny) -> PyResult { - if obj.as_ptr() == unsafe { ffi::Py_None() } { - Ok(None) - } else { - T::extract(obj).map(Some) - } + fn extract_bound(obj: &Bound<'py, PyAny>) -> PyResult { + obj.downcast::()?.try_borrow_mut().map_err(Into::into) } } @@ -355,6 +391,7 @@ where /// If `T` implements `PyTryFrom`, we can convert `&PyAny` to `&T`. /// /// This trait is similar to `std::convert::TryFrom` +#[cfg(feature = "gil-refs")] #[deprecated(since = "0.21.0")] pub trait PyTryFrom<'v>: Sized + PyNativeType { /// Cast from a concrete Python object type to PyObject. @@ -386,6 +423,7 @@ pub trait PyTryFrom<'v>: Sized + PyNativeType { /// Trait implemented by Python object types that allow a checked downcast. /// This trait is similar to `std::convert::TryInto` +#[cfg(feature = "gil-refs")] #[deprecated(since = "0.21.0")] pub trait PyTryInto: Sized { /// Cast from PyObject to a concrete Python object type. @@ -403,9 +441,11 @@ pub trait PyTryInto: Sized { fn try_into_exact(&self) -> Result<&T, PyDowncastError<'_>>; } +#[cfg(feature = "gil-refs")] #[allow(deprecated)] mod implementations { use super::*; + use crate::type_object::PyTypeInfo; // TryFrom implies TryInto impl PyTryInto for PyAny @@ -438,7 +478,7 @@ mod implementations { } } - impl<'v, T> PyTryFrom<'v> for PyCell + impl<'v, T> PyTryFrom<'v> for crate::PyCell where T: 'v + PyClass, { @@ -465,7 +505,7 @@ mod implementations { /// Converts `()` to an empty Python tuple. impl IntoPy> for () { fn into_py(self, py: Python<'_>) -> Py { - PyTuple::empty(py).into() + PyTuple::empty_bound(py).unbind() } } @@ -474,6 +514,8 @@ impl IntoPy> for () { /// # Safety /// /// See safety notes on individual functions. +#[cfg(feature = "gil-refs")] +#[deprecated(since = "0.21.0")] pub unsafe trait FromPyPointer<'p>: Sized { /// Convert from an arbitrary `PyObject`. /// @@ -482,13 +524,22 @@ pub unsafe trait FromPyPointer<'p>: Sized { /// Implementations must ensure the object does not get freed during `'p` /// and ensure that `ptr` is of the correct type. /// Note that it must be safe to decrement the reference count of `ptr`. + #[deprecated( + since = "0.21.0", + note = "use `Py::from_owned_ptr_or_opt(py, ptr)` or `Bound::from_owned_ptr_or_opt(py, ptr)` instead" + )] unsafe fn from_owned_ptr_or_opt(py: Python<'p>, ptr: *mut ffi::PyObject) -> Option<&'p Self>; /// Convert from an arbitrary `PyObject` or panic. /// /// # Safety /// /// Relies on [`from_owned_ptr_or_opt`](#method.from_owned_ptr_or_opt). + #[deprecated( + since = "0.21.0", + note = "use `Py::from_owned_ptr(py, ptr)` or `Bound::from_owned_ptr(py, ptr)` instead" + )] unsafe fn from_owned_ptr_or_panic(py: Python<'p>, ptr: *mut ffi::PyObject) -> &'p Self { + #[allow(deprecated)] Self::from_owned_ptr_or_opt(py, ptr).unwrap_or_else(|| err::panic_after_error(py)) } /// Convert from an arbitrary `PyObject` or panic. @@ -496,7 +547,12 @@ pub unsafe trait FromPyPointer<'p>: Sized { /// # Safety /// /// Relies on [`from_owned_ptr_or_opt`](#method.from_owned_ptr_or_opt). + #[deprecated( + since = "0.21.0", + note = "use `Py::from_owned_ptr(py, ptr)` or `Bound::from_owned_ptr(py, ptr)` instead" + )] unsafe fn from_owned_ptr(py: Python<'p>, ptr: *mut ffi::PyObject) -> &'p Self { + #[allow(deprecated)] Self::from_owned_ptr_or_panic(py, ptr) } /// Convert from an arbitrary `PyObject`. @@ -504,7 +560,12 @@ pub unsafe trait FromPyPointer<'p>: Sized { /// # Safety /// /// Relies on [`from_owned_ptr_or_opt`](#method.from_owned_ptr_or_opt). + #[deprecated( + since = "0.21.0", + note = "use `Py::from_owned_ptr_or_err(py, ptr)` or `Bound::from_owned_ptr_or_err(py, ptr)` instead" + )] unsafe fn from_owned_ptr_or_err(py: Python<'p>, ptr: *mut ffi::PyObject) -> PyResult<&'p Self> { + #[allow(deprecated)] Self::from_owned_ptr_or_opt(py, ptr).ok_or_else(|| err::PyErr::fetch(py)) } /// Convert from an arbitrary borrowed `PyObject`. @@ -512,6 +573,10 @@ pub unsafe trait FromPyPointer<'p>: Sized { /// # Safety /// /// Implementations must ensure the object does not get freed during `'p` and avoid type confusion. + #[deprecated( + since = "0.21.0", + note = "use `Py::from_borrowed_ptr_or_opt(py, ptr)` or `Bound::from_borrowed_ptr_or_opt(py, ptr)` instead" + )] unsafe fn from_borrowed_ptr_or_opt(py: Python<'p>, ptr: *mut ffi::PyObject) -> Option<&'p Self>; /// Convert from an arbitrary borrowed `PyObject`. @@ -519,7 +584,12 @@ pub unsafe trait FromPyPointer<'p>: Sized { /// # Safety /// /// Relies on unsafe fn [`from_borrowed_ptr_or_opt`](#method.from_borrowed_ptr_or_opt). + #[deprecated( + since = "0.21.0", + note = "use `Py::from_borrowed_ptr(py, ptr)` or `Bound::from_borrowed_ptr(py, ptr)` instead" + )] unsafe fn from_borrowed_ptr_or_panic(py: Python<'p>, ptr: *mut ffi::PyObject) -> &'p Self { + #[allow(deprecated)] Self::from_borrowed_ptr_or_opt(py, ptr).unwrap_or_else(|| err::panic_after_error(py)) } /// Convert from an arbitrary borrowed `PyObject`. @@ -527,7 +597,12 @@ pub unsafe trait FromPyPointer<'p>: Sized { /// # Safety /// /// Relies on unsafe fn [`from_borrowed_ptr_or_opt`](#method.from_borrowed_ptr_or_opt). + #[deprecated( + since = "0.21.0", + note = "use `Py::from_borrowed_ptr(py, ptr)` or `Bound::from_borrowed_ptr(py, ptr)` instead" + )] unsafe fn from_borrowed_ptr(py: Python<'p>, ptr: *mut ffi::PyObject) -> &'p Self { + #[allow(deprecated)] Self::from_borrowed_ptr_or_panic(py, ptr) } /// Convert from an arbitrary borrowed `PyObject`. @@ -535,14 +610,21 @@ pub unsafe trait FromPyPointer<'p>: Sized { /// # Safety /// /// Relies on unsafe fn [`from_borrowed_ptr_or_opt`](#method.from_borrowed_ptr_or_opt). + #[deprecated( + since = "0.21.0", + note = "use `Py::from_borrowed_ptr_or_err(py, ptr)` or `Bound::from_borrowed_ptr_or_err(py, ptr)` instead" + )] unsafe fn from_borrowed_ptr_or_err( py: Python<'p>, ptr: *mut ffi::PyObject, ) -> PyResult<&'p Self> { + #[allow(deprecated)] Self::from_borrowed_ptr_or_opt(py, ptr).ok_or_else(|| err::PyErr::fetch(py)) } } +#[cfg(feature = "gil-refs")] +#[allow(deprecated)] unsafe impl<'p, T> FromPyPointer<'p> for T where T: 'p + crate::PyNativeType, @@ -578,8 +660,7 @@ mod test_no_clone {} #[cfg(test)] mod tests { - use crate::{PyObject, Python}; - + #[cfg(feature = "gil-refs")] #[allow(deprecated)] mod deprecated { use super::super::PyTryFrom; @@ -623,22 +704,4 @@ mod tests { }); } } - - #[test] - fn test_option_as_ptr() { - Python::with_gil(|py| { - use crate::AsPyPointer; - let mut option: Option = None; - assert_eq!(option.as_ptr(), std::ptr::null_mut()); - - let none = py.None(); - option = Some(none.into()); - - let ref_cnt = none.get_refcnt(); - assert_eq!(option.as_ptr(), none.as_ptr()); - - // Ensure ref count not changed by as_ptr call - assert_eq!(none.get_refcnt(), ref_cnt); - }); - } } diff --git a/src/conversions/anyhow.rs b/src/conversions/anyhow.rs index b362faf479f..623ee7d548c 100644 --- a/src/conversions/anyhow.rs +++ b/src/conversions/anyhow.rs @@ -35,7 +35,6 @@ //! //! ```rust //! use pyo3::prelude::*; -//! use pyo3::wrap_pyfunction; //! use std::path::PathBuf; //! //! // A wrapper around a Rust function. @@ -48,7 +47,7 @@ //! //! fn main() { //! let error = Python::with_gil(|py| -> PyResult> { -//! let fun = wrap_pyfunction!(py_open, py)?; +//! let fun = wrap_pyfunction_bound!(py_open, py)?; //! let text = fun.call1(("foo.txt",))?.extract::>()?; //! Ok(text) //! }).unwrap_err(); @@ -75,9 +74,9 @@ //! // could call inside an application... //! // This might return a `PyErr`. //! let res = Python::with_gil(|py| { -//! let zlib = PyModule::import(py, "zlib")?; +//! let zlib = PyModule::import_bound(py, "zlib")?; //! let decompress = zlib.getattr("decompress")?; -//! let bytes = PyBytes::new(py, bytes); +//! let bytes = PyBytes::new_bound(py, bytes); //! let value = decompress.call1((bytes,))?; //! value.extract::>() //! })?; @@ -147,9 +146,9 @@ mod test_anyhow { let pyerr = PyErr::from(err); Python::with_gil(|py| { - let locals = [("err", pyerr)].into_py_dict(py); - let pyerr = py.run("raise err", None, Some(locals)).unwrap_err(); - assert_eq!(pyerr.value(py).to_string(), expected_contents); + let locals = [("err", pyerr)].into_py_dict_bound(py); + let pyerr = py.run_bound("raise err", None, Some(&locals)).unwrap_err(); + assert_eq!(pyerr.value_bound(py).to_string(), expected_contents); }) } @@ -164,9 +163,9 @@ mod test_anyhow { let pyerr = PyErr::from(err); Python::with_gil(|py| { - let locals = [("err", pyerr)].into_py_dict(py); - let pyerr = py.run("raise err", None, Some(locals)).unwrap_err(); - assert_eq!(pyerr.value(py).to_string(), expected_contents); + let locals = [("err", pyerr)].into_py_dict_bound(py); + let pyerr = py.run_bound("raise err", None, Some(&locals)).unwrap_err(); + assert_eq!(pyerr.value_bound(py).to_string(), expected_contents); }) } diff --git a/src/conversions/chrono.rs b/src/conversions/chrono.rs index 0ad5b445136..2e220681951 100644 --- a/src/conversions/chrono.rs +++ b/src/conversions/chrono.rs @@ -9,8 +9,6 @@ //! //! ```toml //! [dependencies] -//! # change * to the latest versions -//! pyo3 = { version = "*", features = ["chrono"] } //! chrono = "0.4" #![doc = concat!("pyo3 = { version = \"", env!("CARGO_PKG_VERSION"), "\", features = [\"chrono\"] }")] //! ``` @@ -18,9 +16,12 @@ //! Note that you must use compatible versions of chrono and PyO3. //! The required chrono version may vary based on the version of PyO3. //! -//! # Example: Convert a `PyDateTime` to chrono's `DateTime` +//! # Example: Convert a `datetime.datetime` to chrono's `DateTime` //! //! ```rust +//! # // `chrono::Duration` has been renamed to `chrono::TimeDelta` and its constructors changed +//! # // TODO: upgrade to Chrono 0.4.35+ after upgrading our MSRV to 1.61+ +//! # #![allow(deprecated)] //! use chrono::{DateTime, Duration, TimeZone, Utc}; //! use pyo3::{Python, ToPyObject}; //! @@ -41,21 +42,25 @@ //! }); //! } //! ``` + +// `chrono::Duration` has been renamed to `chrono::TimeDelta` and its constructors changed +// TODO: upgrade to Chrono 0.4.35+ after upgrading our MSRV to 1.61+ +#![allow(deprecated)] + use crate::exceptions::{PyTypeError, PyUserWarning, PyValueError}; #[cfg(Py_LIMITED_API)] use crate::sync::GILOnceCell; -#[cfg(not(Py_LIMITED_API))] use crate::types::any::PyAnyMethods; #[cfg(not(Py_LIMITED_API))] use crate::types::datetime::timezone_from_offset; #[cfg(not(Py_LIMITED_API))] use crate::types::{ - timezone_utc, PyDate, PyDateAccess, PyDateTime, PyDelta, PyDeltaAccess, PyTime, PyTimeAccess, - PyTzInfo, PyTzInfoAccess, + timezone_utc_bound, PyDate, PyDateAccess, PyDateTime, PyDelta, PyDeltaAccess, PyTime, + PyTimeAccess, PyTzInfo, PyTzInfoAccess, }; #[cfg(Py_LIMITED_API)] -use crate::{intern, PyDowncastError}; -use crate::{FromPyObject, IntoPy, PyAny, PyErr, PyObject, PyResult, Python, ToPyObject}; +use crate::{intern, DowncastError}; +use crate::{Bound, FromPyObject, IntoPy, PyAny, PyErr, PyObject, PyResult, Python, ToPyObject}; use chrono::offset::{FixedOffset, Utc}; use chrono::{ DateTime, Datelike, Duration, NaiveDate, NaiveDateTime, NaiveTime, Offset, TimeZone, Timelike, @@ -82,7 +87,7 @@ impl ToPyObject for Duration { // We pass true as the `normalize` parameter since we'd need to do several checks here to // avoid that, and it shouldn't have a big performance impact. // The seconds and microseconds cast should never overflow since it's at most the number of seconds per day - PyDelta::new( + PyDelta::new_bound( py, days.try_into().unwrap_or(i32::MAX), secs.try_into().unwrap(), @@ -109,14 +114,14 @@ impl IntoPy for Duration { } impl FromPyObject<'_> for Duration { - fn extract(ob: &PyAny) -> PyResult { + fn extract_bound(ob: &Bound<'_, PyAny>) -> PyResult { // Python size are much lower than rust size so we do not need bound checks. // 0 <= microseconds < 1000000 // 0 <= seconds < 3600*24 // -999999999 <= days <= 999999999 #[cfg(not(Py_LIMITED_API))] let (days, seconds, microseconds) = { - let delta: &PyDelta = ob.downcast()?; + let delta = ob.downcast::()?; ( delta.get_days().into(), delta.get_seconds().into(), @@ -145,7 +150,7 @@ impl ToPyObject for NaiveDate { let DateArgs { year, month, day } = self.into(); #[cfg(not(Py_LIMITED_API))] { - PyDate::new(py, year, month, day) + PyDate::new_bound(py, year, month, day) .expect("failed to construct date") .into() } @@ -166,10 +171,10 @@ impl IntoPy for NaiveDate { } impl FromPyObject<'_> for NaiveDate { - fn extract(ob: &PyAny) -> PyResult { + fn extract_bound(ob: &Bound<'_, PyAny>) -> PyResult { #[cfg(not(Py_LIMITED_API))] { - let date: &PyDate = ob.downcast()?; + let date = ob.downcast::()?; py_date_to_naive_date(date) } #[cfg(Py_LIMITED_API)] @@ -190,15 +195,16 @@ impl ToPyObject for NaiveTime { truncated_leap_second, } = self.into(); #[cfg(not(Py_LIMITED_API))] - let time = PyTime::new(py, hour, min, sec, micro, None).expect("Failed to construct time"); + let time = + PyTime::new_bound(py, hour, min, sec, micro, None).expect("Failed to construct time"); #[cfg(Py_LIMITED_API)] let time = DatetimeTypes::get(py) .time - .as_ref(py) + .bind(py) .call1((hour, min, sec, micro)) .expect("failed to construct datetime.time"); if truncated_leap_second { - warn_truncated_leap_second(time); + warn_truncated_leap_second(&time); } time.into() } @@ -211,10 +217,10 @@ impl IntoPy for NaiveTime { } impl FromPyObject<'_> for NaiveTime { - fn extract(ob: &PyAny) -> PyResult { + fn extract_bound(ob: &Bound<'_, PyAny>) -> PyResult { #[cfg(not(Py_LIMITED_API))] { - let time: &PyTime = ob.downcast()?; + let time = ob.downcast::()?; py_time_to_naive_time(time) } #[cfg(Py_LIMITED_API)] @@ -238,9 +244,9 @@ impl IntoPy for NaiveDateTime { } impl FromPyObject<'_> for NaiveDateTime { - fn extract(dt: &PyAny) -> PyResult { + fn extract_bound(dt: &Bound<'_, PyAny>) -> PyResult { #[cfg(not(Py_LIMITED_API))] - let dt: &PyDateTime = dt.downcast()?; + let dt = dt.downcast::()?; #[cfg(Py_LIMITED_API)] check_type(dt, &DatetimeTypes::get(dt.py()).datetime, "PyDateTime")?; @@ -248,7 +254,7 @@ impl FromPyObject<'_> for NaiveDateTime { // we return a hard error. We could silently remove tzinfo, or assume local timezone // and do a conversion, but better leave this decision to the user of the library. #[cfg(not(Py_LIMITED_API))] - let has_tzinfo = dt.get_tzinfo().is_some(); + let has_tzinfo = dt.get_tzinfo_bound().is_some(); #[cfg(Py_LIMITED_API)] let has_tzinfo = !dt.getattr(intern!(dt.py(), "tzinfo"))?.is_none(); if has_tzinfo { @@ -265,7 +271,7 @@ impl ToPyObject for DateTime { // FIXME: convert to better timezone representation here than just convert to fixed offset // See https://github.com/PyO3/pyo3/issues/3266 let tz = self.offset().fix().to_object(py); - let tz = tz.downcast(py).unwrap(); + let tz = tz.bind(py).downcast().unwrap(); naive_datetime_to_py_datetime(py, &self.naive_local(), Some(tz)) } } @@ -276,17 +282,17 @@ impl IntoPy for DateTime { } } -impl FromPyObject<'a>> FromPyObject<'_> for DateTime { - fn extract(dt: &PyAny) -> PyResult> { +impl FromPyObject<'py>> FromPyObject<'_> for DateTime { + fn extract_bound(dt: &Bound<'_, PyAny>) -> PyResult> { #[cfg(not(Py_LIMITED_API))] - let dt: &PyDateTime = dt.downcast()?; + let dt = dt.downcast::()?; #[cfg(Py_LIMITED_API)] check_type(dt, &DatetimeTypes::get(dt.py()).datetime, "PyDateTime")?; #[cfg(not(Py_LIMITED_API))] - let tzinfo = dt.get_tzinfo(); + let tzinfo = dt.get_tzinfo_bound(); #[cfg(Py_LIMITED_API)] - let tzinfo: Option<&PyAny> = dt.getattr(intern!(dt.py(), "tzinfo"))?.extract()?; + let tzinfo: Option> = dt.getattr(intern!(dt.py(), "tzinfo"))?.extract()?; let tz = if let Some(tzinfo) = tzinfo { tzinfo.extract()? @@ -311,9 +317,9 @@ impl ToPyObject for FixedOffset { #[cfg(not(Py_LIMITED_API))] { - let td = PyDelta::new(py, 0, seconds_offset, 0, true) + let td = PyDelta::new_bound(py, 0, seconds_offset, 0, true) .expect("failed to construct timedelta"); - timezone_from_offset(py, td) + timezone_from_offset(&td) .expect("Failed to construct PyTimezone") .into() } @@ -339,9 +345,9 @@ impl FromPyObject<'_> for FixedOffset { /// /// Note that the conversion will result in precision lost in microseconds as chrono offset /// does not supports microseconds. - fn extract(ob: &PyAny) -> PyResult { + fn extract_bound(ob: &Bound<'_, PyAny>) -> PyResult { #[cfg(not(Py_LIMITED_API))] - let ob: &PyTzInfo = ob.extract()?; + let ob = ob.downcast::()?; #[cfg(Py_LIMITED_API)] check_type(ob, &DatetimeTypes::get(ob.py()).tzinfo, "PyTzInfo")?; @@ -367,7 +373,7 @@ impl FromPyObject<'_> for FixedOffset { impl ToPyObject for Utc { fn to_object(&self, py: Python<'_>) -> PyObject { - timezone_utc(py).into() + timezone_utc_bound(py).into() } } @@ -378,8 +384,8 @@ impl IntoPy for Utc { } impl FromPyObject<'_> for Utc { - fn extract(ob: &PyAny) -> PyResult { - let py_utc = timezone_utc(ob.py()); + fn extract_bound(ob: &Bound<'_, PyAny>) -> PyResult { + let py_utc = timezone_utc_bound(ob.py()); if ob.eq(py_utc)? { Ok(Utc) } else { @@ -431,8 +437,8 @@ impl From<&NaiveTime> for TimeArgs { fn naive_datetime_to_py_datetime( py: Python<'_>, naive_datetime: &NaiveDateTime, - #[cfg(not(Py_LIMITED_API))] tzinfo: Option<&PyTzInfo>, - #[cfg(Py_LIMITED_API)] tzinfo: Option<&PyAny>, + #[cfg(not(Py_LIMITED_API))] tzinfo: Option<&Bound<'_, PyTzInfo>>, + #[cfg(Py_LIMITED_API)] tzinfo: Option<&Bound<'_, PyAny>>, ) -> PyObject { let DateArgs { year, month, day } = (&naive_datetime.date()).into(); let TimeArgs { @@ -443,29 +449,29 @@ fn naive_datetime_to_py_datetime( truncated_leap_second, } = (&naive_datetime.time()).into(); #[cfg(not(Py_LIMITED_API))] - let datetime = PyDateTime::new(py, year, month, day, hour, min, sec, micro, tzinfo) + let datetime = PyDateTime::new_bound(py, year, month, day, hour, min, sec, micro, tzinfo) .expect("failed to construct datetime"); #[cfg(Py_LIMITED_API)] let datetime = DatetimeTypes::get(py) .datetime - .as_ref(py) + .bind(py) .call1((year, month, day, hour, min, sec, micro, tzinfo)) .expect("failed to construct datetime.datetime"); if truncated_leap_second { - warn_truncated_leap_second(datetime); + warn_truncated_leap_second(&datetime); } datetime.into() } -fn warn_truncated_leap_second(obj: &PyAny) { +fn warn_truncated_leap_second(obj: &Bound<'_, PyAny>) { let py = obj.py(); - if let Err(e) = PyErr::warn( + if let Err(e) = PyErr::warn_bound( py, - py.get_type::(), + &py.get_type_bound::(), "ignored leap-second, `datetime` does not support leap-seconds", 0, ) { - e.write_unraisable(py, Some(obj)) + e.write_unraisable_bound(py, Some(&obj.as_borrowed())) }; } @@ -480,7 +486,7 @@ fn py_date_to_naive_date(py_date: &impl PyDateAccess) -> PyResult { } #[cfg(Py_LIMITED_API)] -fn py_date_to_naive_date(py_date: &PyAny) -> PyResult { +fn py_date_to_naive_date(py_date: &Bound<'_, PyAny>) -> PyResult { NaiveDate::from_ymd_opt( py_date.getattr(intern!(py_date.py(), "year"))?.extract()?, py_date.getattr(intern!(py_date.py(), "month"))?.extract()?, @@ -501,7 +507,7 @@ fn py_time_to_naive_time(py_time: &impl PyTimeAccess) -> PyResult { } #[cfg(Py_LIMITED_API)] -fn py_time_to_naive_time(py_time: &PyAny) -> PyResult { +fn py_time_to_naive_time(py_time: &Bound<'_, PyAny>) -> PyResult { NaiveTime::from_hms_micro_opt( py_time.getattr(intern!(py_time.py(), "hour"))?.extract()?, py_time @@ -518,9 +524,9 @@ fn py_time_to_naive_time(py_time: &PyAny) -> PyResult { } #[cfg(Py_LIMITED_API)] -fn check_type(value: &PyAny, t: &PyObject, type_name: &'static str) -> PyResult<()> { - if !value.is_instance(t.as_ref(value.py()))? { - return Err(PyDowncastError::new(value, type_name).into()); +fn check_type(value: &Bound<'_, PyAny>, t: &PyObject, type_name: &'static str) -> PyResult<()> { + if !value.is_instance(t.bind(value.py()))? { + return Err(DowncastError::new(value, type_name).into()); } Ok(()) } @@ -542,15 +548,15 @@ impl DatetimeTypes { static TYPES: GILOnceCell = GILOnceCell::new(); TYPES .get_or_try_init(py, || { - let datetime = py.import("datetime")?; + let datetime = py.import_bound("datetime")?; let timezone = datetime.getattr("timezone")?; Ok::<_, PyErr>(Self { date: datetime.getattr("date")?.into(), datetime: datetime.getattr("datetime")?.into(), time: datetime.getattr("time")?.into(), timedelta: datetime.getattr("timedelta")?.into(), - timezone: timezone.into(), timezone_utc: timezone.getattr("utc")?.into(), + timezone: timezone.into(), tzinfo: datetime.getattr("tzinfo")?.into(), }) }) @@ -559,8 +565,8 @@ impl DatetimeTypes { } #[cfg(Py_LIMITED_API)] -fn timezone_utc(py: Python<'_>) -> &PyAny { - DatetimeTypes::get(py).timezone_utc.as_ref(py) +fn timezone_utc_bound(py: Python<'_>) -> Bound<'_, PyAny> { + DatetimeTypes::get(py).timezone_utc.bind(py).clone() } #[cfg(test)] @@ -575,19 +581,22 @@ mod tests { // tzdata there to make this work. #[cfg(all(Py_3_9, not(target_os = "windows")))] fn test_zoneinfo_is_not_fixed_offset() { + use crate::types::any::PyAnyMethods; + use crate::types::dict::PyDictMethods; + Python::with_gil(|py| { - let locals = crate::types::PyDict::new(py); - py.run( + let locals = crate::types::PyDict::new_bound(py); + py.run_bound( "import zoneinfo; zi = zoneinfo.ZoneInfo('Europe/London')", None, - Some(locals), + Some(&locals), ) .unwrap(); let result: PyResult = locals.get_item("zi").unwrap().unwrap().extract(); assert!(result.is_err()); let res = result.err().unwrap(); // Also check the error message is what we expect - let msg = res.value(py).repr().unwrap().to_string(); + let msg = res.value_bound(py).repr().unwrap().to_string(); assert_eq!(msg, "TypeError(\"zoneinfo.ZoneInfo(key='Europe/London') is not a fixed offset timezone\")"); }); } @@ -602,7 +611,7 @@ mod tests { // Now test that converting a PyDateTime with tzinfo to a NaiveDateTime fails let res: PyResult = py_datetime.extract(); assert_eq!( - res.unwrap_err().value(py).repr().unwrap().to_string(), + res.unwrap_err().value_bound(py).repr().unwrap().to_string(), "TypeError('expected a datetime without tzinfo')" ); }); @@ -617,14 +626,14 @@ mod tests { // Now test that converting a PyDateTime with tzinfo to a NaiveDateTime fails let res: PyResult> = py_datetime.extract(); assert_eq!( - res.unwrap_err().value(py).repr().unwrap().to_string(), + res.unwrap_err().value_bound(py).repr().unwrap().to_string(), "TypeError('expected a datetime with non-None tzinfo')" ); // Now test that converting a PyDateTime with tzinfo to a NaiveDateTime fails let res: PyResult> = py_datetime.extract(); assert_eq!( - res.unwrap_err().value(py).repr().unwrap().to_string(), + res.unwrap_err().value_bound(py).repr().unwrap().to_string(), "TypeError('expected a datetime with non-None tzinfo')" ); }); @@ -635,7 +644,7 @@ mod tests { // Test that if a user tries to convert a python's timezone aware datetime into a naive // one, the conversion fails. Python::with_gil(|py| { - let none = py.None(); + let none = py.None().into_bound(py); assert_eq!( none.extract::().unwrap_err().to_string(), "TypeError: 'NoneType' object cannot be converted to 'PyDelta'" @@ -682,7 +691,7 @@ mod tests { let delta = delta.to_object(py); let py_delta = new_py_datetime_ob(py, "timedelta", (py_days, py_seconds, py_ms)); assert!( - delta.as_ref(py).eq(py_delta).unwrap(), + delta.bind(py).eq(&py_delta).unwrap(), "{}: {} != {}", name, delta, @@ -778,7 +787,7 @@ mod tests { .to_object(py); let py_date = new_py_datetime_ob(py, "date", (year, month, day)); assert_eq!( - date.as_ref(py).compare(py_date).unwrap(), + date.bind(py).compare(&py_date).unwrap(), Ordering::Equal, "{}: {} != {}", name, @@ -837,7 +846,7 @@ mod tests { ), ); assert_eq!( - datetime.as_ref(py).compare(py_datetime).unwrap(), + datetime.bind(py).compare(&py_datetime).unwrap(), Ordering::Equal, "{}: {} != {}", name, @@ -879,7 +888,7 @@ mod tests { (year, month, day, hour, minute, second, py_ms, py_tz), ); assert_eq!( - datetime.as_ref(py).compare(py_datetime).unwrap(), + datetime.bind(py).compare(&py_datetime).unwrap(), Ordering::Equal, "{}: {} != {}", name, @@ -911,7 +920,7 @@ mod tests { let minute = 8; let second = 9; let micro = 999_999; - let tz_utc = timezone_utc(py); + let tz_utc = timezone_utc_bound(py); let py_datetime = new_py_datetime_ob( py, "datetime", @@ -979,13 +988,13 @@ mod tests { let td = new_py_datetime_ob(py, "timedelta", (0, 3600, 0)); let py_timedelta = new_py_datetime_ob(py, "timezone", (td,)); // Should be equal - assert!(offset.as_ref(py).eq(py_timedelta).unwrap()); + assert!(offset.bind(py).eq(py_timedelta).unwrap()); // Same but with negative values let offset = FixedOffset::east_opt(-3600).unwrap().to_object(py); let td = new_py_datetime_ob(py, "timedelta", (0, -3600, 0)); let py_timedelta = new_py_datetime_ob(py, "timezone", (td,)); - assert!(offset.as_ref(py).eq(py_timedelta).unwrap()); + assert!(offset.bind(py).eq(py_timedelta).unwrap()); }) } @@ -1004,7 +1013,7 @@ mod tests { Python::with_gil(|py| { let utc = Utc.to_object(py); let py_utc = python_utc(py); - assert!(utc.as_ref(py).is(py_utc)); + assert!(utc.bind(py).is(&py_utc)); }) } @@ -1035,7 +1044,7 @@ mod tests { .to_object(py); let py_time = new_py_datetime_ob(py, "time", (hour, minute, second, py_ms)); assert!( - time.as_ref(py).eq(py_time).unwrap(), + time.bind(py).eq(&py_time).unwrap(), "{}: {} != {}", name, time, @@ -1070,12 +1079,12 @@ mod tests { }) } - fn new_py_datetime_ob<'a>( - py: Python<'a>, + fn new_py_datetime_ob<'py>( + py: Python<'py>, name: &str, args: impl IntoPy>, - ) -> &'a PyAny { - py.import("datetime") + ) -> Bound<'py, PyAny> { + py.import_bound("datetime") .unwrap() .getattr(name) .unwrap() @@ -1083,8 +1092,8 @@ mod tests { .unwrap() } - fn python_utc(py: Python<'_>) -> &PyAny { - py.import("datetime") + fn python_utc(py: Python<'_>) -> Bound<'_, PyAny> { + py.import_bound("datetime") .unwrap() .getattr("timezone") .unwrap() @@ -1106,9 +1115,9 @@ mod tests { fn test_pyo3_offset_fixed_frompyobject_created_in_python(timestamp in 0..(i32::MAX as i64), timedelta in -86399i32..=86399i32) { Python::with_gil(|py| { - let globals = [("datetime", py.import("datetime").unwrap())].into_py_dict(py); + let globals = [("datetime", py.import_bound("datetime").unwrap())].into_py_dict_bound(py); let code = format!("datetime.datetime.fromtimestamp({}).replace(tzinfo=datetime.timezone(datetime.timedelta(seconds={})))", timestamp, timedelta); - let t = py.eval(&code, Some(globals), None).unwrap(); + let t = py.eval_bound(&code, Some(&globals), None).unwrap(); // Get ISO 8601 string from python let py_iso_str = t.call_method0("isoformat").unwrap(); diff --git a/src/conversions/chrono_tz.rs b/src/conversions/chrono_tz.rs new file mode 100644 index 00000000000..845814c4dab --- /dev/null +++ b/src/conversions/chrono_tz.rs @@ -0,0 +1,122 @@ +#![cfg(all(Py_3_9, feature = "chrono-tz"))] + +//! Conversions to and from [chrono-tz](https://docs.rs/chrono-tz/)’s `Tz`. +//! +//! This feature requires at least Python 3.9. +//! +//! # Setup +//! +//! To use this feature, add this to your **`Cargo.toml`**: +//! +//! ```toml +//! [dependencies] +//! chrono-tz = "0.8" +#![doc = concat!("pyo3 = { version = \"", env!("CARGO_PKG_VERSION"), "\", features = [\"chrono-tz\"] }")] +//! ``` +//! +//! Note that you must use compatible versions of chrono, chrono-tz and PyO3. +//! The required chrono version may vary based on the version of PyO3. +//! +//! # Example: Convert a `zoneinfo.ZoneInfo` to chrono-tz's `Tz` +//! +//! ```rust,no_run +//! use chrono_tz::Tz; +//! use pyo3::{Python, ToPyObject}; +//! +//! fn main() { +//! pyo3::prepare_freethreaded_python(); +//! Python::with_gil(|py| { +//! // Convert to Python +//! let py_tzinfo = Tz::Europe__Paris.to_object(py); +//! // Convert back to Rust +//! assert_eq!(py_tzinfo.extract::(py).unwrap(), Tz::Europe__Paris); +//! }); +//! } +//! ``` +use crate::exceptions::PyValueError; +use crate::pybacked::PyBackedStr; +use crate::sync::GILOnceCell; +use crate::types::{any::PyAnyMethods, PyType}; +use crate::{ + intern, Bound, FromPyObject, IntoPy, Py, PyAny, PyObject, PyResult, Python, ToPyObject, +}; +use chrono_tz::Tz; +use std::str::FromStr; + +impl ToPyObject for Tz { + fn to_object(&self, py: Python<'_>) -> PyObject { + static ZONE_INFO: GILOnceCell> = GILOnceCell::new(); + ZONE_INFO + .get_or_try_init_type_ref(py, "zoneinfo", "ZoneInfo") + .unwrap() + .call1((self.name(),)) + .unwrap() + .unbind() + } +} + +impl IntoPy for Tz { + fn into_py(self, py: Python<'_>) -> PyObject { + self.to_object(py) + } +} + +impl FromPyObject<'_> for Tz { + fn extract_bound(ob: &Bound<'_, PyAny>) -> PyResult { + Tz::from_str( + &ob.getattr(intern!(ob.py(), "key"))? + .extract::()?, + ) + .map_err(|e| PyValueError::new_err(e.to_string())) + } +} + +#[cfg(all(test, not(windows)))] // Troubles loading timezones on Windows +mod tests { + use super::*; + + #[test] + fn test_frompyobject() { + Python::with_gil(|py| { + assert_eq!( + new_zoneinfo(py, "Europe/Paris").extract::().unwrap(), + Tz::Europe__Paris + ); + assert_eq!(new_zoneinfo(py, "UTC").extract::().unwrap(), Tz::UTC); + assert_eq!( + new_zoneinfo(py, "Etc/GMT-5").extract::().unwrap(), + Tz::Etc__GMTMinus5 + ); + }); + } + + #[test] + fn test_topyobject() { + Python::with_gil(|py| { + let assert_eq = |l: PyObject, r: Bound<'_, PyAny>| { + assert!(l.bind(py).eq(r).unwrap()); + }; + + assert_eq( + Tz::Europe__Paris.to_object(py), + new_zoneinfo(py, "Europe/Paris"), + ); + assert_eq(Tz::UTC.to_object(py), new_zoneinfo(py, "UTC")); + assert_eq( + Tz::Etc__GMTMinus5.to_object(py), + new_zoneinfo(py, "Etc/GMT-5"), + ); + }); + } + + fn new_zoneinfo<'py>(py: Python<'py>, name: &str) -> Bound<'py, PyAny> { + zoneinfo_class(py).call1((name,)).unwrap() + } + + fn zoneinfo_class(py: Python<'_>) -> Bound<'_, PyAny> { + py.import_bound("zoneinfo") + .unwrap() + .getattr("ZoneInfo") + .unwrap() + } +} diff --git a/src/conversions/either.rs b/src/conversions/either.rs index 4a41d2bd52f..84ec88ea009 100644 --- a/src/conversions/either.rs +++ b/src/conversions/either.rs @@ -43,9 +43,11 @@ //! //! [either](https://docs.rs/either/ "A library for easy idiomatic error handling and reporting in Rust applications")’s +#[cfg(feature = "experimental-inspect")] +use crate::inspect::types::TypeInfo; use crate::{ - exceptions::PyTypeError, inspect::types::TypeInfo, FromPyObject, IntoPy, PyAny, PyObject, - PyResult, Python, ToPyObject, + exceptions::PyTypeError, types::any::PyAnyMethods, Bound, FromPyObject, IntoPy, PyAny, + PyObject, PyResult, Python, ToPyObject, }; use either::Either; @@ -80,23 +82,30 @@ where } #[cfg_attr(docsrs, doc(cfg(feature = "either")))] -impl<'source, L, R> FromPyObject<'source> for Either +impl<'py, L, R> FromPyObject<'py> for Either where - L: FromPyObject<'source>, - R: FromPyObject<'source>, + L: FromPyObject<'py>, + R: FromPyObject<'py>, { #[inline] - fn extract(obj: &'source PyAny) -> PyResult { + fn extract_bound(obj: &Bound<'py, PyAny>) -> PyResult { if let Ok(l) = obj.extract::() { Ok(Either::Left(l)) } else if let Ok(r) = obj.extract::() { Ok(Either::Right(r)) } else { - let err_msg = format!("failed to convert the value to '{}'", Self::type_input()); + // TODO: it might be nice to use the `type_input()` name here once `type_input` + // is not experimental, rather than the Rust type names. + let err_msg = format!( + "failed to convert the value to 'Union[{}, {}]'", + std::any::type_name::(), + std::any::type_name::() + ); Err(PyTypeError::new_err(err_msg)) } } + #[cfg(feature = "experimental-inspect")] fn type_input() -> TypeInfo { TypeInfo::union_of(&[L::type_input(), R::type_input()]) } @@ -104,6 +113,8 @@ where #[cfg(test)] mod tests { + use std::borrow::Cow; + use crate::exceptions::PyTypeError; use crate::{Python, ToPyObject}; @@ -123,7 +134,7 @@ mod tests { let r = E::Right("foo".to_owned()); let obj_r = r.to_object(py); - assert_eq!(obj_r.extract::<&str>(py).unwrap(), "foo"); + assert_eq!(obj_r.extract::>(py).unwrap(), "foo"); assert_eq!(obj_r.extract::(py).unwrap(), r); let obj_s = "foo".to_object(py); @@ -131,7 +142,7 @@ mod tests { assert!(err.is_instance_of::(py)); assert_eq!( err.to_string(), - "TypeError: failed to convert the value to 'Union[int, float]'" + "TypeError: failed to convert the value to 'Union[i32, f32]'" ); let obj_i = 42.to_object(py); diff --git a/src/conversions/eyre.rs b/src/conversions/eyre.rs index f881599e2c3..d4704e411c5 100644 --- a/src/conversions/eyre.rs +++ b/src/conversions/eyre.rs @@ -34,7 +34,6 @@ //! //! ```rust //! use pyo3::prelude::*; -//! use pyo3::wrap_pyfunction; //! use std::path::PathBuf; //! //! // A wrapper around a Rust function. @@ -47,7 +46,7 @@ //! //! fn main() { //! let error = Python::with_gil(|py| -> PyResult> { -//! let fun = wrap_pyfunction!(py_open, py)?; +//! let fun = wrap_pyfunction_bound!(py_open, py)?; //! let text = fun.call1(("foo.txt",))?.extract::>()?; //! Ok(text) //! }).unwrap_err(); @@ -74,9 +73,9 @@ //! // could call inside an application... //! // This might return a `PyErr`. //! let res = Python::with_gil(|py| { -//! let zlib = PyModule::import(py, "zlib")?; +//! let zlib = PyModule::import_bound(py, "zlib")?; //! let decompress = zlib.getattr("decompress")?; -//! let bytes = PyBytes::new(py, bytes); +//! let bytes = PyBytes::new_bound(py, bytes); //! let value = decompress.call1((bytes,))?; //! value.extract::>() //! })?; @@ -152,9 +151,9 @@ mod tests { let pyerr = PyErr::from(err); Python::with_gil(|py| { - let locals = [("err", pyerr)].into_py_dict(py); - let pyerr = py.run("raise err", None, Some(locals)).unwrap_err(); - assert_eq!(pyerr.value(py).to_string(), expected_contents); + let locals = [("err", pyerr)].into_py_dict_bound(py); + let pyerr = py.run_bound("raise err", None, Some(&locals)).unwrap_err(); + assert_eq!(pyerr.value_bound(py).to_string(), expected_contents); }) } @@ -169,9 +168,9 @@ mod tests { let pyerr = PyErr::from(err); Python::with_gil(|py| { - let locals = [("err", pyerr)].into_py_dict(py); - let pyerr = py.run("raise err", None, Some(locals)).unwrap_err(); - assert_eq!(pyerr.value(py).to_string(), expected_contents); + let locals = [("err", pyerr)].into_py_dict_bound(py); + let pyerr = py.run_bound("raise err", None, Some(&locals)).unwrap_err(); + assert_eq!(pyerr.value_bound(py).to_string(), expected_contents); }) } diff --git a/src/conversions/hashbrown.rs b/src/conversions/hashbrown.rs index 62a7e87b804..9eea7734bfc 100644 --- a/src/conversions/hashbrown.rs +++ b/src/conversions/hashbrown.rs @@ -17,9 +17,12 @@ //! Note that you must use compatible versions of hashbrown and PyO3. //! The required hashbrown version may vary based on the version of PyO3. use crate::{ - types::set::new_from_iter, + types::any::PyAnyMethods, + types::dict::PyDictMethods, + types::frozenset::PyFrozenSetMethods, + types::set::{new_from_iter, PySetMethods}, types::{IntoPyDict, PyDict, PyFrozenSet, PySet}, - FromPyObject, IntoPy, PyAny, PyErr, PyObject, PyResult, Python, ToPyObject, + Bound, FromPyObject, IntoPy, PyAny, PyErr, PyObject, PyResult, Python, ToPyObject, }; use std::{cmp, hash}; @@ -30,7 +33,7 @@ where H: hash::BuildHasher, { fn to_object(&self, py: Python<'_>) -> PyObject { - IntoPyDict::into_py_dict(self, py).into() + IntoPyDict::into_py_dict_bound(self, py).into() } } @@ -44,21 +47,21 @@ where let iter = self .into_iter() .map(|(k, v)| (k.into_py(py), v.into_py(py))); - IntoPyDict::into_py_dict(iter, py).into() + IntoPyDict::into_py_dict_bound(iter, py).into() } } -impl<'source, K, V, S> FromPyObject<'source> for hashbrown::HashMap +impl<'py, K, V, S> FromPyObject<'py> for hashbrown::HashMap where - K: FromPyObject<'source> + cmp::Eq + hash::Hash, - V: FromPyObject<'source>, + K: FromPyObject<'py> + cmp::Eq + hash::Hash, + V: FromPyObject<'py>, S: hash::BuildHasher + Default, { - fn extract(ob: &'source PyAny) -> Result { - let dict: &PyDict = ob.downcast()?; + fn extract_bound(ob: &Bound<'py, PyAny>) -> Result { + let dict = ob.downcast::()?; let mut ret = hashbrown::HashMap::with_capacity_and_hasher(dict.len(), S::default()); for (k, v) in dict { - ret.insert(K::extract(k)?, V::extract(v)?); + ret.insert(k.extract()?, v.extract()?); } Ok(ret) } @@ -87,17 +90,17 @@ where } } -impl<'source, K, S> FromPyObject<'source> for hashbrown::HashSet +impl<'py, K, S> FromPyObject<'py> for hashbrown::HashSet where - K: FromPyObject<'source> + cmp::Eq + hash::Hash, + K: FromPyObject<'py> + cmp::Eq + hash::Hash, S: hash::BuildHasher + Default, { - fn extract(ob: &'source PyAny) -> PyResult { + fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult { match ob.downcast::() { - Ok(set) => set.iter().map(K::extract).collect(), + Ok(set) => set.iter().map(|any| any.extract()).collect(), Err(err) => { if let Ok(frozen_set) = ob.downcast::() { - frozen_set.iter().map(K::extract).collect() + frozen_set.iter().map(|any| any.extract()).collect() } else { Err(PyErr::from(err)) } @@ -117,7 +120,7 @@ mod tests { map.insert(1, 1); let m = map.to_object(py); - let py_map: &PyDict = m.downcast(py).unwrap(); + let py_map = m.downcast_bound::(py).unwrap(); assert!(py_map.len() == 1); assert!( @@ -139,7 +142,7 @@ mod tests { map.insert(1, 1); let m: PyObject = map.into_py(py); - let py_map: &PyDict = m.downcast(py).unwrap(); + let py_map = m.downcast_bound::(py).unwrap(); assert!(py_map.len() == 1); assert!( @@ -160,7 +163,7 @@ mod tests { let mut map = hashbrown::HashMap::::new(); map.insert(1, 1); - let py_map = map.into_py_dict(py); + let py_map = map.into_py_dict_bound(py); assert_eq!(py_map.len(), 1); assert_eq!( @@ -178,11 +181,11 @@ mod tests { #[test] fn test_extract_hashbrown_hashset() { Python::with_gil(|py| { - let set = PySet::new(py, &[1, 2, 3, 4, 5]).unwrap(); + let set = PySet::new_bound(py, &[1, 2, 3, 4, 5]).unwrap(); let hash_set: hashbrown::HashSet = set.extract().unwrap(); assert_eq!(hash_set, [1, 2, 3, 4, 5].iter().copied().collect()); - let set = PyFrozenSet::new(py, &[1, 2, 3, 4, 5]).unwrap(); + let set = PyFrozenSet::new_bound(py, &[1, 2, 3, 4, 5]).unwrap(); let hash_set: hashbrown::HashSet = set.extract().unwrap(); assert_eq!(hash_set, [1, 2, 3, 4, 5].iter().copied().collect()); }); diff --git a/src/conversions/indexmap.rs b/src/conversions/indexmap.rs index 7c7303e6d83..fdbe057f32d 100644 --- a/src/conversions/indexmap.rs +++ b/src/conversions/indexmap.rs @@ -71,7 +71,7 @@ //! } //! //! #[pymodule] -//! fn my_module(_py: Python<'_>, m: &PyModule) -> PyResult<()> { +//! fn my_module(m: &Bound<'_, PyModule>) -> PyResult<()> { //! m.add_function(wrap_pyfunction!(calculate_statistics, m)?)?; //! Ok(()) //! } @@ -88,7 +88,7 @@ //! ``` use crate::types::*; -use crate::{FromPyObject, IntoPy, PyErr, PyObject, Python, ToPyObject}; +use crate::{Bound, FromPyObject, IntoPy, PyErr, PyObject, Python, ToPyObject}; use std::{cmp, hash}; impl ToPyObject for indexmap::IndexMap @@ -98,7 +98,7 @@ where H: hash::BuildHasher, { fn to_object(&self, py: Python<'_>) -> PyObject { - IntoPyDict::into_py_dict(self, py).into() + IntoPyDict::into_py_dict_bound(self, py).into() } } @@ -112,21 +112,21 @@ where let iter = self .into_iter() .map(|(k, v)| (k.into_py(py), v.into_py(py))); - IntoPyDict::into_py_dict(iter, py).into() + IntoPyDict::into_py_dict_bound(iter, py).into() } } -impl<'source, K, V, S> FromPyObject<'source> for indexmap::IndexMap +impl<'py, K, V, S> FromPyObject<'py> for indexmap::IndexMap where - K: FromPyObject<'source> + cmp::Eq + hash::Hash, - V: FromPyObject<'source>, + K: FromPyObject<'py> + cmp::Eq + hash::Hash, + V: FromPyObject<'py>, S: hash::BuildHasher + Default, { - fn extract(ob: &'source PyAny) -> Result { - let dict: &PyDict = ob.downcast()?; + fn extract_bound(ob: &Bound<'py, PyAny>) -> Result { + let dict = ob.downcast::()?; let mut ret = indexmap::IndexMap::with_capacity_and_hasher(dict.len(), S::default()); for (k, v) in dict { - ret.insert(K::extract(k)?, V::extract(v)?); + ret.insert(k.extract()?, v.extract()?); } Ok(ret) } @@ -145,7 +145,7 @@ mod test_indexmap { map.insert(1, 1); let m = map.to_object(py); - let py_map: &PyDict = m.downcast(py).unwrap(); + let py_map = m.downcast_bound::(py).unwrap(); assert!(py_map.len() == 1); assert!( @@ -171,7 +171,7 @@ mod test_indexmap { map.insert(1, 1); let m: PyObject = map.into_py(py); - let py_map: &PyDict = m.downcast(py).unwrap(); + let py_map = m.downcast_bound::(py).unwrap(); assert!(py_map.len() == 1); assert!( @@ -192,7 +192,7 @@ mod test_indexmap { let mut map = indexmap::IndexMap::::new(); map.insert(1, 1); - let py_map = map.into_py_dict(py); + let py_map = map.into_py_dict_bound(py); assert_eq!(py_map.len(), 1); assert_eq!( @@ -221,7 +221,7 @@ mod test_indexmap { } } - let py_map = map.clone().into_py_dict(py); + let py_map = map.clone().into_py_dict_bound(py); let trip_map = py_map.extract::>().unwrap(); diff --git a/src/conversions/mod.rs b/src/conversions/mod.rs index 680ad9be6d5..53ecf849c07 100644 --- a/src/conversions/mod.rs +++ b/src/conversions/mod.rs @@ -2,12 +2,14 @@ pub mod anyhow; pub mod chrono; +pub mod chrono_tz; pub mod either; pub mod eyre; pub mod hashbrown; pub mod indexmap; pub mod num_bigint; pub mod num_complex; +pub mod num_rational; pub mod rust_decimal; pub mod serde; pub mod smallvec; diff --git a/src/conversions/num_bigint.rs b/src/conversions/num_bigint.rs index 5cc2157d446..196ae28e788 100644 --- a/src/conversions/num_bigint.rs +++ b/src/conversions/num_bigint.rs @@ -31,7 +31,7 @@ //! } //! //! #[pymodule] -//! fn my_module(_py: Python<'_>, m: &PyModule) -> PyResult<()> { +//! fn my_module(m: &Bound<'_, PyModule>) -> PyResult<()> { //! m.add_function(wrap_pyfunction!(add_one, m)?)?; //! Ok(()) //! } @@ -47,8 +47,15 @@ //! assert n + 1 == value //! ``` +#[cfg(not(Py_LIMITED_API))] +use crate::ffi_ptr_ext::FfiPtrExt; +#[cfg(Py_LIMITED_API)] +use crate::types::{bytes::PyBytesMethods, PyBytes}; use crate::{ - ffi, types::*, FromPyObject, IntoPy, Py, PyAny, PyObject, PyResult, Python, ToPyObject, + ffi, + instance::Bound, + types::{any::PyAnyMethods, PyLong}, + FromPyObject, IntoPy, Py, PyAny, PyObject, PyResult, Python, ToPyObject, }; use num_bigint::{BigInt, BigUint}; @@ -58,36 +65,63 @@ use num_bigint::Sign; // for identical functionality between BigInt and BigUint macro_rules! bigint_conversion { - ($rust_ty: ty, $is_signed: expr, $to_bytes: path) => { + ($rust_ty: ty, $is_signed: literal, $to_bytes: path) => { #[cfg_attr(docsrs, doc(cfg(feature = "num-bigint")))] impl ToPyObject for $rust_ty { #[cfg(not(Py_LIMITED_API))] fn to_object(&self, py: Python<'_>) -> PyObject { let bytes = $to_bytes(self); - unsafe { - let obj = ffi::_PyLong_FromByteArray( - bytes.as_ptr().cast(), - bytes.len(), - 1, - $is_signed, - ); - PyObject::from_owned_ptr(py, obj) + #[cfg(not(Py_3_13))] + { + unsafe { + ffi::_PyLong_FromByteArray( + bytes.as_ptr().cast(), + bytes.len(), + 1, + $is_signed.into(), + ) + .assume_owned(py) + .unbind() + } + } + #[cfg(Py_3_13)] + { + if $is_signed { + unsafe { + ffi::PyLong_FromNativeBytes( + bytes.as_ptr().cast(), + bytes.len(), + ffi::Py_ASNATIVEBYTES_LITTLE_ENDIAN, + ) + .assume_owned(py) + } + } else { + unsafe { + ffi::PyLong_FromUnsignedNativeBytes( + bytes.as_ptr().cast(), + bytes.len(), + ffi::Py_ASNATIVEBYTES_LITTLE_ENDIAN, + ) + .assume_owned(py) + } + } + .unbind() } } #[cfg(Py_LIMITED_API)] fn to_object(&self, py: Python<'_>) -> PyObject { let bytes = $to_bytes(self); - let bytes_obj = PyBytes::new(py, &bytes); - let kwargs = if $is_signed > 0 { - let kwargs = PyDict::new(py); + let bytes_obj = PyBytes::new_bound(py, &bytes); + let kwargs = if $is_signed { + let kwargs = crate::types::PyDict::new_bound(py); kwargs.set_item(crate::intern!(py, "signed"), true).unwrap(); Some(kwargs) } else { None }; - py.get_type::() - .call_method("from_bytes", (bytes_obj, "little"), kwargs) + py.get_type_bound::() + .call_method("from_bytes", (bytes_obj, "little"), kwargs.as_ref()) .expect("int.from_bytes() failed during to_object()") // FIXME: #1813 or similar .into() } @@ -102,12 +136,12 @@ macro_rules! bigint_conversion { }; } -bigint_conversion!(BigUint, 0, BigUint::to_bytes_le); -bigint_conversion!(BigInt, 1, BigInt::to_signed_bytes_le); +bigint_conversion!(BigUint, false, BigUint::to_bytes_le); +bigint_conversion!(BigInt, true, BigInt::to_signed_bytes_le); #[cfg_attr(docsrs, doc(cfg(feature = "num-bigint")))] -impl<'source> FromPyObject<'source> for BigInt { - fn extract(ob: &'source PyAny) -> PyResult { +impl<'py> FromPyObject<'py> for BigInt { + fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult { let py = ob.py(); // fast path - checking for subclass of `int` just checks a bit in the type object let num_owned: Py; @@ -115,15 +149,11 @@ impl<'source> FromPyObject<'source> for BigInt { long } else { num_owned = unsafe { Py::from_owned_ptr_or_err(py, ffi::PyNumber_Index(ob.as_ptr()))? }; - num_owned.as_ref(py) + num_owned.bind(py) }; - let n_bits = int_n_bits(num)?; - if n_bits == 0 { - return Ok(BigInt::from(0isize)); - } #[cfg(not(Py_LIMITED_API))] { - let mut buffer = int_to_u32_vec(num, (n_bits + 32) / 32, true)?; + let mut buffer = int_to_u32_vec::(num)?; let sign = if buffer.last().copied().map_or(false, |last| last >> 31 != 0) { // BigInt::new takes an unsigned array, so need to convert from two's complement // flip all bits, 'subtract' 1 (by adding one to the unsigned array) @@ -147,6 +177,10 @@ impl<'source> FromPyObject<'source> for BigInt { } #[cfg(Py_LIMITED_API)] { + let n_bits = int_n_bits(num)?; + if n_bits == 0 { + return Ok(BigInt::from(0isize)); + } let bytes = int_to_py_bytes(num, (n_bits + 8) / 8, true)?; Ok(BigInt::from_signed_bytes_le(bytes.as_bytes())) } @@ -154,8 +188,8 @@ impl<'source> FromPyObject<'source> for BigInt { } #[cfg_attr(docsrs, doc(cfg(feature = "num-bigint")))] -impl<'source> FromPyObject<'source> for BigUint { - fn extract(ob: &'source PyAny) -> PyResult { +impl<'py> FromPyObject<'py> for BigUint { + fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult { let py = ob.py(); // fast path - checking for subclass of `int` just checks a bit in the type object let num_owned: Py; @@ -163,29 +197,39 @@ impl<'source> FromPyObject<'source> for BigUint { long } else { num_owned = unsafe { Py::from_owned_ptr_or_err(py, ffi::PyNumber_Index(ob.as_ptr()))? }; - num_owned.as_ref(py) + num_owned.bind(py) }; - let n_bits = int_n_bits(num)?; - if n_bits == 0 { - return Ok(BigUint::from(0usize)); - } #[cfg(not(Py_LIMITED_API))] { - let buffer = int_to_u32_vec(num, (n_bits + 31) / 32, false)?; + let buffer = int_to_u32_vec::(num)?; Ok(BigUint::new(buffer)) } #[cfg(Py_LIMITED_API)] { + let n_bits = int_n_bits(num)?; + if n_bits == 0 { + return Ok(BigUint::from(0usize)); + } let bytes = int_to_py_bytes(num, (n_bits + 7) / 8, false)?; Ok(BigUint::from_bytes_le(bytes.as_bytes())) } } } -#[cfg(not(Py_LIMITED_API))] +#[cfg(not(any(Py_LIMITED_API, Py_3_13)))] #[inline] -fn int_to_u32_vec(long: &PyLong, n_digits: usize, is_signed: bool) -> PyResult> { - let mut buffer = Vec::with_capacity(n_digits); +fn int_to_u32_vec(long: &Bound<'_, PyLong>) -> PyResult> { + let mut buffer = Vec::new(); + let n_bits = int_n_bits(long)?; + if n_bits == 0 { + return Ok(buffer); + } + let n_digits = if SIGNED { + (n_bits + 32) / 32 + } else { + (n_bits + 31) / 32 + }; + buffer.reserve_exact(n_digits); unsafe { crate::err::error_on_minusone( long.py(), @@ -194,7 +238,7 @@ fn int_to_u32_vec(long: &PyLong, n_digits: usize, is_signed: bool) -> PyResult PyResult(long: &Bound<'_, PyLong>) -> PyResult> { + let mut buffer = Vec::new(); + let mut flags = ffi::Py_ASNATIVEBYTES_LITTLE_ENDIAN; + if !SIGNED { + flags |= ffi::Py_ASNATIVEBYTES_UNSIGNED_BUFFER | ffi::Py_ASNATIVEBYTES_REJECT_NEGATIVE; + } + let n_bytes = + unsafe { ffi::PyLong_AsNativeBytes(long.as_ptr().cast(), std::ptr::null_mut(), 0, flags) }; + let n_bytes_unsigned: usize = n_bytes + .try_into() + .map_err(|_| crate::PyErr::fetch(long.py()))?; + if n_bytes == 0 { + return Ok(buffer); + } + // TODO: use div_ceil when MSRV >= 1.73 + let n_digits = { + let adjust = if n_bytes % 4 == 0 { 0 } else { 1 }; + (n_bytes_unsigned / 4) + adjust + }; + buffer.reserve_exact(n_digits); + unsafe { + ffi::PyLong_AsNativeBytes( + long.as_ptr().cast(), + buffer.as_mut_ptr().cast(), + (n_digits * 4).try_into().unwrap(), + flags, + ); + buffer.set_len(n_digits); + }; + buffer + .iter_mut() + .for_each(|chunk| *chunk = u32::from_le(*chunk)); + + Ok(buffer) +} + #[cfg(Py_LIMITED_API)] -fn int_to_py_bytes(long: &PyLong, n_bytes: usize, is_signed: bool) -> PyResult<&PyBytes> { +fn int_to_py_bytes<'py>( + long: &Bound<'py, PyLong>, + n_bytes: usize, + is_signed: bool, +) -> PyResult> { use crate::intern; let py = long.py(); let kwargs = if is_signed { - let kwargs = PyDict::new(py); + let kwargs = crate::types::PyDict::new_bound(py); kwargs.set_item(intern!(py, "signed"), true)?; Some(kwargs) } else { @@ -220,13 +306,14 @@ fn int_to_py_bytes(long: &PyLong, n_bytes: usize, is_signed: bool) -> PyResult<& let bytes = long.call_method( intern!(py, "to_bytes"), (n_bytes, intern!(py, "little")), - kwargs, + kwargs.as_ref(), )?; - Ok(bytes.downcast()?) + Ok(bytes.downcast_into()?) } #[inline] -fn int_n_bits(long: &PyLong) -> PyResult { +#[cfg(any(not(Py_3_13), Py_LIMITED_API))] +fn int_n_bits(long: &Bound<'_, PyLong>) -> PyResult { let py = long.py(); #[cfg(not(Py_LIMITED_API))] { @@ -242,7 +329,7 @@ fn int_n_bits(long: &PyLong) -> PyResult { { // slow path long.call_method0(crate::intern!(py, "bit_length")) - .and_then(PyAny::extract) + .and_then(|any| any.extract()) } } @@ -269,7 +356,7 @@ mod tests { let mut f0 = 1.to_object(py); let mut f1 = 1.to_object(py); std::iter::from_fn(move || { - let f2 = f0.call_method1(py, "__add__", (f1.as_ref(py),)).unwrap(); + let f2 = f0.call_method1(py, "__add__", (f1.bind(py),)).unwrap(); Some(std::mem::replace(&mut f0, std::mem::replace(&mut f1, f2))) }) } @@ -282,7 +369,7 @@ mod tests { // Python -> Rust assert_eq!(py_result.extract::(py).unwrap(), rs_result); // Rust -> Python - assert!(py_result.as_ref(py).eq(rs_result).unwrap()); + assert!(py_result.bind(py).eq(rs_result).unwrap()); } }); } @@ -295,7 +382,7 @@ mod tests { // Python -> Rust assert_eq!(py_result.extract::(py).unwrap(), rs_result); // Rust -> Python - assert!(py_result.as_ref(py).eq(&rs_result).unwrap()); + assert!(py_result.bind(py).eq(&rs_result).unwrap()); // negate @@ -305,12 +392,12 @@ mod tests { // Python -> Rust assert_eq!(py_result.extract::(py).unwrap(), rs_result); // Rust -> Python - assert!(py_result.as_ref(py).eq(rs_result).unwrap()); + assert!(py_result.bind(py).eq(rs_result).unwrap()); } }); } - fn python_index_class(py: Python<'_>) -> &PyModule { + fn python_index_class(py: Python<'_>) -> Bound<'_, PyModule> { let index_code = indoc!( r#" class C: @@ -320,17 +407,17 @@ mod tests { return self.x "# ); - PyModule::from_code(py, index_code, "index.py", "index").unwrap() + PyModule::from_code_bound(py, index_code, "index.py", "index").unwrap() } #[test] fn convert_index_class() { Python::with_gil(|py| { let index = python_index_class(py); - let locals = PyDict::new(py); + let locals = PyDict::new_bound(py); locals.set_item("index", index).unwrap(); - let ob = py.eval("index.C(10)", None, Some(locals)).unwrap(); - let _: BigInt = FromPyObject::extract(ob).unwrap(); + let ob = py.eval_bound("index.C(10)", None, Some(&locals)).unwrap(); + let _: BigInt = ob.extract().unwrap(); }); } diff --git a/src/conversions/num_complex.rs b/src/conversions/num_complex.rs index a6d76d0f34e..12f208aa8d1 100644 --- a/src/conversions/num_complex.rs +++ b/src/conversions/num_complex.rs @@ -27,7 +27,7 @@ //! ```ignore //! # // not tested because nalgebra isn't supported on msrv //! # // please file an issue if it breaks! -//! use nalgebra::base::{dimension::Const, storage::Storage, Matrix}; +//! use nalgebra::base::{dimension::Const, Matrix}; //! use num_complex::Complex; //! use pyo3::prelude::*; //! @@ -44,7 +44,7 @@ //! } //! //! #[pymodule] -//! fn my_module(_py: Python<'_>, m: &PyModule) -> PyResult<()> { +//! fn my_module(m: &Bound<'_, PyModule>) -> PyResult<()> { //! m.add_function(wrap_pyfunction!(get_eigenvalues, m)?)?; //! Ok(()) //! } @@ -55,14 +55,14 @@ //! # //! # fn main() -> PyResult<()> { //! # Python::with_gil(|py| -> PyResult<()> { -//! # let module = PyModule::new(py, "my_module")?; +//! # let module = PyModule::new_bound(py, "my_module")?; //! # -//! # module.add_function(wrap_pyfunction!(get_eigenvalues, module)?)?; +//! # module.add_function(&wrap_pyfunction!(get_eigenvalues, module)?)?; //! # -//! # let m11 = PyComplex::from_doubles(py, 0_f64, -1_f64); -//! # let m12 = PyComplex::from_doubles(py, 1_f64, 0_f64); -//! # let m21 = PyComplex::from_doubles(py, 2_f64, -1_f64); -//! # let m22 = PyComplex::from_doubles(py, -1_f64, 0_f64); +//! # let m11 = PyComplex::from_doubles_bound(py, 0_f64, -1_f64); +//! # let m12 = PyComplex::from_doubles_bound(py, 1_f64, 0_f64); +//! # let m21 = PyComplex::from_doubles_bound(py, 2_f64, -1_f64); +//! # let m22 = PyComplex::from_doubles_bound(py, -1_f64, 0_f64); //! # //! # let result = module //! # .getattr("get_eigenvalues")? @@ -94,17 +94,34 @@ //! assert result == [complex(1,-1), complex(-2,0)] //! ``` use crate::{ - ffi, types::PyComplex, FromPyObject, PyAny, PyErr, PyObject, PyResult, Python, ToPyObject, + ffi, + ffi_ptr_ext::FfiPtrExt, + types::{any::PyAnyMethods, PyComplex}, + Bound, FromPyObject, PyAny, PyErr, PyObject, PyResult, Python, ToPyObject, }; use num_complex::Complex; use std::os::raw::c_double; impl PyComplex { - /// Creates a new Python `PyComplex` object from `num_complex`'s [`Complex`]. + /// Deprecated form of [`PyComplex::from_complex_bound`] + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`PyComplex::from_complex` will be replaced by `PyComplex::from_complex_bound` in a future PyO3 version" + )] pub fn from_complex>(py: Python<'_>, complex: Complex) -> &PyComplex { + Self::from_complex_bound(py, complex).into_gil_ref() + } + + /// Creates a new Python `PyComplex` object from `num_complex`'s [`Complex`]. + pub fn from_complex_bound>( + py: Python<'_>, + complex: Complex, + ) -> Bound<'_, PyComplex> { unsafe { - let ptr = ffi::PyComplex_FromDoubles(complex.re.into(), complex.im.into()); - py.from_owned_ptr(ptr) + ffi::PyComplex_FromDoubles(complex.re.into(), complex.im.into()) + .assume_owned(py) + .downcast_into_unchecked() } } } @@ -131,8 +148,8 @@ macro_rules! complex_conversion { } #[cfg_attr(docsrs, doc(cfg(feature = "num-complex")))] - impl<'source> FromPyObject<'source> for Complex<$float> { - fn extract(obj: &'source PyAny) -> PyResult> { + impl FromPyObject<'_> for Complex<$float> { + fn extract_bound(obj: &Bound<'_, PyAny>) -> PyResult> { #[cfg(not(any(Py_LIMITED_API, PyPy)))] unsafe { let val = ffi::PyComplex_AsCComplex(obj.as_ptr()); @@ -146,12 +163,14 @@ macro_rules! complex_conversion { #[cfg(any(Py_LIMITED_API, PyPy))] unsafe { + let complex; let obj = if obj.is_instance_of::() { obj } else if let Some(method) = obj.lookup_special(crate::intern!(obj.py(), "__complex__"))? { - method.call0()? + complex = method.call0()?; + &complex } else { // `obj` might still implement `__float__` or `__index__`, which will be // handled by `PyComplex_{Real,Imag}AsDouble`, including propagating any @@ -178,13 +197,13 @@ complex_conversion!(f64); #[cfg(test)] mod tests { use super::*; - use crate::types::PyModule; + use crate::types::{complex::PyComplexMethods, PyModule}; #[test] fn from_complex() { Python::with_gil(|py| { let complex = Complex::new(3.0, 1.2); - let py_c = PyComplex::from_complex(py, complex); + let py_c = PyComplex::from_complex_bound(py, complex); assert_eq!(py_c.real(), 3.0); assert_eq!(py_c.imag(), 1.2); }); @@ -207,7 +226,7 @@ mod tests { #[test] fn from_python_magic() { Python::with_gil(|py| { - let module = PyModule::from_code( + let module = PyModule::from_code_bound( py, r#" class A: @@ -245,7 +264,7 @@ class C: #[test] fn from_python_inherited_magic() { Python::with_gil(|py| { - let module = PyModule::from_code( + let module = PyModule::from_code_bound( py, r#" class First: pass @@ -289,7 +308,7 @@ class C(First, IndexMixin): pass // `type(inst).attr(inst)` equivalent to `inst.attr()` for methods, but this isn't the only // way the descriptor protocol might be implemented. Python::with_gil(|py| { - let module = PyModule::from_code( + let module = PyModule::from_code_bound( py, r#" class A: @@ -312,7 +331,7 @@ class A: fn from_python_nondescriptor_magic() { // Magic methods don't need to implement the descriptor protocol, if they're callable. Python::with_gil(|py| { - let module = PyModule::from_code( + let module = PyModule::from_code_bound( py, r#" class MyComplex: diff --git a/src/conversions/num_rational.rs b/src/conversions/num_rational.rs new file mode 100644 index 00000000000..31eb7ca1c7b --- /dev/null +++ b/src/conversions/num_rational.rs @@ -0,0 +1,277 @@ +#![cfg(feature = "num-rational")] +//! Conversions to and from [num-rational](https://docs.rs/num-rational) types. +//! +//! This is useful for converting between Python's [fractions.Fraction](https://docs.python.org/3/library/fractions.html) into and from a native Rust +//! type. +//! +//! +//! To use this feature, add to your **`Cargo.toml`**: +//! +//! ```toml +//! [dependencies] +#![doc = concat!("pyo3 = { version = \"", env!("CARGO_PKG_VERSION"), "\", features = [\"num-rational\"] }")] +//! num-rational = "0.4.1" +//! ``` +//! +//! # Example +//! +//! Rust code to create a function that adds five to a fraction: +//! +//! ```rust +//! use num_rational::Ratio; +//! use pyo3::prelude::*; +//! +//! #[pyfunction] +//! fn add_five_to_fraction(fraction: Ratio) -> Ratio { +//! fraction + Ratio::new(5, 1) +//! } +//! +//! #[pymodule] +//! fn my_module(m: &Bound<'_, PyModule>) -> PyResult<()> { +//! m.add_function(wrap_pyfunction!(add_five_to_fraction, m)?)?; +//! Ok(()) +//! } +//! ``` +//! +//! Python code that validates the functionality: +//! ```python +//! from my_module import add_five_to_fraction +//! from fractions import Fraction +//! +//! fraction = Fraction(2,1) +//! fraction_plus_five = add_five_to_fraction(f) +//! assert fraction + 5 == fraction_plus_five +//! ``` + +use crate::ffi; +use crate::sync::GILOnceCell; +use crate::types::any::PyAnyMethods; +use crate::types::PyType; +use crate::{Bound, FromPyObject, IntoPy, Py, PyAny, PyObject, PyResult, Python, ToPyObject}; +use std::os::raw::c_char; + +#[cfg(feature = "num-bigint")] +use num_bigint::BigInt; +use num_rational::Ratio; + +static FRACTION_CLS: GILOnceCell> = GILOnceCell::new(); + +fn get_fraction_cls(py: Python<'_>) -> PyResult<&Bound<'_, PyType>> { + FRACTION_CLS.get_or_try_init_type_ref(py, "fractions", "Fraction") +} + +macro_rules! rational_conversion { + ($int: ty) => { + impl<'py> FromPyObject<'py> for Ratio<$int> { + fn extract_bound(obj: &Bound<'py, PyAny>) -> PyResult { + let py = obj.py(); + let py_numerator_obj = unsafe { + Bound::from_owned_ptr_or_err( + py, + ffi::PyObject_GetAttrString( + obj.as_ptr(), + "numerator\0".as_ptr() as *const c_char, + ), + ) + }; + let py_denominator_obj = unsafe { + Bound::from_owned_ptr_or_err( + py, + ffi::PyObject_GetAttrString( + obj.as_ptr(), + "denominator\0".as_ptr() as *const c_char, + ), + ) + }; + let numerator_owned = unsafe { + Bound::from_owned_ptr_or_err( + py, + ffi::PyNumber_Long(py_numerator_obj?.as_ptr()), + )? + }; + let denominator_owned = unsafe { + Bound::from_owned_ptr_or_err( + py, + ffi::PyNumber_Long(py_denominator_obj?.as_ptr()), + )? + }; + let rs_numerator: $int = numerator_owned.extract()?; + let rs_denominator: $int = denominator_owned.extract()?; + Ok(Ratio::new(rs_numerator, rs_denominator)) + } + } + + impl ToPyObject for Ratio<$int> { + fn to_object(&self, py: Python<'_>) -> PyObject { + let fraction_cls = get_fraction_cls(py).expect("failed to load fractions.Fraction"); + let ret = fraction_cls + .call1((self.numer().clone(), self.denom().clone())) + .expect("failed to call fractions.Fraction(value)"); + ret.to_object(py) + } + } + impl IntoPy for Ratio<$int> { + fn into_py(self, py: Python<'_>) -> PyObject { + self.to_object(py) + } + } + }; +} +rational_conversion!(i8); +rational_conversion!(i16); +rational_conversion!(i32); +rational_conversion!(isize); +rational_conversion!(i64); +#[cfg(feature = "num-bigint")] +rational_conversion!(BigInt); +#[cfg(test)] +mod tests { + use super::*; + use crate::types::dict::PyDictMethods; + use crate::types::PyDict; + + #[cfg(not(target_arch = "wasm32"))] + use proptest::prelude::*; + #[test] + fn test_negative_fraction() { + Python::with_gil(|py| { + let locals = PyDict::new_bound(py); + py.run_bound( + "import fractions\npy_frac = fractions.Fraction(-0.125)", + None, + Some(&locals), + ) + .unwrap(); + let py_frac = locals.get_item("py_frac").unwrap().unwrap(); + let roundtripped: Ratio = py_frac.extract().unwrap(); + let rs_frac = Ratio::new(-1, 8); + assert_eq!(roundtripped, rs_frac); + }) + } + #[test] + fn test_obj_with_incorrect_atts() { + Python::with_gil(|py| { + let locals = PyDict::new_bound(py); + py.run_bound( + "not_fraction = \"contains_incorrect_atts\"", + None, + Some(&locals), + ) + .unwrap(); + let py_frac = locals.get_item("not_fraction").unwrap().unwrap(); + assert!(py_frac.extract::>().is_err()); + }) + } + + #[test] + fn test_fraction_with_fraction_type() { + Python::with_gil(|py| { + let locals = PyDict::new_bound(py); + py.run_bound( + "import fractions\npy_frac = fractions.Fraction(fractions.Fraction(10))", + None, + Some(&locals), + ) + .unwrap(); + let py_frac = locals.get_item("py_frac").unwrap().unwrap(); + let roundtripped: Ratio = py_frac.extract().unwrap(); + let rs_frac = Ratio::new(10, 1); + assert_eq!(roundtripped, rs_frac); + }) + } + + #[test] + fn test_fraction_with_decimal() { + Python::with_gil(|py| { + let locals = PyDict::new_bound(py); + py.run_bound( + "import fractions\n\nfrom decimal import Decimal\npy_frac = fractions.Fraction(Decimal(\"1.1\"))", + None, + Some(&locals), + ) + .unwrap(); + let py_frac = locals.get_item("py_frac").unwrap().unwrap(); + let roundtripped: Ratio = py_frac.extract().unwrap(); + let rs_frac = Ratio::new(11, 10); + assert_eq!(roundtripped, rs_frac); + }) + } + + #[test] + fn test_fraction_with_num_den() { + Python::with_gil(|py| { + let locals = PyDict::new_bound(py); + py.run_bound( + "import fractions\npy_frac = fractions.Fraction(10,5)", + None, + Some(&locals), + ) + .unwrap(); + let py_frac = locals.get_item("py_frac").unwrap().unwrap(); + let roundtripped: Ratio = py_frac.extract().unwrap(); + let rs_frac = Ratio::new(10, 5); + assert_eq!(roundtripped, rs_frac); + }) + } + + #[cfg(target_arch = "wasm32")] + #[test] + fn test_int_roundtrip() { + Python::with_gil(|py| { + let rs_frac = Ratio::new(1, 2); + let py_frac: PyObject = rs_frac.into_py(py); + let roundtripped: Ratio = py_frac.extract(py).unwrap(); + assert_eq!(rs_frac, roundtripped); + // float conversion + }) + } + + #[cfg(target_arch = "wasm32")] + #[test] + fn test_big_int_roundtrip() { + Python::with_gil(|py| { + let rs_frac = Ratio::from_float(5.5).unwrap(); + let py_frac: PyObject = rs_frac.clone().into_py(py); + let roundtripped: Ratio = py_frac.extract(py).unwrap(); + assert_eq!(rs_frac, roundtripped); + }) + } + + #[cfg(not(target_arch = "wasm32"))] + proptest! { + #[test] + fn test_int_roundtrip(num in any::(), den in any::()) { + Python::with_gil(|py| { + let rs_frac = Ratio::new(num, den); + let py_frac = rs_frac.into_py(py); + let roundtripped: Ratio = py_frac.extract(py).unwrap(); + assert_eq!(rs_frac, roundtripped); + }) + } + + #[test] + #[cfg(feature = "num-bigint")] + fn test_big_int_roundtrip(num in any::()) { + Python::with_gil(|py| { + let rs_frac = Ratio::from_float(num).unwrap(); + let py_frac = rs_frac.clone().into_py(py); + let roundtripped: Ratio = py_frac.extract(py).unwrap(); + assert_eq!(roundtripped, rs_frac); + }) + } + + } + + #[test] + fn test_infinity() { + Python::with_gil(|py| { + let locals = PyDict::new_bound(py); + let py_bound = py.run_bound( + "import fractions\npy_frac = fractions.Fraction(\"Infinity\")", + None, + Some(&locals), + ); + assert!(py_bound.is_err()); + }) + } +} diff --git a/src/conversions/rust_decimal.rs b/src/conversions/rust_decimal.rs index 173e57851c9..782ca2e80f0 100644 --- a/src/conversions/rust_decimal.rs +++ b/src/conversions/rust_decimal.rs @@ -30,7 +30,7 @@ //! } //! //! #[pymodule] -//! fn my_module(_py: Python<'_>, m: &PyModule) -> PyResult<()> { +//! fn my_module(m: &Bound<'_, PyModule>) -> PyResult<()> { //! m.add_function(wrap_pyfunction!(add_one, m)?)?; //! Ok(()) //! } @@ -51,33 +51,32 @@ use crate::exceptions::PyValueError; use crate::sync::GILOnceCell; +use crate::types::any::PyAnyMethods; +use crate::types::string::PyStringMethods; use crate::types::PyType; -use crate::{intern, FromPyObject, IntoPy, Py, PyAny, PyObject, PyResult, Python, ToPyObject}; +use crate::{Bound, FromPyObject, IntoPy, Py, PyAny, PyObject, PyResult, Python, ToPyObject}; use rust_decimal::Decimal; use std::str::FromStr; impl FromPyObject<'_> for Decimal { - fn extract(obj: &PyAny) -> PyResult { + fn extract_bound(obj: &Bound<'_, PyAny>) -> PyResult { // use the string representation to not be lossy if let Ok(val) = obj.extract() { Ok(Decimal::new(val, 0)) } else { - Decimal::from_str(obj.str()?.to_str()?) - .map_err(|e| PyValueError::new_err(e.to_string())) + let py_str = &obj.str()?; + let rs_str = &py_str.to_cow()?; + Decimal::from_str(rs_str).or_else(|_| { + Decimal::from_scientific(rs_str).map_err(|e| PyValueError::new_err(e.to_string())) + }) } } } static DECIMAL_CLS: GILOnceCell> = GILOnceCell::new(); -fn get_decimal_cls(py: Python<'_>) -> PyResult<&PyType> { - DECIMAL_CLS - .get_or_try_init(py, || { - py.import(intern!(py, "decimal"))? - .getattr(intern!(py, "Decimal"))? - .extract() - }) - .map(|ty| ty.as_ref(py)) +fn get_decimal_cls(py: Python<'_>) -> PyResult<&Bound<'_, PyType>> { + DECIMAL_CLS.get_or_try_init_type_ref(py, "decimal", "Decimal") } impl ToPyObject for Decimal { @@ -104,8 +103,8 @@ impl IntoPy for Decimal { mod test_rust_decimal { use super::*; use crate::err::PyErr; + use crate::types::dict::PyDictMethods; use crate::types::PyDict; - use rust_decimal::Decimal; #[cfg(not(target_arch = "wasm32"))] use proptest::prelude::*; @@ -117,21 +116,21 @@ mod test_rust_decimal { Python::with_gil(|py| { let rs_orig = $rs; let rs_dec = rs_orig.into_py(py); - let locals = PyDict::new(py); + let locals = PyDict::new_bound(py); locals.set_item("rs_dec", &rs_dec).unwrap(); // Checks if Rust Decimal -> Python Decimal conversion is correct - py.run( + py.run_bound( &format!( "import decimal\npy_dec = decimal.Decimal({})\nassert py_dec == rs_dec", $py ), None, - Some(locals), + Some(&locals), ) .unwrap(); // Checks if Python Decimal -> Rust Decimal conversion is correct let py_dec = locals.get_item("py_dec").unwrap().unwrap(); - let py_result: Decimal = FromPyObject::extract(py_dec).unwrap(); + let py_result: Decimal = py_dec.extract().unwrap(); assert_eq!(rs_orig, py_result); }) } @@ -159,13 +158,13 @@ mod test_rust_decimal { let num = Decimal::from_parts(lo, mid, high, negative, scale); Python::with_gil(|py| { let rs_dec = num.into_py(py); - let locals = PyDict::new(py); + let locals = PyDict::new_bound(py); locals.set_item("rs_dec", &rs_dec).unwrap(); - py.run( + py.run_bound( &format!( "import decimal\npy_dec = decimal.Decimal(\"{}\")\nassert py_dec == rs_dec", num), - None, Some(locals)).unwrap(); + None, Some(&locals)).unwrap(); let roundtripped: Decimal = rs_dec.extract(py).unwrap(); assert_eq!(num, roundtripped); }) @@ -185,31 +184,48 @@ mod test_rust_decimal { #[test] fn test_nan() { Python::with_gil(|py| { - let locals = PyDict::new(py); - py.run( + let locals = PyDict::new_bound(py); + py.run_bound( "import decimal\npy_dec = decimal.Decimal(\"NaN\")", None, - Some(locals), + Some(&locals), ) .unwrap(); let py_dec = locals.get_item("py_dec").unwrap().unwrap(); - let roundtripped: Result = FromPyObject::extract(py_dec); + let roundtripped: Result = py_dec.extract(); assert!(roundtripped.is_err()); }) } + #[test] + fn test_scientific_notation() { + Python::with_gil(|py| { + let locals = PyDict::new_bound(py); + py.run_bound( + "import decimal\npy_dec = decimal.Decimal(\"1e3\")", + None, + Some(&locals), + ) + .unwrap(); + let py_dec = locals.get_item("py_dec").unwrap().unwrap(); + let roundtripped: Decimal = py_dec.extract().unwrap(); + let rs_dec = Decimal::from_scientific("1e3").unwrap(); + assert_eq!(rs_dec, roundtripped); + }) + } + #[test] fn test_infinity() { Python::with_gil(|py| { - let locals = PyDict::new(py); - py.run( + let locals = PyDict::new_bound(py); + py.run_bound( "import decimal\npy_dec = decimal.Decimal(\"Infinity\")", None, - Some(locals), + Some(&locals), ) .unwrap(); let py_dec = locals.get_item("py_dec").unwrap().unwrap(); - let roundtripped: Result = FromPyObject::extract(py_dec); + let roundtripped: Result = py_dec.extract(); assert!(roundtripped.is_err()); }) } diff --git a/src/conversions/smallvec.rs b/src/conversions/smallvec.rs index d2e84421da3..96dbfad14b7 100644 --- a/src/conversions/smallvec.rs +++ b/src/conversions/smallvec.rs @@ -18,10 +18,12 @@ use crate::exceptions::PyTypeError; #[cfg(feature = "experimental-inspect")] use crate::inspect::types::TypeInfo; +use crate::types::any::PyAnyMethods; use crate::types::list::new_from_iter; use crate::types::{PySequence, PyString}; use crate::{ - ffi, FromPyObject, IntoPy, PyAny, PyDowncastError, PyObject, PyResult, Python, ToPyObject, + err::DowncastError, ffi, Bound, FromPyObject, IntoPy, PyAny, PyObject, PyResult, Python, + ToPyObject, }; use smallvec::{Array, SmallVec}; @@ -52,12 +54,12 @@ where } } -impl<'a, A> FromPyObject<'a> for SmallVec +impl<'py, A> FromPyObject<'py> for SmallVec where A: Array, - A::Item: FromPyObject<'a>, + A::Item: FromPyObject<'py>, { - fn extract(obj: &'a PyAny) -> PyResult { + fn extract_bound(obj: &Bound<'py, PyAny>) -> PyResult { if obj.is_instance_of::() { return Err(PyTypeError::new_err("Can't extract `str` to `SmallVec`")); } @@ -70,18 +72,18 @@ where } } -fn extract_sequence<'s, A>(obj: &'s PyAny) -> PyResult> +fn extract_sequence<'py, A>(obj: &Bound<'py, PyAny>) -> PyResult> where A: Array, - A::Item: FromPyObject<'s>, + A::Item: FromPyObject<'py>, { // Types that pass `PySequence_Check` usually implement enough of the sequence protocol // to support this function and if not, we will only fail extraction safely. - let seq: &PySequence = unsafe { + let seq = unsafe { if ffi::PySequence_Check(obj.as_ptr()) != 0 { - obj.downcast_unchecked() + obj.downcast_unchecked::() } else { - return Err(PyDowncastError::new(obj, "Sequence").into()); + return Err(DowncastError::new(obj, "Sequence").into()); } }; @@ -102,7 +104,7 @@ mod tests { Python::with_gil(|py| { let sv: SmallVec<[u64; 8]> = [1, 2, 3, 4, 5].iter().cloned().collect(); let hso: PyObject = sv.clone().into_py(py); - let l = PyList::new(py, [1, 2, 3, 4, 5]); + let l = PyList::new_bound(py, [1, 2, 3, 4, 5]); assert!(l.eq(hso).unwrap()); }); } @@ -110,7 +112,7 @@ mod tests { #[test] fn test_smallvec_from_py_object() { Python::with_gil(|py| { - let l = PyList::new(py, [1, 2, 3, 4, 5]); + let l = PyList::new_bound(py, [1, 2, 3, 4, 5]); let sv: SmallVec<[u64; 8]> = l.extract().unwrap(); assert_eq!(sv.as_slice(), [1, 2, 3, 4, 5]); }); @@ -119,7 +121,7 @@ mod tests { #[test] fn test_smallvec_from_py_object_fails() { Python::with_gil(|py| { - let dict = PyDict::new(py); + let dict = PyDict::new_bound(py); let sv: PyResult> = dict.extract(); assert_eq!( sv.unwrap_err().to_string(), @@ -133,7 +135,7 @@ mod tests { Python::with_gil(|py| { let sv: SmallVec<[u64; 8]> = [1, 2, 3, 4, 5].iter().cloned().collect(); let hso: PyObject = sv.to_object(py); - let l = PyList::new(py, [1, 2, 3, 4, 5]); + let l = PyList::new_bound(py, [1, 2, 3, 4, 5]); assert!(l.eq(hso).unwrap()); }); } diff --git a/src/conversions/std/array.rs b/src/conversions/std/array.rs index 167f8070632..14ccfd35413 100644 --- a/src/conversions/std/array.rs +++ b/src/conversions/std/array.rs @@ -1,8 +1,11 @@ +use crate::instance::Bound; +use crate::types::any::PyAnyMethods; use crate::types::PySequence; -use crate::{exceptions, PyErr}; use crate::{ - ffi, FromPyObject, IntoPy, Py, PyAny, PyDowncastError, PyObject, PyResult, Python, ToPyObject, + err::DowncastError, ffi, FromPyObject, IntoPy, Py, PyAny, PyObject, PyResult, Python, + ToPyObject, }; +use crate::{exceptions, PyErr}; impl IntoPy for [T; N] where @@ -42,33 +45,33 @@ where } } -impl<'a, T, const N: usize> FromPyObject<'a> for [T; N] +impl<'py, T, const N: usize> FromPyObject<'py> for [T; N] where - T: FromPyObject<'a>, + T: FromPyObject<'py>, { - fn extract(obj: &'a PyAny) -> PyResult { + fn extract_bound(obj: &Bound<'py, PyAny>) -> PyResult { create_array_from_obj(obj) } } -fn create_array_from_obj<'s, T, const N: usize>(obj: &'s PyAny) -> PyResult<[T; N]> +fn create_array_from_obj<'py, T, const N: usize>(obj: &Bound<'py, PyAny>) -> PyResult<[T; N]> where - T: FromPyObject<'s>, + T: FromPyObject<'py>, { // Types that pass `PySequence_Check` usually implement enough of the sequence protocol // to support this function and if not, we will only fail extraction safely. - let seq: &PySequence = unsafe { + let seq = unsafe { if ffi::PySequence_Check(obj.as_ptr()) != 0 { - obj.downcast_unchecked() + obj.downcast_unchecked::() } else { - return Err(PyDowncastError::new(obj, "Sequence").into()); + return Err(DowncastError::new(obj, "Sequence").into()); } }; let seq_len = seq.len()?; if seq_len != N { return Err(invalid_sequence_length(N, seq_len)); } - array_try_from_fn(|idx| seq.get_item(idx).and_then(PyAny::extract)) + array_try_from_fn(|idx| seq.get_item(idx).and_then(|any| any.extract())) } // TODO use std::array::try_from_fn, if that stabilises: @@ -127,6 +130,7 @@ mod tests { sync::atomic::{AtomicUsize, Ordering}, }; + use crate::types::any::PyAnyMethods; use crate::{types::PyList, IntoPy, PyResult, Python, ToPyObject}; #[test] @@ -154,7 +158,7 @@ mod tests { fn test_extract_bytearray_to_array() { Python::with_gil(|py| { let v: [u8; 33] = py - .eval( + .eval_bound( "bytearray(b'abcabcabcabcabcabcabcabcabcabcabc')", None, None, @@ -170,7 +174,7 @@ mod tests { fn test_extract_small_bytearray_to_array() { Python::with_gil(|py| { let v: [u8; 3] = py - .eval("bytearray(b'abc')", None, None) + .eval_bound("bytearray(b'abc')", None, None) .unwrap() .extract() .unwrap(); @@ -182,11 +186,11 @@ mod tests { Python::with_gil(|py| { let array: [f32; 4] = [0.0, -16.0, 16.0, 42.0]; let pyobject = array.to_object(py); - let pylist: &PyList = pyobject.extract(py).unwrap(); - assert_eq!(pylist[0].extract::().unwrap(), 0.0); - assert_eq!(pylist[1].extract::().unwrap(), -16.0); - assert_eq!(pylist[2].extract::().unwrap(), 16.0); - assert_eq!(pylist[3].extract::().unwrap(), 42.0); + let pylist = pyobject.downcast_bound::(py).unwrap(); + assert_eq!(pylist.get_item(0).unwrap().extract::().unwrap(), 0.0); + assert_eq!(pylist.get_item(1).unwrap().extract::().unwrap(), -16.0); + assert_eq!(pylist.get_item(2).unwrap().extract::().unwrap(), 16.0); + assert_eq!(pylist.get_item(3).unwrap().extract::().unwrap(), 42.0); }); } @@ -194,7 +198,7 @@ mod tests { fn test_extract_invalid_sequence_length() { Python::with_gil(|py| { let v: PyResult<[u8; 3]> = py - .eval("bytearray(b'abcdefg')", None, None) + .eval_bound("bytearray(b'abcdefg')", None, None) .unwrap() .extract(); assert_eq!( @@ -209,18 +213,18 @@ mod tests { Python::with_gil(|py| { let array: [f32; 4] = [0.0, -16.0, 16.0, 42.0]; let pyobject = array.into_py(py); - let pylist: &PyList = pyobject.extract(py).unwrap(); - assert_eq!(pylist[0].extract::().unwrap(), 0.0); - assert_eq!(pylist[1].extract::().unwrap(), -16.0); - assert_eq!(pylist[2].extract::().unwrap(), 16.0); - assert_eq!(pylist[3].extract::().unwrap(), 42.0); + let pylist = pyobject.downcast_bound::(py).unwrap(); + assert_eq!(pylist.get_item(0).unwrap().extract::().unwrap(), 0.0); + assert_eq!(pylist.get_item(1).unwrap().extract::().unwrap(), -16.0); + assert_eq!(pylist.get_item(2).unwrap().extract::().unwrap(), 16.0); + assert_eq!(pylist.get_item(3).unwrap().extract::().unwrap(), 42.0); }); } #[test] fn test_extract_non_iterable_to_array() { Python::with_gil(|py| { - let v = py.eval("42", None, None).unwrap(); + let v = py.eval_bound("42", None, None).unwrap(); v.extract::().unwrap(); v.extract::<[i32; 1]>().unwrap_err(); }); @@ -235,8 +239,8 @@ mod tests { Python::with_gil(|py| { let array: [Foo; 8] = [Foo, Foo, Foo, Foo, Foo, Foo, Foo, Foo]; let pyobject = array.into_py(py); - let list: &PyList = pyobject.downcast(py).unwrap(); - let _cell: &crate::PyCell = list.get_item(4).unwrap().extract().unwrap(); + let list = pyobject.downcast_bound::(py).unwrap(); + let _bound = list.get_item(4).unwrap().downcast::().unwrap(); }); } diff --git a/src/conversions/std/cell.rs b/src/conversions/std/cell.rs new file mode 100644 index 00000000000..69d990910d3 --- /dev/null +++ b/src/conversions/std/cell.rs @@ -0,0 +1,24 @@ +use std::cell::Cell; + +use crate::{ + types::any::PyAnyMethods, Bound, FromPyObject, IntoPy, PyAny, PyObject, PyResult, Python, + ToPyObject, +}; + +impl ToPyObject for Cell { + fn to_object(&self, py: Python<'_>) -> PyObject { + self.get().to_object(py) + } +} + +impl> IntoPy for Cell { + fn into_py(self, py: Python<'_>) -> PyObject { + self.get().into_py(py) + } +} + +impl<'py, T: FromPyObject<'py>> FromPyObject<'py> for Cell { + fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult { + ob.extract().map(Cell::new) + } +} diff --git a/src/conversions/std/duration.rs b/src/conversions/std/duration.rs deleted file mode 100755 index e4540bd0aaa..00000000000 --- a/src/conversions/std/duration.rs +++ /dev/null @@ -1,196 +0,0 @@ -use crate::exceptions::PyValueError; -#[cfg(Py_LIMITED_API)] -use crate::sync::GILOnceCell; -#[cfg(Py_LIMITED_API)] -use crate::types::PyType; -#[cfg(not(Py_LIMITED_API))] -use crate::types::{PyDelta, PyDeltaAccess}; -#[cfg(Py_LIMITED_API)] -use crate::{intern, Py}; -use crate::{FromPyObject, IntoPy, PyAny, PyObject, PyResult, Python, ToPyObject}; -use std::time::Duration; - -const SECONDS_PER_DAY: u64 = 24 * 60 * 60; - -impl FromPyObject<'_> for Duration { - fn extract(obj: &PyAny) -> PyResult { - #[cfg(not(Py_LIMITED_API))] - let (days, seconds, microseconds) = { - let delta: &PyDelta = obj.downcast()?; - ( - delta.get_days(), - delta.get_seconds(), - delta.get_microseconds(), - ) - }; - #[cfg(Py_LIMITED_API)] - let (days, seconds, microseconds): (i32, i32, i32) = { - ( - obj.getattr(intern!(obj.py(), "days"))?.extract()?, - obj.getattr(intern!(obj.py(), "seconds"))?.extract()?, - obj.getattr(intern!(obj.py(), "microseconds"))?.extract()?, - ) - }; - - // We cast - let days = u64::try_from(days).map_err(|_| { - PyValueError::new_err( - "It is not possible to convert a negative timedelta to a Rust Duration", - ) - })?; - let seconds = u64::try_from(seconds).unwrap(); // 0 <= seconds < 3600*24 - let microseconds = u32::try_from(microseconds).unwrap(); // 0 <= microseconds < 1000000 - - // We convert - let total_seconds = days * SECONDS_PER_DAY + seconds; // We casted from i32, this can't overflow - let nanoseconds = microseconds.checked_mul(1_000).unwrap(); // 0 <= microseconds < 1000000 - - Ok(Duration::new(total_seconds, nanoseconds)) - } -} - -impl ToPyObject for Duration { - fn to_object(&self, py: Python<'_>) -> PyObject { - let days = self.as_secs() / SECONDS_PER_DAY; - let seconds = self.as_secs() % SECONDS_PER_DAY; - let microseconds = self.subsec_micros(); - - #[cfg(not(Py_LIMITED_API))] - { - PyDelta::new( - py, - days.try_into() - .expect("Too large Rust duration for timedelta"), - seconds.try_into().unwrap(), - microseconds.try_into().unwrap(), - false, - ) - .expect("failed to construct timedelta (overflow?)") - .into() - } - #[cfg(Py_LIMITED_API)] - { - static TIMEDELTA: GILOnceCell> = GILOnceCell::new(); - TIMEDELTA - .get_or_try_init_type_ref(py, "datetime", "timedelta") - .unwrap() - .call1((days, seconds, microseconds)) - .unwrap() - .into() - } - } -} - -impl IntoPy for Duration { - fn into_py(self, py: Python<'_>) -> PyObject { - self.to_object(py) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use std::panic; - - #[test] - fn test_frompyobject() { - Python::with_gil(|py| { - assert_eq!( - new_timedelta(py, 0, 0, 0).extract::().unwrap(), - Duration::new(0, 0) - ); - assert_eq!( - new_timedelta(py, 1, 0, 0).extract::().unwrap(), - Duration::new(86400, 0) - ); - assert_eq!( - new_timedelta(py, 0, 1, 0).extract::().unwrap(), - Duration::new(1, 0) - ); - assert_eq!( - new_timedelta(py, 0, 0, 1).extract::().unwrap(), - Duration::new(0, 1_000) - ); - assert_eq!( - new_timedelta(py, 1, 1, 1).extract::().unwrap(), - Duration::new(86401, 1_000) - ); - assert_eq!( - timedelta_class(py) - .getattr("max") - .unwrap() - .extract::() - .unwrap(), - Duration::new(86399999999999, 999999000) - ); - }); - } - - #[test] - fn test_frompyobject_negative() { - Python::with_gil(|py| { - assert_eq!( - new_timedelta(py, 0, -1, 0) - .extract::() - .unwrap_err() - .to_string(), - "ValueError: It is not possible to convert a negative timedelta to a Rust Duration" - ); - }) - } - - #[test] - fn test_topyobject() { - Python::with_gil(|py| { - let assert_eq = |l: PyObject, r: &PyAny| { - assert!(l.as_ref(py).eq(r).unwrap()); - }; - - assert_eq( - Duration::new(0, 0).to_object(py), - new_timedelta(py, 0, 0, 0), - ); - assert_eq( - Duration::new(86400, 0).to_object(py), - new_timedelta(py, 1, 0, 0), - ); - assert_eq( - Duration::new(1, 0).to_object(py), - new_timedelta(py, 0, 1, 0), - ); - assert_eq( - Duration::new(0, 1_000).to_object(py), - new_timedelta(py, 0, 0, 1), - ); - assert_eq( - Duration::new(0, 1).to_object(py), - new_timedelta(py, 0, 0, 0), - ); - assert_eq( - Duration::new(86401, 1_000).to_object(py), - new_timedelta(py, 1, 1, 1), - ); - assert_eq( - Duration::new(86399999999999, 999999000).to_object(py), - timedelta_class(py).getattr("max").unwrap(), - ); - }); - } - - #[test] - fn test_topyobject_overflow() { - Python::with_gil(|py| { - assert!(panic::catch_unwind(|| Duration::MAX.to_object(py)).is_err()); - }) - } - - fn new_timedelta(py: Python<'_>, days: i32, seconds: i32, microseconds: i32) -> &PyAny { - timedelta_class(py) - .call1((days, seconds, microseconds)) - .unwrap() - } - - fn timedelta_class(py: Python<'_>) -> &PyAny { - py.import("datetime").unwrap().getattr("timedelta").unwrap() - } -} diff --git a/src/conversions/std/ipaddr.rs b/src/conversions/std/ipaddr.rs index ca3c8728f9b..5d030b445d8 100755 --- a/src/conversions/std/ipaddr.rs +++ b/src/conversions/std/ipaddr.rs @@ -1,12 +1,15 @@ use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; use crate::exceptions::PyValueError; +use crate::instance::Bound; use crate::sync::GILOnceCell; +use crate::types::any::PyAnyMethods; +use crate::types::string::PyStringMethods; use crate::types::PyType; use crate::{intern, FromPyObject, IntoPy, Py, PyAny, PyObject, PyResult, Python, ToPyObject}; impl FromPyObject<'_> for IpAddr { - fn extract(obj: &PyAny) -> PyResult { + fn extract_bound(obj: &Bound<'_, PyAny>) -> PyResult { match obj.getattr(intern!(obj.py(), "packed")) { Ok(packed) => { if let Ok(packed) = packed.extract::<[u8; 4]>() { @@ -19,7 +22,7 @@ impl FromPyObject<'_> for IpAddr { } Err(_) => { // We don't have a .packed attribute, so we try to construct an IP from str(). - obj.str()?.to_str()?.parse().map_err(PyValueError::new_err) + obj.str()?.to_cow()?.parse().map_err(PyValueError::new_err) } } } @@ -33,7 +36,7 @@ impl ToPyObject for Ipv4Addr { .expect("failed to load ipaddress.IPv4Address") .call1((u32::from_be_bytes(self.octets()),)) .expect("failed to construct ipaddress.IPv4Address") - .to_object(py) + .unbind() } } @@ -45,7 +48,7 @@ impl ToPyObject for Ipv6Addr { .expect("failed to load ipaddress.IPv6Address") .call1((u128::from_be_bytes(self.octets()),)) .expect("failed to construct ipaddress.IPv6Address") - .to_object(py) + .unbind() } } @@ -84,7 +87,8 @@ mod test_ipaddr { }; let pyobj = ip.into_py(py); - let repr = pyobj.as_ref(py).repr().unwrap().to_string_lossy(); + let repr = pyobj.bind(py).repr().unwrap(); + let repr = repr.to_string_lossy(); assert_eq!(repr, format!("{}('{}')", py_cls, ip)); let ip2: IpAddr = pyobj.extract(py).unwrap(); @@ -99,11 +103,11 @@ mod test_ipaddr { #[test] fn test_from_pystring() { Python::with_gil(|py| { - let py_str = PyString::new(py, "0:0:0:0:0:0:0:1"); + let py_str = PyString::new_bound(py, "0:0:0:0:0:0:0:1"); let ip: IpAddr = py_str.to_object(py).extract(py).unwrap(); assert_eq!(ip, IpAddr::from_str("::1").unwrap()); - let py_str = PyString::new(py, "invalid"); + let py_str = PyString::new_bound(py, "invalid"); assert!(py_str.to_object(py).extract::(py).is_err()); }); } diff --git a/src/conversions/std/map.rs b/src/conversions/std/map.rs index a53b0ce9222..49eff4c1e8d 100644 --- a/src/conversions/std/map.rs +++ b/src/conversions/std/map.rs @@ -3,7 +3,9 @@ use std::{cmp, collections, hash}; #[cfg(feature = "experimental-inspect")] use crate::inspect::types::TypeInfo; use crate::{ - types::{IntoPyDict, PyDict}, + instance::Bound, + types::dict::PyDictMethods, + types::{any::PyAnyMethods, IntoPyDict, PyDict}, FromPyObject, IntoPy, PyAny, PyErr, PyObject, Python, ToPyObject, }; @@ -14,7 +16,7 @@ where H: hash::BuildHasher, { fn to_object(&self, py: Python<'_>) -> PyObject { - IntoPyDict::into_py_dict(self, py).into() + IntoPyDict::into_py_dict_bound(self, py).into() } } @@ -24,7 +26,7 @@ where V: ToPyObject, { fn to_object(&self, py: Python<'_>) -> PyObject { - IntoPyDict::into_py_dict(self, py).into() + IntoPyDict::into_py_dict_bound(self, py).into() } } @@ -38,7 +40,7 @@ where let iter = self .into_iter() .map(|(k, v)| (k.into_py(py), v.into_py(py))); - IntoPyDict::into_py_dict(iter, py).into() + IntoPyDict::into_py_dict_bound(iter, py).into() } #[cfg(feature = "experimental-inspect")] @@ -56,7 +58,7 @@ where let iter = self .into_iter() .map(|(k, v)| (k.into_py(py), v.into_py(py))); - IntoPyDict::into_py_dict(iter, py).into() + IntoPyDict::into_py_dict_bound(iter, py).into() } #[cfg(feature = "experimental-inspect")] @@ -65,17 +67,17 @@ where } } -impl<'source, K, V, S> FromPyObject<'source> for collections::HashMap +impl<'py, K, V, S> FromPyObject<'py> for collections::HashMap where - K: FromPyObject<'source> + cmp::Eq + hash::Hash, - V: FromPyObject<'source>, + K: FromPyObject<'py> + cmp::Eq + hash::Hash, + V: FromPyObject<'py>, S: hash::BuildHasher + Default, { - fn extract(ob: &'source PyAny) -> Result { - let dict: &PyDict = ob.downcast()?; + fn extract_bound(ob: &Bound<'py, PyAny>) -> Result { + let dict = ob.downcast::()?; let mut ret = collections::HashMap::with_capacity_and_hasher(dict.len(), S::default()); for (k, v) in dict { - ret.insert(K::extract(k)?, V::extract(v)?); + ret.insert(k.extract()?, v.extract()?); } Ok(ret) } @@ -86,16 +88,16 @@ where } } -impl<'source, K, V> FromPyObject<'source> for collections::BTreeMap +impl<'py, K, V> FromPyObject<'py> for collections::BTreeMap where - K: FromPyObject<'source> + cmp::Ord, - V: FromPyObject<'source>, + K: FromPyObject<'py> + cmp::Ord, + V: FromPyObject<'py>, { - fn extract(ob: &'source PyAny) -> Result { - let dict: &PyDict = ob.downcast()?; + fn extract_bound(ob: &Bound<'py, PyAny>) -> Result { + let dict = ob.downcast::()?; let mut ret = collections::BTreeMap::new(); for (k, v) in dict { - ret.insert(K::extract(k)?, V::extract(v)?); + ret.insert(k.extract()?, v.extract()?); } Ok(ret) } @@ -109,7 +111,6 @@ where #[cfg(test)] mod tests { use super::*; - use crate::{IntoPy, PyObject, Python, ToPyObject}; use std::collections::{BTreeMap, HashMap}; #[test] @@ -119,7 +120,7 @@ mod tests { map.insert(1, 1); let m = map.to_object(py); - let py_map: &PyDict = m.downcast(py).unwrap(); + let py_map = m.downcast_bound::(py).unwrap(); assert!(py_map.len() == 1); assert!( @@ -142,7 +143,7 @@ mod tests { map.insert(1, 1); let m = map.to_object(py); - let py_map: &PyDict = m.downcast(py).unwrap(); + let py_map = m.downcast_bound::(py).unwrap(); assert!(py_map.len() == 1); assert!( @@ -165,7 +166,7 @@ mod tests { map.insert(1, 1); let m: PyObject = map.into_py(py); - let py_map: &PyDict = m.downcast(py).unwrap(); + let py_map = m.downcast_bound::(py).unwrap(); assert!(py_map.len() == 1); assert!( @@ -187,7 +188,7 @@ mod tests { map.insert(1, 1); let m: PyObject = map.into_py(py); - let py_map: &PyDict = m.downcast(py).unwrap(); + let py_map = m.downcast_bound::(py).unwrap(); assert!(py_map.len() == 1); assert!( diff --git a/src/conversions/std/mod.rs b/src/conversions/std/mod.rs index ebe1c955cc6..305344b1284 100644 --- a/src/conversions/std/mod.rs +++ b/src/conversions/std/mod.rs @@ -1,11 +1,13 @@ mod array; -mod duration; +mod cell; mod ipaddr; mod map; mod num; +mod option; mod osstr; mod path; mod set; mod slice; mod string; +mod time; mod vec; diff --git a/src/conversions/std/num.rs b/src/conversions/std/num.rs index 1f3bf673a48..effe7c7c062 100644 --- a/src/conversions/std/num.rs +++ b/src/conversions/std/num.rs @@ -1,9 +1,11 @@ +use crate::ffi_ptr_ext::FfiPtrExt; #[cfg(feature = "experimental-inspect")] use crate::inspect::types::TypeInfo; +use crate::types::any::PyAnyMethods; use crate::{ - exceptions, ffi, FromPyObject, IntoPy, PyAny, PyErr, PyObject, PyResult, Python, ToPyObject, + exceptions, ffi, Bound, FromPyObject, IntoPy, PyAny, PyErr, PyObject, PyResult, Python, + ToPyObject, }; -use std::convert::TryFrom; use std::num::{ NonZeroI128, NonZeroI16, NonZeroI32, NonZeroI64, NonZeroI8, NonZeroIsize, NonZeroU128, NonZeroU16, NonZeroU32, NonZeroU64, NonZeroU8, NonZeroUsize, @@ -29,8 +31,8 @@ macro_rules! int_fits_larger_int { } } - impl<'source> FromPyObject<'source> for $rust_type { - fn extract(obj: &'source PyAny) -> PyResult { + impl FromPyObject<'_> for $rust_type { + fn extract_bound(obj: &Bound<'_, PyAny>) -> PyResult { let val: $larger_type = obj.extract()?; <$rust_type>::try_from(val) .map_err(|e| exceptions::PyOverflowError::new_err(e.to_string())) @@ -44,8 +46,33 @@ macro_rules! int_fits_larger_int { }; } +macro_rules! extract_int { + ($obj:ident, $error_val:expr, $pylong_as:expr) => { + extract_int!($obj, $error_val, $pylong_as, false) + }; + + ($obj:ident, $error_val:expr, $pylong_as:expr, $force_index_call: literal) => { + // In python 3.8+ `PyLong_AsLong` and friends takes care of calling `PyNumber_Index`, + // however 3.8 & 3.9 do lossy conversion of floats, hence we only use the + // simplest logic for 3.10+ where that was fixed - python/cpython#82180. + // `PyLong_AsUnsignedLongLong` does not call `PyNumber_Index`, hence the `force_index_call` argument + // See https://github.com/PyO3/pyo3/pull/3742 for detials + if cfg!(Py_3_10) && !$force_index_call { + err_if_invalid_value($obj.py(), $error_val, unsafe { $pylong_as($obj.as_ptr()) }) + } else if let Ok(long) = $obj.downcast::() { + // fast path - checking for subclass of `int` just checks a bit in the type $object + err_if_invalid_value($obj.py(), $error_val, unsafe { $pylong_as(long.as_ptr()) }) + } else { + unsafe { + let num = ffi::PyNumber_Index($obj.as_ptr()).assume_owned_or_err($obj.py())?; + err_if_invalid_value($obj.py(), $error_val, $pylong_as(num.as_ptr())) + } + } + }; +} + macro_rules! int_convert_u64_or_i64 { - ($rust_type:ty, $pylong_from_ll_or_ull:expr, $pylong_as_ll_or_ull:expr) => { + ($rust_type:ty, $pylong_from_ll_or_ull:expr, $pylong_as_ll_or_ull:expr, $force_index_call:literal) => { impl ToPyObject for $rust_type { #[inline] fn to_object(&self, py: Python<'_>) -> PyObject { @@ -63,19 +90,9 @@ macro_rules! int_convert_u64_or_i64 { TypeInfo::builtin("int") } } - impl<'source> FromPyObject<'source> for $rust_type { - fn extract(ob: &'source PyAny) -> PyResult<$rust_type> { - let ptr = ob.as_ptr(); - unsafe { - let num = ffi::PyNumber_Index(ptr); - if num.is_null() { - Err(PyErr::fetch(ob.py())) - } else { - let result = err_if_invalid_value(ob.py(), !0, $pylong_as_ll_or_ull(num)); - ffi::Py_DECREF(num); - result - } - } + impl FromPyObject<'_> for $rust_type { + fn extract_bound(obj: &Bound<'_, PyAny>) -> PyResult<$rust_type> { + extract_int!(obj, !0, $pylong_as_ll_or_ull, $force_index_call) } #[cfg(feature = "experimental-inspect")] @@ -104,19 +121,9 @@ macro_rules! int_fits_c_long { } } - impl<'source> FromPyObject<'source> for $rust_type { - fn extract(obj: &'source PyAny) -> PyResult { - let ptr = obj.as_ptr(); - let val = unsafe { - let num = ffi::PyNumber_Index(ptr); - if num.is_null() { - Err(PyErr::fetch(obj.py())) - } else { - let val = err_if_invalid_value(obj.py(), -1, ffi::PyLong_AsLong(num)); - ffi::Py_DECREF(num); - val - } - }?; + impl<'py> FromPyObject<'py> for $rust_type { + fn extract_bound(obj: &Bound<'_, PyAny>) -> PyResult { + let val: c_long = extract_int!(obj, -1, ffi::PyLong_AsLong)?; <$rust_type>::try_from(val) .map_err(|e| exceptions::PyOverflowError::new_err(e.to_string())) } @@ -146,7 +153,7 @@ int_fits_c_long!(i64); // manual implementation for i64 on systems with 32-bit long #[cfg(any(target_pointer_width = "32", target_os = "windows"))] -int_convert_u64_or_i64!(i64, ffi::PyLong_FromLongLong, ffi::PyLong_AsLongLong); +int_convert_u64_or_i64!(i64, ffi::PyLong_FromLongLong, ffi::PyLong_AsLongLong, false); #[cfg(all(target_pointer_width = "64", not(target_os = "windows")))] int_fits_c_long!(isize); @@ -159,16 +166,17 @@ int_fits_larger_int!(usize, u64); int_convert_u64_or_i64!( u64, ffi::PyLong_FromUnsignedLongLong, - ffi::PyLong_AsUnsignedLongLong + ffi::PyLong_AsUnsignedLongLong, + true ); -#[cfg(not(Py_LIMITED_API))] +#[cfg(all(not(Py_LIMITED_API), not(GraalPy)))] mod fast_128bit_int_conversion { use super::*; // for 128bit Integers macro_rules! int_convert_128 { - ($rust_type: ty, $is_signed: expr) => { + ($rust_type: ty, $is_signed: literal) => { impl ToPyObject for $rust_type { #[inline] fn to_object(&self, py: Python<'_>) -> PyObject { @@ -177,18 +185,44 @@ mod fast_128bit_int_conversion { } impl IntoPy for $rust_type { fn into_py(self, py: Python<'_>) -> PyObject { - // Always use little endian - let bytes = self.to_le_bytes(); - unsafe { - PyObject::from_owned_ptr( - py, + #[cfg(not(Py_3_13))] + { + let bytes = self.to_le_bytes(); + unsafe { ffi::_PyLong_FromByteArray( - bytes.as_ptr() as *const std::os::raw::c_uchar, + bytes.as_ptr().cast(), bytes.len(), 1, - $is_signed, - ), - ) + $is_signed.into(), + ) + .assume_owned(py) + .unbind() + } + } + #[cfg(Py_3_13)] + { + let bytes = self.to_ne_bytes(); + + if $is_signed { + unsafe { + ffi::PyLong_FromNativeBytes( + bytes.as_ptr().cast(), + bytes.len(), + ffi::Py_ASNATIVEBYTES_NATIVE_ENDIAN, + ) + .assume_owned(py) + } + } else { + unsafe { + ffi::PyLong_FromUnsignedNativeBytes( + bytes.as_ptr().cast(), + bytes.len(), + ffi::Py_ASNATIVEBYTES_NATIVE_ENDIAN, + ) + .assume_owned(py) + } + } + .unbind() } } @@ -198,22 +232,48 @@ mod fast_128bit_int_conversion { } } - impl<'source> FromPyObject<'source> for $rust_type { - fn extract(ob: &'source PyAny) -> PyResult<$rust_type> { - let num = unsafe { - PyObject::from_owned_ptr_or_err(ob.py(), ffi::PyNumber_Index(ob.as_ptr()))? - }; - let mut buffer = [0; std::mem::size_of::<$rust_type>()]; + impl FromPyObject<'_> for $rust_type { + fn extract_bound(ob: &Bound<'_, PyAny>) -> PyResult<$rust_type> { + let num = + unsafe { ffi::PyNumber_Index(ob.as_ptr()).assume_owned_or_err(ob.py())? }; + let mut buffer = [0u8; std::mem::size_of::<$rust_type>()]; + #[cfg(not(Py_3_13))] crate::err::error_on_minusone(ob.py(), unsafe { ffi::_PyLong_AsByteArray( num.as_ptr() as *mut ffi::PyLongObject, buffer.as_mut_ptr(), buffer.len(), 1, - $is_signed, + $is_signed.into(), ) })?; - Ok(<$rust_type>::from_le_bytes(buffer)) + #[cfg(Py_3_13)] + { + let mut flags = ffi::Py_ASNATIVEBYTES_NATIVE_ENDIAN; + if !$is_signed { + flags |= ffi::Py_ASNATIVEBYTES_UNSIGNED_BUFFER + | ffi::Py_ASNATIVEBYTES_REJECT_NEGATIVE; + } + let actual_size: usize = unsafe { + ffi::PyLong_AsNativeBytes( + num.as_ptr(), + buffer.as_mut_ptr().cast(), + buffer + .len() + .try_into() + .expect("length of buffer fits in Py_ssize_t"), + flags, + ) + } + .try_into() + .map_err(|_| PyErr::fetch(ob.py()))?; + if actual_size as usize > buffer.len() { + return Err(crate::exceptions::PyOverflowError::new_err( + "Python int larger than 128 bits", + )); + } + } + Ok(<$rust_type>::from_ne_bytes(buffer)) } #[cfg(feature = "experimental-inspect")] @@ -224,12 +284,12 @@ mod fast_128bit_int_conversion { }; } - int_convert_128!(i128, 1); - int_convert_128!(u128, 0); + int_convert_128!(i128, true); + int_convert_128!(u128, false); } // For ABI3 we implement the conversion manually. -#[cfg(Py_LIMITED_API)] +#[cfg(any(Py_LIMITED_API, GraalPy))] mod slow_128bit_int_conversion { use super::*; const SHIFT: usize = 64; @@ -267,8 +327,8 @@ mod slow_128bit_int_conversion { } } - impl<'source> FromPyObject<'source> for $rust_type { - fn extract(ob: &'source PyAny) -> PyResult<$rust_type> { + impl FromPyObject<'_> for $rust_type { + fn extract_bound(ob: &Bound<'_, PyAny>) -> PyResult<$rust_type> { let py = ob.py(); unsafe { let lower = err_if_invalid_value( @@ -326,8 +386,8 @@ macro_rules! nonzero_int_impl { } } - impl<'source> FromPyObject<'source> for $nonzero_type { - fn extract(obj: &'source PyAny) -> PyResult { + impl FromPyObject<'_> for $nonzero_type { + fn extract_bound(obj: &Bound<'_, PyAny>) -> PyResult { let val: $primitive_type = obj.extract()?; <$nonzero_type>::try_from(val) .map_err(|_| exceptions::PyValueError::new_err("invalid zero value")) @@ -357,9 +417,13 @@ nonzero_int_impl!(NonZeroUsize, usize); #[cfg(test)] mod test_128bit_integers { use super::*; + #[cfg(not(target_arch = "wasm32"))] use crate::types::PyDict; + #[cfg(not(target_arch = "wasm32"))] + use crate::types::dict::PyDictMethods; + #[cfg(not(target_arch = "wasm32"))] use proptest::prelude::*; @@ -369,9 +433,9 @@ mod test_128bit_integers { fn test_i128_roundtrip(x: i128) { Python::with_gil(|py| { let x_py = x.into_py(py); - let locals = PyDict::new(py); + let locals = PyDict::new_bound(py); locals.set_item("x_py", x_py.clone_ref(py)).unwrap(); - py.run(&format!("assert x_py == {}", x), None, Some(locals)).unwrap(); + py.run_bound(&format!("assert x_py == {}", x), None, Some(&locals)).unwrap(); let roundtripped: i128 = x_py.extract(py).unwrap(); assert_eq!(x, roundtripped); }) @@ -385,9 +449,9 @@ mod test_128bit_integers { ) { Python::with_gil(|py| { let x_py = x.into_py(py); - let locals = PyDict::new(py); + let locals = PyDict::new_bound(py); locals.set_item("x_py", x_py.clone_ref(py)).unwrap(); - py.run(&format!("assert x_py == {}", x), None, Some(locals)).unwrap(); + py.run_bound(&format!("assert x_py == {}", x), None, Some(&locals)).unwrap(); let roundtripped: NonZeroI128 = x_py.extract(py).unwrap(); assert_eq!(x, roundtripped); }) @@ -400,9 +464,9 @@ mod test_128bit_integers { fn test_u128_roundtrip(x: u128) { Python::with_gil(|py| { let x_py = x.into_py(py); - let locals = PyDict::new(py); + let locals = PyDict::new_bound(py); locals.set_item("x_py", x_py.clone_ref(py)).unwrap(); - py.run(&format!("assert x_py == {}", x), None, Some(locals)).unwrap(); + py.run_bound(&format!("assert x_py == {}", x), None, Some(&locals)).unwrap(); let roundtripped: u128 = x_py.extract(py).unwrap(); assert_eq!(x, roundtripped); }) @@ -416,9 +480,9 @@ mod test_128bit_integers { ) { Python::with_gil(|py| { let x_py = x.into_py(py); - let locals = PyDict::new(py); + let locals = PyDict::new_bound(py); locals.set_item("x_py", x_py.clone_ref(py)).unwrap(); - py.run(&format!("assert x_py == {}", x), None, Some(locals)).unwrap(); + py.run_bound(&format!("assert x_py == {}", x), None, Some(&locals)).unwrap(); let roundtripped: NonZeroU128 = x_py.extract(py).unwrap(); assert_eq!(x, roundtripped); }) @@ -428,7 +492,7 @@ mod test_128bit_integers { #[test] fn test_i128_max() { Python::with_gil(|py| { - let v = std::i128::MAX; + let v = i128::MAX; let obj = v.to_object(py); assert_eq!(v, obj.extract::(py).unwrap()); assert_eq!(v as u128, obj.extract::(py).unwrap()); @@ -439,7 +503,7 @@ mod test_128bit_integers { #[test] fn test_i128_min() { Python::with_gil(|py| { - let v = std::i128::MIN; + let v = i128::MIN; let obj = v.to_object(py); assert_eq!(v, obj.extract::(py).unwrap()); assert!(obj.extract::(py).is_err()); @@ -450,7 +514,7 @@ mod test_128bit_integers { #[test] fn test_u128_max() { Python::with_gil(|py| { - let v = std::u128::MAX; + let v = u128::MAX; let obj = v.to_object(py); assert_eq!(v, obj.extract::(py).unwrap()); assert!(obj.extract::(py).is_err()); @@ -460,7 +524,7 @@ mod test_128bit_integers { #[test] fn test_i128_overflow() { Python::with_gil(|py| { - let obj = py.eval("(1 << 130) * -1", None, None).unwrap(); + let obj = py.eval_bound("(1 << 130) * -1", None, None).unwrap(); let err = obj.extract::().unwrap_err(); assert!(err.is_instance_of::(py)); }) @@ -469,7 +533,7 @@ mod test_128bit_integers { #[test] fn test_u128_overflow() { Python::with_gil(|py| { - let obj = py.eval("1 << 130", None, None).unwrap(); + let obj = py.eval_bound("1 << 130", None, None).unwrap(); let err = obj.extract::().unwrap_err(); assert!(err.is_instance_of::(py)); }) @@ -478,7 +542,7 @@ mod test_128bit_integers { #[test] fn test_nonzero_i128_max() { Python::with_gil(|py| { - let v = NonZeroI128::new(std::i128::MAX).unwrap(); + let v = NonZeroI128::new(i128::MAX).unwrap(); let obj = v.to_object(py); assert_eq!(v, obj.extract::(py).unwrap()); assert_eq!( @@ -492,7 +556,7 @@ mod test_128bit_integers { #[test] fn test_nonzero_i128_min() { Python::with_gil(|py| { - let v = NonZeroI128::new(std::i128::MIN).unwrap(); + let v = NonZeroI128::new(i128::MIN).unwrap(); let obj = v.to_object(py); assert_eq!(v, obj.extract::(py).unwrap()); assert!(obj.extract::(py).is_err()); @@ -503,7 +567,7 @@ mod test_128bit_integers { #[test] fn test_nonzero_u128_max() { Python::with_gil(|py| { - let v = NonZeroU128::new(std::u128::MAX).unwrap(); + let v = NonZeroU128::new(u128::MAX).unwrap(); let obj = v.to_object(py); assert_eq!(v, obj.extract::(py).unwrap()); assert!(obj.extract::(py).is_err()); @@ -513,7 +577,7 @@ mod test_128bit_integers { #[test] fn test_nonzero_i128_overflow() { Python::with_gil(|py| { - let obj = py.eval("(1 << 130) * -1", None, None).unwrap(); + let obj = py.eval_bound("(1 << 130) * -1", None, None).unwrap(); let err = obj.extract::().unwrap_err(); assert!(err.is_instance_of::(py)); }) @@ -522,7 +586,7 @@ mod test_128bit_integers { #[test] fn test_nonzero_u128_overflow() { Python::with_gil(|py| { - let obj = py.eval("1 << 130", None, None).unwrap(); + let obj = py.eval_bound("1 << 130", None, None).unwrap(); let err = obj.extract::().unwrap_err(); assert!(err.is_instance_of::(py)); }) @@ -531,7 +595,7 @@ mod test_128bit_integers { #[test] fn test_nonzero_i128_zero_value() { Python::with_gil(|py| { - let obj = py.eval("0", None, None).unwrap(); + let obj = py.eval_bound("0", None, None).unwrap(); let err = obj.extract::().unwrap_err(); assert!(err.is_instance_of::(py)); }) @@ -540,7 +604,7 @@ mod test_128bit_integers { #[test] fn test_nonzero_u128_zero_value() { Python::with_gil(|py| { - let obj = py.eval("0", None, None).unwrap(); + let obj = py.eval_bound("0", None, None).unwrap(); let err = obj.extract::().unwrap_err(); assert!(err.is_instance_of::(py)); }) @@ -556,7 +620,7 @@ mod tests { #[test] fn test_u32_max() { Python::with_gil(|py| { - let v = std::u32::MAX; + let v = u32::MAX; let obj = v.to_object(py); assert_eq!(v, obj.extract::(py).unwrap()); assert_eq!(u64::from(v), obj.extract::(py).unwrap()); @@ -567,7 +631,7 @@ mod tests { #[test] fn test_i64_max() { Python::with_gil(|py| { - let v = std::i64::MAX; + let v = i64::MAX; let obj = v.to_object(py); assert_eq!(v, obj.extract::(py).unwrap()); assert_eq!(v as u64, obj.extract::(py).unwrap()); @@ -578,7 +642,7 @@ mod tests { #[test] fn test_i64_min() { Python::with_gil(|py| { - let v = std::i64::MIN; + let v = i64::MIN; let obj = v.to_object(py); assert_eq!(v, obj.extract::(py).unwrap()); assert!(obj.extract::(py).is_err()); @@ -589,7 +653,7 @@ mod tests { #[test] fn test_u64_max() { Python::with_gil(|py| { - let v = std::u64::MAX; + let v = u64::MAX; let obj = v.to_object(py); assert_eq!(v, obj.extract::(py).unwrap()); assert!(obj.extract::(py).is_err()); @@ -647,7 +711,7 @@ mod tests { #[test] fn test_nonzero_u32_max() { Python::with_gil(|py| { - let v = NonZeroU32::new(std::u32::MAX).unwrap(); + let v = NonZeroU32::new(u32::MAX).unwrap(); let obj = v.to_object(py); assert_eq!(v, obj.extract::(py).unwrap()); assert_eq!(NonZeroU64::from(v), obj.extract::(py).unwrap()); @@ -658,7 +722,7 @@ mod tests { #[test] fn test_nonzero_i64_max() { Python::with_gil(|py| { - let v = NonZeroI64::new(std::i64::MAX).unwrap(); + let v = NonZeroI64::new(i64::MAX).unwrap(); let obj = v.to_object(py); assert_eq!(v, obj.extract::(py).unwrap()); assert_eq!( @@ -672,7 +736,7 @@ mod tests { #[test] fn test_nonzero_i64_min() { Python::with_gil(|py| { - let v = NonZeroI64::new(std::i64::MIN).unwrap(); + let v = NonZeroI64::new(i64::MIN).unwrap(); let obj = v.to_object(py); assert_eq!(v, obj.extract::(py).unwrap()); assert!(obj.extract::(py).is_err()); @@ -683,7 +747,7 @@ mod tests { #[test] fn test_nonzero_u64_max() { Python::with_gil(|py| { - let v = NonZeroU64::new(std::u64::MAX).unwrap(); + let v = NonZeroU64::new(u64::MAX).unwrap(); let obj = v.to_object(py); assert_eq!(v, obj.extract::(py).unwrap()); assert!(obj.extract::(py).is_err()); @@ -738,4 +802,27 @@ mod tests { test_nonzero_common!(nonzero_usize, NonZeroUsize); test_nonzero_common!(nonzero_i128, NonZeroI128); test_nonzero_common!(nonzero_u128, NonZeroU128); + + #[test] + fn test_i64_bool() { + Python::with_gil(|py| { + let obj = true.to_object(py); + assert_eq!(1, obj.extract::(py).unwrap()); + let obj = false.to_object(py); + assert_eq!(0, obj.extract::(py).unwrap()); + }) + } + + #[test] + fn test_i64_f64() { + Python::with_gil(|py| { + let obj = 12.34f64.to_object(py); + let err = obj.extract::(py).unwrap_err(); + assert!(err.is_instance_of::(py)); + // with no remainder + let obj = 12f64.to_object(py); + let err = obj.extract::(py).unwrap_err(); + assert!(err.is_instance_of::(py)); + }) + } } diff --git a/src/conversions/std/option.rs b/src/conversions/std/option.rs new file mode 100644 index 00000000000..13527315e70 --- /dev/null +++ b/src/conversions/std/option.rs @@ -0,0 +1,73 @@ +use crate::{ + ffi, types::any::PyAnyMethods, AsPyPointer, Bound, FromPyObject, IntoPy, PyAny, PyObject, + PyResult, Python, ToPyObject, +}; + +/// `Option::Some` is converted like `T`. +/// `Option::None` is converted to Python `None`. +impl ToPyObject for Option +where + T: ToPyObject, +{ + fn to_object(&self, py: Python<'_>) -> PyObject { + self.as_ref() + .map_or_else(|| py.None(), |val| val.to_object(py)) + } +} + +impl IntoPy for Option +where + T: IntoPy, +{ + fn into_py(self, py: Python<'_>) -> PyObject { + self.map_or_else(|| py.None(), |val| val.into_py(py)) + } +} + +impl<'py, T> FromPyObject<'py> for Option +where + T: FromPyObject<'py>, +{ + fn extract_bound(obj: &Bound<'py, PyAny>) -> PyResult { + if obj.is_none() { + Ok(None) + } else { + obj.extract().map(Some) + } + } +} + +/// Convert `None` into a null pointer. +unsafe impl AsPyPointer for Option +where + T: AsPyPointer, +{ + #[inline] + fn as_ptr(&self) -> *mut ffi::PyObject { + self.as_ref() + .map_or_else(std::ptr::null_mut, |t| t.as_ptr()) + } +} + +#[cfg(test)] +mod tests { + use crate::{PyObject, Python}; + + #[test] + fn test_option_as_ptr() { + Python::with_gil(|py| { + use crate::AsPyPointer; + let mut option: Option = None; + assert_eq!(option.as_ptr(), std::ptr::null_mut()); + + let none = py.None(); + option = Some(none.clone_ref(py)); + + let ref_cnt = none.get_refcnt(py); + assert_eq!(option.as_ptr(), none.as_ptr()); + + // Ensure ref count not changed by as_ptr call + assert_eq!(none.get_refcnt(py), ref_cnt); + }); + } +} diff --git a/src/conversions/std/osstr.rs b/src/conversions/std/osstr.rs index b2c143866ac..4565c3fbd94 100644 --- a/src/conversions/std/osstr.rs +++ b/src/conversions/std/osstr.rs @@ -1,3 +1,5 @@ +use crate::instance::Bound; +use crate::types::any::PyAnyMethods; use crate::types::PyString; use crate::{ffi, FromPyObject, IntoPy, PyAny, PyObject, PyResult, Python, ToPyObject}; use std::borrow::Cow; @@ -51,8 +53,8 @@ impl ToPyObject for OsStr { // be impossible to implement on Windows. Hence it's omitted entirely impl FromPyObject<'_> for OsString { - fn extract(ob: &PyAny) -> PyResult { - let pystring: &PyString = ob.downcast()?; + fn extract_bound(ob: &Bound<'_, PyAny>) -> PyResult { + let pystring = ob.downcast::()?; #[cfg(not(windows))] { @@ -66,22 +68,22 @@ impl FromPyObject<'_> for OsString { // Create an OsStr view into the raw bytes from Python #[cfg(target_os = "wasi")] - let os_str: &OsStr = std::os::wasi::ffi::OsStrExt::from_bytes( - fs_encoded_bytes.as_ref(ob.py()).as_bytes(), - ); + let os_str: &OsStr = + std::os::wasi::ffi::OsStrExt::from_bytes(fs_encoded_bytes.as_bytes(ob.py())); #[cfg(not(target_os = "wasi"))] - let os_str: &OsStr = std::os::unix::ffi::OsStrExt::from_bytes( - fs_encoded_bytes.as_ref(ob.py()).as_bytes(), - ); + let os_str: &OsStr = + std::os::unix::ffi::OsStrExt::from_bytes(fs_encoded_bytes.as_bytes(ob.py())); Ok(os_str.to_os_string()) } #[cfg(windows)] { + use crate::types::string::PyStringMethods; + // Take the quick and easy shortcut if UTF-8 - if let Ok(utf8_string) = pystring.to_str() { - return Ok(utf8_string.to_owned().into()); + if let Ok(utf8_string) = pystring.to_cow() { + return Ok(utf8_string.into_owned().into()); } // Get an owned allocated wide char buffer from PyString, which we have to deallocate @@ -145,6 +147,7 @@ impl<'a> IntoPy for &'a OsString { #[cfg(test)] mod tests { + use crate::types::{PyAnyMethods, PyStringMethods}; use crate::{types::PyString, IntoPy, PyObject, Python, ToPyObject}; use std::fmt::Debug; use std::{ @@ -177,7 +180,7 @@ mod tests { Python::with_gil(|py| { fn test_roundtrip + Debug>(py: Python<'_>, obj: T) { let pyobject = obj.to_object(py); - let pystring: &PyString = pyobject.extract(py).unwrap(); + let pystring = pyobject.downcast_bound::(py).unwrap(); assert_eq!(pystring.to_string_lossy(), obj.as_ref().to_string_lossy()); let roundtripped_obj: OsString = pystring.extract().unwrap(); assert_eq!(obj.as_ref(), roundtripped_obj.as_os_str()); @@ -198,7 +201,7 @@ mod tests { obj: T, ) { let pyobject = obj.clone().into_py(py); - let pystring: &PyString = pyobject.extract(py).unwrap(); + let pystring = pyobject.downcast_bound::(py).unwrap(); assert_eq!(pystring.to_string_lossy(), obj.as_ref().to_string_lossy()); let roundtripped_obj: OsString = pystring.extract().unwrap(); assert!(obj.as_ref() == roundtripped_obj.as_os_str()); diff --git a/src/conversions/std/path.rs b/src/conversions/std/path.rs index cc579e493db..d7f3121ea10 100644 --- a/src/conversions/std/path.rs +++ b/src/conversions/std/path.rs @@ -1,6 +1,7 @@ -use crate::{ - ffi, FromPyObject, FromPyPointer, IntoPy, PyAny, PyObject, PyResult, Python, ToPyObject, -}; +use crate::ffi_ptr_ext::FfiPtrExt; +use crate::instance::Bound; +use crate::types::any::PyAnyMethods; +use crate::{ffi, FromPyObject, IntoPy, PyAny, PyObject, PyResult, Python, ToPyObject}; use std::borrow::Cow; use std::ffi::OsString; use std::path::{Path, PathBuf}; @@ -14,10 +15,10 @@ impl ToPyObject for Path { // See osstr.rs for why there's no FromPyObject impl for &Path impl FromPyObject<'_> for PathBuf { - fn extract(ob: &PyAny) -> PyResult { + fn extract_bound(ob: &Bound<'_, PyAny>) -> PyResult { // We use os.fspath to get the underlying path as bytes or str - let path = unsafe { PyAny::from_owned_ptr_or_err(ob.py(), ffi::PyOS_FSPath(ob.as_ptr())) }?; - Ok(OsString::extract(path)?.into()) + let path = unsafe { ffi::PyOS_FSPath(ob.as_ptr()).assume_owned_or_err(ob.py())? }; + Ok(path.extract::()?.into()) } } @@ -63,6 +64,7 @@ impl<'a> IntoPy for &'a PathBuf { #[cfg(test)] mod tests { + use crate::types::{PyAnyMethods, PyStringMethods}; use crate::{types::PyString, IntoPy, PyObject, Python, ToPyObject}; use std::borrow::Cow; use std::fmt::Debug; @@ -94,7 +96,7 @@ mod tests { Python::with_gil(|py| { fn test_roundtrip + Debug>(py: Python<'_>, obj: T) { let pyobject = obj.to_object(py); - let pystring: &PyString = pyobject.extract(py).unwrap(); + let pystring = pyobject.downcast_bound::(py).unwrap(); assert_eq!(pystring.to_string_lossy(), obj.as_ref().to_string_lossy()); let roundtripped_obj: PathBuf = pystring.extract().unwrap(); assert_eq!(obj.as_ref(), roundtripped_obj.as_path()); @@ -115,7 +117,7 @@ mod tests { obj: T, ) { let pyobject = obj.clone().into_py(py); - let pystring: &PyString = pyobject.extract(py).unwrap(); + let pystring = pyobject.downcast_bound::(py).unwrap(); assert_eq!(pystring.to_string_lossy(), obj.as_ref().to_string_lossy()); let roundtripped_obj: PathBuf = pystring.extract().unwrap(); assert_eq!(obj.as_ref(), roundtripped_obj.as_path()); diff --git a/src/conversions/std/set.rs b/src/conversions/std/set.rs index 4221c1ff865..c955801a916 100644 --- a/src/conversions/std/set.rs +++ b/src/conversions/std/set.rs @@ -3,7 +3,10 @@ use std::{cmp, collections, hash}; #[cfg(feature = "experimental-inspect")] use crate::inspect::types::TypeInfo; use crate::{ - types::set::new_from_iter, + instance::Bound, + types::any::PyAnyMethods, + types::frozenset::PyFrozenSetMethods, + types::set::{new_from_iter, PySetMethods}, types::{PyFrozenSet, PySet}, FromPyObject, IntoPy, PyAny, PyErr, PyObject, PyResult, Python, ToPyObject, }; @@ -48,17 +51,17 @@ where } } -impl<'source, K, S> FromPyObject<'source> for collections::HashSet +impl<'py, K, S> FromPyObject<'py> for collections::HashSet where - K: FromPyObject<'source> + cmp::Eq + hash::Hash, + K: FromPyObject<'py> + cmp::Eq + hash::Hash, S: hash::BuildHasher + Default, { - fn extract(ob: &'source PyAny) -> PyResult { + fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult { match ob.downcast::() { - Ok(set) => set.iter().map(K::extract).collect(), + Ok(set) => set.iter().map(|any| any.extract()).collect(), Err(err) => { if let Ok(frozen_set) = ob.downcast::() { - frozen_set.iter().map(K::extract).collect() + frozen_set.iter().map(|any| any.extract()).collect() } else { Err(PyErr::from(err)) } @@ -88,16 +91,16 @@ where } } -impl<'source, K> FromPyObject<'source> for collections::BTreeSet +impl<'py, K> FromPyObject<'py> for collections::BTreeSet where - K: FromPyObject<'source> + cmp::Ord, + K: FromPyObject<'py> + cmp::Ord, { - fn extract(ob: &'source PyAny) -> PyResult { + fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult { match ob.downcast::() { - Ok(set) => set.iter().map(K::extract).collect(), + Ok(set) => set.iter().map(|any| any.extract()).collect(), Err(err) => { if let Ok(frozen_set) = ob.downcast::() { - frozen_set.iter().map(K::extract).collect() + frozen_set.iter().map(|any| any.extract()).collect() } else { Err(PyErr::from(err)) } @@ -113,18 +116,18 @@ where #[cfg(test)] mod tests { - use super::{PyFrozenSet, PySet}; + use crate::types::{any::PyAnyMethods, PyFrozenSet, PySet}; use crate::{IntoPy, PyObject, Python, ToPyObject}; use std::collections::{BTreeSet, HashSet}; #[test] fn test_extract_hashset() { Python::with_gil(|py| { - let set = PySet::new(py, &[1, 2, 3, 4, 5]).unwrap(); + let set = PySet::new_bound(py, &[1, 2, 3, 4, 5]).unwrap(); let hash_set: HashSet = set.extract().unwrap(); assert_eq!(hash_set, [1, 2, 3, 4, 5].iter().copied().collect()); - let set = PyFrozenSet::new(py, &[1, 2, 3, 4, 5]).unwrap(); + let set = PyFrozenSet::new_bound(py, &[1, 2, 3, 4, 5]).unwrap(); let hash_set: HashSet = set.extract().unwrap(); assert_eq!(hash_set, [1, 2, 3, 4, 5].iter().copied().collect()); }); @@ -133,11 +136,11 @@ mod tests { #[test] fn test_extract_btreeset() { Python::with_gil(|py| { - let set = PySet::new(py, &[1, 2, 3, 4, 5]).unwrap(); + let set = PySet::new_bound(py, &[1, 2, 3, 4, 5]).unwrap(); let hash_set: BTreeSet = set.extract().unwrap(); assert_eq!(hash_set, [1, 2, 3, 4, 5].iter().copied().collect()); - let set = PyFrozenSet::new(py, &[1, 2, 3, 4, 5]).unwrap(); + let set = PyFrozenSet::new_bound(py, &[1, 2, 3, 4, 5]).unwrap(); let hash_set: BTreeSet = set.extract().unwrap(); assert_eq!(hash_set, [1, 2, 3, 4, 5].iter().copied().collect()); }); diff --git a/src/conversions/std/slice.rs b/src/conversions/std/slice.rs index fbe2d19e1ac..9c9cde06fc7 100644 --- a/src/conversions/std/slice.rs +++ b/src/conversions/std/slice.rs @@ -1,10 +1,15 @@ +use std::borrow::Cow; + #[cfg(feature = "experimental-inspect")] use crate::inspect::types::TypeInfo; -use crate::{types::PyBytes, FromPyObject, IntoPy, PyAny, PyObject, PyResult, Python, ToPyObject}; +use crate::{ + types::{PyByteArray, PyByteArrayMethods, PyBytes}, + IntoPy, Py, PyAny, PyObject, PyResult, Python, ToPyObject, +}; impl<'a> IntoPy for &'a [u8] { fn into_py(self, py: Python<'_>) -> PyObject { - PyBytes::new(py, self).to_object(py) + PyBytes::new_bound(py, self).unbind().into() } #[cfg(feature = "experimental-inspect")] @@ -13,8 +18,21 @@ impl<'a> IntoPy for &'a [u8] { } } -impl<'a> FromPyObject<'a> for &'a [u8] { - fn extract(obj: &'a PyAny) -> PyResult { +#[cfg(feature = "gil-refs")] +impl<'py> crate::FromPyObject<'py> for &'py [u8] { + fn extract_bound(obj: &crate::Bound<'py, PyAny>) -> PyResult { + Ok(obj.clone().into_gil_ref().downcast::()?.as_bytes()) + } + + #[cfg(feature = "experimental-inspect")] + fn type_input() -> TypeInfo { + Self::type_output() + } +} + +#[cfg(not(feature = "gil-refs"))] +impl<'a> crate::conversion::FromPyObjectBound<'a, '_> for &'a [u8] { + fn from_py_object_bound(obj: crate::Borrowed<'a, '_, PyAny>) -> PyResult { Ok(obj.downcast::()?.as_bytes()) } @@ -24,17 +42,94 @@ impl<'a> FromPyObject<'a> for &'a [u8] { } } +/// Special-purpose trait impl to efficiently handle both `bytes` and `bytearray` +/// +/// If the source object is a `bytes` object, the `Cow` will be borrowed and +/// pointing into the source object, and no copying or heap allocations will happen. +/// If it is a `bytearray`, its contents will be copied to an owned `Cow`. +#[cfg(feature = "gil-refs")] +impl<'py> crate::FromPyObject<'py> for Cow<'py, [u8]> { + fn extract_bound(ob: &crate::Bound<'py, PyAny>) -> PyResult { + use crate::types::PyAnyMethods; + if let Ok(bytes) = ob.downcast::() { + return Ok(Cow::Borrowed(bytes.clone().into_gil_ref().as_bytes())); + } + + let byte_array = ob.downcast::()?; + Ok(Cow::Owned(byte_array.to_vec())) + } +} + +#[cfg(not(feature = "gil-refs"))] +impl<'a> crate::conversion::FromPyObjectBound<'a, '_> for Cow<'a, [u8]> { + fn from_py_object_bound(ob: crate::Borrowed<'a, '_, PyAny>) -> PyResult { + if let Ok(bytes) = ob.downcast::() { + return Ok(Cow::Borrowed(bytes.as_bytes())); + } + + let byte_array = ob.downcast::()?; + Ok(Cow::Owned(byte_array.to_vec())) + } + + #[cfg(feature = "experimental-inspect")] + fn type_input() -> TypeInfo { + Self::type_output() + } +} + +impl ToPyObject for Cow<'_, [u8]> { + fn to_object(&self, py: Python<'_>) -> Py { + PyBytes::new_bound(py, self.as_ref()).into() + } +} + +impl IntoPy> for Cow<'_, [u8]> { + fn into_py(self, py: Python<'_>) -> Py { + self.to_object(py) + } +} + #[cfg(test)] mod tests { - use crate::FromPyObject; - use crate::Python; + use std::borrow::Cow; + + use crate::{ + types::{any::PyAnyMethods, PyBytes}, + Python, ToPyObject, + }; #[test] fn test_extract_bytes() { Python::with_gil(|py| { - let py_bytes = py.eval("b'Hello Python'", None, None).unwrap(); - let bytes: &[u8] = FromPyObject::extract(py_bytes).unwrap(); + let py_bytes = py.eval_bound("b'Hello Python'", None, None).unwrap(); + let bytes: &[u8] = py_bytes.extract().unwrap(); assert_eq!(bytes, b"Hello Python"); }); } + + #[test] + fn test_cow_impl() { + Python::with_gil(|py| { + let bytes = py.eval_bound(r#"b"foobar""#, None, None).unwrap(); + let cow = bytes.extract::>().unwrap(); + assert_eq!(cow, Cow::<[u8]>::Borrowed(b"foobar")); + + let byte_array = py + .eval_bound(r#"bytearray(b"foobar")"#, None, None) + .unwrap(); + let cow = byte_array.extract::>().unwrap(); + assert_eq!(cow, Cow::<[u8]>::Owned(b"foobar".to_vec())); + + let something_else_entirely = py.eval_bound("42", None, None).unwrap(); + something_else_entirely + .extract::>() + .unwrap_err(); + + let cow = Cow::<[u8]>::Borrowed(b"foobar").to_object(py); + assert!(cow.bind(py).is_instance_of::()); + + let cow = Cow::<[u8]>::Owned(b"foobar".to_vec()).to_object(py); + assert!(cow.bind(py).is_instance_of::()); + }); + } } diff --git a/src/conversions/std/string.rs b/src/conversions/std/string.rs index b4bc8c26099..5bc05c1a091 100644 --- a/src/conversions/std/string.rs +++ b/src/conversions/std/string.rs @@ -3,7 +3,9 @@ use std::borrow::Cow; #[cfg(feature = "experimental-inspect")] use crate::inspect::types::TypeInfo; use crate::{ - types::PyString, FromPyObject, IntoPy, Py, PyAny, PyObject, PyResult, Python, ToPyObject, + instance::Bound, + types::{any::PyAnyMethods, string::PyStringMethods, PyString}, + FromPyObject, IntoPy, Py, PyAny, PyObject, PyResult, Python, ToPyObject, }; /// Converts a Rust `str` to a Python object. @@ -11,14 +13,14 @@ use crate::{ impl ToPyObject for str { #[inline] fn to_object(&self, py: Python<'_>) -> PyObject { - PyString::new(py, self).into() + PyString::new_bound(py, self).into() } } impl<'a> IntoPy for &'a str { #[inline] fn into_py(self, py: Python<'_>) -> PyObject { - PyString::new(py, self).into() + PyString::new_bound(py, self).into() } #[cfg(feature = "experimental-inspect")] @@ -30,7 +32,7 @@ impl<'a> IntoPy for &'a str { impl<'a> IntoPy> for &'a str { #[inline] fn into_py(self, py: Python<'_>) -> Py { - PyString::new(py, self).into() + PyString::new_bound(py, self).into() } #[cfg(feature = "experimental-inspect")] @@ -44,7 +46,7 @@ impl<'a> IntoPy> for &'a str { impl ToPyObject for Cow<'_, str> { #[inline] fn to_object(&self, py: Python<'_>) -> PyObject { - PyString::new(py, self).into() + PyString::new_bound(py, self).into() } } @@ -65,7 +67,7 @@ impl IntoPy for Cow<'_, str> { impl ToPyObject for String { #[inline] fn to_object(&self, py: Python<'_>) -> PyObject { - PyString::new(py, self).into() + PyString::new_bound(py, self).into() } } @@ -78,7 +80,7 @@ impl ToPyObject for char { impl IntoPy for char { fn into_py(self, py: Python<'_>) -> PyObject { let mut bytes = [0u8; 4]; - PyString::new(py, self.encode_utf8(&mut bytes)).into() + PyString::new_bound(py, self.encode_utf8(&mut bytes)).into() } #[cfg(feature = "experimental-inspect")] @@ -89,7 +91,7 @@ impl IntoPy for char { impl IntoPy for String { fn into_py(self, py: Python<'_>) -> PyObject { - PyString::new(py, &self).into() + PyString::new_bound(py, &self).into() } #[cfg(feature = "experimental-inspect")] @@ -101,7 +103,7 @@ impl IntoPy for String { impl<'a> IntoPy for &'a String { #[inline] fn into_py(self, py: Python<'_>) -> PyObject { - PyString::new(py, self).into() + PyString::new_bound(py, self).into() } #[cfg(feature = "experimental-inspect")] @@ -111,23 +113,60 @@ impl<'a> IntoPy for &'a String { } /// Allows extracting strings from Python objects. -/// Accepts Python `str` and `unicode` objects. -impl<'source> FromPyObject<'source> for &'source str { - fn extract(ob: &'source PyAny) -> PyResult { +/// Accepts Python `str` objects. +#[cfg(feature = "gil-refs")] +impl<'py> FromPyObject<'py> for &'py str { + fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult { + ob.clone().into_gil_ref().downcast::()?.to_str() + } + + #[cfg(feature = "experimental-inspect")] + fn type_input() -> TypeInfo { + ::type_input() + } +} + +#[cfg(all(not(feature = "gil-refs"), any(Py_3_10, not(Py_LIMITED_API))))] +impl<'a> crate::conversion::FromPyObjectBound<'a, '_> for &'a str { + fn from_py_object_bound(ob: crate::Borrowed<'a, '_, PyAny>) -> PyResult { ob.downcast::()?.to_str() } #[cfg(feature = "experimental-inspect")] fn type_input() -> TypeInfo { - ::type_input() + ::type_input() + } +} + +#[cfg(feature = "gil-refs")] +impl<'py> FromPyObject<'py> for Cow<'py, str> { + fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult { + ob.extract().map(Cow::Owned) + } + + #[cfg(feature = "experimental-inspect")] + fn type_input() -> TypeInfo { + ::type_input() + } +} + +#[cfg(not(feature = "gil-refs"))] +impl<'a> crate::conversion::FromPyObjectBound<'a, '_> for Cow<'a, str> { + fn from_py_object_bound(ob: crate::Borrowed<'a, '_, PyAny>) -> PyResult { + ob.downcast::()?.to_cow() + } + + #[cfg(feature = "experimental-inspect")] + fn type_input() -> TypeInfo { + ::type_input() } } /// Allows extracting strings from Python objects. /// Accepts Python `str` and `unicode` objects. impl FromPyObject<'_> for String { - fn extract(obj: &PyAny) -> PyResult { - obj.downcast::()?.to_str().map(ToOwned::to_owned) + fn extract_bound(obj: &Bound<'_, PyAny>) -> PyResult { + obj.downcast::()?.to_cow().map(Cow::into_owned) } #[cfg(feature = "experimental-inspect")] @@ -137,8 +176,8 @@ impl FromPyObject<'_> for String { } impl FromPyObject<'_> for char { - fn extract(obj: &PyAny) -> PyResult { - let s = obj.downcast::()?.to_str()?; + fn extract_bound(obj: &Bound<'_, PyAny>) -> PyResult { + let s = obj.downcast::()?.to_cow()?; let mut iter = s.chars(); if let (Some(ch), None) = (iter.next(), iter.next()) { Ok(ch) @@ -157,8 +196,9 @@ impl FromPyObject<'_> for char { #[cfg(test)] mod tests { + use crate::types::any::PyAnyMethods; use crate::Python; - use crate::{FromPyObject, IntoPy, PyObject, ToPyObject}; + use crate::{IntoPy, PyObject, ToPyObject}; use std::borrow::Cow; #[test] @@ -166,9 +206,9 @@ mod tests { Python::with_gil(|py| { let s = "Hello Python"; let py_string: PyObject = Cow::Borrowed(s).into_py(py); - assert_eq!(s, py_string.extract::<&str>(py).unwrap()); + assert_eq!(s, py_string.extract::>(py).unwrap()); let py_string: PyObject = Cow::::Owned(s.into()).into_py(py); - assert_eq!(s, py_string.extract::<&str>(py).unwrap()); + assert_eq!(s, py_string.extract::>(py).unwrap()); }) } @@ -177,9 +217,9 @@ mod tests { Python::with_gil(|py| { let s = "Hello Python"; let py_string = Cow::Borrowed(s).to_object(py); - assert_eq!(s, py_string.extract::<&str>(py).unwrap()); + assert_eq!(s, py_string.extract::>(py).unwrap()); let py_string = Cow::::Owned(s.into()).to_object(py); - assert_eq!(s, py_string.extract::<&str>(py).unwrap()); + assert_eq!(s, py_string.extract::>(py).unwrap()); }) } @@ -198,7 +238,7 @@ mod tests { let s = "Hello Python"; let py_string = s.to_object(py); - let s2: &str = FromPyObject::extract(py_string.as_ref(py)).unwrap(); + let s2: Cow<'_, str> = py_string.bind(py).extract().unwrap(); assert_eq!(s, s2); }) } @@ -208,7 +248,7 @@ mod tests { Python::with_gil(|py| { let ch = '😃'; let py_string = ch.to_object(py); - let ch2: char = FromPyObject::extract(py_string.as_ref(py)).unwrap(); + let ch2: char = py_string.bind(py).extract().unwrap(); assert_eq!(ch, ch2); }) } @@ -218,7 +258,7 @@ mod tests { Python::with_gil(|py| { let s = "Hello Python"; let py_string = s.to_object(py); - let err: crate::PyResult = FromPyObject::extract(py_string.as_ref(py)); + let err: crate::PyResult = py_string.bind(py).extract(); assert!(err .unwrap_err() .to_string() @@ -235,19 +275,19 @@ mod tests { assert_eq!( s, IntoPy::::into_py(s3, py) - .extract::<&str>(py) + .extract::>(py) .unwrap() ); assert_eq!( s, IntoPy::::into_py(s2, py) - .extract::<&str>(py) + .extract::>(py) .unwrap() ); assert_eq!( s, IntoPy::::into_py(s, py) - .extract::<&str>(py) + .extract::>(py) .unwrap() ); }) diff --git a/src/conversions/std/time.rs b/src/conversions/std/time.rs new file mode 100755 index 00000000000..89d61e696a1 --- /dev/null +++ b/src/conversions/std/time.rs @@ -0,0 +1,399 @@ +use crate::exceptions::{PyOverflowError, PyValueError}; +use crate::sync::GILOnceCell; +use crate::types::any::PyAnyMethods; +#[cfg(Py_LIMITED_API)] +use crate::types::PyType; +#[cfg(not(Py_LIMITED_API))] +use crate::types::{timezone_utc_bound, PyDateTime, PyDelta, PyDeltaAccess}; +#[cfg(Py_LIMITED_API)] +use crate::Py; +use crate::{ + intern, Bound, FromPyObject, IntoPy, PyAny, PyErr, PyObject, PyResult, Python, ToPyObject, +}; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; + +const SECONDS_PER_DAY: u64 = 24 * 60 * 60; + +impl FromPyObject<'_> for Duration { + fn extract_bound(obj: &Bound<'_, PyAny>) -> PyResult { + #[cfg(not(Py_LIMITED_API))] + let (days, seconds, microseconds) = { + let delta = obj.downcast::()?; + ( + delta.get_days(), + delta.get_seconds(), + delta.get_microseconds(), + ) + }; + #[cfg(Py_LIMITED_API)] + let (days, seconds, microseconds): (i32, i32, i32) = { + ( + obj.getattr(intern!(obj.py(), "days"))?.extract()?, + obj.getattr(intern!(obj.py(), "seconds"))?.extract()?, + obj.getattr(intern!(obj.py(), "microseconds"))?.extract()?, + ) + }; + + // We cast + let days = u64::try_from(days).map_err(|_| { + PyValueError::new_err( + "It is not possible to convert a negative timedelta to a Rust Duration", + ) + })?; + let seconds = u64::try_from(seconds).unwrap(); // 0 <= seconds < 3600*24 + let microseconds = u32::try_from(microseconds).unwrap(); // 0 <= microseconds < 1000000 + + // We convert + let total_seconds = days * SECONDS_PER_DAY + seconds; // We casted from i32, this can't overflow + let nanoseconds = microseconds.checked_mul(1_000).unwrap(); // 0 <= microseconds < 1000000 + + Ok(Duration::new(total_seconds, nanoseconds)) + } +} + +impl ToPyObject for Duration { + fn to_object(&self, py: Python<'_>) -> PyObject { + let days = self.as_secs() / SECONDS_PER_DAY; + let seconds = self.as_secs() % SECONDS_PER_DAY; + let microseconds = self.subsec_micros(); + + #[cfg(not(Py_LIMITED_API))] + { + PyDelta::new_bound( + py, + days.try_into() + .expect("Too large Rust duration for timedelta"), + seconds.try_into().unwrap(), + microseconds.try_into().unwrap(), + false, + ) + .expect("failed to construct timedelta (overflow?)") + .into() + } + #[cfg(Py_LIMITED_API)] + { + static TIMEDELTA: GILOnceCell> = GILOnceCell::new(); + TIMEDELTA + .get_or_try_init_type_ref(py, "datetime", "timedelta") + .unwrap() + .call1((days, seconds, microseconds)) + .unwrap() + .into() + } + } +} + +impl IntoPy for Duration { + fn into_py(self, py: Python<'_>) -> PyObject { + self.to_object(py) + } +} + +// Conversions between SystemTime and datetime do not rely on the floating point timestamp of the +// timestamp/fromtimestamp APIs to avoid possible precision loss but goes through the +// timedelta/std::time::Duration types by taking for reference point the UNIX epoch. +// +// TODO: it might be nice to investigate using timestamps anyway, at least when the datetime is a safe range. + +impl FromPyObject<'_> for SystemTime { + fn extract_bound(obj: &Bound<'_, PyAny>) -> PyResult { + let duration_since_unix_epoch: Duration = obj + .call_method1(intern!(obj.py(), "__sub__"), (unix_epoch_py(obj.py()),))? + .extract()?; + UNIX_EPOCH + .checked_add(duration_since_unix_epoch) + .ok_or_else(|| { + PyOverflowError::new_err("Overflow error when converting the time to Rust") + }) + } +} + +impl ToPyObject for SystemTime { + fn to_object(&self, py: Python<'_>) -> PyObject { + let duration_since_unix_epoch = self.duration_since(UNIX_EPOCH).unwrap().into_py(py); + unix_epoch_py(py) + .call_method1(py, intern!(py, "__add__"), (duration_since_unix_epoch,)) + .unwrap() + } +} + +impl IntoPy for SystemTime { + fn into_py(self, py: Python<'_>) -> PyObject { + self.to_object(py) + } +} + +fn unix_epoch_py(py: Python<'_>) -> &PyObject { + static UNIX_EPOCH: GILOnceCell = GILOnceCell::new(); + UNIX_EPOCH + .get_or_try_init(py, || { + #[cfg(not(Py_LIMITED_API))] + { + Ok::<_, PyErr>( + PyDateTime::new_bound( + py, + 1970, + 1, + 1, + 0, + 0, + 0, + 0, + Some(&timezone_utc_bound(py)), + )? + .into(), + ) + } + #[cfg(Py_LIMITED_API)] + { + let datetime = py.import_bound("datetime")?; + let utc = datetime.getattr("timezone")?.getattr("utc")?; + Ok::<_, PyErr>( + datetime + .getattr("datetime")? + .call1((1970, 1, 1, 0, 0, 0, 0, utc)) + .unwrap() + .into(), + ) + } + }) + .unwrap() +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::types::PyDict; + use std::panic; + + #[test] + fn test_duration_frompyobject() { + Python::with_gil(|py| { + assert_eq!( + new_timedelta(py, 0, 0, 0).extract::().unwrap(), + Duration::new(0, 0) + ); + assert_eq!( + new_timedelta(py, 1, 0, 0).extract::().unwrap(), + Duration::new(86400, 0) + ); + assert_eq!( + new_timedelta(py, 0, 1, 0).extract::().unwrap(), + Duration::new(1, 0) + ); + assert_eq!( + new_timedelta(py, 0, 0, 1).extract::().unwrap(), + Duration::new(0, 1_000) + ); + assert_eq!( + new_timedelta(py, 1, 1, 1).extract::().unwrap(), + Duration::new(86401, 1_000) + ); + assert_eq!( + timedelta_class(py) + .getattr("max") + .unwrap() + .extract::() + .unwrap(), + Duration::new(86399999999999, 999999000) + ); + }); + } + + #[test] + fn test_duration_frompyobject_negative() { + Python::with_gil(|py| { + assert_eq!( + new_timedelta(py, 0, -1, 0) + .extract::() + .unwrap_err() + .to_string(), + "ValueError: It is not possible to convert a negative timedelta to a Rust Duration" + ); + }) + } + + #[test] + fn test_duration_topyobject() { + Python::with_gil(|py| { + let assert_eq = |l: PyObject, r: Bound<'_, PyAny>| { + assert!(l.bind(py).eq(r).unwrap()); + }; + + assert_eq( + Duration::new(0, 0).to_object(py), + new_timedelta(py, 0, 0, 0), + ); + assert_eq( + Duration::new(86400, 0).to_object(py), + new_timedelta(py, 1, 0, 0), + ); + assert_eq( + Duration::new(1, 0).to_object(py), + new_timedelta(py, 0, 1, 0), + ); + assert_eq( + Duration::new(0, 1_000).to_object(py), + new_timedelta(py, 0, 0, 1), + ); + assert_eq( + Duration::new(0, 1).to_object(py), + new_timedelta(py, 0, 0, 0), + ); + assert_eq( + Duration::new(86401, 1_000).to_object(py), + new_timedelta(py, 1, 1, 1), + ); + assert_eq( + Duration::new(86399999999999, 999999000).to_object(py), + timedelta_class(py).getattr("max").unwrap(), + ); + }); + } + + #[test] + fn test_duration_topyobject_overflow() { + Python::with_gil(|py| { + assert!(panic::catch_unwind(|| Duration::MAX.to_object(py)).is_err()); + }) + } + + #[test] + fn test_time_frompyobject() { + Python::with_gil(|py| { + assert_eq!( + new_datetime(py, 1970, 1, 1, 0, 0, 0, 0) + .extract::() + .unwrap(), + UNIX_EPOCH + ); + assert_eq!( + new_datetime(py, 2020, 2, 3, 4, 5, 6, 7) + .extract::() + .unwrap(), + UNIX_EPOCH + .checked_add(Duration::new(1580702706, 7000)) + .unwrap() + ); + assert_eq!( + max_datetime(py).extract::().unwrap(), + UNIX_EPOCH + .checked_add(Duration::new(253402300799, 999999000)) + .unwrap() + ); + }); + } + + #[test] + fn test_time_frompyobject_before_epoch() { + Python::with_gil(|py| { + assert_eq!( + new_datetime(py, 1950, 1, 1, 0, 0, 0, 0) + .extract::() + .unwrap_err() + .to_string(), + "ValueError: It is not possible to convert a negative timedelta to a Rust Duration" + ); + }) + } + + #[test] + fn test_time_topyobject() { + Python::with_gil(|py| { + let assert_eq = |l: PyObject, r: Bound<'_, PyAny>| { + assert!(l.bind(py).eq(r).unwrap()); + }; + + assert_eq( + UNIX_EPOCH + .checked_add(Duration::new(1580702706, 7123)) + .unwrap() + .into_py(py), + new_datetime(py, 2020, 2, 3, 4, 5, 6, 7), + ); + assert_eq( + UNIX_EPOCH + .checked_add(Duration::new(253402300799, 999999000)) + .unwrap() + .into_py(py), + max_datetime(py), + ); + }); + } + + #[allow(clippy::too_many_arguments)] + fn new_datetime( + py: Python<'_>, + year: i32, + month: u8, + day: u8, + hour: u8, + minute: u8, + second: u8, + microsecond: u32, + ) -> Bound<'_, PyAny> { + datetime_class(py) + .call1(( + year, + month, + day, + hour, + minute, + second, + microsecond, + tz_utc(py), + )) + .unwrap() + } + + fn max_datetime(py: Python<'_>) -> Bound<'_, PyAny> { + let naive_max = datetime_class(py).getattr("max").unwrap(); + let kargs = PyDict::new_bound(py); + kargs.set_item("tzinfo", tz_utc(py)).unwrap(); + naive_max.call_method("replace", (), Some(&kargs)).unwrap() + } + + #[test] + fn test_time_topyobject_overflow() { + let big_system_time = UNIX_EPOCH + .checked_add(Duration::new(300000000000, 0)) + .unwrap(); + Python::with_gil(|py| { + assert!(panic::catch_unwind(|| big_system_time.into_py(py)).is_err()); + }) + } + + fn tz_utc(py: Python<'_>) -> Bound<'_, PyAny> { + py.import_bound("datetime") + .unwrap() + .getattr("timezone") + .unwrap() + .getattr("utc") + .unwrap() + } + + fn new_timedelta( + py: Python<'_>, + days: i32, + seconds: i32, + microseconds: i32, + ) -> Bound<'_, PyAny> { + timedelta_class(py) + .call1((days, seconds, microseconds)) + .unwrap() + } + + fn datetime_class(py: Python<'_>) -> Bound<'_, PyAny> { + py.import_bound("datetime") + .unwrap() + .getattr("datetime") + .unwrap() + } + + fn timedelta_class(py: Python<'_>) -> Bound<'_, PyAny> { + py.import_bound("datetime") + .unwrap() + .getattr("timedelta") + .unwrap() + } +} diff --git a/src/coroutine.rs b/src/coroutine.rs index 7dd73cbba10..f2feab4af16 100644 --- a/src/coroutine.rs +++ b/src/coroutine.rs @@ -14,8 +14,8 @@ use crate::{ coroutine::{cancel::ThrowCallback, waker::AsyncioWaker}, exceptions::{PyAttributeError, PyRuntimeError, PyStopIteration}, panic::PanicException, - types::{PyIterator, PyString}, - IntoPy, Py, PyAny, PyErr, PyObject, PyResult, Python, + types::{string::PyStringMethods, PyIterator, PyString}, + Bound, IntoPy, Py, PyAny, PyErr, PyObject, PyResult, Python, }; pub(crate) mod cancel; @@ -75,10 +75,10 @@ impl Coroutine { }; // reraise thrown exception it match (throw, &self.throw_callback) { - (Some(exc), Some(cb)) => cb.throw(exc.as_ref(py)), + (Some(exc), Some(cb)) => cb.throw(exc), (Some(exc), None) => { self.close(); - return Err(PyErr::from_value(exc.as_ref(py))); + return Err(PyErr::from_value_bound(exc.into_bound(py))); } (None, _) => {} } @@ -107,7 +107,10 @@ impl Coroutine { if let Some(future) = self.waker.as_ref().unwrap().initialize_future(py)? { // `asyncio.Future` must be awaited; fortunately, it implements `__iter__ = __await__` // and will yield itself if its result has not been set in polling above - if let Some(future) = PyIterator::from_object(future).unwrap().next() { + if let Some(future) = PyIterator::from_bound_object(&future.as_borrowed()) + .unwrap() + .next() + { // future has not been leaked into Python for now, and Rust code can only call // `set_result(None)` in `Wake` implementation, so it's safe to unwrap return Ok(future.unwrap().into()); @@ -115,7 +118,7 @@ impl Coroutine { } // if waker has been waken during future polling, this is roughly equivalent to // `await asyncio.sleep(0)`, so just yield `None`. - Ok(py.None().into()) + Ok(py.None().into_py(py)) } } @@ -132,7 +135,7 @@ impl Coroutine { #[getter] fn __qualname__(&self, py: Python<'_>) -> PyResult> { match (&self.name, &self.qualname_prefix) { - (Some(name), Some(prefix)) => Ok(format!("{}.{}", prefix, name.as_ref(py).to_str()?) + (Some(name), Some(prefix)) => Ok(format!("{}.{}", prefix, name.bind(py).to_cow()?) .as_str() .into_py(py)), (Some(name), None) => Ok(name.clone_ref(py)), @@ -140,7 +143,7 @@ impl Coroutine { } } - fn send(&mut self, py: Python<'_>, _value: &PyAny) -> PyResult { + fn send(&mut self, py: Python<'_>, _value: &Bound<'_, PyAny>) -> PyResult { self.poll(py, None) } diff --git a/src/coroutine/cancel.rs b/src/coroutine/cancel.rs index 7828986c11e..47f5d69430a 100644 --- a/src/coroutine/cancel.rs +++ b/src/coroutine/cancel.rs @@ -1,8 +1,7 @@ -use crate::{PyAny, PyObject}; -use parking_lot::Mutex; +use crate::{Py, PyAny, PyObject}; use std::future::Future; use std::pin::Pin; -use std::sync::Arc; +use std::sync::{Arc, Mutex}; use std::task::{Context, Poll, Waker}; #[derive(Debug, Default)] @@ -25,12 +24,12 @@ impl CancelHandle { /// Returns whether the associated coroutine has been cancelled. pub fn is_cancelled(&self) -> bool { - self.0.lock().exception.is_some() + self.0.lock().unwrap().exception.is_some() } /// Poll to retrieve the exception thrown in the associated coroutine. pub fn poll_cancelled(&mut self, cx: &mut Context<'_>) -> Poll { - let mut inner = self.0.lock(); + let mut inner = self.0.lock().unwrap(); if let Some(exc) = inner.exception.take() { return Poll::Ready(exc); } @@ -68,9 +67,9 @@ impl Future for Cancelled<'_> { pub struct ThrowCallback(Arc>); impl ThrowCallback { - pub(super) fn throw(&self, exc: &PyAny) { - let mut inner = self.0.lock(); - inner.exception = Some(exc.into()); + pub(super) fn throw(&self, exc: Py) { + let mut inner = self.0.lock().unwrap(); + inner.exception = Some(exc); if let Some(waker) = inner.waker.take() { waker.wake(); } diff --git a/src/coroutine/waker.rs b/src/coroutine/waker.rs index 8a1166ce3fb..fc7c54e1f5a 100644 --- a/src/coroutine/waker.rs +++ b/src/coroutine/waker.rs @@ -1,6 +1,7 @@ use crate::sync::GILOnceCell; +use crate::types::any::PyAnyMethods; use crate::types::PyCFunction; -use crate::{intern, wrap_pyfunction, Py, PyAny, PyObject, PyResult, Python}; +use crate::{intern, wrap_pyfunction_bound, Bound, Py, PyAny, PyObject, PyResult, Python}; use pyo3_macros::pyfunction; use std::sync::Arc; use std::task::Wake; @@ -24,10 +25,13 @@ impl AsyncioWaker { self.0.take(); } - pub(super) fn initialize_future<'a>(&'a self, py: Python<'a>) -> PyResult> { + pub(super) fn initialize_future<'py>( + &self, + py: Python<'py>, + ) -> PyResult>> { let init = || LoopAndFuture::new(py).map(Some); let loop_and_future = self.0.get_or_try_init(py, init)?.as_ref(); - Ok(loop_and_future.map(|LoopAndFuture { future, .. }| future.as_ref(py))) + Ok(loop_and_future.map(|LoopAndFuture { future, .. }| future.bind(py))) } } @@ -56,7 +60,7 @@ impl LoopAndFuture { fn new(py: Python<'_>) -> PyResult { static GET_RUNNING_LOOP: GILOnceCell = GILOnceCell::new(); let import = || -> PyResult<_> { - let module = py.import("asyncio")?; + let module = py.import_bound("asyncio")?; Ok(module.getattr("get_running_loop")?.into()) }; let event_loop = GET_RUNNING_LOOP.get_or_try_init(py, import)?.call0(py)?; @@ -66,14 +70,15 @@ impl LoopAndFuture { fn set_result(&self, py: Python<'_>) -> PyResult<()> { static RELEASE_WAITER: GILOnceCell> = GILOnceCell::new(); - let release_waiter = RELEASE_WAITER - .get_or_try_init(py, || wrap_pyfunction!(release_waiter, py).map(Into::into))?; + let release_waiter = RELEASE_WAITER.get_or_try_init(py, || { + wrap_pyfunction_bound!(release_waiter, py).map(Bound::unbind) + })?; // `Future.set_result` must be called in event loop thread, // so it requires `call_soon_threadsafe` let call_soon_threadsafe = self.event_loop.call_method1( py, intern!(py, "call_soon_threadsafe"), - (release_waiter, self.future.as_ref(py)), + (release_waiter, self.future.bind(py)), ); if let Err(err) = call_soon_threadsafe { // `call_soon_threadsafe` will raise if the event loop is closed; @@ -92,7 +97,7 @@ impl LoopAndFuture { /// Future can be cancelled by the event loop before being waken. /// See #[pyfunction(crate = "crate")] -fn release_waiter(future: &PyAny) -> PyResult<()> { +fn release_waiter(future: &Bound<'_, PyAny>) -> PyResult<()> { let done = future.call_method0(intern!(future.py(), "done"))?; if !done.extract::()? { future.call_method1(intern!(future.py(), "set_result"), (future.py().None(),))?; diff --git a/src/derive_utils.rs b/src/derive_utils.rs index 4ccb38f901b..a47f489ceb8 100644 --- a/src/derive_utils.rs +++ b/src/derive_utils.rs @@ -13,7 +13,7 @@ impl<'a> PyFunctionArguments<'a> { match self { PyFunctionArguments::Python(py) => (py, None), PyFunctionArguments::PyModule(module) => { - let py = module.py(); + let py = crate::PyNativeType::py(module); (py, Some(module)) } } diff --git a/src/err/err_state.rs b/src/err/err_state.rs index b9bda9b7013..14345b275c9 100644 --- a/src/err/err_state.rs +++ b/src/err/err_state.rs @@ -2,10 +2,9 @@ use crate::{ exceptions::{PyBaseException, PyTypeError}, ffi, types::{PyTraceback, PyType}, - IntoPy, Py, PyAny, PyObject, PyTypeInfo, Python, + Bound, IntoPy, Py, PyAny, PyObject, PyTypeInfo, Python, }; -#[derive(Clone)] pub(crate) struct PyErrStateNormalized { #[cfg(not(Py_3_12))] ptype: Py, @@ -16,25 +15,32 @@ pub(crate) struct PyErrStateNormalized { impl PyErrStateNormalized { #[cfg(not(Py_3_12))] - pub(crate) fn ptype<'py>(&'py self, py: Python<'py>) -> &'py PyType { - self.ptype.as_ref(py) + pub(crate) fn ptype<'py>(&self, py: Python<'py>) -> Bound<'py, PyType> { + self.ptype.bind(py).clone() } #[cfg(Py_3_12)] - pub(crate) fn ptype<'py>(&'py self, py: Python<'py>) -> &'py PyType { - self.pvalue.as_ref(py).get_type() + pub(crate) fn ptype<'py>(&self, py: Python<'py>) -> Bound<'py, PyType> { + use crate::types::any::PyAnyMethods; + self.pvalue.bind(py).get_type() } #[cfg(not(Py_3_12))] - pub(crate) fn ptraceback<'py>(&'py self, py: Python<'py>) -> Option<&'py PyTraceback> { + pub(crate) fn ptraceback<'py>(&self, py: Python<'py>) -> Option> { self.ptraceback .as_ref() - .map(|traceback| traceback.as_ref(py)) + .map(|traceback| traceback.bind(py).clone()) } #[cfg(Py_3_12)] - pub(crate) fn ptraceback<'py>(&'py self, py: Python<'py>) -> Option<&'py PyTraceback> { - unsafe { py.from_owned_ptr_or_opt(ffi::PyException_GetTraceback(self.pvalue.as_ptr())) } + pub(crate) fn ptraceback<'py>(&self, py: Python<'py>) -> Option> { + use crate::ffi_ptr_ext::FfiPtrExt; + use crate::types::any::PyAnyMethods; + unsafe { + ffi::PyException_GetTraceback(self.pvalue.as_ptr()) + .assume_owned_or_opt(py) + .map(|b| b.downcast_into_unchecked()) + } } #[cfg(Py_3_12)] @@ -56,6 +62,19 @@ impl PyErrStateNormalized { ptraceback: Py::from_owned_ptr_or_opt(py, ptraceback), } } + + pub fn clone_ref(&self, py: Python<'_>) -> Self { + Self { + #[cfg(not(Py_3_12))] + ptype: self.ptype.clone_ref(py), + pvalue: self.pvalue.clone_ref(py), + #[cfg(not(Py_3_12))] + ptraceback: self + .ptraceback + .as_ref() + .map(|ptraceback| ptraceback.clone_ref(py)), + } + } } pub(crate) struct PyErrStateLazyFnOutput { @@ -93,19 +112,20 @@ where } impl PyErrState { - pub(crate) fn lazy(ptype: &PyAny, args: impl PyErrArguments + 'static) -> Self { - let ptype = ptype.into(); + pub(crate) fn lazy(ptype: Py, args: impl PyErrArguments + 'static) -> Self { PyErrState::Lazy(Box::new(move |py| PyErrStateLazyFnOutput { ptype, pvalue: args.arguments(py), })) } - pub(crate) fn normalized(pvalue: &PyBaseException) -> Self { + pub(crate) fn normalized(pvalue: Bound<'_, PyBaseException>) -> Self { + #[cfg(not(Py_3_12))] + use crate::types::any::PyAnyMethods; + Self::Normalized(PyErrStateNormalized { #[cfg(not(Py_3_12))] ptype: pvalue.get_type().into(), - pvalue: pvalue.into(), #[cfg(not(Py_3_12))] ptraceback: unsafe { Py::from_owned_ptr_or_opt( @@ -113,6 +133,7 @@ impl PyErrState { ffi::PyException_GetTraceback(pvalue.as_ptr()), ) }, + pvalue: pvalue.into(), }) } diff --git a/src/err/impls.rs b/src/err/impls.rs index f49ac3eb1a8..f14a839b471 100644 --- a/src/err/impls.rs +++ b/src/err/impls.rs @@ -124,6 +124,8 @@ mod tests { #[test] fn io_errors() { + use crate::types::any::PyAnyMethods; + let check_err = |kind, expected_ty| { Python::with_gil(|py| { let rust_err = io::Error::new(kind, "some error msg"); @@ -139,8 +141,8 @@ mod tests { let py_err_recovered_from_rust_err: PyErr = rust_err_from_py_err.into(); assert!(py_err_recovered_from_rust_err - .value(py) - .is(py_error_clone.value(py))); // It should be the same exception + .value_bound(py) + .is(py_error_clone.value_bound(py))); // It should be the same exception }) }; diff --git a/src/err/mod.rs b/src/err/mod.rs index 6c439e6553d..f87952091ad 100644 --- a/src/err/mod.rs +++ b/src/err/mod.rs @@ -2,12 +2,14 @@ use crate::instance::Bound; use crate::panic::PanicException; use crate::type_object::PyTypeInfo; use crate::types::any::PyAnyMethods; -use crate::types::{PyTraceback, PyType}; +use crate::types::{string::PyStringMethods, typeobject::PyTypeMethods, PyTraceback, PyType}; +#[cfg(feature = "gil-refs")] +use crate::PyNativeType; use crate::{ exceptions::{self, PyBaseException}, ffi, }; -use crate::{IntoPy, Py, PyAny, PyObject, Python, ToPyObject}; +use crate::{Borrowed, IntoPy, Py, PyAny, PyObject, Python, ToPyObject}; use std::borrow::Cow; use std::cell::UnsafeCell; use std::ffi::CString; @@ -24,9 +26,9 @@ use err_state::{PyErrState, PyErrStateLazyFnOutput, PyErrStateNormalized}; /// compatibility with `?` and other Rust errors) this type supports creating exceptions instances /// in a lazy fashion, where the full Python object for the exception is created only when needed. /// -/// Accessing the contained exception in any way, such as with [`value`](PyErr::value), -/// [`get_type`](PyErr::get_type), or [`is_instance`](PyErr::is_instance) will create the full -/// exception object if it was not already created. +/// Accessing the contained exception in any way, such as with [`value_bound`](PyErr::value_bound), +/// [`get_type_bound`](PyErr::get_type_bound), or [`is_instance_bound`](PyErr::is_instance_bound) +/// will create the full exception object if it was not already created. pub struct PyErr { // Safety: can only hand out references when in the "normalized" state. Will never change // after normalization. @@ -45,11 +47,13 @@ pub type PyResult = Result; /// Error that indicates a failure to convert a PyAny to a more specific Python type. #[derive(Debug)] +#[cfg(feature = "gil-refs")] pub struct PyDowncastError<'a> { from: &'a PyAny, to: Cow<'static, str>, } +#[cfg(feature = "gil-refs")] impl<'a> PyDowncastError<'a> { /// Create a new `PyDowncastError` representing a failure to convert the object /// `from` into the type named in `to`. @@ -59,12 +63,20 @@ impl<'a> PyDowncastError<'a> { to: to.into(), } } + + /// Compatibility API to convert the Bound variant `DowncastError` into the + /// gil-ref variant + pub(crate) fn from_downcast_err(DowncastError { from, to }: DowncastError<'a, 'a>) -> Self { + #[allow(deprecated)] + let from = unsafe { from.py().from_borrowed_ptr(from.as_ptr()) }; + Self { from, to } + } } /// Error that indicates a failure to convert a PyAny to a more specific Python type. #[derive(Debug)] pub struct DowncastError<'a, 'py> { - from: &'a Bound<'py, PyAny>, + from: Borrowed<'a, 'py, PyAny>, to: Cow<'static, str>, } @@ -72,6 +84,16 @@ impl<'a, 'py> DowncastError<'a, 'py> { /// Create a new `PyDowncastError` representing a failure to convert the object /// `from` into the type named in `to`. pub fn new(from: &'a Bound<'py, PyAny>, to: impl Into>) -> Self { + DowncastError { + from: from.as_borrowed(), + to: to.into(), + } + } + #[cfg(not(feature = "gil-refs"))] + pub(crate) fn new_from_borrowed( + from: Borrowed<'a, 'py, PyAny>, + to: impl Into>, + ) -> Self { DowncastError { from, to: to.into(), @@ -95,6 +117,14 @@ impl<'py> DowncastIntoError<'py> { to: to.into(), } } + + /// Consumes this `DowncastIntoError` and returns the original object, allowing continued + /// use of it after a failed conversion. + /// + /// See [`downcast_into`][PyAnyMethods::downcast_into] for an example. + pub fn into_inner(self) -> Bound<'py, PyAny> { + self.from + } } impl PyErr { @@ -108,7 +138,7 @@ impl PyErr { /// /// This exception instance will be initialized lazily. This avoids the need for the Python GIL /// to be held, but requires `args` to be `Send` and `Sync`. If `args` is not `Send` or `Sync`, - /// consider using [`PyErr::from_value`] instead. + /// consider using [`PyErr::from_value_bound`] instead. /// /// If `T` does not inherit from `BaseException`, then a `TypeError` will be returned. /// @@ -126,7 +156,7 @@ impl PyErr { /// } /// # /// # Python::with_gil(|py| { - /// # let fun = pyo3::wrap_pyfunction!(always_throws, py).unwrap(); + /// # let fun = pyo3::wrap_pyfunction_bound!(always_throws, py).unwrap(); /// # let err = fun.call0().expect_err("called a function that should always return an error but the return value was Ok"); /// # assert!(err.is_instance_of::(py)) /// # }); @@ -144,7 +174,7 @@ impl PyErr { /// } /// # /// # Python::with_gil(|py| { - /// # let fun = pyo3::wrap_pyfunction!(always_throws, py).unwrap(); + /// # let fun = pyo3::wrap_pyfunction_bound!(always_throws, py).unwrap(); /// # let err = fun.call0().expect_err("called a function that should always return an error but the return value was Ok"); /// # assert!(err.is_instance_of::(py)) /// # }); @@ -157,12 +187,25 @@ impl PyErr { { PyErr::from_state(PyErrState::Lazy(Box::new(move |py| { PyErrStateLazyFnOutput { - ptype: T::type_object(py).into(), + ptype: T::type_object_bound(py).into(), pvalue: args.arguments(py), } }))) } + /// Deprecated form of [`PyErr::from_type_bound`] + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`PyErr::from_type` will be replaced by `PyErr::from_type_bound` in a future PyO3 version" + )] + pub fn from_type(ty: &PyType, args: A) -> PyErr + where + A: PyErrArguments + Send + Sync + 'static, + { + PyErr::from_state(PyErrState::lazy(ty.into(), args)) + } + /// Constructs a new PyErr from the given Python type and arguments. /// /// `ty` is the exception type; usually one of the standard exceptions @@ -173,11 +216,21 @@ impl PyErr { /// If `ty` does not inherit from `BaseException`, then a `TypeError` will be returned. /// /// If calling `ty` with `args` raises an exception, that exception will be returned. - pub fn from_type(ty: &PyType, args: A) -> PyErr + pub fn from_type_bound(ty: Bound<'_, PyType>, args: A) -> PyErr where A: PyErrArguments + Send + Sync + 'static, { - PyErr::from_state(PyErrState::lazy(ty, args)) + PyErr::from_state(PyErrState::lazy(ty.unbind().into_any(), args)) + } + + /// Deprecated form of [`PyErr::from_value_bound`]. + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`PyErr::from_value` will be replaced by `PyErr::from_value_bound` in a future PyO3 version" + )] + pub fn from_value(obj: &PyAny) -> PyErr { + PyErr::from_value_bound(obj.as_borrowed().to_owned()) } /// Creates a new PyErr. @@ -191,53 +244,78 @@ impl PyErr { /// # Examples /// ```rust /// use pyo3::prelude::*; + /// use pyo3::PyTypeInfo; /// use pyo3::exceptions::PyTypeError; - /// use pyo3::types::{PyType, PyString}; + /// use pyo3::types::PyString; /// /// Python::with_gil(|py| { /// // Case #1: Exception object - /// let err = PyErr::from_value(PyTypeError::new_err("some type error").value(py)); + /// let err = PyErr::from_value_bound(PyTypeError::new_err("some type error") + /// .value_bound(py).clone().into_any()); /// assert_eq!(err.to_string(), "TypeError: some type error"); /// /// // Case #2: Exception type - /// let err = PyErr::from_value(PyType::new::(py)); + /// let err = PyErr::from_value_bound(PyTypeError::type_object_bound(py).into_any()); /// assert_eq!(err.to_string(), "TypeError: "); /// /// // Case #3: Invalid exception value - /// let err = PyErr::from_value(PyString::new(py, "foo").into()); + /// let err = PyErr::from_value_bound(PyString::new_bound(py, "foo").into_any()); /// assert_eq!( /// err.to_string(), /// "TypeError: exceptions must derive from BaseException" /// ); /// }); /// ``` - pub fn from_value(obj: &PyAny) -> PyErr { - let state = if let Ok(obj) = obj.downcast::() { - PyErrState::normalized(obj) - } else { - // Assume obj is Type[Exception]; let later normalization handle if this - // is not the case - PyErrState::lazy(obj, Option::::None) + pub fn from_value_bound(obj: Bound<'_, PyAny>) -> PyErr { + let state = match obj.downcast_into::() { + Ok(obj) => PyErrState::normalized(obj), + Err(err) => { + // Assume obj is Type[Exception]; let later normalization handle if this + // is not the case + let obj = err.into_inner(); + let py = obj.py(); + PyErrState::lazy(obj.into_py(py), py.None()) + } }; PyErr::from_state(state) } + /// Deprecated form of [`PyErr::get_type_bound`]. + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`PyErr::get_type` will be replaced by `PyErr::get_type_bound` in a future PyO3 version" + )] + pub fn get_type<'py>(&'py self, py: Python<'py>) -> &'py PyType { + self.get_type_bound(py).into_gil_ref() + } + /// Returns the type of this exception. /// /// # Examples /// ```rust - /// use pyo3::{exceptions::PyTypeError, types::PyType, PyErr, Python}; + /// use pyo3::{prelude::*, exceptions::PyTypeError, types::PyType}; /// /// Python::with_gil(|py| { /// let err: PyErr = PyTypeError::new_err(("some type error",)); - /// assert!(err.get_type(py).is(PyType::new::(py))); + /// assert!(err.get_type_bound(py).is(&PyType::new_bound::(py))); /// }); /// ``` - pub fn get_type<'py>(&'py self, py: Python<'py>) -> &'py PyType { + pub fn get_type_bound<'py>(&self, py: Python<'py>) -> Bound<'py, PyType> { self.normalized(py).ptype(py) } + /// Deprecated form of [`PyErr::value_bound`]. + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`PyErr::value` will be replaced by `PyErr::value_bound` in a future PyO3 version" + )] + pub fn value<'py>(&'py self, py: Python<'py>) -> &'py PyBaseException { + self.value_bound(py).as_gil_ref() + } + /// Returns the value of this exception. /// /// # Examples @@ -248,11 +326,11 @@ impl PyErr { /// Python::with_gil(|py| { /// let err: PyErr = PyTypeError::new_err(("some type error",)); /// assert!(err.is_instance_of::(py)); - /// assert_eq!(err.value(py).to_string(), "some type error"); + /// assert_eq!(err.value_bound(py).to_string(), "some type error"); /// }); /// ``` - pub fn value<'py>(&'py self, py: Python<'py>) -> &'py PyBaseException { - self.normalized(py).pvalue.as_ref(py) + pub fn value_bound<'py>(&self, py: Python<'py>) -> &Bound<'py, PyBaseException> { + self.normalized(py).pvalue.bind(py) } /// Consumes self to take ownership of the exception value contained in this error. @@ -270,6 +348,16 @@ impl PyErr { exc } + /// Deprecated form of [`PyErr::traceback_bound`]. + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`PyErr::traceback` will be replaced by `PyErr::traceback_bound` in a future PyO3 version" + )] + pub fn traceback<'py>(&'py self, py: Python<'py>) -> Option<&'py PyTraceback> { + self.normalized(py).ptraceback(py).map(|b| b.into_gil_ref()) + } + /// Returns the traceback of this exception object. /// /// # Examples @@ -278,10 +366,10 @@ impl PyErr { /// /// Python::with_gil(|py| { /// let err = PyTypeError::new_err(("some type error",)); - /// assert!(err.traceback(py).is_none()); + /// assert!(err.traceback_bound(py).is_none()); /// }); /// ``` - pub fn traceback<'py>(&'py self, py: Python<'py>) -> Option<&'py PyTraceback> { + pub fn traceback_bound<'py>(&self, py: Python<'py>) -> Option> { self.normalized(py).ptraceback(py) } @@ -340,7 +428,7 @@ impl PyErr { if ptype.as_ptr() == PanicException::type_object_raw(py).cast() { let msg = pvalue .as_ref() - .and_then(|obj| obj.as_ref(py).str().ok()) + .and_then(|obj| obj.bind(py).str().ok()) .map(|py_str| py_str.to_string_lossy().into()) .unwrap_or_else(|| String::from("Unwrapped panic from Python code")); @@ -362,7 +450,7 @@ impl PyErr { #[cfg(Py_3_12)] fn _take(py: Python<'_>) -> Option { let state = PyErrStateNormalized::take(py)?; - let pvalue = state.pvalue.as_ref(py); + let pvalue = state.pvalue.bind(py); if pvalue.get_type().as_ptr() == PanicException::type_object_raw(py).cast() { let msg: String = pvalue .str() @@ -398,7 +486,7 @@ impl PyErr { /// /// Use this function when the error is expected to have been set, for example from /// [PyErr::occurred] or by an error return value from a C FFI function. - #[cfg_attr(all(debug_assertions, track_caller), track_caller)] + #[cfg_attr(debug_assertions, track_caller)] #[inline] pub fn fetch(py: Python<'_>) -> PyErr { const FAILED_TO_FETCH: &str = "attempted to fetch exception but none was set"; @@ -411,6 +499,28 @@ impl PyErr { } } + /// Deprecated form of [`PyErr::new_type_bound`] + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`PyErr::new_type` will be replaced by `PyErr::new_type_bound` in a future PyO3 version" + )] + pub fn new_type( + py: Python<'_>, + name: &str, + doc: Option<&str>, + base: Option<&PyType>, + dict: Option, + ) -> PyResult> { + Self::new_type_bound( + py, + name, + doc, + base.map(PyNativeType::as_borrowed).as_deref(), + dict, + ) + } + /// Creates a new exception type with the given name and docstring. /// /// - `base` can be an existing exception type to subclass, or a tuple of classes. @@ -425,11 +535,11 @@ impl PyErr { /// # Panics /// /// This function will panic if `name` or `doc` cannot be converted to [`CString`]s. - pub fn new_type( - py: Python<'_>, + pub fn new_type_bound<'py>( + py: Python<'py>, name: &str, doc: Option<&str>, - base: Option<&PyType>, + base: Option<&Bound<'py, PyType>>, dict: Option, ) -> PyResult> { let base: *mut ffi::PyObject = match base { @@ -469,15 +579,22 @@ impl PyErr { pub fn display(&self, py: Python<'_>) { #[cfg(Py_3_12)] unsafe { - ffi::PyErr_DisplayException(self.value(py).as_ptr()) + ffi::PyErr_DisplayException(self.value_bound(py).as_ptr()) } #[cfg(not(Py_3_12))] unsafe { + // keep the bound `traceback` alive for entire duration of + // PyErr_Display. if we inline this, the `Bound` will be dropped + // after the argument got evaluated, leading to call with a dangling + // pointer. + let traceback = self.traceback_bound(py); + let type_bound = self.get_type_bound(py); ffi::PyErr_Display( - self.get_type(py).as_ptr(), - self.value(py).as_ptr(), - self.traceback(py) + type_bound.as_ptr(), + self.value_bound(py).as_ptr(), + traceback + .as_ref() .map_or(std::ptr::null_mut(), |traceback| traceback.as_ptr()), ) } @@ -505,13 +622,25 @@ impl PyErr { where T: ToPyObject, { - self.is_instance(py, exc.to_object(py).as_ref(py)) + self.is_instance_bound(py, exc.to_object(py).bind(py)) } - /// Returns true if the current exception is instance of `T`. + /// Deprecated form of `PyErr::is_instance_bound`. + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`PyErr::is_instance` will be replaced by `PyErr::is_instance_bound` in a future PyO3 version" + )] #[inline] pub fn is_instance(&self, py: Python<'_>, ty: &PyAny) -> bool { - (unsafe { ffi::PyErr_GivenExceptionMatches(self.get_type(py).as_ptr(), ty.as_ptr()) }) != 0 + self.is_instance_bound(py, &ty.as_borrowed()) + } + + /// Returns true if the current exception is instance of `T`. + #[inline] + pub fn is_instance_bound(&self, py: Python<'_>, ty: &Bound<'_, PyAny>) -> bool { + let type_bound = self.get_type_bound(py); + (unsafe { ffi::PyErr_GivenExceptionMatches(type_bound.as_ptr(), ty.as_ptr()) }) != 0 } /// Returns true if the current exception is instance of `T`. @@ -520,7 +649,7 @@ impl PyErr { where T: PyTypeInfo, { - self.is_instance(py, T::type_object(py)) + self.is_instance_bound(py, &T::type_object_bound(py)) } /// Writes the error back to the Python interpreter's global state. @@ -533,6 +662,17 @@ impl PyErr { .restore(py) } + /// Deprecated form of `PyErr::write_unraisable_bound`. + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`PyErr::write_unraisable` will be replaced by `PyErr::write_unraisable_bound` in a future PyO3 version" + )] + #[inline] + pub fn write_unraisable(self, py: Python<'_>, obj: Option<&PyAny>) { + self.write_unraisable_bound(py, obj.map(PyAny::as_borrowed).as_deref()) + } + /// Reports the error as unraisable. /// /// This calls `sys.unraisablehook()` using the current exception and obj argument. @@ -555,16 +695,26 @@ impl PyErr { /// # fn main() -> PyResult<()> { /// Python::with_gil(|py| { /// match failing_function() { - /// Err(pyerr) => pyerr.write_unraisable(py, None), + /// Err(pyerr) => pyerr.write_unraisable_bound(py, None), /// Ok(..) => { /* do something here */ } /// } /// Ok(()) /// }) /// # } #[inline] - pub fn write_unraisable(self, py: Python<'_>, obj: Option<&PyAny>) { + pub fn write_unraisable_bound(self, py: Python<'_>, obj: Option<&Bound<'_, PyAny>>) { self.restore(py); - unsafe { ffi::PyErr_WriteUnraisable(obj.map_or(std::ptr::null_mut(), |x| x.as_ptr())) } + unsafe { ffi::PyErr_WriteUnraisable(obj.map_or(std::ptr::null_mut(), Bound::as_ptr)) } + } + + /// Deprecated form of [`PyErr::warn_bound`]. + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`PyErr::warn` will be replaced by `PyErr::warn_bound` in a future PyO3 version" + )] + pub fn warn(py: Python<'_>, category: &PyAny, message: &str, stacklevel: i32) -> PyResult<()> { + Self::warn_bound(py, &category.as_borrowed(), message, stacklevel) } /// Issues a warning message. @@ -575,20 +725,25 @@ impl PyErr { /// /// The `category` should be one of the `Warning` classes available in /// [`pyo3::exceptions`](crate::exceptions), or a subclass. The Python - /// object can be retrieved using [`Python::get_type()`]. + /// object can be retrieved using [`Python::get_type_bound()`]. /// /// Example: /// ```rust /// # use pyo3::prelude::*; /// # fn main() -> PyResult<()> { /// Python::with_gil(|py| { - /// let user_warning = py.get_type::(); - /// PyErr::warn(py, user_warning, "I am warning you", 0)?; + /// let user_warning = py.get_type_bound::(); + /// PyErr::warn_bound(py, &user_warning, "I am warning you", 0)?; /// Ok(()) /// }) /// # } /// ``` - pub fn warn(py: Python<'_>, category: &PyAny, message: &str, stacklevel: i32) -> PyResult<()> { + pub fn warn_bound<'py>( + py: Python<'py>, + category: &Bound<'py, PyAny>, + message: &str, + stacklevel: i32, + ) -> PyResult<()> { let message = CString::new(message)?; error_on_minusone(py, unsafe { ffi::PyErr_WarnEx( @@ -599,6 +754,32 @@ impl PyErr { }) } + /// Deprecated form of [`PyErr::warn_explicit_bound`]. + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`PyErr::warn_explicit` will be replaced by `PyErr::warn_explicit_bound` in a future PyO3 version" + )] + pub fn warn_explicit( + py: Python<'_>, + category: &PyAny, + message: &str, + filename: &str, + lineno: i32, + module: Option<&str>, + registry: Option<&PyAny>, + ) -> PyResult<()> { + Self::warn_explicit_bound( + py, + &category.as_borrowed(), + message, + filename, + lineno, + module, + registry.map(PyNativeType::as_borrowed).as_deref(), + ) + } + /// Issues a warning message, with more control over the warning attributes. /// /// May return a `PyErr` if warnings-as-errors is enabled. @@ -607,14 +788,14 @@ impl PyErr { /// /// The `category` should be one of the `Warning` classes available in /// [`pyo3::exceptions`](crate::exceptions), or a subclass. - pub fn warn_explicit( - py: Python<'_>, - category: &PyAny, + pub fn warn_explicit_bound<'py>( + py: Python<'py>, + category: &Bound<'py, PyAny>, message: &str, filename: &str, lineno: i32, module: Option<&str>, - registry: Option<&PyAny>, + registry: Option<&Bound<'py, PyAny>>, ) -> PyResult<()> { let message = CString::new(message)?; let filename = CString::new(filename)?; @@ -643,35 +824,43 @@ impl PyErr { /// /// # Examples /// ```rust - /// use pyo3::{exceptions::PyTypeError, PyErr, Python}; + /// use pyo3::{exceptions::PyTypeError, PyErr, Python, prelude::PyAnyMethods}; /// Python::with_gil(|py| { /// let err: PyErr = PyTypeError::new_err(("some type error",)); /// let err_clone = err.clone_ref(py); - /// assert!(err.get_type(py).is(err_clone.get_type(py))); - /// assert!(err.value(py).is(err_clone.value(py))); - /// match err.traceback(py) { - /// None => assert!(err_clone.traceback(py).is_none()), - /// Some(tb) => assert!(err_clone.traceback(py).unwrap().is(tb)), + /// assert!(err.get_type_bound(py).is(&err_clone.get_type_bound(py))); + /// assert!(err.value_bound(py).is(err_clone.value_bound(py))); + /// match err.traceback_bound(py) { + /// None => assert!(err_clone.traceback_bound(py).is_none()), + /// Some(tb) => assert!(err_clone.traceback_bound(py).unwrap().is(&tb)), /// } /// }); /// ``` #[inline] pub fn clone_ref(&self, py: Python<'_>) -> PyErr { - PyErr::from_state(PyErrState::Normalized(self.normalized(py).clone())) + PyErr::from_state(PyErrState::Normalized(self.normalized(py).clone_ref(py))) } /// Return the cause (either an exception instance, or None, set by `raise ... from ...`) /// associated with the exception, as accessible from Python through `__cause__`. pub fn cause(&self, py: Python<'_>) -> Option { - let value = self.value(py); - let obj = - unsafe { py.from_owned_ptr_or_opt::(ffi::PyException_GetCause(value.as_ptr())) }; - obj.map(Self::from_value) + use crate::ffi_ptr_ext::FfiPtrExt; + let obj = unsafe { + ffi::PyException_GetCause(self.value_bound(py).as_ptr()).assume_owned_or_opt(py) + }; + // PyException_GetCause is documented as potentially returning PyNone, but only GraalPy seems to actually do that + #[cfg(GraalPy)] + if let Some(cause) = &obj { + if cause.is_none() { + return None; + } + } + obj.map(Self::from_value_bound) } /// Set the cause associated with the exception, pass `None` to clear it. pub fn set_cause(&self, py: Python<'_>, cause: Option) { - let value = self.value(py); + let value = self.value_bound(py); let cause = cause.map(|err| err.into_value(py)); unsafe { // PyException_SetCause _steals_ a reference to cause, so must use .into_ptr() @@ -730,9 +919,9 @@ impl std::fmt::Debug for PyErr { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { Python::with_gil(|py| { f.debug_struct("PyErr") - .field("type", self.get_type(py)) - .field("value", self.value(py)) - .field("traceback", &self.traceback(py)) + .field("type", &self.get_type_bound(py)) + .field("value", self.value_bound(py)) + .field("traceback", &self.traceback_bound(py)) .finish() }) } @@ -741,7 +930,7 @@ impl std::fmt::Debug for PyErr { impl std::fmt::Display for PyErr { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { Python::with_gil(|py| { - let value = self.value(py); + let value = self.value_bound(py); let type_name = value.get_type().qualname().map_err(|_| std::fmt::Error)?; write!(f, "{}", type_name)?; if let Ok(s) = value.str() { @@ -783,7 +972,7 @@ impl PyErrArguments for PyDowncastErrorArguments { format!( "'{}' object cannot be converted to '{}'", self.from - .as_ref(py) + .bind(py) .qualname() .as_deref() .unwrap_or(""), @@ -793,7 +982,26 @@ impl PyErrArguments for PyDowncastErrorArguments { } } +/// Python exceptions that can be converted to [`PyErr`]. +/// +/// This is used to implement [`From> for PyErr`]. +/// +/// Users should not need to implement this trait directly. It is implemented automatically in the +/// [`crate::import_exception!`] and [`crate::create_exception!`] macros. +pub trait ToPyErr {} + +impl<'py, T> std::convert::From> for PyErr +where + T: ToPyErr, +{ + #[inline] + fn from(err: Bound<'py, T>) -> PyErr { + PyErr::from_value_bound(err.into_any()) + } +} + /// Convert `PyDowncastError` to Python `TypeError`. +#[cfg(feature = "gil-refs")] impl<'a> std::convert::From> for PyErr { fn from(err: PyDowncastError<'_>) -> PyErr { let args = PyDowncastErrorArguments { @@ -805,11 +1013,13 @@ impl<'a> std::convert::From> for PyErr { } } +#[cfg(feature = "gil-refs")] impl<'a> std::error::Error for PyDowncastError<'a> {} +#[cfg(feature = "gil-refs")] impl<'a> std::fmt::Display for PyDowncastError<'a> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { - display_downcast_error(f, Bound::borrowed_from_gil_ref(&self.from), &self.to) + display_downcast_error(f, &self.from.as_borrowed(), &self.to) } } @@ -829,7 +1039,7 @@ impl std::error::Error for DowncastError<'_, '_> {} impl std::fmt::Display for DowncastError<'_, '_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { - display_downcast_error(f, self.from, &self.to) + display_downcast_error(f, &self.from, &self.to) } } @@ -866,6 +1076,7 @@ fn display_downcast_error( ) } +#[track_caller] pub fn panic_after_error(_py: Python<'_>) -> ! { unsafe { ffi::PyErr_Print(); @@ -997,7 +1208,7 @@ mod tests { Python::with_gil(|py| { let err = py - .run("raise Exception('banana')", None, None) + .run_bound("raise Exception('banana')", None, None) .expect_err("raising should have given us an error"); let debug_str = format!("{:?}", err); @@ -1022,7 +1233,7 @@ mod tests { fn err_display() { Python::with_gil(|py| { let err = py - .run("raise Exception('banana')", None, None) + .run_bound("raise Exception('banana')", None, None) .expect_err("raising should have given us an error"); assert_eq!(err.to_string(), "Exception: banana"); }); @@ -1044,18 +1255,22 @@ mod tests { fn test_pyerr_matches() { Python::with_gil(|py| { let err = PyErr::new::("foo"); - assert!(err.matches(py, PyValueError::type_object(py))); + assert!(err.matches(py, PyValueError::type_object_bound(py))); assert!(err.matches( py, - (PyValueError::type_object(py), PyTypeError::type_object(py)) + ( + PyValueError::type_object_bound(py), + PyTypeError::type_object_bound(py) + ) )); - assert!(!err.matches(py, PyTypeError::type_object(py))); + assert!(!err.matches(py, PyTypeError::type_object_bound(py))); // String is not a valid exception class, so we should get a TypeError - let err: PyErr = PyErr::from_type(crate::types::PyString::type_object(py), "foo"); - assert!(err.matches(py, PyTypeError::type_object(py))); + let err: PyErr = + PyErr::from_type_bound(crate::types::PyString::type_object_bound(py), "foo"); + assert!(err.matches(py, PyTypeError::type_object_bound(py))); }) } @@ -1063,12 +1278,12 @@ mod tests { fn test_pyerr_cause() { Python::with_gil(|py| { let err = py - .run("raise Exception('banana')", None, None) + .run_bound("raise Exception('banana')", None, None) .expect_err("raising should have given us an error"); assert!(err.cause(py).is_none()); let err = py - .run( + .run_bound( "raise Exception('banana') from Exception('apple')", None, None, @@ -1093,47 +1308,55 @@ mod tests { #[test] fn warnings() { + use crate::types::any::PyAnyMethods; // Note: although the warning filter is interpreter global, keeping the // GIL locked should prevent effects to be visible to other testing // threads. Python::with_gil(|py| { - let cls = py.get_type::(); + let cls = py.get_type_bound::(); // Reset warning filter to default state - let warnings = py.import("warnings").unwrap(); + let warnings = py.import_bound("warnings").unwrap(); warnings.call_method0("resetwarnings").unwrap(); // First, test the warning is emitted assert_warnings!( py, - { PyErr::warn(py, cls, "I am warning you", 0).unwrap() }, + { PyErr::warn_bound(py, &cls, "I am warning you", 0).unwrap() }, [(exceptions::PyUserWarning, "I am warning you")] ); // Test with raising warnings - .call_method1("simplefilter", ("error", cls)) + .call_method1("simplefilter", ("error", &cls)) .unwrap(); - PyErr::warn(py, cls, "I am warning you", 0).unwrap_err(); + PyErr::warn_bound(py, &cls, "I am warning you", 0).unwrap_err(); // Test with error for an explicit module warnings.call_method0("resetwarnings").unwrap(); warnings - .call_method1("filterwarnings", ("error", "", cls, "pyo3test")) + .call_method1("filterwarnings", ("error", "", &cls, "pyo3test")) .unwrap(); // This has the wrong module and will not raise, just be emitted assert_warnings!( py, - { PyErr::warn(py, cls, "I am warning you", 0).unwrap() }, + { PyErr::warn_bound(py, &cls, "I am warning you", 0).unwrap() }, [(exceptions::PyUserWarning, "I am warning you")] ); - let err = - PyErr::warn_explicit(py, cls, "I am warning you", "pyo3test.py", 427, None, None) - .unwrap_err(); + let err = PyErr::warn_explicit_bound( + py, + &cls, + "I am warning you", + "pyo3test.py", + 427, + None, + None, + ) + .unwrap_err(); assert!(err - .value(py) + .value_bound(py) .getattr("args") .unwrap() .get_item(0) diff --git a/src/exceptions.rs b/src/exceptions.rs index 6ff662b4ae4..d6a6e859e3b 100644 --- a/src/exceptions.rs +++ b/src/exceptions.rs @@ -9,7 +9,7 @@ //! yourself to import Python classes that are ultimately derived from //! `BaseException`. -use crate::{ffi, PyResult, Python}; +use crate::{ffi, Bound, PyResult, Python}; use std::ffi::CStr; use std::ops; use std::os::raw::c_char; @@ -19,29 +19,24 @@ use std::os::raw::c_char; #[macro_export] macro_rules! impl_exception_boilerplate { ($name: ident) => { + // FIXME https://github.com/PyO3/pyo3/issues/3903 + #[allow(unknown_lints, non_local_definitions)] + #[cfg(feature = "gil-refs")] impl ::std::convert::From<&$name> for $crate::PyErr { #[inline] fn from(err: &$name) -> $crate::PyErr { + #[allow(deprecated)] $crate::PyErr::from_value(err) } } - impl $name { - /// Creates a new [`PyErr`] of this type. - /// - /// [`PyErr`]: https://docs.rs/pyo3/latest/pyo3/struct.PyErr.html "PyErr in pyo3" - #[inline] - pub fn new_err(args: A) -> $crate::PyErr - where - A: $crate::PyErrArguments + ::std::marker::Send + ::std::marker::Sync + 'static, - { - $crate::PyErr::new::<$name, A>(args) - } - } + $crate::impl_exception_boilerplate_bound!($name); + #[cfg(feature = "gil-refs")] impl ::std::error::Error for $name { fn source(&self) -> ::std::option::Option<&(dyn ::std::error::Error + 'static)> { unsafe { + #[allow(deprecated)] let cause: &$crate::exceptions::PyBaseException = self .py() .from_owned_ptr_or_opt($crate::ffi::PyException_GetCause(self.as_ptr()))?; @@ -50,6 +45,28 @@ macro_rules! impl_exception_boilerplate { } } } + + impl $crate::ToPyErr for $name {} + }; +} + +#[doc(hidden)] +#[macro_export] +macro_rules! impl_exception_boilerplate_bound { + ($name: ident) => { + impl $name { + /// Creates a new [`PyErr`] of this type. + /// + /// [`PyErr`]: https://docs.rs/pyo3/latest/pyo3/struct.PyErr.html "PyErr in pyo3" + #[inline] + #[allow(dead_code)] + pub fn new_err(args: A) -> $crate::PyErr + where + A: $crate::PyErrArguments + ::std::marker::Send + ::std::marker::Sync + 'static, + { + $crate::PyErr::new::<$name, A>(args) + } + } }; } @@ -71,7 +88,7 @@ macro_rules! impl_exception_boilerplate { /// import_exception!(socket, gaierror); /// /// Python::with_gil(|py| { -/// let ctx = [("gaierror", py.get_type::())].into_py_dict(py); +/// let ctx = [("gaierror", py.get_type_bound::())].into_py_dict_bound(py); /// pyo3::py_run!(py, *ctx, "import socket; assert gaierror is socket.gaierror"); /// }); /// @@ -99,32 +116,58 @@ macro_rules! import_exception { impl $name { fn type_object_raw(py: $crate::Python<'_>) -> *mut $crate::ffi::PyTypeObject { - use $crate::sync::GILOnceCell; - static TYPE_OBJECT: GILOnceCell<$crate::Py<$crate::types::PyType>> = - GILOnceCell::new(); + use $crate::types::PyTypeMethods; + static TYPE_OBJECT: $crate::impl_::exceptions::ImportedExceptionTypeObject = + $crate::impl_::exceptions::ImportedExceptionTypeObject::new(stringify!($module), stringify!($name)); + TYPE_OBJECT.get(py).as_type_ptr() + } + } + }; +} - TYPE_OBJECT - .get_or_init(py, || { - let imp = py - .import(stringify!($module)) - .unwrap_or_else(|err| { - let traceback = err - .traceback(py) - .map(|tb| tb.format().expect("raised exception will have a traceback")) - .unwrap_or_default(); - ::std::panic!("Can not import module {}: {}\n{}", stringify!($module), err, traceback); - }); - let cls = imp.getattr(stringify!($name)).expect(concat!( - "Can not load exception class: {}.{}", - stringify!($module), - ".", - stringify!($name) - )); - - cls.extract() - .expect("Imported exception should be a type object") - }) - .as_ptr() as *mut _ +/// Variant of [`import_exception`](crate::import_exception) that does not emit code needed to +/// use the imported exception type as a GIL Ref. +/// +/// This is useful only during migration as a way to avoid generating needless code. +#[macro_export] +macro_rules! import_exception_bound { + ($module: expr, $name: ident) => { + /// A Rust type representing an exception defined in Python code. + /// + /// This type was created by the [`pyo3::import_exception_bound!`] macro - see its documentation + /// for more information. + /// + /// [`pyo3::import_exception_bound!`]: https://docs.rs/pyo3/latest/pyo3/macro.import_exception.html "import_exception in pyo3" + #[repr(transparent)] + #[allow(non_camel_case_types)] // E.g. `socket.herror` + pub struct $name($crate::PyAny); + + $crate::impl_exception_boilerplate_bound!($name); + + // FIXME remove this: was necessary while `PyTypeInfo` requires `HasPyGilRef`, + // should change in 0.22. + #[cfg(feature = "gil-refs")] + unsafe impl $crate::type_object::HasPyGilRef for $name { + type AsRefTarget = $crate::PyAny; + } + + $crate::pyobject_native_type_info!( + $name, + $name::type_object_raw, + ::std::option::Option::Some(stringify!($module)) + ); + + impl $crate::types::DerefToPyAny for $name {} + + impl $name { + fn type_object_raw(py: $crate::Python<'_>) -> *mut $crate::ffi::PyTypeObject { + use $crate::types::PyTypeMethods; + static TYPE_OBJECT: $crate::impl_::exceptions::ImportedExceptionTypeObject = + $crate::impl_::exceptions::ImportedExceptionTypeObject::new( + stringify!($module), + stringify!($name), + ); + TYPE_OBJECT.get(py).as_type_ptr() } } }; @@ -156,26 +199,26 @@ macro_rules! import_exception { /// } /// /// #[pymodule] -/// fn my_module(py: Python<'_>, m: &PyModule) -> PyResult<()> { -/// m.add("MyError", py.get_type::())?; -/// m.add_function(wrap_pyfunction!(raise_myerror, py)?)?; +/// fn my_module(m: &Bound<'_, PyModule>) -> PyResult<()> { +/// m.add("MyError", m.py().get_type_bound::())?; +/// m.add_function(wrap_pyfunction!(raise_myerror, m)?)?; /// Ok(()) /// } /// # fn main() -> PyResult<()> { /// # Python::with_gil(|py| -> PyResult<()> { -/// # let fun = wrap_pyfunction!(raise_myerror, py)?; -/// # let locals = pyo3::types::PyDict::new(py); -/// # locals.set_item("MyError", py.get_type::())?; +/// # let fun = wrap_pyfunction_bound!(raise_myerror, py)?; +/// # let locals = pyo3::types::PyDict::new_bound(py); +/// # locals.set_item("MyError", py.get_type_bound::())?; /// # locals.set_item("raise_myerror", fun)?; /// # -/// # py.run( +/// # py.run_bound( /// # "try: /// # raise_myerror() /// # except MyError as e: /// # assert e.__doc__ == 'Some description.' /// # assert str(e) == 'Some error happened.'", /// # None, -/// # Some(locals), +/// # Some(&locals), /// # )?; /// # /// # Ok(()) @@ -243,11 +286,11 @@ macro_rules! create_exception_type_object { TYPE_OBJECT .get_or_init(py, || - $crate::PyErr::new_type( + $crate::PyErr::new_type_bound( py, concat!(stringify!($module), ".", stringify!($name)), $doc, - ::std::option::Option::Some(py.get_type::<$base>()), + ::std::option::Option::Some(&py.get_type_bound::<$base>()), ::std::option::Option::None, ).expect("Failed to initialize new exception type.") ).as_ptr() as *mut $crate::ffi::PyTypeObject @@ -314,7 +357,7 @@ fn always_throws() -> PyResult<()> { } # # Python::with_gil(|py| { -# let fun = pyo3::wrap_pyfunction!(always_throws, py).unwrap(); +# let fun = pyo3::wrap_pyfunction_bound!(always_throws, py).unwrap(); # let err = fun.call0().expect_err(\"called a function that should always return an error but the return value was Ok\"); # assert!(err.is_instance_of::(py)) # }); @@ -337,7 +380,7 @@ use pyo3::prelude::*; use pyo3::exceptions::Py", $name, "; Python::with_gil(|py| { - let result: PyResult<()> = py.run(\"raise ", $name, "\", None, None); + let result: PyResult<()> = py.run_bound(\"raise ", $name, "\", None, None); let error_type = match result { Ok(_) => \"Not an error\", @@ -401,14 +444,14 @@ impl_native_exception!( PyExc_FloatingPointError, native_doc!("FloatingPointError") ); -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] impl_native_exception!( PyOSError, PyExc_OSError, native_doc!("OSError"), ffi::PyOSErrorObject ); -#[cfg(PyPy)] +#[cfg(any(PyPy, GraalPy))] impl_native_exception!(PyOSError, PyExc_OSError, native_doc!("OSError")); impl_native_exception!(PyImportError, PyExc_ImportError, native_doc!("ImportError")); @@ -447,14 +490,14 @@ impl_native_exception!( PyExc_NotImplementedError, native_doc!("NotImplementedError") ); -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] impl_native_exception!( PySyntaxError, PyExc_SyntaxError, native_doc!("SyntaxError"), ffi::PySyntaxErrorObject ); -#[cfg(PyPy)] +#[cfg(any(PyPy, GraalPy))] impl_native_exception!(PySyntaxError, PyExc_SyntaxError, native_doc!("SyntaxError")); impl_native_exception!( PyReferenceError, @@ -462,14 +505,14 @@ impl_native_exception!( native_doc!("ReferenceError") ); impl_native_exception!(PySystemError, PyExc_SystemError, native_doc!("SystemError")); -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] impl_native_exception!( PySystemExit, PyExc_SystemExit, native_doc!("SystemExit"), ffi::PySystemExitObject ); -#[cfg(PyPy)] +#[cfg(any(PyPy, GraalPy))] impl_native_exception!(PySystemExit, PyExc_SystemExit, native_doc!("SystemExit")); impl_native_exception!(PyTypeError, PyExc_TypeError, native_doc!("TypeError")); impl_native_exception!( @@ -477,14 +520,14 @@ impl_native_exception!( PyExc_UnboundLocalError, native_doc!("UnboundLocalError") ); -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] impl_native_exception!( PyUnicodeError, PyExc_UnicodeError, native_doc!("UnicodeError"), ffi::PyUnicodeErrorObject ); -#[cfg(PyPy)] +#[cfg(any(PyPy, GraalPy))] impl_native_exception!( PyUnicodeError, PyExc_UnicodeError, @@ -610,7 +653,12 @@ impl_windows_native_exception!( ); impl PyUnicodeDecodeError { - /// Creates a Python `UnicodeDecodeError`. + /// Deprecated form of [`PyUnicodeDecodeError::new_bound`]. + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`PyUnicodeDecodeError::new` will be replaced by `PyUnicodeDecodeError::new_bound` in a future PyO3 version" + )] pub fn new<'p>( py: Python<'p>, encoding: &CStr, @@ -618,16 +666,45 @@ impl PyUnicodeDecodeError { range: ops::Range, reason: &CStr, ) -> PyResult<&'p PyUnicodeDecodeError> { + Ok(PyUnicodeDecodeError::new_bound(py, encoding, input, range, reason)?.into_gil_ref()) + } + + /// Creates a Python `UnicodeDecodeError`. + pub fn new_bound<'p>( + py: Python<'p>, + encoding: &CStr, + input: &[u8], + range: ops::Range, + reason: &CStr, + ) -> PyResult> { + use crate::ffi_ptr_ext::FfiPtrExt; + use crate::py_result_ext::PyResultExt; unsafe { - py.from_owned_ptr_or_err(ffi::PyUnicodeDecodeError_Create( + ffi::PyUnicodeDecodeError_Create( encoding.as_ptr(), input.as_ptr() as *const c_char, input.len() as ffi::Py_ssize_t, range.start as ffi::Py_ssize_t, range.end as ffi::Py_ssize_t, reason.as_ptr(), - )) + ) + .assume_owned_or_err(py) } + .downcast_into() + } + + /// Deprecated form of [`PyUnicodeDecodeError::new_utf8_bound`]. + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`PyUnicodeDecodeError::new_utf8` will be replaced by `PyUnicodeDecodeError::new_utf8_bound` in a future PyO3 version" + )] + pub fn new_utf8<'p>( + py: Python<'p>, + input: &[u8], + err: std::str::Utf8Error, + ) -> PyResult<&'p PyUnicodeDecodeError> { + Ok(PyUnicodeDecodeError::new_utf8_bound(py, input, err)?.into_gil_ref()) } /// Creates a Python `UnicodeDecodeError` from a Rust UTF-8 decoding error. @@ -643,7 +720,7 @@ impl PyUnicodeDecodeError { /// Python::with_gil(|py| { /// let invalid_utf8 = b"fo\xd8o"; /// let err = std::str::from_utf8(invalid_utf8).expect_err("should be invalid utf8"); - /// let decode_err = PyUnicodeDecodeError::new_utf8(py, invalid_utf8, err)?; + /// let decode_err = PyUnicodeDecodeError::new_utf8_bound(py, invalid_utf8, err)?; /// assert_eq!( /// decode_err.to_string(), /// "'utf-8' codec can't decode byte 0xd8 in position 2: invalid utf-8" @@ -651,13 +728,13 @@ impl PyUnicodeDecodeError { /// Ok(()) /// }) /// # } - pub fn new_utf8<'p>( + pub fn new_utf8_bound<'p>( py: Python<'p>, input: &[u8], err: std::str::Utf8Error, - ) -> PyResult<&'p PyUnicodeDecodeError> { + ) -> PyResult> { let pos = err.valid_up_to(); - PyUnicodeDecodeError::new( + PyUnicodeDecodeError::new_bound( py, CStr::from_bytes_with_nul(b"utf-8\0").unwrap(), input, @@ -731,7 +808,7 @@ macro_rules! test_exception { use super::$exc_ty; $crate::Python::with_gil(|py| { - use std::error::Error; + use $crate::types::PyAnyMethods; let err: $crate::PyErr = { None $( @@ -742,13 +819,19 @@ macro_rules! test_exception { assert!(err.is_instance_of::<$exc_ty>(py)); - let value: &$exc_ty = err.value(py).downcast().unwrap(); - assert!(value.source().is_none()); + let value = err.value_bound(py).as_any().downcast::<$exc_ty>().unwrap(); - err.set_cause(py, Some($crate::exceptions::PyValueError::new_err("a cause"))); - assert!(value.source().is_some()); + #[cfg(feature = "gil-refs")] + { + use std::error::Error; + let value = value.as_gil_ref(); + assert!(value.source().is_none()); - assert!($crate::PyErr::from(value).is_instance_of::<$exc_ty>(py)); + err.set_cause(py, Some($crate::exceptions::PyValueError::new_err("a cause"))); + assert!(value.source().is_some()); + } + + assert!($crate::PyErr::from(value.clone()).is_instance_of::<$exc_ty>(py)); }) } }; @@ -799,22 +882,25 @@ pub mod socket { #[cfg(test)] mod tests { use super::*; + use crate::types::any::PyAnyMethods; use crate::types::{IntoPyDict, PyDict}; - use crate::{PyErr, Python}; + use crate::PyErr; + #[cfg(feature = "gil-refs")] + use crate::PyNativeType; - import_exception!(socket, gaierror); - import_exception!(email.errors, MessageError); + import_exception_bound!(socket, gaierror); + import_exception_bound!(email.errors, MessageError); #[test] fn test_check_exception() { Python::with_gil(|py| { let err: PyErr = gaierror::new_err(()); let socket = py - .import("socket") + .import_bound("socket") .map_err(|e| e.display(py)) .expect("could not import socket"); - let d = PyDict::new(py); + let d = PyDict::new_bound(py); d.set_item("socket", socket) .map_err(|e| e.display(py)) .expect("could not setitem"); @@ -823,7 +909,7 @@ mod tests { .map_err(|e| e.display(py)) .expect("could not setitem"); - py.run("assert isinstance(exc, socket.gaierror)", None, Some(d)) + py.run_bound("assert isinstance(exc, socket.gaierror)", None, Some(&d)) .map_err(|e| e.display(py)) .expect("assertion failed"); }); @@ -834,11 +920,11 @@ mod tests { Python::with_gil(|py| { let err: PyErr = MessageError::new_err(()); let email = py - .import("email") + .import_bound("email") .map_err(|e| e.display(py)) .expect("could not import email"); - let d = PyDict::new(py); + let d = PyDict::new_bound(py); d.set_item("email", email) .map_err(|e| e.display(py)) .expect("could not setitem"); @@ -846,10 +932,10 @@ mod tests { .map_err(|e| e.display(py)) .expect("could not setitem"); - py.run( + py.run_bound( "assert isinstance(exc, email.errors.MessageError)", None, - Some(d), + Some(&d), ) .map_err(|e| e.display(py)) .expect("assertion failed"); @@ -861,21 +947,21 @@ mod tests { create_exception!(mymodule, CustomError, PyException); Python::with_gil(|py| { - let error_type = py.get_type::(); - let ctx = [("CustomError", error_type)].into_py_dict(py); + let error_type = py.get_type_bound::(); + let ctx = [("CustomError", error_type)].into_py_dict_bound(py); let type_description: String = py - .eval("str(CustomError)", None, Some(ctx)) + .eval_bound("str(CustomError)", None, Some(&ctx)) .unwrap() .extract() .unwrap(); assert_eq!(type_description, ""); - py.run( + py.run_bound( "assert CustomError('oops').args == ('oops',)", None, - Some(ctx), + Some(&ctx), ) .unwrap(); - py.run("assert CustomError.__doc__ is None", None, Some(ctx)) + py.run_bound("assert CustomError.__doc__ is None", None, Some(&ctx)) .unwrap(); }); } @@ -884,10 +970,10 @@ mod tests { fn custom_exception_dotted_module() { create_exception!(mymodule.exceptions, CustomError, PyException); Python::with_gil(|py| { - let error_type = py.get_type::(); - let ctx = [("CustomError", error_type)].into_py_dict(py); + let error_type = py.get_type_bound::(); + let ctx = [("CustomError", error_type)].into_py_dict_bound(py); let type_description: String = py - .eval("str(CustomError)", None, Some(ctx)) + .eval_bound("str(CustomError)", None, Some(&ctx)) .unwrap() .extract() .unwrap(); @@ -903,22 +989,26 @@ mod tests { create_exception!(mymodule, CustomError, PyException, "Some docs"); Python::with_gil(|py| { - let error_type = py.get_type::(); - let ctx = [("CustomError", error_type)].into_py_dict(py); + let error_type = py.get_type_bound::(); + let ctx = [("CustomError", error_type)].into_py_dict_bound(py); let type_description: String = py - .eval("str(CustomError)", None, Some(ctx)) + .eval_bound("str(CustomError)", None, Some(&ctx)) .unwrap() .extract() .unwrap(); assert_eq!(type_description, ""); - py.run( + py.run_bound( "assert CustomError('oops').args == ('oops',)", None, - Some(ctx), + Some(&ctx), + ) + .unwrap(); + py.run_bound( + "assert CustomError.__doc__ == 'Some docs'", + None, + Some(&ctx), ) .unwrap(); - py.run("assert CustomError.__doc__ == 'Some docs'", None, Some(ctx)) - .unwrap(); }); } @@ -932,24 +1022,24 @@ mod tests { ); Python::with_gil(|py| { - let error_type = py.get_type::(); - let ctx = [("CustomError", error_type)].into_py_dict(py); + let error_type = py.get_type_bound::(); + let ctx = [("CustomError", error_type)].into_py_dict_bound(py); let type_description: String = py - .eval("str(CustomError)", None, Some(ctx)) + .eval_bound("str(CustomError)", None, Some(&ctx)) .unwrap() .extract() .unwrap(); assert_eq!(type_description, ""); - py.run( + py.run_bound( "assert CustomError('oops').args == ('oops',)", None, - Some(ctx), + Some(&ctx), ) .unwrap(); - py.run( + py.run_bound( "assert CustomError.__doc__ == 'Some more docs'", None, - Some(ctx), + Some(&ctx), ) .unwrap(); }); @@ -959,10 +1049,10 @@ mod tests { fn native_exception_debug() { Python::with_gil(|py| { let exc = py - .run("raise Exception('banana')", None, None) + .run_bound("raise Exception('banana')", None, None) .expect_err("raising should have given us an error") .into_value(py) - .into_ref(py); + .into_bound(py); assert_eq!( format!("{:?}", exc), exc.repr().unwrap().extract::().unwrap() @@ -974,10 +1064,10 @@ mod tests { fn native_exception_display() { Python::with_gil(|py| { let exc = py - .run("raise Exception('banana')", None, None) + .run_bound("raise Exception('banana')", None, None) .expect_err("raising should have given us an error") .into_value(py) - .into_ref(py); + .into_bound(py); assert_eq!( exc.to_string(), exc.str().unwrap().extract::().unwrap() @@ -986,12 +1076,14 @@ mod tests { } #[test] + #[cfg(feature = "gil-refs")] fn native_exception_chain() { use std::error::Error; Python::with_gil(|py| { + #[allow(deprecated)] let exc = py - .run( + .run_bound( "raise Exception('banana') from TypeError('peach')", None, None, @@ -1017,7 +1109,7 @@ mod tests { #[cfg_attr(invalid_from_utf8_lint, allow(invalid_from_utf8))] let err = std::str::from_utf8(invalid_utf8).expect_err("should be invalid utf8"); Python::with_gil(|py| { - let decode_err = PyUnicodeDecodeError::new_utf8(py, invalid_utf8, err).unwrap(); + let decode_err = PyUnicodeDecodeError::new_utf8_bound(py, invalid_utf8, err).unwrap(); assert_eq!( format!("{:?}", decode_err), "UnicodeDecodeError('utf-8', b'fo\\xd8o', 2, 3, 'invalid utf-8')" @@ -1073,10 +1165,14 @@ mod tests { let invalid_utf8 = b"fo\xd8o"; #[cfg_attr(invalid_from_utf8_lint, allow(invalid_from_utf8))] let err = std::str::from_utf8(invalid_utf8).expect_err("should be invalid utf8"); - PyErr::from_value(PyUnicodeDecodeError::new_utf8(py, invalid_utf8, err).unwrap()) + PyErr::from_value_bound( + PyUnicodeDecodeError::new_utf8_bound(py, invalid_utf8, err) + .unwrap() + .into_any(), + ) }); test_exception!(PyUnicodeEncodeError, |py| py - .eval("chr(40960).encode('ascii')", None, None) + .eval_bound("chr(40960).encode('ascii')", None, None) .unwrap_err()); test_exception!(PyUnicodeTranslateError, |_| { PyUnicodeTranslateError::new_err(("\u{3042}", 0, 1, "ouch")) diff --git a/src/ffi/tests.rs b/src/ffi/tests.rs index 2cbd84dc6db..5aee1618472 100644 --- a/src/ffi/tests.rs +++ b/src/ffi/tests.rs @@ -1,11 +1,15 @@ use crate::ffi::*; +use crate::types::any::PyAnyMethods; use crate::Python; +#[cfg(all(PyPy, feature = "macros"))] +use crate::types::PyString; + +#[cfg(not(any(Py_LIMITED_API, PyPy)))] +use crate::types::PyString; + #[cfg(not(Py_LIMITED_API))] -use crate::{ - types::{PyDict, PyString}, - IntoPy, Py, PyAny, -}; +use crate::{types::PyDict, Bound, IntoPy, Py, PyAny}; #[cfg(not(any(Py_3_12, Py_LIMITED_API)))] use libc::wchar_t; @@ -15,16 +19,16 @@ use libc::wchar_t; fn test_datetime_fromtimestamp() { Python::with_gil(|py| { let args: Py = (100,).into_py(py); - let dt: &PyAny = unsafe { + let dt = unsafe { PyDateTime_IMPORT(); - py.from_owned_ptr(PyDateTime_FromTimestamp(args.as_ptr())) + Bound::from_owned_ptr(py, PyDateTime_FromTimestamp(args.as_ptr())) }; - let locals = PyDict::new(py); + let locals = PyDict::new_bound(py); locals.set_item("dt", dt).unwrap(); - py.run( + py.run_bound( "import datetime; assert dt == datetime.datetime.fromtimestamp(100)", None, - Some(locals), + Some(&locals), ) .unwrap(); }) @@ -36,16 +40,16 @@ fn test_datetime_fromtimestamp() { fn test_date_fromtimestamp() { Python::with_gil(|py| { let args: Py = (100,).into_py(py); - let dt: &PyAny = unsafe { + let dt = unsafe { PyDateTime_IMPORT(); - py.from_owned_ptr(PyDate_FromTimestamp(args.as_ptr())) + Bound::from_owned_ptr(py, PyDate_FromTimestamp(args.as_ptr())) }; - let locals = PyDict::new(py); + let locals = PyDict::new_bound(py); locals.set_item("dt", dt).unwrap(); - py.run( + py.run_bound( "import datetime; assert dt == datetime.date.fromtimestamp(100)", None, - Some(locals), + Some(&locals), ) .unwrap(); }) @@ -56,16 +60,16 @@ fn test_date_fromtimestamp() { #[test] fn test_utc_timezone() { Python::with_gil(|py| { - let utc_timezone: &PyAny = unsafe { + let utc_timezone: Bound<'_, PyAny> = unsafe { PyDateTime_IMPORT(); - py.from_borrowed_ptr(PyDateTime_TimeZone_UTC()) + Bound::from_borrowed_ptr(py, PyDateTime_TimeZone_UTC()) }; - let locals = PyDict::new(py); + let locals = PyDict::new_bound(py); locals.set_item("utc_timezone", utc_timezone).unwrap(); - py.run( + py.run_bound( "import datetime; assert utc_timezone is datetime.timezone.utc", None, - Some(locals), + Some(&locals), ) .unwrap(); }) @@ -76,11 +80,11 @@ fn test_utc_timezone() { #[cfg(feature = "macros")] #[cfg_attr(target_arch = "wasm32", ignore)] // DateTime import fails on wasm for mysterious reasons fn test_timezone_from_offset() { - use crate::types::PyDelta; + use crate::{ffi_ptr_ext::FfiPtrExt, types::PyDelta}; Python::with_gil(|py| { - let delta = PyDelta::new(py, 0, 100, 0, false).unwrap(); - let tz: &PyAny = unsafe { py.from_borrowed_ptr(PyTimeZone_FromOffset(delta.as_ptr())) }; + let delta = PyDelta::new_bound(py, 0, 100, 0, false).unwrap(); + let tz = unsafe { PyTimeZone_FromOffset(delta.as_ptr()).assume_owned(py) }; crate::py_run!( py, tz, @@ -94,16 +98,13 @@ fn test_timezone_from_offset() { #[cfg(feature = "macros")] #[cfg_attr(target_arch = "wasm32", ignore)] // DateTime import fails on wasm for mysterious reasons fn test_timezone_from_offset_and_name() { - use crate::types::PyDelta; + use crate::{ffi_ptr_ext::FfiPtrExt, types::PyDelta}; Python::with_gil(|py| { - let delta = PyDelta::new(py, 0, 100, 0, false).unwrap(); - let tzname = PyString::new(py, "testtz"); - let tz: &PyAny = unsafe { - py.from_borrowed_ptr(PyTimeZone_FromOffsetAndName( - delta.as_ptr(), - tzname.as_ptr(), - )) + let delta = PyDelta::new_bound(py, 0, 100, 0, false).unwrap(); + let tzname = PyString::new_bound(py, "testtz"); + let tz = unsafe { + PyTimeZone_FromOffsetAndName(delta.as_ptr(), tzname.as_ptr()).assume_owned(py) }; crate::py_run!( py, @@ -162,12 +163,12 @@ fn ascii_object_bitfield() { } #[test] -#[cfg(not(Py_LIMITED_API))] +#[cfg(not(any(Py_LIMITED_API, PyPy)))] #[cfg_attr(Py_3_10, allow(deprecated))] fn ascii() { Python::with_gil(|py| { // This test relies on implementation details of PyString. - let s = PyString::new(py, "hello, world"); + let s = PyString::new_bound(py, "hello, world"); let ptr = s.as_ptr(); unsafe { @@ -204,12 +205,12 @@ fn ascii() { } #[test] -#[cfg(not(Py_LIMITED_API))] +#[cfg(not(any(Py_LIMITED_API, PyPy)))] #[cfg_attr(Py_3_10, allow(deprecated))] fn ucs4() { Python::with_gil(|py| { let s = "哈哈🐈"; - let py_string = PyString::new(py, s); + let py_string = PyString::new_bound(py, s); let ptr = py_string.as_ptr(); unsafe { @@ -252,39 +253,37 @@ fn ucs4() { #[cfg_attr(target_arch = "wasm32", ignore)] // DateTime import fails on wasm for mysterious reasons #[cfg(not(PyPy))] fn test_get_tzinfo() { - use crate::types::timezone_utc; + use crate::types::timezone_utc_bound; crate::Python::with_gil(|py| { use crate::types::{PyDateTime, PyTime}; - use crate::PyAny; - let utc = timezone_utc(py); + let utc = &timezone_utc_bound(py); - let dt = PyDateTime::new(py, 2018, 1, 1, 0, 0, 0, 0, Some(utc)).unwrap(); + let dt = PyDateTime::new_bound(py, 2018, 1, 1, 0, 0, 0, 0, Some(utc)).unwrap(); assert!( - unsafe { py.from_borrowed_ptr::(PyDateTime_DATE_GET_TZINFO(dt.as_ptr())) } + unsafe { Bound::from_borrowed_ptr(py, PyDateTime_DATE_GET_TZINFO(dt.as_ptr())) } .is(utc) ); - let dt = PyDateTime::new(py, 2018, 1, 1, 0, 0, 0, 0, None).unwrap(); + let dt = PyDateTime::new_bound(py, 2018, 1, 1, 0, 0, 0, 0, None).unwrap(); assert!( - unsafe { py.from_borrowed_ptr::(PyDateTime_DATE_GET_TZINFO(dt.as_ptr())) } + unsafe { Bound::from_borrowed_ptr(py, PyDateTime_DATE_GET_TZINFO(dt.as_ptr())) } .is_none() ); - let t = PyTime::new(py, 0, 0, 0, 0, Some(utc)).unwrap(); + let t = PyTime::new_bound(py, 0, 0, 0, 0, Some(utc)).unwrap(); assert!( - unsafe { py.from_borrowed_ptr::(PyDateTime_TIME_GET_TZINFO(t.as_ptr())) } - .is(utc) + unsafe { Bound::from_borrowed_ptr(py, PyDateTime_TIME_GET_TZINFO(t.as_ptr())) }.is(utc) ); - let t = PyTime::new(py, 0, 0, 0, 0, None).unwrap(); + let t = PyTime::new_bound(py, 0, 0, 0, 0, None).unwrap(); assert!( - unsafe { py.from_borrowed_ptr::(PyDateTime_TIME_GET_TZINFO(t.as_ptr())) } + unsafe { Bound::from_borrowed_ptr(py, PyDateTime_TIME_GET_TZINFO(t.as_ptr())) } .is_none() ); }) @@ -293,7 +292,7 @@ fn test_get_tzinfo() { #[test] fn test_inc_dec_ref() { Python::with_gil(|py| { - let obj = py.eval("object()", None, None).unwrap(); + let obj = py.eval_bound("object()", None, None).unwrap(); let ref_count = obj.get_refcnt(); let ptr = obj.as_ptr(); @@ -314,15 +313,15 @@ fn test_inc_dec_ref_immortal() { Python::with_gil(|py| { let obj = py.None(); - let ref_count = obj.get_refcnt(); + let ref_count = obj.get_refcnt(py); let ptr = obj.as_ptr(); unsafe { Py_INCREF(ptr) }; - assert_eq!(obj.get_refcnt(), ref_count); + assert_eq!(obj.get_refcnt(py), ref_count); unsafe { Py_DECREF(ptr) }; - assert_eq!(obj.get_refcnt(), ref_count); + assert_eq!(obj.get_refcnt(py), ref_count); }) } diff --git a/src/ffi_ptr_ext.rs b/src/ffi_ptr_ext.rs index 0eca2c36dd5..183b0e3734e 100644 --- a/src/ffi_ptr_ext.rs +++ b/src/ffi_ptr_ext.rs @@ -1,21 +1,13 @@ +use crate::sealed::Sealed; use crate::{ ffi, instance::{Borrowed, Bound}, PyAny, PyResult, Python, }; -mod sealed { - use super::*; - - pub trait Sealed {} - - impl Sealed for *mut ffi::PyObject {} -} - -use sealed::Sealed; - pub(crate) trait FfiPtrExt: Sealed { unsafe fn assume_owned_or_err(self, py: Python<'_>) -> PyResult>; + unsafe fn assume_owned_or_opt(self, py: Python<'_>) -> Option>; unsafe fn assume_owned(self, py: Python<'_>) -> Bound<'_, PyAny>; /// Assumes this pointer is borrowed from a parent object. @@ -42,6 +34,12 @@ impl FfiPtrExt for *mut ffi::PyObject { } #[inline] + unsafe fn assume_owned_or_opt(self, py: Python<'_>) -> Option> { + Bound::from_owned_ptr_or_opt(py, self) + } + + #[inline] + #[track_caller] unsafe fn assume_owned(self, py: Python<'_>) -> Bound<'_, PyAny> { Bound::from_owned_ptr(py, self) } @@ -60,6 +58,7 @@ impl FfiPtrExt for *mut ffi::PyObject { } #[inline] + #[track_caller] unsafe fn assume_borrowed<'a>(self, py: Python<'_>) -> Borrowed<'a, '_, PyAny> { Borrowed::from_ptr(py, self) } diff --git a/src/gil.rs b/src/gil.rs index 1f07e7a6835..17deca7c7f1 100644 --- a/src/gil.rs +++ b/src/gil.rs @@ -1,30 +1,20 @@ //! Interaction with Python's global interpreter lock +#[cfg(feature = "gil-refs")] use crate::impl_::not_send::{NotSend, NOT_SEND}; +#[cfg(pyo3_disable_reference_pool)] +use crate::impl_::panic::PanicTrap; use crate::{ffi, Python}; -use parking_lot::{const_mutex, Mutex, Once}; use std::cell::Cell; -#[cfg(debug_assertions)] +#[cfg(all(feature = "gil-refs", debug_assertions))] use std::cell::RefCell; -#[cfg(not(debug_assertions))] +#[cfg(all(feature = "gil-refs", not(debug_assertions)))] use std::cell::UnsafeCell; -use std::{mem, ptr::NonNull}; +use std::{mem, ptr::NonNull, sync}; -static START: Once = Once::new(); +static START: sync::Once = sync::Once::new(); -cfg_if::cfg_if! { - if #[cfg(thread_local_const_init)] { - use std::thread_local as thread_local_const_init; - } else { - macro_rules! thread_local_const_init { - ($($(#[$attr:meta])* static $name:ident: $ty:ty = const { $init:expr };)*) => ( - thread_local! { $($(#[$attr])* static $name: $ty = $init;)* } - ) - } - } -} - -thread_local_const_init! { +std::thread_local! { /// This is an internal counter in pyo3 monitoring whether this thread has the GIL. /// /// It will be incremented whenever a GILGuard or GILPool is created, and decremented whenever @@ -37,9 +27,9 @@ thread_local_const_init! { static GIL_COUNT: Cell = const { Cell::new(0) }; /// Temporarily hold objects that will be released when the GILPool drops. - #[cfg(debug_assertions)] + #[cfg(all(feature = "gil-refs", debug_assertions))] static OWNED_OBJECTS: RefCell = const { RefCell::new(Vec::new()) }; - #[cfg(not(debug_assertions))] + #[cfg(all(feature = "gil-refs", not(debug_assertions)))] static OWNED_OBJECTS: UnsafeCell = const { UnsafeCell::new(Vec::new()) }; } @@ -67,7 +57,7 @@ fn gil_is_acquired() -> bool { /// /// This function is unavailable under PyPy because PyPy cannot be embedded in Rust (or any other /// software). Support for this is tracked on the -/// [PyPy issue tracker](https://foss.heptapod.net/pypy/pypy/-/issues/3286). +/// [PyPy issue tracker](https://github.com/pypy/pypy/issues/3836). /// /// # Examples /// ```rust @@ -75,10 +65,10 @@ fn gil_is_acquired() -> bool { /// /// # fn main() -> PyResult<()> { /// pyo3::prepare_freethreaded_python(); -/// Python::with_gil(|py| py.run("print('Hello World')", None, None)) +/// Python::with_gil(|py| py.run_bound("print('Hello World')", None, None)) /// # } /// ``` -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] pub fn prepare_freethreaded_python() { // Protect against race conditions when Python is not yet initialized and multiple threads // concurrently call 'prepare_freethreaded_python()'. Note that we do not protect against @@ -118,14 +108,14 @@ pub fn prepare_freethreaded_python() { /// ```rust /// unsafe { /// pyo3::with_embedded_python_interpreter(|py| { -/// if let Err(e) = py.run("print('Hello World')", None, None) { +/// if let Err(e) = py.run_bound("print('Hello World')", None, None) { /// // We must make sure to not return a `PyErr`! /// e.print(py); /// } /// }); /// } /// ``` -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] pub unsafe fn with_embedded_python_interpreter(f: F) -> R where F: for<'p> FnOnce(Python<'p>) -> R, @@ -138,18 +128,16 @@ where ffi::Py_InitializeEx(0); - // Safety: the GIL is already held because of the Py_IntializeEx call. - let pool = GILPool::new(); - - // Import the threading module - this ensures that it will associate this thread as the "main" - // thread, which is important to avoid an `AssertionError` at finalization. - pool.python().import("threading").unwrap(); + let result = { + let guard = GILGuard::assume(); + let py = guard.python(); + // Import the threading module - this ensures that it will associate this thread as the "main" + // thread, which is important to avoid an `AssertionError` at finalization. + py.import_bound("threading").unwrap(); - // Execute the closure. - let result = f(pool.python()); - - // Drop the pool before finalizing. - drop(pool); + // Execute the closure. + f(py) + }; // Finalize the Python interpreter. ffi::Py_Finalize(); @@ -158,19 +146,28 @@ where } /// RAII type that represents the Global Interpreter Lock acquisition. -pub(crate) struct GILGuard { - gstate: ffi::PyGILState_STATE, - pool: mem::ManuallyDrop, +pub(crate) enum GILGuard { + /// Indicates the GIL was already held with this GILGuard was acquired. + Assumed, + /// Indicates that we actually acquired the GIL when this GILGuard was acquired + Ensured { + gstate: ffi::PyGILState_STATE, + #[cfg(feature = "gil-refs")] + #[allow(deprecated)] + pool: mem::ManuallyDrop, + }, } impl GILGuard { /// PyO3 internal API for acquiring the GIL. The public API is Python::with_gil. /// - /// If the GIL was already acquired via PyO3, this returns `None`. Otherwise, - /// the GIL will be acquired and a new `GILPool` created. - pub(crate) fn acquire() -> Option { + /// If the GIL was already acquired via PyO3, this returns + /// `GILGuard::Assumed`. Otherwise, the GIL will be acquired and + /// `GILGuard::Ensured` will be returned. + pub(crate) fn acquire() -> Self { if gil_is_acquired() { - return None; + increment_gil_count(); + return GILGuard::Assumed; } // Maybe auto-initialize the GIL: @@ -180,14 +177,14 @@ impl GILGuard { // auto-initialize so this avoids breaking existing builds. // - Otherwise, just check the GIL is initialized. cfg_if::cfg_if! { - if #[cfg(all(feature = "auto-initialize", not(PyPy)))] { + if #[cfg(all(feature = "auto-initialize", not(any(PyPy, GraalPy))))] { prepare_freethreaded_python(); } else { // This is a "hack" to make running `cargo test` for PyO3 convenient (i.e. no need // to specify `--features auto-initialize` manually. Tests within the crate itself // all depend on the auto-initialize feature for conciseness but Cargo does not // provide a mechanism to specify required features for tests. - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] if option_env!("CARGO_PRIMARY_PACKAGE").is_some() { prepare_freethreaded_python(); } @@ -216,69 +213,94 @@ impl GILGuard { /// This can be called in "unsafe" contexts where the normal interpreter state /// checking performed by `GILGuard::acquire` may fail. This includes calling /// as part of multi-phase interpreter initialization. - pub(crate) fn acquire_unchecked() -> Option { + pub(crate) fn acquire_unchecked() -> Self { if gil_is_acquired() { - return None; + increment_gil_count(); + return GILGuard::Assumed; } let gstate = unsafe { ffi::PyGILState_Ensure() }; // acquire GIL - let pool = unsafe { mem::ManuallyDrop::new(GILPool::new()) }; + #[cfg(feature = "gil-refs")] + #[allow(deprecated)] + { + let pool = unsafe { mem::ManuallyDrop::new(GILPool::new()) }; + increment_gil_count(); + GILGuard::Ensured { gstate, pool } + } + + #[cfg(not(feature = "gil-refs"))] + { + increment_gil_count(); + // Update counts of PyObjects / Py that have been cloned or dropped since last acquisition + #[cfg(not(pyo3_disable_reference_pool))] + POOL.update_counts(unsafe { Python::assume_gil_acquired() }); - Some(GILGuard { gstate, pool }) + GILGuard::Ensured { gstate } + } + } + /// Acquires the `GILGuard` while assuming that the GIL is already held. + pub(crate) unsafe fn assume() -> Self { + increment_gil_count(); + GILGuard::Assumed + } + + /// Gets the Python token associated with this [`GILGuard`]. + #[inline] + pub fn python(&self) -> Python<'_> { + unsafe { Python::assume_gil_acquired() } } } /// The Drop implementation for `GILGuard` will release the GIL. impl Drop for GILGuard { fn drop(&mut self) { - unsafe { - // Drop the objects in the pool before attempting to release the thread state - mem::ManuallyDrop::drop(&mut self.pool); - - ffi::PyGILState_Release(self.gstate); + match self { + GILGuard::Assumed => {} + #[cfg(feature = "gil-refs")] + GILGuard::Ensured { gstate, pool } => unsafe { + // Drop the objects in the pool before attempting to release the thread state + mem::ManuallyDrop::drop(pool); + + ffi::PyGILState_Release(*gstate); + }, + #[cfg(not(feature = "gil-refs"))] + GILGuard::Ensured { gstate } => unsafe { + ffi::PyGILState_Release(*gstate); + }, } + decrement_gil_count(); } } // Vector of PyObject type PyObjVec = Vec>; -/// Thread-safe storage for objects which were inc_ref / dec_ref while the GIL was not held. +#[cfg(not(pyo3_disable_reference_pool))] +/// Thread-safe storage for objects which were dec_ref while the GIL was not held. struct ReferencePool { - // .0 is INCREFs, .1 is DECREFs - pointer_ops: Mutex<(PyObjVec, PyObjVec)>, + pending_decrefs: sync::Mutex, } +#[cfg(not(pyo3_disable_reference_pool))] impl ReferencePool { const fn new() -> Self { Self { - pointer_ops: const_mutex((Vec::new(), Vec::new())), + pending_decrefs: sync::Mutex::new(Vec::new()), } } - fn register_incref(&self, obj: NonNull) { - self.pointer_ops.lock().0.push(obj); - } - fn register_decref(&self, obj: NonNull) { - self.pointer_ops.lock().1.push(obj); + self.pending_decrefs.lock().unwrap().push(obj); } fn update_counts(&self, _py: Python<'_>) { - let mut ops = self.pointer_ops.lock(); - if ops.0.is_empty() && ops.1.is_empty() { + let mut pending_decrefs = self.pending_decrefs.lock().unwrap(); + if pending_decrefs.is_empty() { return; } - let (increfs, decrefs) = mem::take(&mut *ops); - drop(ops); - - // Always increase reference counts first - as otherwise objects which have a - // nonzero total reference count might be incorrectly dropped by Python during - // this update. - for ptr in increfs { - unsafe { ffi::Py_INCREF(ptr.as_ptr()) }; - } + let decrefs = mem::take(&mut *pending_decrefs); + drop(pending_decrefs); for ptr in decrefs { unsafe { ffi::Py_DECREF(ptr.as_ptr()) }; @@ -286,8 +308,10 @@ impl ReferencePool { } } +#[cfg(not(pyo3_disable_reference_pool))] unsafe impl Sync for ReferencePool {} +#[cfg(not(pyo3_disable_reference_pool))] static POOL: ReferencePool = ReferencePool::new(); /// A guard which can be used to temporarily release the GIL and restore on `Drop`. @@ -312,6 +336,7 @@ impl Drop for SuspendGIL { ffi::PyEval_RestoreThread(self.tstate); // Update counts of PyObjects / Py that were cloned or dropped while the GIL was released. + #[cfg(not(pyo3_disable_reference_pool))] POOL.update_counts(Python::assume_gil_acquired()); } } @@ -358,6 +383,11 @@ impl Drop for LockGIL { /// /// [Memory Management]: https://pyo3.rs/main/memory.html#gil-bound-memory +#[cfg(feature = "gil-refs")] +#[deprecated( + since = "0.21.0", + note = "`GILPool` has no function if PyO3's deprecated GIL Refs API is not used" +)] pub struct GILPool { /// Initial length of owned objects and anys. /// `Option` is used since TSL can be broken when `new` is called from `atexit`. @@ -365,19 +395,21 @@ pub struct GILPool { _not_send: NotSend, } +#[cfg(feature = "gil-refs")] +#[allow(deprecated)] impl GILPool { /// Creates a new [`GILPool`]. This function should only ever be called with the GIL held. /// - /// It is recommended not to use this API directly, but instead to use [`Python::new_pool`], as + /// It is recommended not to use this API directly, but instead to use `Python::new_pool`, as /// that guarantees the GIL is held. /// /// # Safety /// - /// As well as requiring the GIL, see the safety notes on [`Python::new_pool`]. + /// As well as requiring the GIL, see the safety notes on `Python::new_pool`. #[inline] pub unsafe fn new() -> GILPool { - increment_gil_count(); // Update counts of PyObjects / Py that have been cloned or dropped since last acquisition + #[cfg(not(pyo3_disable_reference_pool))] POOL.update_counts(Python::assume_gil_acquired()); GILPool { start: OWNED_OBJECTS @@ -401,6 +433,8 @@ impl GILPool { } } +#[cfg(feature = "gil-refs")] +#[allow(deprecated)] impl Drop for GILPool { fn drop(&mut self) { if let Some(start) = self.start { @@ -423,23 +457,21 @@ impl Drop for GILPool { } } } - decrement_gil_count(); } } -/// Registers a Python object pointer inside the release pool, to have its reference count increased -/// the next time the GIL is acquired in pyo3. -/// -/// If the GIL is held, the reference count will be increased immediately instead of being queued -/// for later. +/// Increments the reference count of a Python object if the GIL is held. If +/// the GIL is not held, this function will panic. /// /// # Safety /// The object must be an owned Python reference. +#[cfg(feature = "py-clone")] +#[track_caller] pub unsafe fn register_incref(obj: NonNull) { if gil_is_acquired() { ffi::Py_INCREF(obj.as_ptr()) } else { - POOL.register_incref(obj); + panic!("Cannot clone pointer into Python heap without the GIL being held."); } } @@ -451,11 +483,21 @@ pub unsafe fn register_incref(obj: NonNull) { /// /// # Safety /// The object must be an owned Python reference. +#[track_caller] pub unsafe fn register_decref(obj: NonNull) { if gil_is_acquired() { ffi::Py_DECREF(obj.as_ptr()) } else { + #[cfg(not(pyo3_disable_reference_pool))] POOL.register_decref(obj); + #[cfg(all( + pyo3_disable_reference_pool, + not(pyo3_leak_on_drop_without_reference_pool) + ))] + { + let _trap = PanicTrap::new("Aborting the process to avoid panic-from-drop."); + panic!("Cannot drop pointer into Python heap without the GIL being held."); + } } } @@ -463,6 +505,7 @@ pub unsafe fn register_decref(obj: NonNull) { /// /// # Safety /// The object must be an owned Python reference. +#[cfg(feature = "gil-refs")] pub unsafe fn register_owned(_py: Python<'_>, obj: NonNull) { debug_assert!(gil_is_acquired()); // Ignores the error in case this function called from `atexit`. @@ -506,22 +549,22 @@ fn decrement_gil_count() { #[cfg(test)] mod tests { - use super::{gil_is_acquired, GILPool, GIL_COUNT, OWNED_OBJECTS, POOL}; - use crate::{ffi, gil, PyObject, Python, ToPyObject}; - #[cfg(not(target_arch = "wasm32"))] - use parking_lot::{const_mutex, Condvar, Mutex}; + #[cfg(not(pyo3_disable_reference_pool))] + use super::{gil_is_acquired, POOL}; + #[cfg(feature = "gil-refs")] + #[allow(deprecated)] + use super::{GILPool, GIL_COUNT, OWNED_OBJECTS}; + use crate::types::any::PyAnyMethods; + #[cfg(feature = "gil-refs")] + use crate::{ffi, gil}; + use crate::{PyObject, Python}; use std::ptr::NonNull; fn get_object(py: Python<'_>) -> PyObject { - // Convenience function for getting a single unique object, using `new_pool` so as to leave - // the original pool state unchanged. - let pool = unsafe { py.new_pool() }; - let py = pool.python(); - - let obj = py.eval("object()", None, None).unwrap(); - obj.to_object(py) + py.eval_bound("object()", None, None).unwrap().unbind() } + #[cfg(feature = "gil-refs")] fn owned_object_count() -> usize { #[cfg(debug_assertions)] let len = OWNED_OBJECTS.with(|owned_objects| owned_objects.borrow().len()); @@ -530,31 +573,26 @@ mod tests { len } - fn pool_inc_refs_does_not_contain(obj: &PyObject) -> bool { + #[cfg(not(pyo3_disable_reference_pool))] + fn pool_dec_refs_does_not_contain(obj: &PyObject) -> bool { !POOL - .pointer_ops + .pending_decrefs .lock() - .0 + .unwrap() .contains(&unsafe { NonNull::new_unchecked(obj.as_ptr()) }) } - fn pool_dec_refs_does_not_contain(obj: &PyObject) -> bool { - !POOL - .pointer_ops + #[cfg(all(not(pyo3_disable_reference_pool), not(target_arch = "wasm32")))] + fn pool_dec_refs_contains(obj: &PyObject) -> bool { + POOL.pending_decrefs .lock() - .1 + .unwrap() .contains(&unsafe { NonNull::new_unchecked(obj.as_ptr()) }) } - #[cfg(not(target_arch = "wasm32"))] - fn pool_dirty_with( - inc_refs: Vec>, - dec_refs: Vec>, - ) -> bool { - *POOL.pointer_ops.lock() == (inc_refs, dec_refs) - } - #[test] + #[cfg(feature = "gil-refs")] + #[allow(deprecated)] fn test_owned() { Python::with_gil(|py| { let obj = get_object(py); @@ -580,6 +618,8 @@ mod tests { } #[test] + #[cfg(feature = "gil-refs")] + #[allow(deprecated)] fn test_owned_nested() { Python::with_gil(|py| { let obj = get_object(py); @@ -621,18 +661,20 @@ mod tests { let reference = obj.clone_ref(py); assert_eq!(obj.get_refcnt(py), 2); - assert!(pool_inc_refs_does_not_contain(&obj)); + #[cfg(not(pyo3_disable_reference_pool))] + assert!(pool_dec_refs_does_not_contain(&obj)); - // With the GIL held, reference cound will be decreased immediately. + // With the GIL held, reference count will be decreased immediately. drop(reference); assert_eq!(obj.get_refcnt(py), 1); + #[cfg(not(pyo3_disable_reference_pool))] assert!(pool_dec_refs_does_not_contain(&obj)); }); } #[test] - #[cfg(not(target_arch = "wasm32"))] // We are building wasm Python with pthreads disabled + #[cfg(all(not(pyo3_disable_reference_pool), not(target_arch = "wasm32")))] // We are building wasm Python with pthreads disabled fn test_pyobject_drop_without_gil_doesnt_decrease_refcnt() { let obj = Python::with_gil(|py| { let obj = get_object(py); @@ -640,7 +682,7 @@ mod tests { let reference = obj.clone_ref(py); assert_eq!(obj.get_refcnt(py), 2); - assert!(pool_inc_refs_does_not_contain(&obj)); + assert!(pool_dec_refs_does_not_contain(&obj)); // Drop reference in a separate thread which doesn't have the GIL. std::thread::spawn(move || drop(reference)).join().unwrap(); @@ -648,23 +690,20 @@ mod tests { // The reference count should not have changed (the GIL has always // been held by this thread), it is remembered to release later. assert_eq!(obj.get_refcnt(py), 2); - assert!(pool_dirty_with( - vec![], - vec![NonNull::new(obj.as_ptr()).unwrap()] - )); + assert!(pool_dec_refs_contains(&obj)); obj }); // Next time the GIL is acquired, the reference is released Python::with_gil(|py| { assert_eq!(obj.get_refcnt(py), 1); - let non_null = unsafe { NonNull::new_unchecked(obj.as_ptr()) }; - assert!(!POOL.pointer_ops.lock().0.contains(&non_null)); - assert!(!POOL.pointer_ops.lock().1.contains(&non_null)); + assert!(pool_dec_refs_does_not_contain(&obj)); }); } #[test] + #[cfg(feature = "gil-refs")] + #[allow(deprecated)] fn test_gil_counts() { // Check with_gil and GILPool both increase counts correctly let get_gil_count = || GIL_COUNT.with(|c| c.get()); @@ -674,19 +713,19 @@ mod tests { assert_eq!(get_gil_count(), 1); let pool = unsafe { GILPool::new() }; - assert_eq!(get_gil_count(), 2); + assert_eq!(get_gil_count(), 1); let pool2 = unsafe { GILPool::new() }; - assert_eq!(get_gil_count(), 3); + assert_eq!(get_gil_count(), 1); drop(pool); - assert_eq!(get_gil_count(), 2); + assert_eq!(get_gil_count(), 1); Python::with_gil(|_| { // nested with_gil doesn't update gil count assert_eq!(get_gil_count(), 2); }); - assert_eq!(get_gil_count(), 2); + assert_eq!(get_gil_count(), 1); drop(pool2); assert_eq!(get_gil_count(), 1); @@ -717,19 +756,16 @@ mod tests { } } + #[cfg(feature = "py-clone")] #[test] + #[should_panic] fn test_allow_threads_updates_refcounts() { Python::with_gil(|py| { // Make a simple object with 1 reference let obj = get_object(py); assert!(obj.get_refcnt(py) == 1); - // Clone the object without the GIL to use internal tracking - let escaped_ref = py.allow_threads().with(|| obj.clone()); - // But after the block the refcounts are updated - assert!(obj.get_refcnt(py) == 2); - drop(escaped_ref); - assert!(obj.get_refcnt(py) == 1); - drop(obj); + // Clone the object without the GIL which should panic + py.allow_threads().with(|| obj.clone()); }); } @@ -737,13 +773,14 @@ mod tests { fn dropping_gil_does_not_invalidate_references() { // Acquiring GIL for the second time should be safe - see #864 Python::with_gil(|py| { - let obj = Python::with_gil(|_| py.eval("object()", None, None).unwrap()); + let obj = Python::with_gil(|_| py.eval_bound("object()", None, None).unwrap()); // After gil2 drops, obj should still have a reference count of one assert_eq!(obj.get_refcnt(), 1); }) } + #[cfg(feature = "py-clone")] #[test] fn test_clone_with_gil() { Python::with_gil(|py| { @@ -757,146 +794,9 @@ mod tests { }) } - #[cfg(not(target_arch = "wasm32"))] - struct Event { - set: Mutex, - wait: Condvar, - } - - #[cfg(not(target_arch = "wasm32"))] - impl Event { - const fn new() -> Self { - Self { - set: const_mutex(false), - wait: Condvar::new(), - } - } - - fn set(&self) { - *self.set.lock() = true; - self.wait.notify_all(); - } - - fn wait(&self) { - let mut set = self.set.lock(); - while !*set { - self.wait.wait(&mut set); - } - } - } - - #[test] - #[cfg(not(target_arch = "wasm32"))] // We are building wasm Python with pthreads disabled - fn test_clone_without_gil() { - use crate::{Py, PyAny}; - use std::{sync::Arc, thread}; - - // Some events for synchronizing - static GIL_ACQUIRED: Event = Event::new(); - static OBJECT_CLONED: Event = Event::new(); - static REFCNT_CHECKED: Event = Event::new(); - - Python::with_gil(|py| { - let obj: Arc> = Arc::new(get_object(py)); - let thread_obj = Arc::clone(&obj); - - let count = obj.get_refcnt(py); - println!( - "1: The object has been created and its reference count is {}", - count - ); - - let handle = thread::spawn(move || { - Python::with_gil(move |py| { - println!("3. The GIL has been acquired on another thread."); - GIL_ACQUIRED.set(); - - // Wait while the main thread registers obj in POOL - OBJECT_CLONED.wait(); - println!("5. Checking refcnt"); - assert_eq!(thread_obj.get_refcnt(py), count); - - REFCNT_CHECKED.set(); - }) - }); - - let cloned = py.allow_threads().with(|| { - println!("2. The GIL has been released."); - - // Wait until the GIL has been acquired on the thread. - GIL_ACQUIRED.wait(); - - println!("4. The other thread is now hogging the GIL, we clone without it held"); - // Cloning without GIL should not update reference count - let cloned = Py::clone(&*obj); - OBJECT_CLONED.set(); - cloned - }); - - REFCNT_CHECKED.wait(); - - println!("6. The main thread has acquired the GIL again and processed the pool."); - - // Total reference count should be one higher - assert_eq!(obj.get_refcnt(py), count + 1); - - // Clone dropped - drop(cloned); - // Ensure refcount of the arc is 1 - handle.join().unwrap(); - - // Overall count is now back to the original, and should be no pending change - assert_eq!(Arc::try_unwrap(obj).unwrap().get_refcnt(py), count); - }); - } - - #[test] - #[cfg(not(target_arch = "wasm32"))] // We are building wasm Python with pthreads disabled - fn test_clone_in_other_thread() { - use crate::Py; - use std::{sync::Arc, thread}; - - // Some events for synchronizing - static OBJECT_CLONED: Event = Event::new(); - - let (obj, count, ptr) = Python::with_gil(|py| { - let obj = Arc::new(get_object(py)); - let count = obj.get_refcnt(py); - let thread_obj = Arc::clone(&obj); - - // Start a thread which does not have the GIL, and clone it - let t = thread::spawn(move || { - // Cloning without GIL should not update reference count - #[allow(clippy::redundant_clone)] - let _ = Py::clone(&*thread_obj); - OBJECT_CLONED.set(); - }); - - OBJECT_CLONED.wait(); - assert_eq!(count, obj.get_refcnt(py)); - - t.join().unwrap(); - let ptr = NonNull::new(obj.as_ptr()).unwrap(); - - // The pointer should appear once in the incref pool, and once in the - // decref pool (for the clone being created and also dropped) - assert!(POOL.pointer_ops.lock().0.contains(&ptr)); - assert!(POOL.pointer_ops.lock().1.contains(&ptr)); - - (obj, count, ptr) - }); - - Python::with_gil(|py| { - // Acquiring the gil clears the pool - assert!(!POOL.pointer_ops.lock().0.contains(&ptr)); - assert!(!POOL.pointer_ops.lock().1.contains(&ptr)); - - // Overall count is still unchanged - assert_eq!(count, obj.get_refcnt(py)); - }); - } - #[test] + #[cfg(not(pyo3_disable_reference_pool))] + #[cfg(feature = "gil-refs")] fn test_update_counts_does_not_deadlock() { // update_counts can run arbitrary Python code during Py_DECREF. // if the locking is implemented incorrectly, it will deadlock. @@ -907,6 +807,7 @@ mod tests { unsafe extern "C" fn capsule_drop(capsule: *mut ffi::PyObject) { // This line will implicitly call update_counts // -> and so cause deadlock if update_counts is not handling recursion correctly. + #[allow(deprecated)] let pool = GILPool::new(); // Rebuild obj so that it can be dropped diff --git a/src/impl_.rs b/src/impl_.rs index 77f9ff4ea1f..71ba397cb94 100644 --- a/src/impl_.rs +++ b/src/impl_.rs @@ -6,9 +6,10 @@ //! APIs may may change at any time without documentation in the CHANGELOG and without //! breaking semver guarantees. -#[cfg(feature = "macros")] +#[cfg(feature = "experimental-async")] pub mod coroutine; pub mod deprecations; +pub mod exceptions; pub mod extract_argument; pub mod freelist; pub mod frompyobject; diff --git a/src/impl_/coroutine.rs b/src/impl_/coroutine.rs index 32f3e94ad8a..1d3119400a0 100644 --- a/src/impl_/coroutine.rs +++ b/src/impl_/coroutine.rs @@ -1,18 +1,19 @@ use std::{ future::Future, - mem, ops::{Deref, DerefMut}, }; use crate::{ coroutine::{cancel::ThrowCallback, Coroutine}, + instance::Bound, + pycell::impl_::PyClassBorrowChecker, pyclass::boolean_struct::False, - types::PyString, - IntoPy, Py, PyAny, PyCell, PyClass, PyErr, PyObject, PyResult, Python, + types::{PyAnyMethods, PyString}, + IntoPy, Py, PyAny, PyClass, PyErr, PyObject, PyResult, Python, }; pub fn new_coroutine( - name: &PyString, + name: &Bound<'_, PyString>, qualname_prefix: Option<&'static str>, throw_callback: Option, future: F, @@ -22,21 +23,25 @@ where T: IntoPy, E: Into, { - Coroutine::new(Some(name.into()), qualname_prefix, throw_callback, future) + Coroutine::new( + Some(name.clone().into()), + qualname_prefix, + throw_callback, + future, + ) } fn get_ptr(obj: &Py) -> *mut T { - // SAFETY: Py can be casted as *const PyCell - unsafe { &*(obj.as_ptr() as *const PyCell) }.get_ptr() + obj.get_class_object().get_ptr() } pub struct RefGuard(Py); impl RefGuard { - pub fn new(obj: &PyAny) -> PyResult { - let owned: Py = obj.extract()?; - mem::forget(owned.try_borrow(obj.py())?); - Ok(RefGuard(owned)) + pub fn new(obj: &Bound<'_, PyAny>) -> PyResult { + let bound = obj.downcast::()?; + bound.get_class_object().borrow_checker().try_borrow()?; + Ok(RefGuard(bound.clone().unbind())) } } @@ -50,17 +55,23 @@ impl Deref for RefGuard { impl Drop for RefGuard { fn drop(&mut self) { - Python::with_gil(|gil| self.0.as_ref(gil).release_ref()) + Python::with_gil(|gil| { + self.0 + .bind(gil) + .get_class_object() + .borrow_checker() + .release_borrow() + }) } } pub struct RefMutGuard>(Py); impl> RefMutGuard { - pub fn new(obj: &PyAny) -> PyResult { - let owned: Py = obj.extract()?; - mem::forget(owned.try_borrow_mut(obj.py())?); - Ok(RefMutGuard(owned)) + pub fn new(obj: &Bound<'_, PyAny>) -> PyResult { + let bound = obj.downcast::()?; + bound.get_class_object().borrow_checker().try_borrow_mut()?; + Ok(RefMutGuard(bound.clone().unbind())) } } @@ -81,6 +92,12 @@ impl> DerefMut for RefMutGuard { impl> Drop for RefMutGuard { fn drop(&mut self) { - Python::with_gil(|gil| self.0.as_ref(gil).release_mut()) + Python::with_gil(|gil| { + self.0 + .bind(gil) + .get_class_object() + .borrow_checker() + .release_borrow_mut() + }) } } diff --git a/src/impl_/deprecations.rs b/src/impl_/deprecations.rs index 6b9930ac69b..eb5caa8dffb 100644 --- a/src/impl_/deprecations.rs +++ b/src/impl_/deprecations.rs @@ -1,4 +1,76 @@ //! Symbols used to denote deprecated usages of PyO3's proc macros. +use crate::{PyResult, Python}; + #[deprecated(since = "0.20.0", note = "use `#[new]` instead of `#[__new__]`")] pub const PYMETHODS_NEW_DEPRECATED_FORM: () = (); + +pub fn inspect_type(t: T, _: &GilRefs) -> T { + t +} + +pub fn inspect_fn(f: fn(A) -> PyResult, _: &GilRefs) -> fn(A) -> PyResult { + f +} + +pub struct GilRefs(OptionGilRefs); +pub struct OptionGilRefs(NotAGilRef); +pub struct NotAGilRef(std::marker::PhantomData); + +pub trait IsGilRef {} + +#[cfg(feature = "gil-refs")] +impl IsGilRef for &'_ T {} + +impl GilRefs { + #[allow(clippy::new_without_default)] + pub fn new() -> Self { + GilRefs(OptionGilRefs(NotAGilRef(std::marker::PhantomData))) + } +} + +impl GilRefs> { + #[deprecated(since = "0.21.0", note = "use `wrap_pyfunction_bound!` instead")] + pub fn is_python(&self) {} +} + +impl GilRefs { + #[deprecated( + since = "0.21.0", + note = "use `&Bound<'_, T>` instead for this function argument" + )] + pub fn function_arg(&self) {} + #[deprecated( + since = "0.21.0", + note = "use `&Bound<'_, PyAny>` as the argument for this `from_py_with` extractor" + )] + pub fn from_py_with_arg(&self) {} +} + +impl OptionGilRefs> { + #[deprecated( + since = "0.21.0", + note = "use `Option<&Bound<'_, T>>` instead for this function argument" + )] + pub fn function_arg(&self) {} +} + +impl NotAGilRef { + pub fn function_arg(&self) {} + pub fn from_py_with_arg(&self) {} + pub fn is_python(&self) {} +} + +impl std::ops::Deref for GilRefs { + type Target = OptionGilRefs; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl std::ops::Deref for OptionGilRefs { + type Target = NotAGilRef; + fn deref(&self) -> &Self::Target { + &self.0 + } +} diff --git a/src/impl_/exceptions.rs b/src/impl_/exceptions.rs new file mode 100644 index 00000000000..eafac1edfa2 --- /dev/null +++ b/src/impl_/exceptions.rs @@ -0,0 +1,28 @@ +use crate::{sync::GILOnceCell, types::PyType, Bound, Py, Python}; + +pub struct ImportedExceptionTypeObject { + imported_value: GILOnceCell>, + module: &'static str, + name: &'static str, +} + +impl ImportedExceptionTypeObject { + pub const fn new(module: &'static str, name: &'static str) -> Self { + Self { + imported_value: GILOnceCell::new(), + module, + name, + } + } + + pub fn get<'py>(&self, py: Python<'py>) -> &Bound<'py, PyType> { + self.imported_value + .get_or_try_init_type_ref(py, self.module, self.name) + .unwrap_or_else(|e| { + panic!( + "failed to import exception {}.{}: {}", + self.module, self.name, e + ) + }) + } +} diff --git a/src/impl_/extract_argument.rs b/src/impl_/extract_argument.rs index 437ece483ce..5f652d75122 100644 --- a/src/impl_/extract_argument.rs +++ b/src/impl_/extract_argument.rs @@ -1,11 +1,17 @@ use crate::{ + conversion::FromPyObjectBound, exceptions::PyTypeError, ffi, pyclass::boolean_struct::False, - types::{PyDict, PyString, PyTuple}, - FromPyObject, PyAny, PyClass, PyErr, PyRef, PyRefMut, PyResult, Python, + types::{any::PyAnyMethods, dict::PyDictMethods, tuple::PyTupleMethods, PyDict, PyTuple}, + Borrowed, Bound, PyAny, PyClass, PyErr, PyRef, PyRefMut, PyResult, PyTypeCheck, Python, }; +/// Helper type used to keep implementation more concise. +/// +/// (Function argument extraction borrows input arguments.) +type PyArg<'py> = Borrowed<'py, 'py, PyAny>; + /// A trait which is used to help PyO3 macros extract function arguments. /// /// `#[pyclass]` structs need to extract as `PyRef` and `PyRefMut` @@ -16,21 +22,62 @@ use crate::{ /// There exists a trivial blanket implementation for `T: FromPyObject` with `Holder = ()`. pub trait PyFunctionArgument<'a, 'py>: Sized + 'a { type Holder: FunctionArgumentHolder; - fn extract(obj: &'py PyAny, holder: &'a mut Self::Holder) -> PyResult; + fn extract(obj: &'a Bound<'py, PyAny>, holder: &'a mut Self::Holder) -> PyResult; } impl<'a, 'py, T> PyFunctionArgument<'a, 'py> for T where - T: FromPyObject<'py> + 'a, + T: FromPyObjectBound<'a, 'py> + 'a, { type Holder = (); #[inline] - fn extract(obj: &'py PyAny, _: &'a mut ()) -> PyResult { + fn extract(obj: &'a Bound<'py, PyAny>, _: &'a mut ()) -> PyResult { obj.extract() } } +impl<'a, 'py, T: 'py> PyFunctionArgument<'a, 'py> for &'a Bound<'py, T> +where + T: PyTypeCheck, +{ + type Holder = Option<()>; + + #[inline] + fn extract(obj: &'a Bound<'py, PyAny>, _: &'a mut Option<()>) -> PyResult { + obj.downcast().map_err(Into::into) + } +} + +impl<'a, 'py, T: 'py> PyFunctionArgument<'a, 'py> for Option<&'a Bound<'py, T>> +where + T: PyTypeCheck, +{ + type Holder = (); + + #[inline] + fn extract(obj: &'a Bound<'py, PyAny>, _: &'a mut ()) -> PyResult { + if obj.is_none() { + Ok(None) + } else { + Ok(Some(obj.downcast()?)) + } + } +} + +#[cfg(all(Py_LIMITED_API, not(any(feature = "gil-refs", Py_3_10))))] +impl<'a> PyFunctionArgument<'a, '_> for &'a str { + type Holder = Option>; + + #[inline] + fn extract( + obj: &'a Bound<'_, PyAny>, + holder: &'a mut Option>, + ) -> PyResult { + Ok(holder.insert(obj.extract()?)) + } +} + /// Trait for types which can be a function argument holder - they should /// to be able to const-initialize to an empty value. pub trait FunctionArgumentHolder: Sized { @@ -47,7 +94,7 @@ impl FunctionArgumentHolder for Option { #[inline] pub fn extract_pyclass_ref<'a, 'py: 'a, T: PyClass>( - obj: &'py PyAny, + obj: &'a Bound<'py, PyAny>, holder: &'a mut Option>, ) -> PyResult<&'a T> { Ok(&*holder.insert(obj.extract()?)) @@ -55,7 +102,7 @@ pub fn extract_pyclass_ref<'a, 'py: 'a, T: PyClass>( #[inline] pub fn extract_pyclass_ref_mut<'a, 'py: 'a, T: PyClass>( - obj: &'py PyAny, + obj: &'a Bound<'py, PyAny>, holder: &'a mut Option>, ) -> PyResult<&'a mut T> { Ok(&mut *holder.insert(obj.extract()?)) @@ -64,7 +111,7 @@ pub fn extract_pyclass_ref_mut<'a, 'py: 'a, T: PyClass>( /// The standard implementation of how PyO3 extracts a `#[pyfunction]` or `#[pymethod]` function argument. #[doc(hidden)] pub fn extract_argument<'a, 'py, T>( - obj: &'py PyAny, + obj: &'a Bound<'py, PyAny>, holder: &'a mut T::Holder, arg_name: &str, ) -> PyResult @@ -81,7 +128,7 @@ where /// does not implement `PyFunctionArgument` for `T: PyClass`. #[doc(hidden)] pub fn extract_optional_argument<'a, 'py, T>( - obj: Option<&'py PyAny>, + obj: Option<&'a Bound<'py, PyAny>>, holder: &'a mut T::Holder, arg_name: &str, default: fn() -> Option, @@ -105,7 +152,7 @@ where /// Alternative to [`extract_argument`] used when the argument has a default value provided by an annotation. #[doc(hidden)] pub fn extract_argument_with_default<'a, 'py, T>( - obj: Option<&'py PyAny>, + obj: Option<&'a Bound<'py, PyAny>>, holder: &'a mut T::Holder, arg_name: &str, default: fn() -> T, @@ -121,12 +168,12 @@ where /// Alternative to [`extract_argument`] used when the argument has a `#[pyo3(from_py_with)]` annotation. #[doc(hidden)] -pub fn from_py_with<'py, T>( - obj: &'py PyAny, +pub fn from_py_with<'a, 'py, T>( + obj: &'a Bound<'py, PyAny>, arg_name: &str, - extractor: fn(&'py PyAny) -> PyResult, + extractor: impl Into>, ) -> PyResult { - match extractor(obj) { + match extractor.into().call(obj) { Ok(value) => Ok(value), Err(e) => Err(argument_extraction_error(obj.py(), arg_name, e)), } @@ -134,10 +181,10 @@ pub fn from_py_with<'py, T>( /// Alternative to [`extract_argument`] used when the argument has a `#[pyo3(from_py_with)]` annotation and also a default value. #[doc(hidden)] -pub fn from_py_with_with_default<'py, T>( - obj: Option<&'py PyAny>, +pub fn from_py_with_with_default<'a, 'py, T>( + obj: Option<&'a Bound<'py, PyAny>>, arg_name: &str, - extractor: fn(&'py PyAny) -> PyResult, + extractor: impl Into>, default: fn() -> T, ) -> PyResult { match obj { @@ -153,9 +200,15 @@ pub fn from_py_with_with_default<'py, T>( #[doc(hidden)] #[cold] pub fn argument_extraction_error(py: Python<'_>, arg_name: &str, error: PyErr) -> PyErr { - if error.get_type(py).is(py.get_type::()) { - let remapped_error = - PyTypeError::new_err(format!("argument '{}': {}", arg_name, error.value(py))); + if error + .get_type_bound(py) + .is(&py.get_type_bound::()) + { + let remapped_error = PyTypeError::new_err(format!( + "argument '{}': {}", + arg_name, + error.value_bound(py) + )); remapped_error.set_cause(py, error.cause(py)); remapped_error } else { @@ -170,7 +223,9 @@ pub fn argument_extraction_error(py: Python<'_>, arg_name: &str, error: PyErr) - /// `argument` must not be `None` #[doc(hidden)] #[inline] -pub unsafe fn unwrap_required_argument(argument: Option<&PyAny>) -> &PyAny { +pub unsafe fn unwrap_required_argument<'a, 'py>( + argument: Option<&'a Bound<'py, PyAny>>, +) -> &'a Bound<'py, PyAny> { match argument { Some(value) => value, #[cfg(debug_assertions)] @@ -217,7 +272,7 @@ impl FunctionDescription { args: *const *mut ffi::PyObject, nargs: ffi::Py_ssize_t, kwnames: *mut ffi::PyObject, - output: &mut [Option<&'py PyAny>], + output: &mut [Option>], ) -> PyResult<(V::Varargs, K::Varkeywords)> where V: VarargsHandler<'py>, @@ -234,8 +289,10 @@ impl FunctionDescription { ); // Handle positional arguments - // Safety: Option<&PyAny> has the same memory layout as `*mut ffi::PyObject` - let args: *const Option<&PyAny> = args.cast(); + // Safety: + // - Option has the same memory layout as `*mut ffi::PyObject` + // - we both have the GIL and can borrow these input references for the `'py` lifetime. + let args: *const Option> = args.cast(); let positional_args_provided = nargs as usize; let remaining_positional_args = if args.is_null() { debug_assert_eq!(positional_args_provided, 0); @@ -255,13 +312,20 @@ impl FunctionDescription { // Handle keyword arguments let mut varkeywords = K::Varkeywords::default(); - if let Some(kwnames) = py.from_borrowed_ptr_or_opt::(kwnames) { - // Safety: &PyAny has the same memory layout as `*mut ffi::PyObject` - let kwargs = - ::std::slice::from_raw_parts((args as *const &PyAny).offset(nargs), kwnames.len()); + + // Safety: kwnames is known to be a pointer to a tuple, or null + // - we both have the GIL and can borrow this input reference for the `'py` lifetime. + let kwnames: Option> = + Borrowed::from_ptr_or_opt(py, kwnames).map(|kwnames| kwnames.downcast_unchecked()); + if let Some(kwnames) = kwnames { + // Safety: PyArg has the same memory layout as `*mut ffi::PyObject` + let kwargs = ::std::slice::from_raw_parts( + (args as *const PyArg<'py>).offset(nargs), + kwnames.len(), + ); self.handle_kwargs::( - kwnames.iter().zip(kwargs.iter().copied()), + kwnames.iter_borrowed().zip(kwargs.iter().copied()), &mut varkeywords, num_positional_parameters, output, @@ -293,14 +357,20 @@ impl FunctionDescription { py: Python<'py>, args: *mut ffi::PyObject, kwargs: *mut ffi::PyObject, - output: &mut [Option<&'py PyAny>], + output: &mut [Option>], ) -> PyResult<(V::Varargs, K::Varkeywords)> where V: VarargsHandler<'py>, K: VarkeywordsHandler<'py>, { - let args = py.from_borrowed_ptr::(args); - let kwargs: ::std::option::Option<&PyDict> = py.from_borrowed_ptr_or_opt(kwargs); + // Safety: + // - `args` is known to be a tuple + // - `kwargs` is known to be a dict or null + // - we both have the GIL and can borrow these input references for the `'py` lifetime. + let args: Borrowed<'py, 'py, PyTuple> = + Borrowed::from_ptr(py, args).downcast_unchecked::(); + let kwargs: Option> = + Borrowed::from_ptr_or_opt(py, kwargs).map(|kwargs| kwargs.downcast_unchecked()); let num_positional_parameters = self.positional_parameter_names.len(); @@ -312,17 +382,26 @@ impl FunctionDescription { ); // Copy positional arguments into output - for (i, arg) in args.iter().take(num_positional_parameters).enumerate() { + for (i, arg) in args + .iter_borrowed() + .take(num_positional_parameters) + .enumerate() + { output[i] = Some(arg); } // If any arguments remain, push them to varargs (if possible) or error - let varargs = V::handle_varargs_tuple(args, self)?; + let varargs = V::handle_varargs_tuple(&args, self)?; // Handle keyword arguments let mut varkeywords = K::Varkeywords::default(); if let Some(kwargs) = kwargs { - self.handle_kwargs::(kwargs, &mut varkeywords, num_positional_parameters, output)? + self.handle_kwargs::( + kwargs.iter_borrowed(), + &mut varkeywords, + num_positional_parameters, + output, + )? } // Once all inputs have been processed, check that all required arguments have been provided. @@ -339,11 +418,11 @@ impl FunctionDescription { kwargs: I, varkeywords: &mut K::Varkeywords, num_positional_parameters: usize, - output: &mut [Option<&'py PyAny>], + output: &mut [Option>], ) -> PyResult<()> where K: VarkeywordsHandler<'py>, - I: IntoIterator, + I: IntoIterator, PyArg<'py>)>, { debug_assert_eq!( num_positional_parameters, @@ -355,11 +434,21 @@ impl FunctionDescription { ); let mut positional_only_keyword_arguments = Vec::new(); for (kwarg_name_py, value) in kwargs { - // All keyword arguments should be UTF-8 strings, but we'll check, just in case. - // If it isn't, then it will be handled below as a varkeyword (which may raise an - // error if this function doesn't accept **kwargs). Rust source is always UTF-8 - // and so all argument names in `#[pyfunction]` signature must be UTF-8. - if let Ok(kwarg_name) = kwarg_name_py.downcast::()?.to_str() { + // Safety: All keyword arguments should be UTF-8 strings, but if it's not, `.to_str()` + // will return an error anyway. + #[cfg(any(Py_3_10, not(Py_LIMITED_API)))] + let kwarg_name = + unsafe { kwarg_name_py.downcast_unchecked::() }.to_str(); + + #[cfg(all(not(Py_3_10), Py_LIMITED_API))] + let kwarg_name = kwarg_name_py.extract::(); + + if let Ok(kwarg_name_owned) = kwarg_name { + #[cfg(any(Py_3_10, not(Py_LIMITED_API)))] + let kwarg_name = kwarg_name_owned; + #[cfg(all(not(Py_3_10), Py_LIMITED_API))] + let kwarg_name: &str = &kwarg_name_owned; + // Try to place parameter in keyword only parameters if let Some(i) = self.find_keyword_parameter_in_keyword_only(kwarg_name) { if output[i + num_positional_parameters] @@ -378,7 +467,7 @@ impl FunctionDescription { // kwarg to conflict with a postional-only argument - the value // will go into **kwargs anyway. if K::handle_varkeyword(varkeywords, kwarg_name_py, value, self).is_err() { - positional_only_keyword_arguments.push(kwarg_name); + positional_only_keyword_arguments.push(kwarg_name_owned); } } else if output[i].replace(value).is_some() { return Err(self.multiple_values_for_argument(kwarg_name)); @@ -391,6 +480,11 @@ impl FunctionDescription { } if !positional_only_keyword_arguments.is_empty() { + #[cfg(all(not(Py_3_10), Py_LIMITED_API))] + let positional_only_keyword_arguments: Vec<_> = positional_only_keyword_arguments + .iter() + .map(std::ops::Deref::deref) + .collect(); return Err(self.positional_only_keyword_arguments(&positional_only_keyword_arguments)); } @@ -417,7 +511,7 @@ impl FunctionDescription { #[inline] fn ensure_no_missing_required_positional_arguments( &self, - output: &[Option<&PyAny>], + output: &[Option>], positional_args_provided: usize, ) -> PyResult<()> { if positional_args_provided < self.required_positional_parameters { @@ -433,7 +527,7 @@ impl FunctionDescription { #[inline] fn ensure_no_missing_required_keyword_arguments( &self, - output: &[Option<&PyAny>], + output: &[Option>], ) -> PyResult<()> { let keyword_output = &output[self.positional_parameter_names.len()..]; for (param, out) in self.keyword_only_parameters.iter().zip(keyword_output) { @@ -478,11 +572,11 @@ impl FunctionDescription { } #[cold] - fn unexpected_keyword_argument(&self, argument: &PyAny) -> PyErr { + fn unexpected_keyword_argument(&self, argument: PyArg<'_>) -> PyErr { PyTypeError::new_err(format!( "{} got an unexpected keyword argument '{}'", self.full_name(), - argument + argument.as_any() )) } @@ -515,7 +609,7 @@ impl FunctionDescription { } #[cold] - fn missing_required_keyword_arguments(&self, keyword_outputs: &[Option<&PyAny>]) -> PyErr { + fn missing_required_keyword_arguments(&self, keyword_outputs: &[Option>]) -> PyErr { debug_assert_eq!(self.keyword_only_parameters.len(), keyword_outputs.len()); let missing_keyword_only_arguments: Vec<_> = self @@ -536,7 +630,7 @@ impl FunctionDescription { } #[cold] - fn missing_required_positional_arguments(&self, output: &[Option<&PyAny>]) -> PyErr { + fn missing_required_positional_arguments(&self, output: &[Option>]) -> PyErr { let missing_positional_arguments: Vec<_> = self .positional_parameter_names .iter() @@ -556,14 +650,14 @@ pub trait VarargsHandler<'py> { /// Called by `FunctionDescription::extract_arguments_fastcall` with any additional arguments. fn handle_varargs_fastcall( py: Python<'py>, - varargs: &[Option<&PyAny>], + varargs: &[Option>], function_description: &FunctionDescription, ) -> PyResult; /// Called by `FunctionDescription::extract_arguments_tuple_dict` with the original tuple. /// /// Additional arguments are those in the tuple slice starting from `function_description.positional_parameter_names.len()`. fn handle_varargs_tuple( - args: &'py PyTuple, + args: &Bound<'py, PyTuple>, function_description: &FunctionDescription, ) -> PyResult; } @@ -577,7 +671,7 @@ impl<'py> VarargsHandler<'py> for NoVarargs { #[inline] fn handle_varargs_fastcall( _py: Python<'py>, - varargs: &[Option<&PyAny>], + varargs: &[Option>], function_description: &FunctionDescription, ) -> PyResult { let extra_arguments = varargs.len(); @@ -591,7 +685,7 @@ impl<'py> VarargsHandler<'py> for NoVarargs { #[inline] fn handle_varargs_tuple( - args: &'py PyTuple, + args: &Bound<'py, PyTuple>, function_description: &FunctionDescription, ) -> PyResult { let positional_parameter_count = function_description.positional_parameter_names.len(); @@ -608,19 +702,19 @@ impl<'py> VarargsHandler<'py> for NoVarargs { pub struct TupleVarargs; impl<'py> VarargsHandler<'py> for TupleVarargs { - type Varargs = &'py PyTuple; + type Varargs = Bound<'py, PyTuple>; #[inline] fn handle_varargs_fastcall( py: Python<'py>, - varargs: &[Option<&PyAny>], + varargs: &[Option>], _function_description: &FunctionDescription, ) -> PyResult { - Ok(PyTuple::new(py, varargs)) + Ok(PyTuple::new_bound(py, varargs)) } #[inline] fn handle_varargs_tuple( - args: &'py PyTuple, + args: &Bound<'py, PyTuple>, function_description: &FunctionDescription, ) -> PyResult { let positional_parameters = function_description.positional_parameter_names.len(); @@ -633,8 +727,8 @@ pub trait VarkeywordsHandler<'py> { type Varkeywords: Default; fn handle_varkeyword( varkeywords: &mut Self::Varkeywords, - name: &'py PyAny, - value: &'py PyAny, + name: PyArg<'py>, + value: PyArg<'py>, function_description: &FunctionDescription, ) -> PyResult<()>; } @@ -647,8 +741,8 @@ impl<'py> VarkeywordsHandler<'py> for NoVarkeywords { #[inline] fn handle_varkeyword( _varkeywords: &mut Self::Varkeywords, - name: &'py PyAny, - _value: &'py PyAny, + name: PyArg<'py>, + _value: PyArg<'py>, function_description: &FunctionDescription, ) -> PyResult<()> { Err(function_description.unexpected_keyword_argument(name)) @@ -659,28 +753,29 @@ impl<'py> VarkeywordsHandler<'py> for NoVarkeywords { pub struct DictVarkeywords; impl<'py> VarkeywordsHandler<'py> for DictVarkeywords { - type Varkeywords = Option<&'py PyDict>; + type Varkeywords = Option>; #[inline] fn handle_varkeyword( varkeywords: &mut Self::Varkeywords, - name: &'py PyAny, - value: &'py PyAny, + name: PyArg<'py>, + value: PyArg<'py>, _function_description: &FunctionDescription, ) -> PyResult<()> { varkeywords - .get_or_insert_with(|| PyDict::new(name.py())) + .get_or_insert_with(|| PyDict::new_bound(name.py())) .set_item(name, value) } } fn push_parameter_list(msg: &mut String, parameter_names: &[&str]) { + let len = parameter_names.len(); for (i, parameter) in parameter_names.iter().enumerate() { if i != 0 { - if parameter_names.len() > 2 { + if len > 2 { msg.push(','); } - if i == parameter_names.len() - 1 { + if i == len - 1 { msg.push_str(" and ") } else { msg.push(' ') @@ -695,10 +790,8 @@ fn push_parameter_list(msg: &mut String, parameter_names: &[&str]) { #[cfg(test)] mod tests { - use crate::{ - types::{IntoPyDict, PyTuple}, - PyAny, Python, ToPyObject, - }; + use crate::types::{IntoPyDict, PyTuple}; + use crate::Python; use super::{push_parameter_list, FunctionDescription, NoVarargs, NoVarkeywords}; @@ -714,8 +807,8 @@ mod tests { }; Python::with_gil(|py| { - let args = PyTuple::new(py, Vec::<&PyAny>::new()); - let kwargs = [("foo".to_object(py).into_ref(py), 0u8)].into_py_dict(py); + let args = PyTuple::empty_bound(py); + let kwargs = [("foo", 0u8)].into_py_dict_bound(py); let err = unsafe { function_description .extract_arguments_tuple_dict::( @@ -745,8 +838,8 @@ mod tests { }; Python::with_gil(|py| { - let args = PyTuple::new(py, Vec::<&PyAny>::new()); - let kwargs = [(1u8.to_object(py).into_ref(py), 1u8)].into_py_dict(py); + let args = PyTuple::empty_bound(py); + let kwargs = [(1u8, 1u8)].into_py_dict_bound(py); let err = unsafe { function_description .extract_arguments_tuple_dict::( @@ -759,7 +852,7 @@ mod tests { }; assert_eq!( err.to_string(), - "TypeError: 'int' object cannot be converted to 'PyString'" + "TypeError: example() got an unexpected keyword argument '1'" ); }) } @@ -776,7 +869,7 @@ mod tests { }; Python::with_gil(|py| { - let args = PyTuple::new(py, Vec::<&PyAny>::new()); + let args = PyTuple::empty_bound(py); let mut output = [None, None]; let err = unsafe { function_description.extract_arguments_tuple_dict::( diff --git a/src/impl_/frompyobject.rs b/src/impl_/frompyobject.rs index a0c7b13df7c..1e46efaeae3 100644 --- a/src/impl_/frompyobject.rs +++ b/src/impl_/frompyobject.rs @@ -1,5 +1,36 @@ +use crate::types::any::PyAnyMethods; +use crate::Bound; use crate::{exceptions::PyTypeError, FromPyObject, PyAny, PyErr, PyResult, Python}; +pub enum Extractor<'a, 'py, T> { + Bound(fn(&'a Bound<'py, PyAny>) -> PyResult), + #[cfg(feature = "gil-refs")] + GilRef(fn(&'a PyAny) -> PyResult), +} + +impl<'a, 'py, T> From) -> PyResult> for Extractor<'a, 'py, T> { + fn from(value: fn(&'a Bound<'py, PyAny>) -> PyResult) -> Self { + Self::Bound(value) + } +} + +#[cfg(feature = "gil-refs")] +impl<'a, T> From PyResult> for Extractor<'a, '_, T> { + fn from(value: fn(&'a PyAny) -> PyResult) -> Self { + Self::GilRef(value) + } +} + +impl<'a, 'py, T> Extractor<'a, 'py, T> { + pub(crate) fn call(self, obj: &'a Bound<'py, PyAny>) -> PyResult { + match self { + Extractor::Bound(f) => f(obj), + #[cfg(feature = "gil-refs")] + Extractor::GilRef(f) => f(obj.as_gil_ref()), + } + } +} + #[cold] pub fn failed_to_extract_enum( py: Python<'_>, @@ -41,7 +72,7 @@ fn extract_traceback(py: Python<'_>, mut error: PyErr) -> String { } pub fn extract_struct_field<'py, T>( - obj: &'py PyAny, + obj: &Bound<'py, PyAny>, struct_name: &str, field_name: &str, ) -> PyResult @@ -59,13 +90,13 @@ where } } -pub fn extract_struct_field_with<'py, T>( - extractor: impl FnOnce(&'py PyAny) -> PyResult, - obj: &'py PyAny, +pub fn extract_struct_field_with<'a, 'py, T>( + extractor: impl Into>, + obj: &'a Bound<'py, PyAny>, struct_name: &str, field_name: &str, ) -> PyResult { - match extractor(obj) { + match extractor.into().call(obj) { Ok(value) => Ok(value), Err(err) => Err(failed_to_extract_struct_field( obj.py(), @@ -92,7 +123,7 @@ fn failed_to_extract_struct_field( } pub fn extract_tuple_struct_field<'py, T>( - obj: &'py PyAny, + obj: &Bound<'py, PyAny>, struct_name: &str, index: usize, ) -> PyResult @@ -110,13 +141,13 @@ where } } -pub fn extract_tuple_struct_field_with<'py, T>( - extractor: impl FnOnce(&'py PyAny) -> PyResult, - obj: &'py PyAny, +pub fn extract_tuple_struct_field_with<'a, 'py, T>( + extractor: impl Into>, + obj: &'a Bound<'py, PyAny>, struct_name: &str, index: usize, ) -> PyResult { - match extractor(obj) { + match extractor.into().call(obj) { Ok(value) => Ok(value), Err(err) => Err(failed_to_extract_tuple_struct_field( obj.py(), diff --git a/src/impl_/not_send.rs b/src/impl_/not_send.rs index 97c2984aff8..382e07a14ee 100644 --- a/src/impl_/not_send.rs +++ b/src/impl_/not_send.rs @@ -6,4 +6,5 @@ use crate::Python; /// Workaround for lack of !Send on stable (). pub(crate) struct NotSend(PhantomData<*mut Python<'static>>); +#[cfg(feature = "gil-refs")] pub(crate) const NOT_SEND: NotSend = NotSend(PhantomData); diff --git a/src/impl_/pycell.rs b/src/impl_/pycell.rs index 39811aebd29..93514c7bb29 100644 --- a/src/impl_/pycell.rs +++ b/src/impl_/pycell.rs @@ -1,2 +1,4 @@ //! Externally-accessible implementation of pycell -pub use crate::pycell::impl_::{GetBorrowChecker, PyClassMutability}; +pub use crate::pycell::impl_::{ + GetBorrowChecker, PyClassMutability, PyClassObject, PyClassObjectBase, PyClassObjectLayout, +}; diff --git a/src/impl_/pyclass.rs b/src/impl_/pyclass.rs index 5ee67dc998d..a3e466670a4 100644 --- a/src/impl_/pyclass.rs +++ b/src/impl_/pyclass.rs @@ -1,13 +1,15 @@ +#[cfg(feature = "gil-refs")] +use crate::PyNativeType; use crate::{ exceptions::{PyAttributeError, PyNotImplementedError, PyRuntimeError, PyValueError}, ffi, impl_::freelist::FreeList, - impl_::pycell::{GetBorrowChecker, PyClassMutability}, + impl_::pycell::{GetBorrowChecker, PyClassMutability, PyClassObjectLayout}, internal_tricks::extract_c_string, - pycell::PyCellLayout, pyclass_init::PyObjectInit, + types::any::PyAnyMethods, types::PyBool, - Py, PyAny, PyCell, PyClass, PyErr, PyMethodDefType, PyNativeType, PyResult, PyTypeInfo, Python, + Borrowed, Py, PyAny, PyClass, PyErr, PyMethodDefType, PyResult, PyTypeInfo, Python, }; use std::{ borrow::Cow, @@ -24,13 +26,13 @@ pub use lazy_type_object::LazyTypeObject; /// Gets the offset of the dictionary from the start of the object in bytes. #[inline] pub fn dict_offset() -> ffi::Py_ssize_t { - PyCell::::dict_offset() + PyClassObject::::dict_offset() } /// Gets the offset of the weakref list from the start of the object in bytes. #[inline] pub fn weaklist_offset() -> ffi::Py_ssize_t { - PyCell::::weaklist_offset() + PyClassObject::::weaklist_offset() } /// Represents the `__dict__` field for `#[pyclass]`. @@ -167,7 +169,12 @@ pub trait PyClassImpl: Sized + 'static { /// The closest native ancestor. This is `PyAny` by default, and when you declare /// `#[pyclass(extends=PyDict)]`, it's `PyDict`. + #[cfg(feature = "gil-refs")] type BaseNativeType: PyTypeInfo + PyNativeType; + /// The closest native ancestor. This is `PyAny` by default, and when you declare + /// `#[pyclass(extends=PyDict)]`, it's `PyDict`. + #[cfg(not(feature = "gil-refs"))] + type BaseNativeType: PyTypeInfo; /// This handles following two situations: /// 1. In case `T` is `Send`, stub `ThreadChecker` is used and does nothing. @@ -510,11 +517,11 @@ macro_rules! define_pyclass_binary_operator_slot { #[inline] unsafe fn $lhs( self, - _py: Python<'_>, + py: Python<'_>, _slf: *mut ffi::PyObject, _other: *mut ffi::PyObject, ) -> PyResult<*mut ffi::PyObject> { - Ok(ffi::_Py_NewRef(ffi::Py_NotImplemented())) + Ok(py.NotImplemented().into_ptr()) } } @@ -525,11 +532,11 @@ macro_rules! define_pyclass_binary_operator_slot { #[inline] unsafe fn $rhs( self, - _py: Python<'_>, + py: Python<'_>, _slf: *mut ffi::PyObject, _other: *mut ffi::PyObject, ) -> PyResult<*mut ffi::PyObject> { - Ok(ffi::_Py_NewRef(ffi::Py_NotImplemented())) + Ok(py.NotImplemented().into_ptr()) } } @@ -700,12 +707,12 @@ slot_fragment_trait! { #[inline] unsafe fn __pow__( self, - _py: Python<'_>, + py: Python<'_>, _slf: *mut ffi::PyObject, _other: *mut ffi::PyObject, _mod: *mut ffi::PyObject, ) -> PyResult<*mut ffi::PyObject> { - Ok(ffi::_Py_NewRef(ffi::Py_NotImplemented())) + Ok(py.NotImplemented().into_ptr()) } } @@ -716,12 +723,12 @@ slot_fragment_trait! { #[inline] unsafe fn __rpow__( self, - _py: Python<'_>, + py: Python<'_>, _slf: *mut ffi::PyObject, _other: *mut ffi::PyObject, _mod: *mut ffi::PyObject, ) -> PyResult<*mut ffi::PyObject> { - Ok(ffi::_Py_NewRef(ffi::Py_NotImplemented())) + Ok(py.NotImplemented().into_ptr()) } } @@ -761,11 +768,11 @@ slot_fragment_trait! { #[inline] unsafe fn __lt__( self, - _py: Python<'_>, + py: Python<'_>, _slf: *mut ffi::PyObject, _other: *mut ffi::PyObject, ) -> PyResult<*mut ffi::PyObject> { - Ok(ffi::_Py_NewRef(ffi::Py_NotImplemented())) + Ok(py.NotImplemented().into_ptr()) } } @@ -776,11 +783,11 @@ slot_fragment_trait! { #[inline] unsafe fn __le__( self, - _py: Python<'_>, + py: Python<'_>, _slf: *mut ffi::PyObject, _other: *mut ffi::PyObject, ) -> PyResult<*mut ffi::PyObject> { - Ok(ffi::_Py_NewRef(ffi::Py_NotImplemented())) + Ok(py.NotImplemented().into_ptr()) } } @@ -791,11 +798,11 @@ slot_fragment_trait! { #[inline] unsafe fn __eq__( self, - _py: Python<'_>, + py: Python<'_>, _slf: *mut ffi::PyObject, _other: *mut ffi::PyObject, ) -> PyResult<*mut ffi::PyObject> { - Ok(ffi::_Py_NewRef(ffi::Py_NotImplemented())) + Ok(py.NotImplemented().into_ptr()) } } @@ -811,9 +818,9 @@ slot_fragment_trait! { other: *mut ffi::PyObject, ) -> PyResult<*mut ffi::PyObject> { // By default `__ne__` will try `__eq__` and invert the result - let slf: &PyAny = py.from_borrowed_ptr(slf); - let other: &PyAny = py.from_borrowed_ptr(other); - slf.eq(other).map(|is_eq| PyBool::new(py, !is_eq).into_ptr()) + let slf = Borrowed::from_ptr(py, slf); + let other = Borrowed::from_ptr(py, other); + slf.eq(other).map(|is_eq| PyBool::new_bound(py, !is_eq).to_owned().into_ptr()) } } @@ -824,11 +831,11 @@ slot_fragment_trait! { #[inline] unsafe fn __gt__( self, - _py: Python<'_>, + py: Python<'_>, _slf: *mut ffi::PyObject, _other: *mut ffi::PyObject, ) -> PyResult<*mut ffi::PyObject> { - Ok(ffi::_Py_NewRef(ffi::Py_NotImplemented())) + Ok(py.NotImplemented().into_ptr()) } } @@ -839,11 +846,11 @@ slot_fragment_trait! { #[inline] unsafe fn __ge__( self, - _py: Python<'_>, + py: Python<'_>, _slf: *mut ffi::PyObject, _other: *mut ffi::PyObject, ) -> PyResult<*mut ffi::PyObject> { - Ok(ffi::_Py_NewRef(ffi::Py_NotImplemented())) + Ok(py.NotImplemented().into_ptr()) } } @@ -851,6 +858,7 @@ slot_fragment_trait! { #[macro_export] macro_rules! generate_pyclass_richcompare_slot { ($cls:ty) => {{ + #[allow(unknown_lints, non_local_definitions)] impl $cls { #[allow(non_snake_case)] unsafe extern "C" fn __pymethod___richcmp____( @@ -881,6 +889,8 @@ macro_rules! generate_pyclass_richcompare_slot { } pub use generate_pyclass_richcompare_slot; +use super::pycell::PyClassObject; + /// Implements a freelist. /// /// Do not implement this trait manually. Instead, use `#[pyclass(freelist = N)]` @@ -1067,7 +1077,7 @@ impl ThreadCheckerImpl { "{} is unsendable, but is being dropped on another thread", type_name )) - .write_unraisable(py, None); + .write_unraisable_bound(py, None); return false; } @@ -1092,8 +1102,15 @@ impl PyClassThreadChecker for ThreadCheckerImpl { } /// Trait denoting that this class is suitable to be used as a base type for PyClass. + +#[cfg_attr( + all(diagnostic_namespace, feature = "abi3"), + diagnostic::on_unimplemented( + note = "with the `abi3` feature enabled, PyO3 does not support subclassing native types" + ) +)] pub trait PyClassBaseType: Sized { - type LayoutAsBase: PyCellLayout; + type LayoutAsBase: PyClassObjectLayout; type BaseNativeType; type Initializer: PyObjectInit; type PyClassMutability: PyClassMutability; @@ -1103,7 +1120,7 @@ pub trait PyClassBaseType: Sized { /// /// In the future this will be extended to immutable PyClasses too. impl PyClassBaseType for T { - type LayoutAsBase = crate::pycell::PyCell; + type LayoutAsBase = crate::impl_::pycell::PyClassObject; type BaseNativeType = T::BaseNativeType; type Initializer = crate::pyclass_init::PyClassInitializer; type PyClassMutability = T::PyClassMutability; @@ -1111,7 +1128,7 @@ impl PyClassBaseType for T { /// Implementation of tp_dealloc for pyclasses without gc pub(crate) unsafe extern "C" fn tp_dealloc(obj: *mut ffi::PyObject) { - crate::impl_::trampoline::dealloc(obj, PyCell::::tp_dealloc) + crate::impl_::trampoline::dealloc(obj, PyClassObject::::tp_dealloc) } /// Implementation of tp_dealloc for pyclasses with gc @@ -1120,7 +1137,7 @@ pub(crate) unsafe extern "C" fn tp_dealloc_with_gc(obj: *mut ffi::Py { ffi::PyObject_GC_UnTrack(obj.cast()); } - crate::impl_::trampoline::dealloc(obj, PyCell::::tp_dealloc) + crate::impl_::trampoline::dealloc(obj, PyClassObject::::tp_dealloc) } pub(crate) unsafe extern "C" fn get_sequence_item_from_mapping( diff --git a/src/impl_/pyclass/lazy_type_object.rs b/src/impl_/pyclass/lazy_type_object.rs index af52033a702..f83fa4c5186 100644 --- a/src/impl_/pyclass/lazy_type_object.rs +++ b/src/impl_/pyclass/lazy_type_object.rs @@ -12,7 +12,7 @@ use crate::{ pyclass::{create_type_object, PyClassTypeObject}, sync::{GILOnceCell, GILProtected}, types::PyType, - PyClass, PyErr, PyMethodDefType, PyObject, PyResult, Python, + Bound, PyClass, PyErr, PyMethodDefType, PyObject, PyResult, Python, }; use super::PyClassItemsIter; @@ -32,6 +32,7 @@ struct LazyTypeObjectInner { impl LazyTypeObject { /// Creates an uninitialized `LazyTypeObject`. + #[allow(clippy::new_without_default)] pub const fn new() -> Self { LazyTypeObject( LazyTypeObjectInner { @@ -46,7 +47,7 @@ impl LazyTypeObject { impl LazyTypeObject { /// Gets the type object contained by this `LazyTypeObject`, initializing it if needed. - pub fn get_or_init<'py>(&'py self, py: Python<'py>) -> &'py PyType { + pub fn get_or_init<'py>(&self, py: Python<'py>) -> &Bound<'py, PyType> { self.get_or_try_init(py).unwrap_or_else(|err| { err.print(py); panic!("failed to create type object for {}", T::NAME) @@ -54,7 +55,7 @@ impl LazyTypeObject { } /// Fallible version of the above. - pub(crate) fn get_or_try_init<'py>(&'py self, py: Python<'py>) -> PyResult<&'py PyType> { + pub(crate) fn get_or_try_init<'py>(&self, py: Python<'py>) -> PyResult<&Bound<'py, PyType>> { self.0 .get_or_try_init(py, create_type_object::, T::NAME, T::items_iter()) } @@ -65,18 +66,18 @@ impl LazyTypeObjectInner { // so that this code is only instantiated once, instead of for every T // like the generic LazyTypeObject methods above. fn get_or_try_init<'py>( - &'py self, + &self, py: Python<'py>, init: fn(Python<'py>) -> PyResult, name: &str, items_iter: PyClassItemsIter, - ) -> PyResult<&'py PyType> { + ) -> PyResult<&Bound<'py, PyType>> { (|| -> PyResult<_> { let type_object = self .value .get_or_try_init(py, || init(py))? .type_object - .as_ref(py); + .bind(py); self.ensure_init(type_object, name, items_iter)?; Ok(type_object) })() @@ -91,7 +92,7 @@ impl LazyTypeObjectInner { fn ensure_init( &self, - type_object: &PyType, + type_object: &Bound<'_, PyType>, name: &str, items_iter: PyClassItemsIter, ) -> PyResult<()> { @@ -152,7 +153,7 @@ impl LazyTypeObjectInner { if let PyMethodDefType::ClassAttribute(attr) = def { let key = attr.attribute_c_string().unwrap(); - match (attr.meth.0)(py) { + match (attr.meth)(py) { Ok(val) => items.push((key, val)), Err(err) => { return Err(wrap_in_runtime_error( diff --git a/src/impl_/pyfunction.rs b/src/impl_/pyfunction.rs index 14cdbd48f85..0be5174487f 100644 --- a/src/impl_/pyfunction.rs +++ b/src/impl_/pyfunction.rs @@ -1,10 +1,80 @@ -use crate::{derive_utils::PyFunctionArguments, types::PyCFunction, PyResult}; +use crate::{ + types::{PyCFunction, PyModule}, + Borrowed, Bound, PyResult, Python, +}; pub use crate::impl_::pymethods::PyMethodDef; -pub fn _wrap_pyfunction<'a>( - method_def: &PyMethodDef, - py_or_module: impl Into>, -) -> PyResult<&'a PyCFunction> { - PyCFunction::internal_new(method_def, py_or_module.into()) +/// Trait to enable the use of `wrap_pyfunction` with both `Python` and `PyModule`, +/// and also to infer the return type of either `&'py PyCFunction` or `Bound<'py, PyCFunction>`. +pub trait WrapPyFunctionArg<'py, T> { + fn wrap_pyfunction(self, method_def: &PyMethodDef) -> PyResult; +} + +impl<'py> WrapPyFunctionArg<'py, Bound<'py, PyCFunction>> for Bound<'py, PyModule> { + fn wrap_pyfunction(self, method_def: &PyMethodDef) -> PyResult> { + PyCFunction::internal_new(self.py(), method_def, Some(&self)) + } +} + +impl<'py> WrapPyFunctionArg<'py, Bound<'py, PyCFunction>> for &'_ Bound<'py, PyModule> { + fn wrap_pyfunction(self, method_def: &PyMethodDef) -> PyResult> { + PyCFunction::internal_new(self.py(), method_def, Some(self)) + } +} + +impl<'py> WrapPyFunctionArg<'py, Bound<'py, PyCFunction>> for Borrowed<'_, 'py, PyModule> { + fn wrap_pyfunction(self, method_def: &PyMethodDef) -> PyResult> { + PyCFunction::internal_new(self.py(), method_def, Some(&self)) + } +} + +impl<'py> WrapPyFunctionArg<'py, Bound<'py, PyCFunction>> for &'_ Borrowed<'_, 'py, PyModule> { + fn wrap_pyfunction(self, method_def: &PyMethodDef) -> PyResult> { + PyCFunction::internal_new(self.py(), method_def, Some(self)) + } +} + +// For Python<'py>, only the GIL Ref form exists to avoid causing type inference to kick in. +// The `wrap_pyfunction_bound!` macro is needed for the Bound form. +#[cfg(feature = "gil-refs")] +impl<'py> WrapPyFunctionArg<'py, &'py PyCFunction> for Python<'py> { + fn wrap_pyfunction(self, method_def: &PyMethodDef) -> PyResult<&'py PyCFunction> { + PyCFunction::internal_new(self, method_def, None).map(Bound::into_gil_ref) + } +} + +#[cfg(not(feature = "gil-refs"))] +impl<'py> WrapPyFunctionArg<'py, Bound<'py, PyCFunction>> for Python<'py> { + fn wrap_pyfunction(self, method_def: &PyMethodDef) -> PyResult> { + PyCFunction::internal_new(self, method_def, None) + } +} + +#[cfg(feature = "gil-refs")] +impl<'py> WrapPyFunctionArg<'py, &'py PyCFunction> for &'py PyModule { + fn wrap_pyfunction(self, method_def: &PyMethodDef) -> PyResult<&'py PyCFunction> { + use crate::PyNativeType; + PyCFunction::internal_new(self.py(), method_def, Some(&self.as_borrowed())) + .map(Bound::into_gil_ref) + } +} + +/// Helper for `wrap_pyfunction_bound!` to guarantee return type of `Bound<'py, PyCFunction>`. +pub struct OnlyBound(pub T); + +impl<'py, T> WrapPyFunctionArg<'py, Bound<'py, PyCFunction>> for OnlyBound +where + T: WrapPyFunctionArg<'py, Bound<'py, PyCFunction>>, +{ + fn wrap_pyfunction(self, method_def: &PyMethodDef) -> PyResult> { + WrapPyFunctionArg::wrap_pyfunction(self.0, method_def) + } +} + +#[cfg(feature = "gil-refs")] +impl<'py> WrapPyFunctionArg<'py, Bound<'py, PyCFunction>> for OnlyBound> { + fn wrap_pyfunction(self, method_def: &PyMethodDef) -> PyResult> { + PyCFunction::internal_new(self.0, method_def, None) + } } diff --git a/src/impl_/pymethods.rs b/src/impl_/pymethods.rs index e403aa23c79..44b2af25650 100644 --- a/src/impl_/pymethods.rs +++ b/src/impl_/pymethods.rs @@ -3,8 +3,14 @@ use crate::exceptions::PyStopAsyncIteration; use crate::gil::LockGIL; use crate::impl_::panic::PanicTrap; use crate::internal_tricks::extract_c_string; +use crate::pycell::{PyBorrowError, PyBorrowMutError}; +use crate::pyclass::boolean_struct::False; +use crate::types::any::PyAnyMethods; +#[cfg(feature = "gil-refs")] +use crate::types::{PyModule, PyType}; use crate::{ - ffi, PyAny, PyCell, PyClass, PyErr, PyObject, PyResult, PyTraverseError, PyVisit, Python, + ffi, Borrowed, Bound, DowncastError, Py, PyAny, PyClass, PyClassInitializer, PyErr, PyObject, + PyRef, PyRefMut, PyResult, PyTraverseError, PyTypeCheck, PyVisit, Python, }; use std::borrow::Cow; use std::ffi::CStr; @@ -21,7 +27,7 @@ pub struct IPowModulo(*mut ffi::PyObject); /// Python 3.7 and older - __ipow__ does not have modulo argument correctly populated. #[cfg(not(Py_3_8))] #[repr(transparent)] -pub struct IPowModulo(std::mem::MaybeUninit<*mut ffi::PyObject>); +pub struct IPowModulo(#[allow(dead_code)] std::mem::MaybeUninit<*mut ffi::PyObject>); /// Helper to use as pymethod ffi definition #[allow(non_camel_case_types)] @@ -34,14 +40,15 @@ pub type ipowfunc = unsafe extern "C" fn( impl IPowModulo { #[cfg(Py_3_8)] #[inline] - pub fn to_borrowed_any(self, py: Python<'_>) -> &PyAny { - unsafe { py.from_borrowed_ptr::(self.0) } + pub fn as_ptr(self) -> *mut ffi::PyObject { + self.0 } #[cfg(not(Py_3_8))] #[inline] - pub fn to_borrowed_any(self, py: Python<'_>) -> &PyAny { - unsafe { py.from_borrowed_ptr::(ffi::Py_None()) } + pub fn as_ptr(self) -> *mut ffi::PyObject { + // Safety: returning a borrowed pointer to Python `None` singleton + unsafe { ffi::Py_None() } } } @@ -64,27 +71,13 @@ pub enum PyMethodDefType { #[derive(Copy, Clone, Debug)] pub enum PyMethodType { - PyCFunction(PyCFunction), - PyCFunctionWithKeywords(PyCFunctionWithKeywords), + PyCFunction(ffi::PyCFunction), + PyCFunctionWithKeywords(ffi::PyCFunctionWithKeywords), #[cfg(not(Py_LIMITED_API))] - PyCFunctionFastWithKeywords(PyCFunctionFastWithKeywords), -} - -// These newtype structs serve no purpose other than wrapping which are function pointers - because -// function pointers aren't allowed in const fn, but types wrapping them are! -#[derive(Clone, Copy, Debug)] -pub struct PyCFunction(pub ffi::PyCFunction); -#[derive(Clone, Copy, Debug)] -pub struct PyCFunctionWithKeywords(pub ffi::PyCFunctionWithKeywords); -#[cfg(not(Py_LIMITED_API))] -#[derive(Clone, Copy, Debug)] -pub struct PyCFunctionFastWithKeywords(pub ffi::_PyCFunctionFastWithKeywords); -#[derive(Clone, Copy)] -pub struct PyGetter(pub Getter); -#[derive(Clone, Copy)] -pub struct PySetter(pub Setter); -#[derive(Clone, Copy)] -pub struct PyClassAttributeFactory(pub for<'p> fn(Python<'p>) -> PyResult); + PyCFunctionFastWithKeywords(ffi::_PyCFunctionFastWithKeywords), +} + +pub type PyClassAttributeFactory = for<'p> fn(Python<'p>) -> PyResult; // TODO: it would be nice to use CStr in these types, but then the constructors can't be const fn // until `CStr::from_bytes_with_nul_unchecked` is const fn. @@ -112,14 +105,14 @@ impl PyClassAttributeDef { #[derive(Clone)] pub struct PyGetterDef { pub(crate) name: &'static str, - pub(crate) meth: PyGetter, + pub(crate) meth: Getter, pub(crate) doc: &'static str, } #[derive(Clone)] pub struct PySetterDef { pub(crate) name: &'static str, - pub(crate) meth: PySetter, + pub(crate) meth: Setter, pub(crate) doc: &'static str, } @@ -131,7 +124,11 @@ unsafe impl Sync for PySetterDef {} impl PyMethodDef { /// Define a function with no `*args` and `**kwargs`. - pub const fn noargs(name: &'static str, cfunction: PyCFunction, doc: &'static str) -> Self { + pub const fn noargs( + name: &'static str, + cfunction: ffi::PyCFunction, + doc: &'static str, + ) -> Self { Self { ml_name: name, ml_meth: PyMethodType::PyCFunction(cfunction), @@ -143,7 +140,7 @@ impl PyMethodDef { /// Define a function that can take `*args` and `**kwargs`. pub const fn cfunction_with_keywords( name: &'static str, - cfunction: PyCFunctionWithKeywords, + cfunction: ffi::PyCFunctionWithKeywords, doc: &'static str, ) -> Self { Self { @@ -158,7 +155,7 @@ impl PyMethodDef { #[cfg(not(Py_LIMITED_API))] pub const fn fastcall_cfunction_with_keywords( name: &'static str, - cfunction: PyCFunctionFastWithKeywords, + cfunction: ffi::_PyCFunctionFastWithKeywords, doc: &'static str, ) -> Self { Self { @@ -177,15 +174,13 @@ impl PyMethodDef { /// Convert `PyMethodDef` to Python method definition struct `ffi::PyMethodDef` pub(crate) fn as_method_def(&self) -> PyResult<(ffi::PyMethodDef, PyMethodDefDestructor)> { let meth = match self.ml_meth { - PyMethodType::PyCFunction(meth) => ffi::PyMethodDefPointer { - PyCFunction: meth.0, - }, + PyMethodType::PyCFunction(meth) => ffi::PyMethodDefPointer { PyCFunction: meth }, PyMethodType::PyCFunctionWithKeywords(meth) => ffi::PyMethodDefPointer { - PyCFunctionWithKeywords: meth.0, + PyCFunctionWithKeywords: meth, }, #[cfg(not(Py_LIMITED_API))] PyMethodType::PyCFunctionFastWithKeywords(meth) => ffi::PyMethodDefPointer { - _PyCFunctionFastWithKeywords: meth.0, + _PyCFunctionFastWithKeywords: meth, }, }; @@ -227,7 +222,7 @@ pub(crate) type Setter = impl PyGetterDef { /// Define a getter. - pub const fn new(name: &'static str, getter: PyGetter, doc: &'static str) -> Self { + pub const fn new(name: &'static str, getter: Getter, doc: &'static str) -> Self { Self { name, meth: getter, @@ -238,7 +233,7 @@ impl PyGetterDef { impl PySetterDef { /// Define a setter. - pub const fn new(name: &'static str, setter: PySetter, doc: &'static str) -> Self { + pub const fn new(name: &'static str, setter: Setter, doc: &'static str) -> Self { Self { name, meth: setter, @@ -268,8 +263,8 @@ where let trap = PanicTrap::new("uncaught panic inside __traverse__ handler"); let py = Python::assume_gil_acquired(); - let slf = py.from_borrowed_ptr::>(slf); - let borrow = slf.try_borrow_threadsafe(); + let slf = Borrowed::from_ptr_unchecked(py, slf).downcast_unchecked::(); + let borrow = PyRef::try_borrow_threadsafe(&slf); let visit = PyVisit::from_raw(visit, arg, py); let retval = if let Ok(borrow) = borrow { @@ -466,3 +461,115 @@ pub trait AsyncIterResultOptionKind { } impl AsyncIterResultOptionKind for Result, Error> {} + +/// Used in `#[classmethod]` to pass the class object to the method +/// and also in `#[pyfunction(pass_module)]`. +/// +/// This is a wrapper to avoid implementing `From` for GIL Refs. +/// +/// Once the GIL Ref API is fully removed, it should be possible to simplify +/// this to just `&'a Bound<'py, T>` and `From` implementations. +pub struct BoundRef<'a, 'py, T>(pub &'a Bound<'py, T>); + +impl<'a, 'py> BoundRef<'a, 'py, PyAny> { + pub unsafe fn ref_from_ptr(py: Python<'py>, ptr: &'a *mut ffi::PyObject) -> Self { + BoundRef(Bound::ref_from_ptr(py, ptr)) + } + + pub unsafe fn ref_from_ptr_or_opt( + py: Python<'py>, + ptr: &'a *mut ffi::PyObject, + ) -> Option { + Bound::ref_from_ptr_or_opt(py, ptr).as_ref().map(BoundRef) + } + + pub fn downcast(self) -> Result, DowncastError<'a, 'py>> { + self.0.downcast::().map(BoundRef) + } + + pub unsafe fn downcast_unchecked(self) -> BoundRef<'a, 'py, T> { + BoundRef(self.0.downcast_unchecked::()) + } +} + +// GIL Ref implementations for &'a T ran into trouble with orphan rules, +// so explicit implementations are used instead for the two relevant types. +#[cfg(feature = "gil-refs")] +impl<'a> From> for &'a PyType { + #[inline] + fn from(bound: BoundRef<'a, 'a, PyType>) -> Self { + bound.0.as_gil_ref() + } +} + +#[cfg(feature = "gil-refs")] +impl<'a> From> for &'a PyModule { + #[inline] + fn from(bound: BoundRef<'a, 'a, PyModule>) -> Self { + bound.0.as_gil_ref() + } +} + +#[allow(deprecated)] +#[cfg(feature = "gil-refs")] +impl<'a, 'py, T: PyClass> From> for &'a crate::PyCell { + #[inline] + fn from(bound: BoundRef<'a, 'py, T>) -> Self { + bound.0.as_gil_ref() + } +} + +impl<'a, 'py, T: PyClass> TryFrom> for PyRef<'py, T> { + type Error = PyBorrowError; + #[inline] + fn try_from(value: BoundRef<'a, 'py, T>) -> Result { + value.0.try_borrow() + } +} + +impl<'a, 'py, T: PyClass> TryFrom> for PyRefMut<'py, T> { + type Error = PyBorrowMutError; + #[inline] + fn try_from(value: BoundRef<'a, 'py, T>) -> Result { + value.0.try_borrow_mut() + } +} + +impl<'a, 'py, T> From> for Bound<'py, T> { + #[inline] + fn from(bound: BoundRef<'a, 'py, T>) -> Self { + bound.0.clone() + } +} + +impl<'a, 'py, T> From> for &'a Bound<'py, T> { + #[inline] + fn from(bound: BoundRef<'a, 'py, T>) -> Self { + bound.0 + } +} + +impl From> for Py { + #[inline] + fn from(bound: BoundRef<'_, '_, T>) -> Self { + bound.0.clone().unbind() + } +} + +impl<'py, T> std::ops::Deref for BoundRef<'_, 'py, T> { + type Target = Bound<'py, T>; + #[inline] + fn deref(&self) -> &Self::Target { + self.0 + } +} + +pub unsafe fn tp_new_impl( + py: Python<'_>, + initializer: PyClassInitializer, + target_type: *mut ffi::PyTypeObject, +) -> PyResult<*mut ffi::PyObject> { + initializer + .create_class_object_of_type(py, target_type) + .map(Bound::into_ptr) +} diff --git a/src/impl_/pymodule.rs b/src/impl_/pymodule.rs index 7c5243fcf1c..5f04d888a50 100644 --- a/src/impl_/pymodule.rs +++ b/src/impl_/pymodule.rs @@ -1,13 +1,30 @@ //! Implementation details of `#[pymodule]` which need to be accessible from proc-macro generated code. -use std::cell::UnsafeCell; +use std::{cell::UnsafeCell, marker::PhantomData}; -#[cfg(all(not(PyPy), Py_3_9, not(all(windows, Py_LIMITED_API, not(Py_3_10)))))] +#[cfg(all( + not(any(PyPy, GraalPy)), + Py_3_9, + not(all(windows, Py_LIMITED_API, not(Py_3_10))), + not(target_has_atomic = "64"), +))] use portable_atomic::{AtomicI64, Ordering}; +#[cfg(all( + not(any(PyPy, GraalPy)), + Py_3_9, + not(all(windows, Py_LIMITED_API, not(Py_3_10))), + target_has_atomic = "64", +))] +use std::sync::atomic::{AtomicI64, Ordering}; -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] use crate::exceptions::PyImportError; -use crate::{ffi, sync::GILOnceCell, types::PyModule, Py, PyResult, Python}; +use crate::{ + ffi, + sync::GILOnceCell, + types::{PyCFunction, PyModule, PyModuleMethods}, + Bound, Py, PyClass, PyMethodDef, PyResult, PyTypeInfo, Python, +}; /// `Sync` wrapper of `ffi::PyModuleDef`. pub struct ModuleDef { @@ -15,14 +32,18 @@ pub struct ModuleDef { ffi_def: UnsafeCell, initializer: ModuleInitializer, /// Interpreter ID where module was initialized (not applicable on PyPy). - #[cfg(all(not(PyPy), Py_3_9, not(all(windows, Py_LIMITED_API, not(Py_3_10)))))] + #[cfg(all( + not(any(PyPy, GraalPy)), + Py_3_9, + not(all(windows, Py_LIMITED_API, not(Py_3_10))) + ))] interpreter: AtomicI64, /// Initialized module object, cached to avoid reinitialization. module: GILOnceCell>, } /// Wrapper to enable initializer to be used in const fns. -pub struct ModuleInitializer(pub for<'py> fn(Python<'py>, &PyModule) -> PyResult<()>); +pub struct ModuleInitializer(pub for<'py> fn(&Bound<'py, PyModule>) -> PyResult<()>); unsafe impl Sync for ModuleDef {} @@ -58,7 +79,11 @@ impl ModuleDef { ffi_def, initializer, // -1 is never expected to be a valid interpreter ID - #[cfg(all(not(PyPy), Py_3_9, not(all(windows, Py_LIMITED_API, not(Py_3_10)))))] + #[cfg(all( + not(any(PyPy, GraalPy)), + Py_3_9, + not(all(windows, Py_LIMITED_API, not(Py_3_10))) + ))] interpreter: AtomicI64::new(-1), module: GILOnceCell::new(), } @@ -67,13 +92,14 @@ impl ModuleDef { pub fn make_module(&'static self, py: Python<'_>) -> PyResult> { #[cfg(all(PyPy, not(Py_3_8)))] { + use crate::types::any::PyAnyMethods; const PYPY_GOOD_VERSION: [u8; 3] = [7, 3, 8]; let version = py - .import("sys")? + .import_bound("sys")? .getattr("implementation")? .getattr("version")?; - if version.lt(crate::types::PyTuple::new(py, PYPY_GOOD_VERSION))? { - let warn = py.import("warnings")?.getattr("warn")?; + if version.lt(crate::types::PyTuple::new_bound(py, PYPY_GOOD_VERSION))? { + let warn = py.import_bound("warnings")?.getattr("warn")?; warn.call1(( "PyPy 3.7 versions older than 7.3.8 are known to have binary \ compatibility issues which may cause segfaults. Please upgrade.", @@ -84,7 +110,7 @@ impl ModuleDef { // that static data is not reused across interpreters. // // PyPy does not have subinterpreters, so no need to check interpreter ID. - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] { // PyInterpreterState_Get is only available on 3.9 and later, but is missing // from python3.dll for Windows stable API on 3.9 @@ -125,18 +151,77 @@ impl ModuleDef { ffi::PyModule_Create(self.ffi_def.get()), )? }; - (self.initializer.0)(py, module.as_ref(py))?; + self.initializer.0(module.bind(py))?; Ok(module) }) .map(|py_module| py_module.clone_ref(py)) } } +/// Trait to add an element (class, function...) to a module. +/// +/// Currently only implemented for classes. +pub trait PyAddToModule { + fn add_to_module(&'static self, module: &Bound<'_, PyModule>) -> PyResult<()>; +} + +/// For adding native types (non-pyclass) to a module. +pub struct AddTypeToModule(PhantomData); + +impl AddTypeToModule { + #[allow(clippy::new_without_default)] + pub const fn new() -> Self { + AddTypeToModule(PhantomData) + } +} + +impl PyAddToModule for AddTypeToModule { + fn add_to_module(&'static self, module: &Bound<'_, PyModule>) -> PyResult<()> { + module.add(T::NAME, T::type_object_bound(module.py())) + } +} + +/// For adding a class to a module. +pub struct AddClassToModule(PhantomData); + +impl AddClassToModule { + #[allow(clippy::new_without_default)] + pub const fn new() -> Self { + AddClassToModule(PhantomData) + } +} + +impl PyAddToModule for AddClassToModule { + fn add_to_module(&'static self, module: &Bound<'_, PyModule>) -> PyResult<()> { + module.add_class::() + } +} + +/// For adding a function to a module. +impl PyAddToModule for PyMethodDef { + fn add_to_module(&'static self, module: &Bound<'_, PyModule>) -> PyResult<()> { + module.add_function(PyCFunction::internal_new(module.py(), self, Some(module))?) + } +} + +/// For adding a module to a module. +impl PyAddToModule for ModuleDef { + fn add_to_module(&'static self, module: &Bound<'_, PyModule>) -> PyResult<()> { + module.add_submodule(self.make_module(module.py())?.bind(module.py())) + } +} + #[cfg(test)] mod tests { - use std::sync::atomic::{AtomicBool, Ordering}; + use std::{ + borrow::Cow, + sync::atomic::{AtomicBool, Ordering}, + }; - use crate::{types::PyModule, PyResult, Python}; + use crate::{ + types::{any::PyAnyMethods, module::PyModuleMethods, PyModule}, + Bound, PyResult, Python, + }; use super::{ModuleDef, ModuleInitializer}; @@ -146,19 +231,19 @@ mod tests { ModuleDef::new( "test_module\0", "some doc\0", - ModuleInitializer(|_, m| { + ModuleInitializer(|m| { m.add("SOME_CONSTANT", 42)?; Ok(()) }), ) }; Python::with_gil(|py| { - let module = MODULE_DEF.make_module(py).unwrap().into_ref(py); + let module = MODULE_DEF.make_module(py).unwrap().into_bound(py); assert_eq!( module .getattr("__name__") .unwrap() - .extract::<&str>() + .extract::>() .unwrap(), "test_module", ); @@ -166,7 +251,7 @@ mod tests { module .getattr("__doc__") .unwrap() - .extract::<&str>() + .extract::>() .unwrap(), "some doc", ); @@ -191,7 +276,7 @@ mod tests { static INIT_CALLED: AtomicBool = AtomicBool::new(false); #[allow(clippy::unnecessary_wraps)] - fn init(_: Python<'_>, _: &PyModule) -> PyResult<()> { + fn init(_: &Bound<'_, PyModule>) -> PyResult<()> { INIT_CALLED.store(true, Ordering::SeqCst); Ok(()) } @@ -202,7 +287,7 @@ mod tests { assert_eq!((*module_def.ffi_def.get()).m_doc, DOC.as_ptr() as _); Python::with_gil(|py| { - module_def.initializer.0(py, py.import("builtins").unwrap()).unwrap(); + module_def.initializer.0(&py.import_bound("builtins").unwrap()).unwrap(); assert!(INIT_CALLED.load(Ordering::SeqCst)); }) } diff --git a/src/impl_/trampoline.rs b/src/impl_/trampoline.rs index 5ae75e0fd2e..f485258e5e5 100644 --- a/src/impl_/trampoline.rs +++ b/src/impl_/trampoline.rs @@ -9,9 +9,10 @@ use std::{ panic::{self, UnwindSafe}, }; +use crate::gil::GILGuard; use crate::{ - callback::PyCallbackOutput, ffi, impl_::panic::PanicTrap, methods::IPowModulo, - panic::PanicException, types::PyModule, GILPool, Py, PyResult, Python, + callback::PyCallbackOutput, ffi, ffi_ptr_ext::FfiPtrExt, impl_::panic::PanicTrap, + methods::IPowModulo, panic::PanicException, types::PyModule, Py, PyResult, Python, }; #[inline] @@ -22,12 +23,14 @@ pub unsafe fn module_init( } #[inline] +#[allow(clippy::used_underscore_binding)] pub unsafe fn noargs( slf: *mut ffi::PyObject, - args: *mut ffi::PyObject, + _args: *mut ffi::PyObject, f: for<'py> unsafe fn(Python<'py>, *mut ffi::PyObject) -> PyResult<*mut ffi::PyObject>, ) -> *mut ffi::PyObject { - debug_assert!(args.is_null()); + #[cfg(not(GraalPy))] // this is not specified and GraalPy does not pass null here + debug_assert!(_args.is_null()); trampoline(|py| f(py, slf)) } @@ -167,15 +170,19 @@ trampoline!( /// /// Panics during execution are trapped so that they don't propagate through any /// outer FFI boundary. +/// +/// The GIL must already be held when this is called. #[inline] -pub(crate) fn trampoline(body: F) -> R +pub(crate) unsafe fn trampoline(body: F) -> R where F: for<'py> FnOnce(Python<'py>) -> PyResult + UnwindSafe, R: PyCallbackOutput, { let trap = PanicTrap::new("uncaught panic at ffi boundary"); - let pool = unsafe { GILPool::new() }; - let py = pool.python(); + + // SAFETY: This function requires the GIL to already be held. + let guard = GILGuard::assume(); + let py = guard.python(); let out = panic_result_into_callback_output( py, panic::catch_unwind(move || -> PyResult<_> { body(py) }), @@ -212,19 +219,23 @@ where /// /// # Safety /// -/// ctx must be either a valid ffi::PyObject or NULL +/// - ctx must be either a valid ffi::PyObject or NULL +/// - The GIL must already be held when this is called. #[inline] unsafe fn trampoline_unraisable(body: F, ctx: *mut ffi::PyObject) where F: for<'py> FnOnce(Python<'py>) -> PyResult<()> + UnwindSafe, { let trap = PanicTrap::new("uncaught panic at ffi boundary"); - let pool = GILPool::new(); - let py = pool.python(); + + // SAFETY: The GIL is already held. + let guard = GILGuard::assume(); + let py = guard.python(); + if let Err(py_err) = panic::catch_unwind(move || body(py)) .unwrap_or_else(|payload| Err(PanicException::from_panic_payload(payload))) { - py_err.write_unraisable(py, py.from_borrowed_ptr_or_opt(ctx)); + py_err.write_unraisable_bound(py, ctx.assume_borrowed_or_opt(py).as_deref()); } trap.disarm(); } diff --git a/src/inspect/types.rs b/src/inspect/types.rs index d8f8e6a19ea..13ce62585b1 100644 --- a/src/inspect/types.rs +++ b/src/inspect/types.rs @@ -464,7 +464,10 @@ mod conversion { assert_display(&String::type_input(), "str"); assert_display(&<&[u8]>::type_output(), "bytes"); - assert_display(&<&[u8]>::type_input(), "bytes"); + assert_display( + &<&[u8] as crate::conversion::FromPyObjectBound>::type_input(), + "bytes", + ); } #[test] diff --git a/src/instance.rs b/src/instance.rs index ee5a0f7ec10..bb1a677b069 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -1,16 +1,18 @@ -use crate::err::{self, PyDowncastError, PyErr, PyResult}; -use crate::pycell::{PyBorrowError, PyBorrowMutError, PyCell}; +use crate::err::{self, PyErr, PyResult}; +use crate::impl_::pycell::PyClassObject; +use crate::pycell::{PyBorrowError, PyBorrowMutError}; use crate::pyclass::boolean_struct::{False, True}; +#[cfg(feature = "gil-refs")] use crate::type_object::HasPyGilRef; -use crate::types::any::PyAnyMethods; -use crate::types::{PyDict, PyString, PyTuple}; +use crate::types::{any::PyAnyMethods, string::PyStringMethods, typeobject::PyTypeMethods}; +use crate::types::{DerefToPyAny, PyDict, PyString, PyTuple}; use crate::{ - ffi, AsPyPointer, FromPyObject, IntoPy, PyAny, PyClass, PyClassInitializer, PyRef, PyRefMut, - PyTypeInfo, Python, ToPyObject, + ffi, AsPyPointer, DowncastError, FromPyObject, IntoPy, PyAny, PyClass, PyClassInitializer, + PyRef, PyRefMut, PyTypeInfo, Python, ToPyObject, }; use crate::{gil, PyTypeCheck}; use std::marker::PhantomData; -use std::mem::{self, ManuallyDrop}; +use std::mem::ManuallyDrop; use std::ops::Deref; use std::ptr::NonNull; @@ -23,10 +25,29 @@ use std::ptr::NonNull; /// # Safety /// /// This trait must only be implemented for types which cannot be accessed without the GIL. +#[cfg(feature = "gil-refs")] pub unsafe trait PyNativeType: Sized { /// The form of this which is stored inside a `Py` smart pointer. type AsRefSource: HasPyGilRef; + /// Cast `&self` to a `Borrowed` smart pointer. + /// + /// `Borrowed` implements `Deref>`, so can also be used in locations + /// where `Bound` is expected. + /// + /// This is available as a migration tool to adjust code from the deprecated "GIL Refs" + /// API to the `Bound` smart pointer API. + #[inline] + fn as_borrowed(&self) -> Borrowed<'_, '_, Self::AsRefSource> { + // Safety: &'py Self is expected to be a Python pointer, + // so has the same layout as Borrowed<'py, 'py, T> + Borrowed( + unsafe { NonNull::new_unchecked(self as *const Self as *mut _) }, + PhantomData, + self.py(), + ) + } + /// Returns a GIL marker constrained to the lifetime of this type. #[inline] fn py(&self) -> Python<'_> { @@ -47,33 +68,287 @@ pub unsafe trait PyNativeType: Sized { #[repr(transparent)] pub struct Bound<'py, T>(Python<'py>, ManuallyDrop>); +impl<'py, T> Bound<'py, T> +where + T: PyClass, +{ + /// Creates a new instance `Bound` of a `#[pyclass]` on the Python heap. + /// + /// # Examples + /// + /// ```rust + /// use pyo3::prelude::*; + /// + /// #[pyclass] + /// struct Foo {/* fields omitted */} + /// + /// # fn main() -> PyResult<()> { + /// let foo: Py = Python::with_gil(|py| -> PyResult<_> { + /// let foo: Bound<'_, Foo> = Bound::new(py, Foo {})?; + /// Ok(foo.into()) + /// })?; + /// # Python::with_gil(move |_py| drop(foo)); + /// # Ok(()) + /// # } + /// ``` + pub fn new( + py: Python<'py>, + value: impl Into>, + ) -> PyResult> { + value.into().create_class_object(py) + } +} + impl<'py> Bound<'py, PyAny> { - /// Constructs a new Bound from a pointer. Panics if ptr is null. - pub(crate) unsafe fn from_owned_ptr(py: Python<'py>, ptr: *mut ffi::PyObject) -> Self { + /// Constructs a new `Bound<'py, PyAny>` from a pointer. Panics if `ptr` is null. + /// + /// # Safety + /// + /// - `ptr` must be a valid pointer to a Python object + /// - `ptr` must be an owned Python reference, as the `Bound<'py, PyAny>` will assume ownership + #[inline] + #[track_caller] + pub unsafe fn from_owned_ptr(py: Python<'py>, ptr: *mut ffi::PyObject) -> Self { Self(py, ManuallyDrop::new(Py::from_owned_ptr(py, ptr))) } - // /// Constructs a new Bound from a pointer. Returns None if ptr is null. - // /// - // /// Safety: ptr must be a valid pointer to a Python object, or NULL. - // pub unsafe fn from_owned_ptr_or_opt(py: Python<'py>, ptr: *mut ffi::PyObject) -> Option { - // Py::from_owned_ptr_or_opt(py, ptr).map(|obj| Self(py, ManuallyDrop::new(obj))) - // } + /// Constructs a new `Bound<'py, PyAny>` from a pointer. Returns `None` if `ptr` is null. + /// + /// # Safety + /// + /// - `ptr` must be a valid pointer to a Python object, or null + /// - `ptr` must be an owned Python reference, as the `Bound<'py, PyAny>` will assume ownership + #[inline] + pub unsafe fn from_owned_ptr_or_opt(py: Python<'py>, ptr: *mut ffi::PyObject) -> Option { + Py::from_owned_ptr_or_opt(py, ptr).map(|obj| Self(py, ManuallyDrop::new(obj))) + } - /// Constructs a new Bound from a pointer. Returns error if ptr is null. - pub(crate) unsafe fn from_owned_ptr_or_err( + /// Constructs a new `Bound<'py, PyAny>` from a pointer. Returns an `Err` by calling `PyErr::fetch` + /// if `ptr` is null. + /// + /// # Safety + /// + /// - `ptr` must be a valid pointer to a Python object, or null + /// - `ptr` must be an owned Python reference, as the `Bound<'py, PyAny>` will assume ownership + #[inline] + pub unsafe fn from_owned_ptr_or_err( py: Python<'py>, ptr: *mut ffi::PyObject, ) -> PyResult { Py::from_owned_ptr_or_err(py, ptr).map(|obj| Self(py, ManuallyDrop::new(obj))) } + + /// Constructs a new `Bound<'py, PyAny>` from a pointer by creating a new Python reference. + /// Panics if `ptr` is null. + /// + /// # Safety + /// + /// - `ptr` must be a valid pointer to a Python object + #[inline] + #[track_caller] + pub unsafe fn from_borrowed_ptr(py: Python<'py>, ptr: *mut ffi::PyObject) -> Self { + Self(py, ManuallyDrop::new(Py::from_borrowed_ptr(py, ptr))) + } + + /// Constructs a new `Bound<'py, PyAny>` from a pointer by creating a new Python reference. + /// Returns `None` if `ptr` is null. + /// + /// # Safety + /// + /// - `ptr` must be a valid pointer to a Python object, or null + #[inline] + pub unsafe fn from_borrowed_ptr_or_opt( + py: Python<'py>, + ptr: *mut ffi::PyObject, + ) -> Option { + Py::from_borrowed_ptr_or_opt(py, ptr).map(|obj| Self(py, ManuallyDrop::new(obj))) + } + + /// Constructs a new `Bound<'py, PyAny>` from a pointer by creating a new Python reference. + /// Returns an `Err` by calling `PyErr::fetch` if `ptr` is null. + /// + /// # Safety + /// + /// - `ptr` must be a valid pointer to a Python object, or null + #[inline] + pub unsafe fn from_borrowed_ptr_or_err( + py: Python<'py>, + ptr: *mut ffi::PyObject, + ) -> PyResult { + Py::from_borrowed_ptr_or_err(py, ptr).map(|obj| Self(py, ManuallyDrop::new(obj))) + } + + /// This slightly strange method is used to obtain `&Bound` from a pointer in macro code + /// where we need to constrain the lifetime `'a` safely. + /// + /// Note that `'py` is required to outlive `'a` implicitly by the nature of the fact that + /// `&'a Bound<'py>` means that `Bound<'py>` exists for at least the lifetime `'a`. + /// + /// # Safety + /// - `ptr` must be a valid pointer to a Python object for the lifetime `'a`. The `ptr` can + /// be either a borrowed reference or an owned reference, it does not matter, as this is + /// just `&Bound` there will never be any ownership transfer. + #[inline] + pub(crate) unsafe fn ref_from_ptr<'a>( + _py: Python<'py>, + ptr: &'a *mut ffi::PyObject, + ) -> &'a Self { + &*(ptr as *const *mut ffi::PyObject).cast::>() + } + + /// Variant of the above which returns `None` for null pointers. + /// + /// # Safety + /// - `ptr` must be a valid pointer to a Python object for the lifetime `'a, or null. + #[inline] + pub(crate) unsafe fn ref_from_ptr_or_opt<'a>( + _py: Python<'py>, + ptr: &'a *mut ffi::PyObject, + ) -> &'a Option { + &*(ptr as *const *mut ffi::PyObject).cast::>>() + } } -impl<'py, T> Bound<'py, T> { - /// Helper to cast to Bound<'py, PyAny> - pub(crate) fn as_any(&self) -> &Bound<'py, PyAny> { - // Safety: all Bound have the same memory layout, and all Bound are valid Bound - unsafe { std::mem::transmute(self) } +impl<'py, T> Bound<'py, T> +where + T: PyClass, +{ + /// Immutably borrows the value `T`. + /// + /// This borrow lasts while the returned [`PyRef`] exists. + /// Multiple immutable borrows can be taken out at the same time. + /// + /// For frozen classes, the simpler [`get`][Self::get] is available. + /// + /// # Examples + /// + /// ```rust + /// # use pyo3::prelude::*; + /// # + /// #[pyclass] + /// struct Foo { + /// inner: u8, + /// } + /// + /// # fn main() -> PyResult<()> { + /// Python::with_gil(|py| -> PyResult<()> { + /// let foo: Bound<'_, Foo> = Bound::new(py, Foo { inner: 73 })?; + /// let inner: &u8 = &foo.borrow().inner; + /// + /// assert_eq!(*inner, 73); + /// Ok(()) + /// })?; + /// # Ok(()) + /// # } + /// ``` + /// + /// # Panics + /// + /// Panics if the value is currently mutably borrowed. For a non-panicking variant, use + /// [`try_borrow`](#method.try_borrow). + #[inline] + #[track_caller] + pub fn borrow(&self) -> PyRef<'py, T> { + PyRef::borrow(self) + } + + /// Mutably borrows the value `T`. + /// + /// This borrow lasts while the returned [`PyRefMut`] exists. + /// + /// # Examples + /// + /// ``` + /// # use pyo3::prelude::*; + /// # + /// #[pyclass] + /// struct Foo { + /// inner: u8, + /// } + /// + /// # fn main() -> PyResult<()> { + /// Python::with_gil(|py| -> PyResult<()> { + /// let foo: Bound<'_, Foo> = Bound::new(py, Foo { inner: 73 })?; + /// foo.borrow_mut().inner = 35; + /// + /// assert_eq!(foo.borrow().inner, 35); + /// Ok(()) + /// })?; + /// # Ok(()) + /// # } + /// ``` + /// + /// # Panics + /// Panics if the value is currently borrowed. For a non-panicking variant, use + /// [`try_borrow_mut`](#method.try_borrow_mut). + #[inline] + #[track_caller] + pub fn borrow_mut(&self) -> PyRefMut<'py, T> + where + T: PyClass, + { + PyRefMut::borrow(self) + } + + /// Attempts to immutably borrow the value `T`, returning an error if the value is currently mutably borrowed. + /// + /// The borrow lasts while the returned [`PyRef`] exists. + /// + /// This is the non-panicking variant of [`borrow`](#method.borrow). + /// + /// For frozen classes, the simpler [`get`][Self::get] is available. + #[inline] + pub fn try_borrow(&self) -> Result, PyBorrowError> { + PyRef::try_borrow(self) + } + + /// Attempts to mutably borrow the value `T`, returning an error if the value is currently borrowed. + /// + /// The borrow lasts while the returned [`PyRefMut`] exists. + /// + /// This is the non-panicking variant of [`borrow_mut`](#method.borrow_mut). + #[inline] + pub fn try_borrow_mut(&self) -> Result, PyBorrowMutError> + where + T: PyClass, + { + PyRefMut::try_borrow(self) + } + + /// Provide an immutable borrow of the value `T` without acquiring the GIL. + /// + /// This is available if the class is [`frozen`][macro@crate::pyclass] and [`Sync`]. + /// + /// # Examples + /// + /// ``` + /// use std::sync::atomic::{AtomicUsize, Ordering}; + /// # use pyo3::prelude::*; + /// + /// #[pyclass(frozen)] + /// struct FrozenCounter { + /// value: AtomicUsize, + /// } + /// + /// Python::with_gil(|py| { + /// let counter = FrozenCounter { value: AtomicUsize::new(0) }; + /// + /// let py_counter = Bound::new(py, counter).unwrap(); + /// + /// py_counter.get().value.fetch_add(1, Ordering::Relaxed); + /// }); + /// ``` + #[inline] + pub fn get(&self) -> &T + where + T: PyClass + Sync, + { + self.1.get() + } + + #[inline] + pub(crate) fn get_class_object(&self) -> &PyClassObject { + self.1.get_class_object() } } @@ -97,10 +372,8 @@ fn python_format( f: &mut std::fmt::Formatter<'_>, ) -> Result<(), std::fmt::Error> { match format_result { - Result::Ok(s) => return f.write_str(&s.as_gil_ref().to_string_lossy()), - Result::Err(err) => { - err.write_unraisable(any.py(), std::option::Option::Some(any.as_gil_ref())) - } + Result::Ok(s) => return f.write_str(&s.to_string_lossy()), + Result::Err(err) => err.write_unraisable_bound(any.py(), Some(any)), } match any.get_type().name() { @@ -109,9 +382,12 @@ fn python_format( } } +// The trait bound is needed to avoid running into the auto-deref recursion +// limit (error[E0055]), because `Bound` would deref into itself. See: +// https://github.com/rust-lang/rust/issues/19509 impl<'py, T> Deref for Bound<'py, T> where - T: AsRef, + T: DerefToPyAny, { type Target = Bound<'py, PyAny>; @@ -121,29 +397,30 @@ where } } -impl<'py, T> AsRef> for Bound<'py, T> -where - T: AsRef, -{ +impl<'py, T> AsRef> for Bound<'py, T> { + #[inline] fn as_ref(&self) -> &Bound<'py, PyAny> { self.as_any() } } impl Clone for Bound<'_, T> { + #[inline] fn clone(&self) -> Self { Self(self.0, ManuallyDrop::new(self.1.clone_ref(self.0))) } } impl Drop for Bound<'_, T> { + #[inline] fn drop(&mut self) { - unsafe { ffi::Py_DECREF(self.1.as_ptr()) } + unsafe { ffi::Py_DECREF(self.as_ptr()) } } } impl<'py, T> Bound<'py, T> { /// Returns the GIL token associated with this object. + #[inline] pub fn py(&self) -> Python<'py> { self.0 } @@ -169,49 +446,85 @@ impl<'py, T> Bound<'py, T> { /// of the pointer or decrease the reference count (e.g. with [`pyo3::ffi::Py_DecRef`](crate::ffi::Py_DecRef)). #[inline] pub fn into_ptr(self) -> *mut ffi::PyObject { - self.into_non_null().as_ptr() + ManuallyDrop::new(self).as_ptr() } - /// Internal helper to convert e.g. &'a &'py PyDict to &'a Bound<'py, PyDict> for - /// backwards-compatibility during migration to removal of pool. - #[doc(hidden)] // public and doc(hidden) to use in examples and tests for now - pub fn borrowed_from_gil_ref<'a, U>(gil_ref: &'a &'py U) -> &'a Self - where - U: PyNativeType, - { - // Safety: &'py T::AsRefTarget is expected to be a Python pointer, - // so &'a &'py T::AsRefTarget has the same layout as &'a Bound<'py, T> - unsafe { std::mem::transmute(gil_ref) } + /// Helper to cast to `Bound<'py, PyAny>`. + #[inline] + pub fn as_any(&self) -> &Bound<'py, PyAny> { + // Safety: all Bound have the same memory layout, and all Bound are valid + // Bound, so pointer casting is valid. + unsafe { &*(self as *const Self).cast::>() } + } + + /// Helper to cast to `Bound<'py, PyAny>`, transferring ownership. + #[inline] + pub fn into_any(self) -> Bound<'py, PyAny> { + // Safety: all Bound are valid Bound + Bound(self.0, ManuallyDrop::new(self.unbind().into_any())) + } + + /// Casts this `Bound` to a `Borrowed` smart pointer. + #[inline] + pub fn as_borrowed<'a>(&'a self) -> Borrowed<'a, 'py, T> { + Borrowed( + unsafe { NonNull::new_unchecked(self.as_ptr()) }, + PhantomData, + self.py(), + ) + } + + /// Removes the connection for this `Bound` from the GIL, allowing + /// it to cross thread boundaries. + #[inline] + pub fn unbind(self) -> Py { + // Safety: the type T is known to be correct and the ownership of the + // pointer is transferred to the new Py instance. + let non_null = (ManuallyDrop::new(self).1).0; + unsafe { Py::from_non_null(non_null) } } - /// Internal helper to get to pool references for backwards compatibility - #[doc(hidden)] // public and doc(hidden) to use in examples and tests for now + /// Removes the connection for this `Bound` from the GIL, allowing + /// it to cross thread boundaries, without transferring ownership. + #[inline] + pub fn as_unbound(&self) -> &Py { + &self.1 + } + + /// Casts this `Bound` as the corresponding "GIL Ref" type. + /// + /// This is a helper to be used for migration from the deprecated "GIL Refs" API. + #[inline] + #[cfg(feature = "gil-refs")] pub fn as_gil_ref(&'py self) -> &'py T::AsRefTarget where T: HasPyGilRef, { - unsafe { self.py().from_borrowed_ptr(self.as_ptr()) } + #[allow(deprecated)] + unsafe { + self.py().from_borrowed_ptr(self.as_ptr()) + } } - /// Internal helper to get to pool references for backwards compatibility - #[doc(hidden)] // public but hidden, to use for tests for now + /// Casts this `Bound` as the corresponding "GIL Ref" type, registering the pointer on the + /// [release pool](Python::from_owned_ptr). + /// + /// This is a helper to be used for migration from the deprecated "GIL Refs" API. + #[inline] + #[cfg(feature = "gil-refs")] pub fn into_gil_ref(self) -> &'py T::AsRefTarget where T: HasPyGilRef, { - unsafe { self.py().from_owned_ptr(self.into_ptr()) } - } - - // Internal helper to convert `self` into a `NonNull` which owns the - // Python reference. - pub(crate) fn into_non_null(self) -> NonNull { - // wrap in ManuallyDrop to avoid running Drop for self and decreasing - // the reference count - ManuallyDrop::new(self).1 .0 + #[allow(deprecated)] + unsafe { + self.py().from_owned_ptr(self.into_ptr()) + } } } unsafe impl AsPyPointer for Bound<'_, T> { + #[inline] fn as_ptr(&self) -> *mut ffi::PyObject { self.1.as_ptr() } @@ -227,48 +540,90 @@ unsafe impl AsPyPointer for Bound<'_, T> { pub struct Borrowed<'a, 'py, T>(NonNull, PhantomData<&'a Py>, Python<'py>); impl<'py, T> Borrowed<'_, 'py, T> { - /// Creates a new owned `Bound` from this borrowed reference by increasing the reference count. - pub(crate) fn to_owned(self) -> Bound<'py, T> { - unsafe { ffi::Py_INCREF(self.as_ptr()) }; - Bound( - self.py(), - ManuallyDrop::new(unsafe { Py::from_non_null(self.0) }), - ) + /// Creates a new owned [`Bound`] from this borrowed reference by + /// increasing the reference count. + /// + /// # Example + /// ``` + /// use pyo3::{prelude::*, types::PyTuple}; + /// + /// # fn main() -> PyResult<()> { + /// Python::with_gil(|py| -> PyResult<()> { + /// let tuple = PyTuple::new_bound(py, [1, 2, 3]); + /// + /// // borrows from `tuple`, so can only be + /// // used while `tuple` stays alive + /// let borrowed = tuple.get_borrowed_item(0)?; + /// + /// // creates a new owned reference, which + /// // can be used indendently of `tuple` + /// let bound = borrowed.to_owned(); + /// drop(tuple); + /// + /// assert_eq!(bound.extract::().unwrap(), 1); + /// Ok(()) + /// }) + /// # } + pub fn to_owned(self) -> Bound<'py, T> { + (*self).clone() } } impl<'a, 'py> Borrowed<'a, 'py, PyAny> { + /// Constructs a new `Borrowed<'a, 'py, PyAny>` from a pointer. Panics if `ptr` is null. + /// + /// Prefer to use [`Bound::from_borrowed_ptr`], as that avoids the major safety risk + /// of needing to precisely define the lifetime `'a` for which the borrow is valid. + /// /// # Safety - /// This is similar to `std::slice::from_raw_parts`, the lifetime `'a` is completely defined by - /// the caller and it's the caller's responsibility to ensure that the reference this is - /// derived from is valid for the lifetime `'a`. - pub(crate) unsafe fn from_ptr_or_err( - py: Python<'py>, - ptr: *mut ffi::PyObject, - ) -> PyResult { - NonNull::new(ptr).map_or_else( - || Err(PyErr::fetch(py)), - |ptr| Ok(Self(ptr, PhantomData, py)), + /// + /// - `ptr` must be a valid pointer to a Python object + /// - similar to `std::slice::from_raw_parts`, the lifetime `'a` is completely defined by + /// the caller and it is the caller's responsibility to ensure that the reference this is + /// derived from is valid for the lifetime `'a`. + #[inline] + #[track_caller] + pub unsafe fn from_ptr(py: Python<'py>, ptr: *mut ffi::PyObject) -> Self { + Self( + NonNull::new(ptr).unwrap_or_else(|| crate::err::panic_after_error(py)), + PhantomData, + py, ) } + /// Constructs a new `Borrowed<'a, 'py, PyAny>` from a pointer. Returns `None` if `ptr` is null. + /// + /// Prefer to use [`Bound::from_borrowed_ptr_or_opt`], as that avoids the major safety risk + /// of needing to precisely define the lifetime `'a` for which the borrow is valid. + /// /// # Safety - /// This is similar to `std::slice::from_raw_parts`, the lifetime `'a` is completely defined by - /// the caller and it's the caller's responsibility to ensure that the reference this is - /// derived from is valid for the lifetime `'a`. - pub(crate) unsafe fn from_ptr_or_opt(py: Python<'py>, ptr: *mut ffi::PyObject) -> Option { + /// + /// - `ptr` must be a valid pointer to a Python object, or null + /// - similar to `std::slice::from_raw_parts`, the lifetime `'a` is completely defined by + /// the caller and it is the caller's responsibility to ensure that the reference this is + /// derived from is valid for the lifetime `'a`. + #[inline] + pub unsafe fn from_ptr_or_opt(py: Python<'py>, ptr: *mut ffi::PyObject) -> Option { NonNull::new(ptr).map(|ptr| Self(ptr, PhantomData, py)) } + /// Constructs a new `Borrowed<'a, 'py, PyAny>` from a pointer. Returns an `Err` by calling `PyErr::fetch` + /// if `ptr` is null. + /// + /// Prefer to use [`Bound::from_borrowed_ptr_or_err`], as that avoids the major safety risk + /// of needing to precisely define the lifetime `'a` for which the borrow is valid. + /// /// # Safety - /// This is similar to `std::slice::from_raw_parts`, the lifetime `'a` is completely defined by - /// the caller and it's the caller's responsibility to ensure that the reference this is - /// derived from is valid for the lifetime `'a`. - pub(crate) unsafe fn from_ptr(py: Python<'py>, ptr: *mut ffi::PyObject) -> Self { - Self( - NonNull::new(ptr).unwrap_or_else(|| crate::err::panic_after_error(py)), - PhantomData, - py, + /// + /// - `ptr` must be a valid pointer to a Python object, or null + /// - similar to `std::slice::from_raw_parts`, the lifetime `'a` is completely defined by + /// the caller and it is the caller's responsibility to ensure that the reference this is + /// derived from is valid for the lifetime `'a`. + #[inline] + pub unsafe fn from_ptr_or_err(py: Python<'py>, ptr: *mut ffi::PyObject) -> PyResult { + NonNull::new(ptr).map_or_else( + || Err(PyErr::fetch(py)), + |ptr| Ok(Self(ptr, PhantomData, py)), ) } @@ -276,36 +631,55 @@ impl<'a, 'py> Borrowed<'a, 'py, PyAny> { /// This is similar to `std::slice::from_raw_parts`, the lifetime `'a` is completely defined by /// the caller and it's the caller's responsibility to ensure that the reference this is /// derived from is valid for the lifetime `'a`. + #[inline] pub(crate) unsafe fn from_ptr_unchecked(py: Python<'py>, ptr: *mut ffi::PyObject) -> Self { Self(NonNull::new_unchecked(ptr), PhantomData, py) } + + #[inline] + #[cfg(not(feature = "gil-refs"))] + pub(crate) fn downcast(self) -> Result, DowncastError<'a, 'py>> + where + T: PyTypeCheck, + { + if T::type_check(&self) { + // Safety: type_check is responsible for ensuring that the type is correct + Ok(unsafe { self.downcast_unchecked() }) + } else { + Err(DowncastError::new_from_borrowed(self, T::NAME)) + } + } + + /// Converts this `PyAny` to a concrete Python type without checking validity. + /// + /// # Safety + /// Callers must ensure that the type is valid or risk type confusion. + #[inline] + pub(crate) unsafe fn downcast_unchecked(self) -> Borrowed<'a, 'py, T> { + Borrowed(self.0, PhantomData, self.2) + } } impl<'a, 'py, T> From<&'a Bound<'py, T>> for Borrowed<'a, 'py, T> { /// Create borrow on a Bound + #[inline] fn from(instance: &'a Bound<'py, T>) -> Self { - Self( - unsafe { NonNull::new_unchecked(instance.as_ptr()) }, - PhantomData, - instance.py(), - ) + instance.as_borrowed() } } +#[cfg(feature = "gil-refs")] impl<'py, T> Borrowed<'py, 'py, T> where T: HasPyGilRef, { - pub(crate) fn from_gil_ref(gil_ref: &'py T::AsRefTarget) -> Self { - // Safety: &'py T::AsRefTarget is expected to be a Python pointer, - // so &'py T::AsRefTarget has the same layout as Self. - unsafe { std::mem::transmute(gil_ref) } + pub(crate) fn into_gil_ref(self) -> &'py T::AsRefTarget { + // Safety: self is a borrow over `'py`. + #[allow(deprecated)] + unsafe { + self.py().from_borrowed_ptr(self.0.as_ptr()) + } } - - // pub(crate) fn into_gil_ref(self) -> &'py T::AsRefTarget { - // // Safety: self is a borrow over `'py`. - // unsafe { self.py().from_borrowed_ptr(self.0.as_ptr()) } - // } } impl std::fmt::Debug for Borrowed<'_, '_, T> { @@ -325,6 +699,7 @@ impl<'py, T> Deref for Borrowed<'_, 'py, T> { } impl Clone for Borrowed<'_, '_, T> { + #[inline] fn clone(&self) -> Self { *self } @@ -332,22 +707,43 @@ impl Clone for Borrowed<'_, '_, T> { impl Copy for Borrowed<'_, '_, T> {} +impl ToPyObject for Borrowed<'_, '_, T> { + /// Converts `Py` instance -> PyObject. + #[inline] + fn to_object(&self, py: Python<'_>) -> PyObject { + (*self).into_py(py) + } +} + +impl IntoPy for Borrowed<'_, '_, T> { + /// Converts `Py` instance -> PyObject. + #[inline] + fn into_py(self, py: Python<'_>) -> PyObject { + self.to_owned().into_py(py) + } +} + /// A GIL-independent reference to an object allocated on the Python heap. /// /// This type does not auto-dereference to the inner object because you must prove you hold the GIL to access it. /// Instead, call one of its methods to access the inner object: -/// - [`Py::as_ref`], to borrow a GIL-bound reference to the contained object. +/// - [`Py::bind`] or [`Py::into_bound`], to borrow a GIL-bound reference to the contained object. /// - [`Py::borrow`], [`Py::try_borrow`], [`Py::borrow_mut`], or [`Py::try_borrow_mut`], /// to get a (mutable) reference to a contained pyclass, using a scheme similar to std's [`RefCell`]. -/// See the [`PyCell` guide entry](https://pyo3.rs/latest/class.html#pycell-and-interior-mutability) +/// See the [guide entry](https://pyo3.rs/latest/class.html#bound-and-interior-mutability) /// for more information. -/// - You can call methods directly on `Py` with [`Py::call`], [`Py::call_method`] and friends. +/// - You can call methods directly on `Py` with [`Py::call_bound`], [`Py::call_method_bound`] and friends. /// These require passing in the [`Python<'py>`](crate::Python) token but are otherwise similar to the corresponding /// methods on [`PyAny`]. /// -/// # Example: Storing Python objects in structs +/// # Example: Storing Python objects in `#[pyclass]` structs +/// +/// Usually `Bound<'py, T>` is recommended for interacting with Python objects as its lifetime `'py` +/// is an association to the GIL and that enables many operations to be done as efficiently as possible. +/// +/// However, `#[pyclass]` structs cannot carry a lifetime, so `Py` is the only way to store +/// a Python object in a `#[pyclass]` struct. /// -/// As all the native Python objects only appear as references, storing them in structs doesn't work well. /// For example, this won't compile: /// /// ```compile_fail @@ -356,7 +752,7 @@ impl Copy for Borrowed<'_, '_, T> {} /// # /// #[pyclass] /// struct Foo<'py> { -/// inner: &'py PyDict, +/// inner: Bound<'py, PyDict>, /// } /// /// impl Foo { @@ -364,9 +760,9 @@ impl Copy for Borrowed<'_, '_, T> {} /// let foo = Python::with_gil(|py| { /// // `py` will only last for this scope. /// -/// // `&PyDict` derives its lifetime from `py` and +/// // `Bound<'py, PyDict>` inherits the GIL lifetime from `py` and /// // so won't be able to outlive this closure. -/// let dict: &PyDict = PyDict::new(py); +/// let dict: Bound<'_, PyDict> = PyDict::new_bound(py); /// /// // because `Foo` contains `dict` its lifetime /// // is now also tied to `py`. @@ -395,7 +791,7 @@ impl Copy for Borrowed<'_, '_, T> {} /// #[new] /// fn __new__() -> Foo { /// Python::with_gil(|py| { -/// let dict: Py = PyDict::new(py).into(); +/// let dict: Py = PyDict::new_bound(py).unbind(); /// Foo { inner: dict } /// }) /// } @@ -403,12 +799,12 @@ impl Copy for Borrowed<'_, '_, T> {} /// # /// # fn main() -> PyResult<()> { /// # Python::with_gil(|py| { -/// # let m = pyo3::types::PyModule::new(py, "test")?; +/// # let m = pyo3::types::PyModule::new_bound(py, "test")?; /// # m.add_class::()?; /// # -/// # let foo: &PyCell = m.getattr("Foo")?.call0()?.downcast()?; +/// # let foo: Bound<'_, Foo> = m.getattr("Foo")?.call0()?.downcast_into()?; /// # let dict = &foo.borrow().inner; -/// # let dict: &PyDict = dict.as_ref(py); +/// # let dict: &Bound<'_, PyDict> = dict.bind(py); /// # /// # Ok(()) /// # }) @@ -440,10 +836,10 @@ impl Copy for Borrowed<'_, '_, T> {} /// # /// # fn main() -> PyResult<()> { /// # Python::with_gil(|py| { -/// # let m = pyo3::types::PyModule::new(py, "test")?; +/// # let m = pyo3::types::PyModule::new_bound(py, "test")?; /// # m.add_class::()?; /// # -/// # let foo: &PyCell = m.getattr("Foo")?.call0()?.downcast()?; +/// # let foo: Bound<'_, Foo> = m.getattr("Foo")?.call0()?.downcast_into()?; /// # let bar = &foo.borrow().inner; /// # let bar: &Bar = &*bar.borrow(py); /// # @@ -467,12 +863,14 @@ impl Copy for Borrowed<'_, '_, T> {} /// /// # fn main() { /// Python::with_gil(|py| { -/// let first: Py = PyDict::new(py).into(); +/// let first: Py = PyDict::new_bound(py).unbind(); /// /// // All of these are valid syntax /// let second = Py::clone_ref(&first, py); /// let third = first.clone_ref(py); +/// #[cfg(feature = "py-clone")] /// let fourth = Py::clone(&first); +/// #[cfg(feature = "py-clone")] /// let fifth = first.clone(); /// /// // Disposing of our original `Py` just decrements the reference count. @@ -480,7 +878,9 @@ impl Copy for Borrowed<'_, '_, T> {} /// /// // They all point to the same object /// assert!(second.is(&third)); +/// #[cfg(feature = "py-clone")] /// assert!(fourth.is(&fifth)); +/// #[cfg(feature = "py-clone")] /// assert!(second.is(&fourth)); /// }); /// # } @@ -506,10 +906,14 @@ impl Copy for Borrowed<'_, '_, T> {} /// Otherwise, the reference count will be decreased the next time the GIL is /// reacquired. /// +/// If you happen to be already holding the GIL, [`Py::drop_ref`] will decrease +/// the Python reference count immediately and will execute slightly faster than +/// relying on implicit [`Drop`]s. +/// /// # A note on `Send` and `Sync` /// /// Accessing this object is threadsafe, since any access to its API requires a [`Python<'py>`](crate::Python) token. -/// As you can only get this by acquiring the GIL, `Py<...>` "implements [`Send`] and [`Sync`]. +/// As you can only get this by acquiring the GIL, `Py<...>` implements [`Send`] and [`Sync`]. /// /// [`Rc`]: std::rc::Rc /// [`RefCell`]: std::cell::RefCell @@ -536,21 +940,20 @@ where /// struct Foo {/* fields omitted */} /// /// # fn main() -> PyResult<()> { - /// Python::with_gil(|py| -> PyResult> { + /// let foo = Python::with_gil(|py| -> PyResult<_> { /// let foo: Py = Py::new(py, Foo {})?; /// Ok(foo) /// })?; + /// # Python::with_gil(move |_py| drop(foo)); /// # Ok(()) /// # } /// ``` pub fn new(py: Python<'_>, value: impl Into>) -> PyResult> { - let initializer = value.into(); - let obj = initializer.create_cell(py)?; - let ob = unsafe { Py::from_owned_ptr(py, obj as _) }; - Ok(ob) + Bound::new(py, value).map(Bound::unbind) } } +#[cfg(feature = "gil-refs")] impl Py where T: HasPyGilRef, @@ -576,10 +979,10 @@ where /// # use pyo3::types::PyList; /// # /// Python::with_gil(|py| { - /// let list: Py = PyList::empty(py).into(); - /// // FIXME as_ref() no longer makes sense with new Py API, remove this doc - /// // let list: &PyList = list.as_ref(py); - /// // assert_eq!(list.len(), 0); + /// let list: Py = PyList::empty_bound(py).into(); + /// # #[allow(deprecated)] + /// let list: &PyList = list.as_ref(py); + /// assert_eq!(list.len(), 0); /// }); /// ``` /// @@ -593,10 +996,15 @@ where /// /// Python::with_gil(|py| { /// let my_class: Py = Py::new(py, MyClass {}).unwrap(); + /// # #[allow(deprecated)] /// let my_class_cell: &PyCell = my_class.as_ref(py); /// assert!(my_class_cell.try_borrow().is_ok()); /// }); /// ``` + #[deprecated( + since = "0.21.0", + note = "use `obj.bind(py)` instead of `obj.as_ref(py)`" + )] pub fn as_ref<'py>(&'py self, _py: Python<'py>) -> &'py T::AsRefTarget { let any = self.as_ptr() as *const PyAny; unsafe { PyNativeType::unchecked_downcast(&*any) } @@ -641,11 +1049,19 @@ where /// // This reference's lifetime is determined by `py`'s lifetime. /// // Because that originates from outside this function, /// // this return value is allowed. + /// # #[allow(deprecated)] /// obj.into_ref(py) /// } /// ``` + #[deprecated( + since = "0.21.0", + note = "use `obj.into_bound(py)` instead of `obj.into_ref(py)`" + )] pub fn into_ref(self, py: Python<'_>) -> &T::AsRefTarget { - unsafe { py.from_owned_ptr(self.into_ptr()) } + #[allow(deprecated)] + unsafe { + py.from_owned_ptr(self.into_ptr()) + } } } @@ -671,9 +1087,22 @@ impl Py { /// of the pointer or decrease the reference count (e.g. with [`pyo3::ffi::Py_DecRef`](crate::ffi::Py_DecRef)). #[inline] pub fn into_ptr(self) -> *mut ffi::PyObject { - let ptr = self.0.as_ptr(); - std::mem::forget(self); - ptr + ManuallyDrop::new(self).0.as_ptr() + } + + /// Helper to cast to `Py`. + #[inline] + pub fn as_any(&self) -> &Py { + // Safety: all Py have the same memory layout, and all Py are valid + // Py, so pointer casting is valid. + unsafe { &*(self as *const Self).cast::>() } + } + + /// Helper to cast to `Py`, transferring ownership. + #[inline] + pub fn into_any(self) -> Py { + // Safety: all Py are valid Py + unsafe { Py::from_non_null(ManuallyDrop::new(self).0) } } } @@ -688,8 +1117,7 @@ where /// /// For frozen classes, the simpler [`get`][Self::get] is available. /// - /// Equivalent to `self.as_ref(py).borrow()` - - /// see [`PyCell::borrow`](crate::pycell::PyCell::borrow). + /// Equivalent to `self.bind(py).borrow()` - see [`Bound::borrow`]. /// /// # Examples /// @@ -717,16 +1145,17 @@ where /// /// Panics if the value is currently mutably borrowed. For a non-panicking variant, use /// [`try_borrow`](#method.try_borrow). + #[inline] + #[track_caller] pub fn borrow<'py>(&'py self, py: Python<'py>) -> PyRef<'py, T> { - self.as_ref(py).borrow() + self.bind(py).borrow() } /// Mutably borrows the value `T`. /// /// This borrow lasts while the returned [`PyRefMut`] exists. /// - /// Equivalent to `self.as_ref(py).borrow_mut()` - - /// see [`PyCell::borrow_mut`](crate::pycell::PyCell::borrow_mut). + /// Equivalent to `self.bind(py).borrow_mut()` - see [`Bound::borrow_mut`]. /// /// # Examples /// @@ -753,11 +1182,13 @@ where /// # Panics /// Panics if the value is currently borrowed. For a non-panicking variant, use /// [`try_borrow_mut`](#method.try_borrow_mut). + #[inline] + #[track_caller] pub fn borrow_mut<'py>(&'py self, py: Python<'py>) -> PyRefMut<'py, T> where T: PyClass, { - self.as_ref(py).borrow_mut() + self.bind(py).borrow_mut() } /// Attempts to immutably borrow the value `T`, returning an error if the value is currently mutably borrowed. @@ -768,10 +1199,10 @@ where /// /// For frozen classes, the simpler [`get`][Self::get] is available. /// - /// Equivalent to `self.as_ref(py).borrow_mut()` - - /// see [`PyCell::try_borrow`](crate::pycell::PyCell::try_borrow). + /// Equivalent to `self.bind(py).try_borrow()` - see [`Bound::try_borrow`]. + #[inline] pub fn try_borrow<'py>(&'py self, py: Python<'py>) -> Result, PyBorrowError> { - self.as_ref(py).try_borrow() + self.bind(py).try_borrow() } /// Attempts to mutably borrow the value `T`, returning an error if the value is currently borrowed. @@ -780,8 +1211,8 @@ where /// /// This is the non-panicking variant of [`borrow_mut`](#method.borrow_mut). /// - /// Equivalent to `self.as_ref(py).try_borrow_mut()` - - /// see [`PyCell::try_borrow_mut`](crate::pycell::PyCell::try_borrow_mut). + /// Equivalent to `self.bind(py).try_borrow_mut()` - see [`Bound::try_borrow_mut`]. + #[inline] pub fn try_borrow_mut<'py>( &'py self, py: Python<'py>, @@ -789,7 +1220,7 @@ where where T: PyClass, { - self.as_ref(py).try_borrow_mut() + self.bind(py).try_borrow_mut() } /// Provide an immutable borrow of the value `T` without acquiring the GIL. @@ -814,39 +1245,49 @@ where /// }); /// /// cell.get().value.fetch_add(1, Ordering::Relaxed); + /// # Python::with_gil(move |_py| drop(cell)); /// ``` + #[inline] pub fn get(&self) -> &T where T: PyClass + Sync, { - let any = self.as_ptr() as *const PyAny; - // SAFETY: The class itself is frozen and `Sync` and we do not access anything but `cell.contents.value`. - unsafe { - let cell: &PyCell = PyNativeType::unchecked_downcast(&*any); - &*cell.get_ptr() - } + // Safety: The class itself is frozen and `Sync` + unsafe { &*self.get_class_object().get_ptr() } + } + + /// Get a view on the underlying `PyClass` contents. + #[inline] + pub(crate) fn get_class_object(&self) -> &PyClassObject { + let class_object = self.as_ptr().cast::>(); + // Safety: Bound is known to contain an object which is laid out in memory as a + // PyClassObject. + unsafe { &*class_object } } } impl Py { /// Attaches this `Py` to the given Python context, allowing access to further Python APIs. + #[inline] pub fn bind<'py>(&self, _py: Python<'py>) -> &Bound<'py, T> { // Safety: `Bound` has the same layout as `Py` unsafe { &*(self as *const Py).cast() } } /// Same as `bind` but takes ownership of `self`. + #[inline] pub fn into_bound(self, py: Python<'_>) -> Bound<'_, T> { Bound(py, ManuallyDrop::new(self)) } /// Same as `bind` but produces a `Borrowed` instead of a `Bound`. + #[inline] pub fn bind_borrowed<'a, 'py>(&'a self, py: Python<'py>) -> Borrowed<'a, 'py, T> { Borrowed(self.0, PhantomData, py) } /// Returns whether `self` and `other` point to the same object. To compare - /// the equality of two objects (the `==` operator), use [`eq`](PyAny::eq). + /// the equality of two objects (the `==` operator), use [`eq`](PyAnyMethods::eq). /// /// This is equivalent to the Python expression `self is other`. #[inline] @@ -874,7 +1315,7 @@ impl Py { /// /// # fn main() { /// Python::with_gil(|py| { - /// let first: Py = PyDict::new(py).into(); + /// let first: Py = PyDict::new_bound(py).unbind(); /// let second = Py::clone_ref(&first, py); /// /// // Both point to the same object @@ -887,6 +1328,35 @@ impl Py { unsafe { Py::from_borrowed_ptr(py, self.0.as_ptr()) } } + /// Drops `self` and immediately decreases its reference count. + /// + /// This method is a micro-optimisation over [`Drop`] if you happen to be holding the GIL + /// already. + /// + /// Note that if you are using [`Bound`], you do not need to use [`Self::drop_ref`] since + /// [`Bound`] guarantees that the GIL is held. + /// + /// # Examples + /// + /// ```rust + /// use pyo3::prelude::*; + /// use pyo3::types::PyDict; + /// + /// # fn main() { + /// Python::with_gil(|py| { + /// let object: Py = PyDict::new_bound(py).unbind(); + /// + /// // some usage of object + /// + /// object.drop_ref(py); + /// }); + /// # } + /// ``` + #[inline] + pub fn drop_ref(self, py: Python<'_>) { + let _ = self.into_bound(py); + } + /// Returns whether the object is considered to be None. /// /// This is equivalent to the Python expression `self is None`. @@ -922,11 +1392,14 @@ impl Py { /// Extracts some type from the Python object. /// /// This is a wrapper function around `FromPyObject::extract()`. - pub fn extract<'p, D>(&'p self, py: Python<'p>) -> PyResult + pub fn extract<'a, 'py, D>(&'a self, py: Python<'py>) -> PyResult where - D: FromPyObject<'p>, + D: crate::conversion::FromPyObjectBound<'a, 'py>, + // TODO it might be possible to relax this bound in future, to allow + // e.g. `.extract::<&str>(py)` where `py` is short-lived. + 'py: 'a, { - FromPyObject::extract(unsafe { py.from_borrowed_ptr(self.as_ptr()) }) + self.bind(py).as_any().extract() } /// Retrieves an attribute value. @@ -940,7 +1413,7 @@ impl Py { /// # Example: `intern!`ing the attribute name /// /// ``` - /// # use pyo3::{intern, pyfunction, types::PyModule, IntoPy, Py, Python, PyObject, PyResult}; + /// # use pyo3::{prelude::*, intern}; /// # /// #[pyfunction] /// fn version(sys: Py, py: Python<'_>) -> PyResult { @@ -948,7 +1421,7 @@ impl Py { /// } /// # /// # Python::with_gil(|py| { - /// # let sys = py.import("sys").unwrap().into_py(py); + /// # let sys = py.import_bound("sys").unwrap().unbind(); /// # version(sys, py).unwrap(); /// # }); /// ``` @@ -956,7 +1429,7 @@ impl Py { where N: IntoPy>, { - self.bind(py).as_any().getattr(attr_name).map(Into::into) + self.bind(py).as_any().getattr(attr_name).map(Bound::unbind) } /// Sets an attribute value. @@ -977,7 +1450,7 @@ impl Py { /// } /// # /// # Python::with_gil(|py| { - /// # let ob = PyModule::new(py, "empty").unwrap().into_py(py); + /// # let ob = PyModule::new_bound(py, "empty").unwrap().into_py(py); /// # set_answer(ob, py).unwrap(); /// # }); /// ``` @@ -991,30 +1464,65 @@ impl Py { .setattr(attr_name, value.into_py(py).into_bound(py)) } + /// Deprecated form of [`call_bound`][Py::call_bound]. + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`call` will be replaced by `call_bound` in a future PyO3 version" + )] + #[inline] + pub fn call(&self, py: Python<'_>, args: A, kwargs: Option<&PyDict>) -> PyResult + where + A: IntoPy>, + { + self.call_bound(py, args, kwargs.map(PyDict::as_borrowed).as_deref()) + } + /// Calls the object. /// /// This is equivalent to the Python expression `self(*args, **kwargs)`. - pub fn call( + pub fn call_bound( &self, py: Python<'_>, args: impl IntoPy>, - kwargs: Option<&PyDict>, + kwargs: Option<&Bound<'_, PyDict>>, ) -> PyResult { - self.bind(py).as_any().call(args, kwargs).map(Into::into) + self.bind(py).as_any().call(args, kwargs).map(Bound::unbind) } /// Calls the object with only positional arguments. /// /// This is equivalent to the Python expression `self(*args)`. pub fn call1(&self, py: Python<'_>, args: impl IntoPy>) -> PyResult { - self.bind(py).as_any().call1(args).map(Into::into) + self.bind(py).as_any().call1(args).map(Bound::unbind) } /// Calls the object without arguments. /// /// This is equivalent to the Python expression `self()`. pub fn call0(&self, py: Python<'_>) -> PyResult { - self.bind(py).as_any().call0().map(Into::into) + self.bind(py).as_any().call0().map(Bound::unbind) + } + + /// Deprecated form of [`call_method_bound`][Py::call_method_bound]. + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`call_method` will be replaced by `call_method_bound` in a future PyO3 version" + )] + #[inline] + pub fn call_method( + &self, + py: Python<'_>, + name: N, + args: A, + kwargs: Option<&PyDict>, + ) -> PyResult + where + N: IntoPy>, + A: IntoPy>, + { + self.call_method_bound(py, name, args, kwargs.map(PyDict::as_borrowed).as_deref()) } /// Calls a method on the object. @@ -1023,12 +1531,12 @@ impl Py { /// /// To avoid repeated temporary allocations of Python strings, the [`intern!`](crate::intern) /// macro can be used to intern `name`. - pub fn call_method( + pub fn call_method_bound( &self, py: Python<'_>, name: N, args: A, - kwargs: Option<&PyDict>, + kwargs: Option<&Bound<'_, PyDict>>, ) -> PyResult where N: IntoPy>, @@ -1037,7 +1545,7 @@ impl Py { self.bind(py) .as_any() .call_method(name, args, kwargs) - .map(Into::into) + .map(Bound::unbind) } /// Calls a method on the object with only positional arguments. @@ -1054,7 +1562,7 @@ impl Py { self.bind(py) .as_any() .call_method1(name, args) - .map(Into::into) + .map(Bound::unbind) } /// Calls a method on the object with no arguments. @@ -1067,7 +1575,7 @@ impl Py { where N: IntoPy>, { - self.bind(py).as_any().call_method0(name).map(Into::into) + self.bind(py).as_any().call_method0(name).map(Bound::unbind) } /// Create a `Py` instance by taking ownership of the given FFI pointer. @@ -1081,6 +1589,7 @@ impl Py { /// # Panics /// Panics if `ptr` is null. #[inline] + #[track_caller] pub unsafe fn from_owned_ptr(py: Python<'_>, ptr: *mut ffi::PyObject) -> Py { match NonNull::new(ptr) { Some(nonnull_ptr) => Py(nonnull_ptr, PhantomData), @@ -1124,6 +1633,7 @@ impl Py { /// # Panics /// Panics if `ptr` is null. #[inline] + #[track_caller] pub unsafe fn from_borrowed_ptr(py: Python<'_>, ptr: *mut ffi::PyObject) -> Py { match Self::from_borrowed_ptr_or_opt(py, ptr) { Some(slf) => slf, @@ -1166,24 +1676,16 @@ impl Py { /// /// # Safety /// `ptr` must point to a Python object of type T. - #[inline] - pub(crate) unsafe fn from_non_null(ptr: NonNull) -> Self { + unsafe fn from_non_null(ptr: NonNull) -> Self { Self(ptr, PhantomData) } - - /// Returns the inner pointer without decreasing the refcount. - #[inline] - fn into_non_null(self) -> NonNull { - let pointer = self.0; - mem::forget(self); - pointer - } } impl ToPyObject for Py { /// Converts `Py` instance -> PyObject. + #[inline] fn to_object(&self, py: Python<'_>) -> PyObject { - unsafe { PyObject::from_borrowed_ptr(py, self.as_ptr()) } + self.clone_ref(py).into_any() } } @@ -1192,7 +1694,7 @@ impl IntoPy for Py { /// Consumes `self` without calling `Py_DECREF()`. #[inline] fn into_py(self, _py: Python<'_>) -> PyObject { - unsafe { PyObject::from_non_null(self.into_non_null()) } + self.into_any() } } @@ -1204,27 +1706,26 @@ impl IntoPy for &'_ Py { } impl ToPyObject for Bound<'_, T> { - /// Converts `Py` instance -> PyObject. + /// Converts `&Bound` instance -> PyObject, increasing the reference count. + #[inline] fn to_object(&self, py: Python<'_>) -> PyObject { - unsafe { PyObject::from_borrowed_ptr(py, self.as_ptr()) } + self.clone().into_py(py) } } impl IntoPy for Bound<'_, T> { - /// Converts a `Py` instance to `PyObject`. - /// Consumes `self` without calling `Py_DECREF()`. + /// Converts a `Bound` instance to `PyObject`. #[inline] fn into_py(self, _py: Python<'_>) -> PyObject { - unsafe { PyObject::from_non_null(self.into_non_null()) } + self.into_any().unbind() } } impl IntoPy for &Bound<'_, T> { - /// Converts a `Py` instance to `PyObject`. - /// Consumes `self` without calling `Py_DECREF()`. + /// Converts `&Bound` instance -> PyObject, increasing the reference count. #[inline] - fn into_py(self, _py: Python<'_>) -> PyObject { - unsafe { PyObject::from_non_null(self.clone().into_non_null()) } + fn into_py(self, py: Python<'_>) -> PyObject { + self.to_object(py) } } @@ -1236,18 +1737,14 @@ unsafe impl crate::AsPyPointer for Py { } } -impl std::convert::From<&'_ PyAny> for PyObject { - fn from(obj: &PyAny) -> Self { - unsafe { Py::from_borrowed_ptr(obj.py(), obj.as_ptr()) } - } -} - +#[cfg(feature = "gil-refs")] impl std::convert::From<&'_ T> for PyObject where - T: PyNativeType + AsRef, + T: PyNativeType, { + #[inline] fn from(obj: &T) -> Self { - unsafe { Py::from_borrowed_ptr(obj.py(), obj.as_ref().as_ptr()) } + obj.as_borrowed().to_owned().into_any().unbind() } } @@ -1257,7 +1754,7 @@ where { #[inline] fn from(other: Py) -> Self { - unsafe { Self::from_non_null(other.into_non_null()) } + other.into_any() } } @@ -1275,17 +1772,19 @@ where impl std::convert::From> for Py { #[inline] fn from(other: Bound<'_, T>) -> Self { - unsafe { Self::from_non_null(other.into_non_null()) } + other.unbind() } } // `&PyCell` can be converted to `Py` -impl std::convert::From<&PyCell> for Py +#[cfg(feature = "gil-refs")] +#[allow(deprecated)] +impl std::convert::From<&crate::PyCell> for Py where T: PyClass, { - fn from(cell: &PyCell) -> Self { - unsafe { Py::from_borrowed_ptr(cell.py(), cell.as_ptr()) } + fn from(cell: &crate::PyCell) -> Self { + cell.as_borrowed().to_owned().unbind() } } @@ -1308,9 +1807,12 @@ where } /// If the GIL is held this increments `self`'s reference count. -/// Otherwise this registers the [`Py`]`` instance to have its reference count -/// incremented the next time PyO3 acquires the GIL. +/// Otherwise, it will panic. +/// +/// Only available if the `py-clone` feature is enabled. +#[cfg(feature = "py-clone")] impl Clone for Py { + #[track_caller] fn clone(&self) -> Self { unsafe { gil::register_incref(self.0); @@ -1319,8 +1821,16 @@ impl Clone for Py { } } -/// Dropping a `Py` instance decrements the reference count on the object by 1. +/// Dropping a `Py` instance decrements the reference count +/// on the object by one if the GIL is held. +/// +/// Otherwise and by default, this registers the underlying pointer to have its reference count +/// decremented the next time PyO3 acquires the GIL. +/// +/// However, if the `pyo3_disable_reference_pool` conditional compilation flag +/// is enabled, it will abort the process. impl Drop for Py { + #[track_caller] fn drop(&mut self) { unsafe { gil::register_decref(self.0); @@ -1328,31 +1838,23 @@ impl Drop for Py { } } -impl<'a, T> FromPyObject<'a> for Py +impl FromPyObject<'_> for Py where - T: PyTypeInfo, - &'a T::AsRefTarget: FromPyObject<'a>, - T::AsRefTarget: 'a + AsPyPointer, + T: PyTypeCheck, { /// Extracts `Self` from the source `PyObject`. - fn extract(ob: &'a PyAny) -> PyResult { - unsafe { - ob.extract::<&T::AsRefTarget>() - .map(|val| Py::from_borrowed_ptr(ob.py(), val.as_ptr())) - } + fn extract_bound(ob: &Bound<'_, PyAny>) -> PyResult { + ob.extract::>().map(Bound::unbind) } } -impl<'a, T> FromPyObject<'a> for Bound<'a, T> +impl<'py, T> FromPyObject<'py> for Bound<'py, T> where - T: PyTypeInfo, + T: PyTypeCheck, { /// Extracts `Self` from the source `PyObject`. - fn extract(ob: &'a PyAny) -> PyResult { - Bound::borrowed_from_gil_ref(&ob) - .downcast() - .map(Clone::clone) - .map_err(Into::into) + fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult { + ob.downcast().cloned().map_err(Into::into) } } @@ -1360,6 +1862,7 @@ where /// /// However for GIL lifetime reasons, cause() cannot be implemented for `Py`. /// Use .as_ref() to get the GIL-scoped error if you need to inspect the cause. +#[cfg(feature = "gil-refs")] impl std::error::Error for Py where T: std::error::Error + PyTypeInfo, @@ -1370,10 +1873,9 @@ where impl std::fmt::Display for Py where T: PyTypeInfo, - T::AsRefTarget: std::fmt::Display, { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - Python::with_gil(|py| std::fmt::Display::fmt(self.as_ref(py), f)) + Python::with_gil(|py| std::fmt::Display::fmt(self.bind(py), f)) } } @@ -1392,6 +1894,24 @@ impl std::fmt::Debug for Py { pub type PyObject = Py; impl PyObject { + /// Deprecated form of [`PyObject::downcast_bound`] + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`PyObject::downcast` will be replaced by `PyObject::downcast_bound` in a future PyO3 version" + )] + #[inline] + pub fn downcast<'py, T>( + &'py self, + py: Python<'py>, + ) -> Result<&'py T, crate::err::PyDowncastError<'py>> + where + T: PyTypeCheck, + { + self.downcast_bound::(py) + .map(Bound::as_gil_ref) + .map_err(crate::err::PyDowncastError::from_downcast_err) + } /// Downcast this `PyObject` to a concrete Python type or pyclass. /// /// Note that you can often avoid downcasting yourself by just specifying @@ -1407,10 +1927,10 @@ impl PyObject { /// use pyo3::types::{PyDict, PyList}; /// /// Python::with_gil(|py| { - /// let any: PyObject = PyDict::new(py).into(); + /// let any: PyObject = PyDict::new_bound(py).into(); /// - /// assert!(any.downcast::(py).is_ok()); - /// assert!(any.downcast::(py).is_err()); + /// assert!(any.downcast_bound::(py).is_ok()); + /// assert!(any.downcast_bound::(py).is_err()); /// }); /// ``` /// @@ -1431,9 +1951,9 @@ impl PyObject { /// Python::with_gil(|py| { /// let class: PyObject = Py::new(py, Class { i: 0 }).unwrap().into_py(py); /// - /// let class_cell: &PyCell = class.downcast(py)?; + /// let class_bound = class.downcast_bound::(py)?; /// - /// class_cell.borrow_mut().i += 1; + /// class_bound.borrow_mut().i += 1; /// /// // Alternatively you can get a `PyRefMut` directly /// let class_ref: PyRefMut<'_, Class> = class.extract(py)?; @@ -1443,45 +1963,72 @@ impl PyObject { /// # } /// ``` #[inline] - pub fn downcast<'py, T>(&'py self, py: Python<'py>) -> Result<&'py T, PyDowncastError<'py>> + pub fn downcast_bound<'py, T>( + &self, + py: Python<'py>, + ) -> Result<&Bound<'py, T>, DowncastError<'_, 'py>> where - T: PyTypeCheck, + T: PyTypeCheck, { - self.as_ref(py).downcast() + self.bind(py).downcast() } - /// Casts the PyObject to a concrete Python object type without checking validity. + /// Deprecated form of [`PyObject::downcast_bound_unchecked`] /// /// # Safety /// /// Callers must ensure that the type is valid or risk type confusion. + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`PyObject::downcast_unchecked` will be replaced by `PyObject::downcast_bound_unchecked` in a future PyO3 version" + )] #[inline] - pub unsafe fn downcast_unchecked<'p, T>(&'p self, py: Python<'p>) -> &T + pub unsafe fn downcast_unchecked<'py, T>(&'py self, py: Python<'py>) -> &T where T: HasPyGilRef, { - self.as_ref(py).downcast_unchecked() + self.downcast_bound_unchecked::(py).as_gil_ref() + } + + /// Casts the PyObject to a concrete Python object type without checking validity. + /// + /// # Safety + /// + /// Callers must ensure that the type is valid or risk type confusion. + #[inline] + pub unsafe fn downcast_bound_unchecked<'py, T>(&self, py: Python<'py>) -> &Bound<'py, T> { + self.bind(py).downcast_unchecked() } } #[cfg(test)] mod tests { use super::{Bound, Py, PyObject}; - use crate::types::{PyDict, PyString}; - use crate::{PyAny, PyResult, Python, ToPyObject}; + use crate::types::any::PyAnyMethods; + use crate::types::{dict::IntoPyDict, PyDict, PyString}; + use crate::types::{PyCapsule, PyStringMethods}; + use crate::{ffi, Borrowed, PyAny, PyResult, Python, ToPyObject}; #[test] - fn test_call0() { + fn test_call() { Python::with_gil(|py| { - let obj = py.get_type::().to_object(py); - assert_eq!( - obj.call0(py) - .unwrap() - .as_ref(py) - .repr() + let obj = py.get_type_bound::().to_object(py); + + let assert_repr = |obj: &Bound<'_, PyAny>, expected: &str| { + assert_eq!(obj.repr().unwrap().to_cow().unwrap(), expected); + }; + + assert_repr(obj.call0(py).unwrap().bind(py), "{}"); + assert_repr(obj.call1(py, ()).unwrap().bind(py), "{}"); + assert_repr(obj.call_bound(py, (), None).unwrap().bind(py), "{}"); + + assert_repr(obj.call1(py, ((('x', 1),),)).unwrap().bind(py), "{'x': 1}"); + assert_repr( + obj.call_bound(py, (), Some(&[('x', 1)].into_py_dict_bound(py))) .unwrap() - .to_string_lossy(), - "{}" + .bind(py), + "{'x': 1}", ); }) } @@ -1489,10 +2036,10 @@ mod tests { #[test] fn test_call_for_non_existing_method() { Python::with_gil(|py| { - let obj: PyObject = PyDict::new(py).into(); + let obj: PyObject = PyDict::new_bound(py).into(); assert!(obj.call_method0(py, "asdf").is_err()); assert!(obj - .call_method(py, "nonexistent_method", (1,), None) + .call_method_bound(py, "nonexistent_method", (1,), None) .is_err()); assert!(obj.call_method0(py, "nonexistent_method").is_err()); assert!(obj.call_method1(py, "nonexistent_method", (1,)).is_err()); @@ -1502,17 +2049,19 @@ mod tests { #[test] fn py_from_dict() { let dict: Py = Python::with_gil(|py| { - let native = PyDict::new(py); + let native = PyDict::new_bound(py); Py::from(native) }); - assert_eq!(Python::with_gil(|py| dict.get_refcnt(py)), 1); + Python::with_gil(move |py| { + assert_eq!(dict.get_refcnt(py), 1); + }); } #[test] fn pyobject_from_py() { Python::with_gil(|py| { - let dict: Py = PyDict::new(py).into(); + let dict: Py = PyDict::new_bound(py).unbind(); let cnt = dict.get_refcnt(py); let p: PyObject = dict.into(); assert_eq!(p.get_refcnt(py), cnt); @@ -1529,7 +2078,7 @@ class A: pass a = A() "#; - let module = PyModule::from_code(py, CODE, "", "")?; + let module = PyModule::from_code_bound(py, CODE, "", "")?; let instance: Py = module.getattr("a")?.into(); instance.getattr(py, "foo").unwrap_err(); @@ -1538,8 +2087,8 @@ a = A() assert!(instance .getattr(py, "foo")? - .as_ref(py) - .eq(PyString::new(py, "bar"))?); + .bind(py) + .eq(PyString::new_bound(py, "bar"))?); instance.getattr(py, "foo")?; Ok(()) @@ -1556,7 +2105,7 @@ class A: pass a = A() "#; - let module = PyModule::from_code(py, CODE, "", "")?; + let module = PyModule::from_code_bound(py, CODE, "", "")?; let instance: Py = module.getattr("a")?.into(); let foo = crate::intern!(py, "foo"); @@ -1564,7 +2113,7 @@ a = A() instance.getattr(py, foo).unwrap_err(); instance.setattr(py, foo, bar)?; - assert!(instance.getattr(py, foo)?.as_ref(py).eq(bar)?); + assert!(instance.getattr(py, foo)?.bind(py).eq(bar)?); Ok(()) }) } @@ -1572,7 +2121,7 @@ a = A() #[test] fn invalid_attr() -> PyResult<()> { Python::with_gil(|py| { - let instance: Py = py.eval("object()", None, None)?.into(); + let instance: Py = py.eval_bound("object()", None, None)?.into(); instance.getattr(py, "foo").unwrap_err(); @@ -1585,7 +2134,7 @@ a = A() #[test] fn test_py2_from_py_object() { Python::with_gil(|py| { - let instance: &PyAny = py.eval("object()", None, None).unwrap(); + let instance = py.eval_bound("object()", None, None).unwrap(); let ptr = instance.as_ptr(); let instance: Bound<'_, PyAny> = instance.extract().unwrap(); assert_eq!(instance.as_ptr(), ptr); @@ -1595,10 +2144,13 @@ a = A() #[test] fn test_py2_into_py_object() { Python::with_gil(|py| { - let instance: Bound<'_, PyAny> = - Bound::borrowed_from_gil_ref(&py.eval("object()", None, None).unwrap()).clone(); + let instance = py + .eval_bound("object()", None, None) + .unwrap() + .as_borrowed() + .to_owned(); let ptr = instance.as_ptr(); - let instance: PyObject = instance.clone().into(); + let instance: PyObject = instance.clone().unbind(); assert_eq!(instance.as_ptr(), ptr); }) } @@ -1608,7 +2160,7 @@ a = A() fn test_is_ellipsis() { Python::with_gil(|py| { let v = py - .eval("...", None, None) + .eval_bound("...", None, None) .map_err(|e| e.display(py)) .unwrap() .to_object(py); @@ -1636,18 +2188,139 @@ a = A() }); } + #[test] + fn test_bound_as_any() { + Python::with_gil(|py| { + let obj = PyString::new_bound(py, "hello world"); + let any = obj.as_any(); + assert_eq!(any.as_ptr(), obj.as_ptr()); + }); + } + + #[test] + fn test_bound_into_any() { + Python::with_gil(|py| { + let obj = PyString::new_bound(py, "hello world"); + let any = obj.clone().into_any(); + assert_eq!(any.as_ptr(), obj.as_ptr()); + }); + } + + #[test] + fn test_bound_py_conversions() { + Python::with_gil(|py| { + let obj: Bound<'_, PyString> = PyString::new_bound(py, "hello world"); + let obj_unbound: &Py = obj.as_unbound(); + let _: &Bound<'_, PyString> = obj_unbound.bind(py); + + let obj_unbound: Py = obj.unbind(); + let obj: Bound<'_, PyString> = obj_unbound.into_bound(py); + + assert_eq!(obj.to_cow().unwrap(), "hello world"); + }); + } + + #[test] + fn bound_from_borrowed_ptr_constructors() { + // More detailed tests of the underlying semantics in pycell.rs + Python::with_gil(|py| { + fn check_drop<'py>( + py: Python<'py>, + method: impl FnOnce(*mut ffi::PyObject) -> Bound<'py, PyAny>, + ) { + let mut dropped = false; + let capsule = PyCapsule::new_bound_with_destructor( + py, + (&mut dropped) as *mut _ as usize, + None, + |ptr, _| unsafe { std::ptr::write(ptr as *mut bool, true) }, + ) + .unwrap(); + + let bound = method(capsule.as_ptr()); + assert!(!dropped); + + // creating the bound should have increased the refcount + drop(capsule); + assert!(!dropped); + + // dropping the bound should now also decrease the refcount and free the object + drop(bound); + assert!(dropped); + } + + check_drop(py, |ptr| unsafe { Bound::from_borrowed_ptr(py, ptr) }); + check_drop(py, |ptr| unsafe { + Bound::from_borrowed_ptr_or_opt(py, ptr).unwrap() + }); + check_drop(py, |ptr| unsafe { + Bound::from_borrowed_ptr_or_err(py, ptr).unwrap() + }); + }) + } + + #[test] + fn borrowed_ptr_constructors() { + // More detailed tests of the underlying semantics in pycell.rs + Python::with_gil(|py| { + fn check_drop<'py>( + py: Python<'py>, + method: impl FnOnce(&*mut ffi::PyObject) -> Borrowed<'_, 'py, PyAny>, + ) { + let mut dropped = false; + let capsule = PyCapsule::new_bound_with_destructor( + py, + (&mut dropped) as *mut _ as usize, + None, + |ptr, _| unsafe { std::ptr::write(ptr as *mut bool, true) }, + ) + .unwrap(); + + let ptr = &capsule.as_ptr(); + let _borrowed = method(ptr); + assert!(!dropped); + + // creating the borrow should not have increased the refcount + drop(capsule); + assert!(dropped); + } + + check_drop(py, |&ptr| unsafe { Borrowed::from_ptr(py, ptr) }); + check_drop(py, |&ptr| unsafe { + Borrowed::from_ptr_or_opt(py, ptr).unwrap() + }); + check_drop(py, |&ptr| unsafe { + Borrowed::from_ptr_or_err(py, ptr).unwrap() + }); + }) + } + + #[test] + fn explicit_drop_ref() { + Python::with_gil(|py| { + let object: Py = PyDict::new_bound(py).unbind(); + let object2 = object.clone_ref(py); + + assert_eq!(object.as_ptr(), object2.as_ptr()); + assert_eq!(object.get_refcnt(py), 2); + + object.drop_ref(py); + + assert_eq!(object2.get_refcnt(py), 1); + + object2.drop_ref(py); + }); + } + #[cfg(feature = "macros")] mod using_macros { - use crate::PyCell; - use super::*; - #[crate::pyclass] - #[pyo3(crate = "crate")] + #[crate::pyclass(crate = "crate")] struct SomeClass(i32); #[test] - fn instance_borrow_methods() { + fn py_borrow_methods() { // More detailed tests of the underlying semantics in pycell.rs Python::with_gil(|py| { let instance = Py::new(py, SomeClass(0)).unwrap(); @@ -1666,9 +2339,44 @@ a = A() } #[test] + fn bound_borrow_methods() { + // More detailed tests of the underlying semantics in pycell.rs + Python::with_gil(|py| { + let instance = Bound::new(py, SomeClass(0)).unwrap(); + assert_eq!(instance.borrow().0, 0); + assert_eq!(instance.try_borrow().unwrap().0, 0); + assert_eq!(instance.borrow_mut().0, 0); + assert_eq!(instance.try_borrow_mut().unwrap().0, 0); + + instance.borrow_mut().0 = 123; + + assert_eq!(instance.borrow().0, 123); + assert_eq!(instance.try_borrow().unwrap().0, 123); + assert_eq!(instance.borrow_mut().0, 123); + assert_eq!(instance.try_borrow_mut().unwrap().0, 123); + }) + } + + #[crate::pyclass(frozen, crate = "crate")] + struct FrozenClass(i32); + + #[test] + fn test_frozen_get() { + Python::with_gil(|py| { + for i in 0..10 { + let instance = Py::new(py, FrozenClass(i)).unwrap(); + assert_eq!(instance.get().0, i); + + assert_eq!(instance.bind(py).get().0, i); + } + }) + } + + #[test] + #[cfg(feature = "gil-refs")] #[allow(deprecated)] fn cell_tryfrom() { - use crate::PyTryInto; + use crate::{PyCell, PyTryInto}; // More detailed tests of the underlying semantics in pycell.rs Python::with_gil(|py| { let instance: &PyAny = Py::new(py, SomeClass(0)).unwrap().into_ref(py); diff --git a/src/internal_tricks.rs b/src/internal_tricks.rs index 551ddf5a910..75f23edbbd8 100644 --- a/src/internal_tricks.rs +++ b/src/internal_tricks.rs @@ -46,6 +46,7 @@ pub(crate) fn get_ssize_index(index: usize) -> Py_ssize_t { } /// Implementations used for slice indexing PySequence, PyTuple, and PyList +#[cfg(feature = "gil-refs")] macro_rules! index_impls { ( $ty:ty, @@ -154,6 +155,7 @@ macro_rules! index_impls { #[inline(never)] #[cold] #[track_caller] +#[cfg(feature = "gil-refs")] pub(crate) fn index_len_fail(index: usize, ty_name: &str, len: usize) -> ! { panic!( "index {} out of range for {} of length {}", @@ -164,6 +166,7 @@ pub(crate) fn index_len_fail(index: usize, ty_name: &str, len: usize) -> ! { #[inline(never)] #[cold] #[track_caller] +#[cfg(feature = "gil-refs")] pub(crate) fn slice_start_index_len_fail(index: usize, ty_name: &str, len: usize) -> ! { panic!( "range start index {} out of range for {} of length {}", @@ -174,6 +177,7 @@ pub(crate) fn slice_start_index_len_fail(index: usize, ty_name: &str, len: usize #[inline(never)] #[cold] #[track_caller] +#[cfg(feature = "gil-refs")] pub(crate) fn slice_end_index_len_fail(index: usize, ty_name: &str, len: usize) -> ! { panic!( "range end index {} out of range for {} of length {}", @@ -184,6 +188,7 @@ pub(crate) fn slice_end_index_len_fail(index: usize, ty_name: &str, len: usize) #[inline(never)] #[cold] #[track_caller] +#[cfg(feature = "gil-refs")] pub(crate) fn slice_index_order_fail(index: usize, end: usize) -> ! { panic!("slice index starts at {} but ends at {}", index, end); } diff --git a/src/lib.rs b/src/lib.rs index 4089db45fbc..52875146936 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,7 +9,14 @@ rust_2021_prelude_collisions, warnings ), - allow(unused_variables, unused_assignments, unused_extern_crates) + allow( + unused_variables, + unused_assignments, + unused_extern_crates, + // FIXME https://github.com/rust-lang/rust/issues/121621#issuecomment-1965156376 + unknown_lints, + non_local_definitions, + ) )))] //! Rust bindings to the Python interpreter. @@ -22,28 +29,37 @@ //! //! PyO3 has several core types that you should familiarize yourself with: //! -//! ## The Python<'py> object +//! ## The `Python<'py>` object, and the `'py` lifetime //! -//! Holding the [global interpreter lock] (GIL) is modeled with the [`Python<'py>`](crate::Python) -//! token. All APIs that require that the GIL is held require this token as proof that you really -//! are holding the GIL. It can be explicitly acquired and is also implicitly acquired by PyO3 as -//! it wraps Rust functions and structs into Python functions and objects. +//! Holding the [global interpreter lock] (GIL) is modeled with the [`Python<'py>`](Python) token. Many +//! Python APIs require that the GIL is held, and PyO3 uses this token as proof that these APIs +//! can be called safely. It can be explicitly acquired and is also implicitly acquired by PyO3 +//! as it wraps Rust functions and structs into Python functions and objects. //! -//! ## The GIL-dependent types +//! The [`Python<'py>`](Python) token's lifetime `'py` is common to many PyO3 APIs: +//! - Types that also have the `'py` lifetime, such as the [`Bound<'py, T>`](Bound) smart pointer, are +//! bound to the Python GIL and rely on this to offer their functionality. These types often +//! have a [`.py()`](Bound::py) method to get the associated [`Python<'py>`](Python) token. +//! - Functions which depend on the `'py` lifetime, such as [`PyList::new_bound`](types::PyList::new_bound), +//! require a [`Python<'py>`](Python) token as an input. Sometimes the token is passed implicitly by +//! taking a [`Bound<'py, T>`](Bound) or other type which is bound to the `'py` lifetime. +//! - Traits which depend on the `'py` lifetime, such as [`FromPyObject<'py>`](FromPyObject), usually have +//! inputs or outputs which depend on the lifetime. Adding the lifetime to the trait allows +//! these inputs and outputs to express their binding to the GIL in the Rust type system. //! -//! For example `&`[`PyAny`]. These are only ever seen as references, with a lifetime that is only -//! valid for as long as the GIL is held, which is why using them doesn't require a -//! [`Python<'py>`](crate::Python) token. The underlying Python object, if mutable, can be mutated -//! through any reference. +//! ## Python object smart pointers //! -//! See the [guide][types] for an explanation of the different Python object types. +//! PyO3 has two core smart pointers to refer to Python objects, [`Py`](Py) and its GIL-bound +//! form [`Bound<'py, T>`](Bound) which carries the `'py` lifetime. (There is also +//! [`Borrowed<'a, 'py, T>`](instance::Borrowed), but it is used much more rarely). //! -//! ## The GIL-independent types +//! The type parameter `T` in these smart pointers can be filled by: +//! - [`PyAny`], e.g. `Py` or `Bound<'py, PyAny>`, where the Python object type is not +//! known. `Py` is so common it has a type alias [`PyObject`]. +//! - Concrete Python types like [`PyList`](types::PyList) or [`PyTuple`](types::PyTuple). +//! - Rust types which are exposed to Python using the [`#[pyclass]`](macro@pyclass) macro. //! -//! When wrapped in [`Py`]`<...>`, like with [`Py`]`<`[`PyAny`]`>` or [`Py`]``, Python -//! objects no longer have a limited lifetime which makes them easier to store in structs and pass -//! between functions. However, you cannot do much with them without a -//! [`Python<'py>`](crate::Python) token, for which you’d need to reacquire the GIL. +//! See the [guide][types] for an explanation of the different Python object types. //! //! ## PyErr //! @@ -81,6 +97,7 @@ //! The following features enable interactions with other crates in the Rust ecosystem: //! - [`anyhow`]: Enables a conversion from [anyhow]’s [`Error`][anyhow_error] type to [`PyErr`]. //! - [`chrono`]: Enables a conversion from [chrono]'s structures to the equivalent Python ones. +//! - [`chrono-tz`]: Enables a conversion from [chrono-tz]'s `Tz` enum. Requires Python 3.9+. //! - [`either`]: Enables conversions between Python objects and [either]'s [`Either`] type. //! - [`eyre`]: Enables a conversion from [eyre]’s [`Report`] type to [`PyErr`]. //! - [`hashbrown`]: Enables conversions between Python objects and [hashbrown]'s [`HashMap`] and @@ -90,6 +107,7 @@ //! [`BigUint`] types. //! - [`num-complex`]: Enables conversions between Python objects and [num-complex]'s [`Complex`] //! type. +//! - [`num-rational`]: Enables conversions between Python's fractions.Fraction and [num-rational]'s types //! - [`rust_decimal`]: Enables conversions between Python's decimal.Decimal and [rust_decimal]'s //! [`Decimal`] type. //! - [`serde`]: Allows implementing [serde]'s [`Serialize`] and [`Deserialize`] traits for @@ -110,7 +128,7 @@ //! //! PyO3 supports the following software versions: //! - Python 3.7 and up (CPython and PyPy) -//! - Rust 1.56 and up +//! - Rust 1.63 and up //! //! # Example: Building a native Python module //! @@ -155,7 +173,7 @@ //! //! /// A Python module implemented in Rust. //! #[pymodule] -//! fn string_sum(py: Python<'_>, m: &PyModule) -> PyResult<()> { +//! fn string_sum(m: &Bound<'_, PyModule>) -> PyResult<()> { //! m.add_function(wrap_pyfunction!(sum_as_string, m)?)?; //! //! Ok(()) @@ -212,12 +230,12 @@ //! //! fn main() -> PyResult<()> { //! Python::with_gil(|py| { -//! let sys = py.import("sys")?; +//! let sys = py.import_bound("sys")?; //! let version: String = sys.getattr("version")?.extract()?; //! -//! let locals = [("os", py.import("os")?)].into_py_dict(py); +//! let locals = [("os", py.import_bound("os")?)].into_py_dict_bound(py); //! let code = "os.getenv('USER') or os.getenv('USERNAME') or 'Unknown'"; -//! let user: String = py.eval(code, None, Some(&locals))?.extract()?; +//! let user: String = py.eval_bound(code, None, Some(&locals))?.extract()?; //! //! println!("Hello {}, I'm Python {}", user, version); //! Ok(()) @@ -252,7 +270,9 @@ //! [`Deserialize`]: https://docs.rs/serde/latest/serde/trait.Deserialize.html //! [`Serialize`]: https://docs.rs/serde/latest/serde/trait.Serialize.html //! [chrono]: https://docs.rs/chrono/ "Date and Time for Rust." +//! [chrono-tz]: https://docs.rs/chrono-tz/ "TimeZone implementations for chrono from the IANA database." //! [`chrono`]: ./chrono/index.html "Documentation about the `chrono` feature." +//! [`chrono-tz`]: ./chrono-tz/index.html "Documentation about the `chrono-tz` feature." //! [either]: https://docs.rs/either/ "A type that represents one of two alternatives." //! [`either`]: ./either/index.html "Documentation about the `either` feature." //! [`Either`]: https://docs.rs/either/latest/either/enum.Either.html @@ -264,21 +284,23 @@ //! [`maturin`]: https://github.com/PyO3/maturin "Build and publish crates with pyo3, rust-cpython and cffi bindings as well as rust binaries as python packages" //! [`num-bigint`]: ./num_bigint/index.html "Documentation about the `num-bigint` feature." //! [`num-complex`]: ./num_complex/index.html "Documentation about the `num-complex` feature." +//! [`num-rational`]: ./num_rational/index.html "Documentation about the `num-rational` feature." //! [`pyo3-build-config`]: https://docs.rs/pyo3-build-config //! [rust_decimal]: https://docs.rs/rust_decimal //! [`rust_decimal`]: ./rust_decimal/index.html "Documenation about the `rust_decimal` feature." //! [`Decimal`]: https://docs.rs/rust_decimal/latest/rust_decimal/struct.Decimal.html //! [`serde`]: <./serde/index.html> "Documentation about the `serde` feature." -//! [calling_rust]: https://pyo3.rs/latest/python_from_rust.html "Calling Python from Rust - PyO3 user guide" +//! [calling_rust]: https://pyo3.rs/latest/python-from-rust.html "Calling Python from Rust - PyO3 user guide" //! [examples subdirectory]: https://github.com/PyO3/pyo3/tree/main/examples //! [feature flags]: https://doc.rust-lang.org/cargo/reference/features.html "Features - The Cargo Book" //! [global interpreter lock]: https://docs.python.org/3/glossary.html#term-global-interpreter-lock //! [hashbrown]: https://docs.rs/hashbrown //! [smallvec]: https://docs.rs/smallvec //! [indexmap]: https://docs.rs/indexmap -//! [manual_builds]: https://pyo3.rs/latest/building_and_distribution.html#manual-builds "Manual builds - Building and Distribution - PyO3 user guide" +//! [manual_builds]: https://pyo3.rs/latest/building-and-distribution.html#manual-builds "Manual builds - Building and Distribution - PyO3 user guide" //! [num-bigint]: https://docs.rs/num-bigint //! [num-complex]: https://docs.rs/num-complex +//! [num-rational]: https://docs.rs/num-rational //! [serde]: https://docs.rs/serde //! [setuptools-rust]: https://github.com/PyO3/setuptools-rust "Setuptools plugin for Rust extensions" //! [the guide]: https://pyo3.rs "PyO3 user guide" @@ -288,28 +310,35 @@ //! [Rust from Python]: https://github.com/PyO3/pyo3#using-rust-from-python //! [Features chapter of the guide]: https://pyo3.rs/latest/features.html#features-reference "Features Reference - PyO3 user guide" pub use crate::class::*; -pub use crate::conversion::{AsPyPointer, FromPyObject, FromPyPointer, IntoPy, ToPyObject}; +pub use crate::conversion::{AsPyPointer, FromPyObject, IntoPy, ToPyObject}; +#[cfg(feature = "gil-refs")] +#[allow(deprecated)] +pub use crate::conversion::{FromPyPointer, PyTryFrom, PyTryInto}; +#[cfg(feature = "gil-refs")] +pub use crate::err::PyDowncastError; +pub use crate::err::{DowncastError, DowncastIntoError, PyErr, PyErrArguments, PyResult, ToPyErr}; +#[cfg(feature = "gil-refs")] #[allow(deprecated)] -pub use crate::conversion::{PyTryFrom, PyTryInto}; -pub use crate::err::{ - DowncastError, DowncastIntoError, PyDowncastError, PyErr, PyErrArguments, PyResult, -}; pub use crate::gil::GILPool; -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] pub use crate::gil::{prepare_freethreaded_python, with_embedded_python_interpreter}; -pub use crate::instance::{Py, PyNativeType, PyObject}; +#[cfg(feature = "gil-refs")] +pub use crate::instance::PyNativeType; +pub use crate::instance::{Borrowed, Bound, Py, PyObject}; pub use crate::marker::Python; -pub use crate::pycell::{PyCell, PyRef, PyRefMut}; +#[cfg(feature = "gil-refs")] +#[allow(deprecated)] +pub use crate::pycell::PyCell; +pub use crate::pycell::{PyRef, PyRefMut}; pub use crate::pyclass::PyClass; pub use crate::pyclass_init::PyClassInitializer; pub use crate::type_object::{PyTypeCheck, PyTypeInfo}; pub use crate::types::PyAny; pub use crate::version::PythonVersionInfo; -// Expected to become public API in 0.21 under a different name -pub(crate) use crate::instance::Bound; pub(crate) mod ffi_ptr_ext; pub(crate) mod py_result_ext; +pub(crate) mod sealed; /// Old module which contained some implementation details of the `#[pyproto]` module. /// @@ -319,9 +348,6 @@ pub(crate) mod py_result_ext; /// For compatibility reasons this has not yet been removed, however will be done so /// once is resolved. pub mod class { - #[doc(hidden)] - pub use crate::impl_::pymethods as methods; - pub use self::gc::{PyTraverseError, PyVisit}; #[doc(hidden)] @@ -329,6 +355,16 @@ pub mod class { PyClassAttributeDef, PyGetterDef, PyMethodDef, PyMethodDefType, PyMethodType, PySetterDef, }; + #[doc(hidden)] + pub mod methods { + // frozen with the contents of the `impl_::pymethods` module in 0.20, + // this should probably all be replaced with deprecated type aliases and removed. + pub use crate::impl_::pymethods::{ + IPowModulo, PyClassAttributeDef, PyGetterDef, PyMethodDef, PyMethodDefType, + PyMethodType, PySetterDef, + }; + } + /// Old module which contained some implementation details of the `#[pyproto]` module. /// /// Prefer using the same content from `pyo3::pyclass`, e.g. `use pyo3::pyclass::CompareOp` instead @@ -401,10 +437,11 @@ pub mod buffer; pub mod callback; pub mod conversion; mod conversions; -#[cfg(feature = "macros")] +#[cfg(feature = "experimental-async")] pub mod coroutine; #[macro_use] #[doc(hidden)] +#[cfg(feature = "gil-refs")] pub mod derive_utils; mod err; pub mod exceptions; @@ -419,6 +456,7 @@ pub mod marshal; pub mod sync; pub mod panic; pub mod prelude; +pub mod pybacked; pub mod pycell; pub mod pyclass; pub mod pyclass_init; @@ -435,7 +473,7 @@ pub use pyo3_macros::{pyfunction, pymethods, pymodule, FromPyObject}; /// A proc macro used to expose Rust structs and fieldless enums as Python objects. /// -#[doc = include_str!("../guide/pyclass_parameters.md")] +#[doc = include_str!("../guide/pyclass-parameters.md")] /// /// For more on creating Python classes, /// see the [class section of the guide][1]. @@ -451,6 +489,16 @@ mod macros; #[cfg(feature = "experimental-inspect")] pub mod inspect; +/// Ths module only contains re-exports of pyo3 deprecation warnings and exists +/// purely to make compiler error messages nicer. +/// +/// (The compiler uses this module in error messages, probably because it's a public +/// re-export at a shorter path than `pyo3::impl_::deprecations`.) +#[doc(hidden)] +pub mod deprecations { + pub use crate::impl_::deprecations::*; +} + /// Test readme and user guide #[cfg(doctest)] pub mod doc_test { @@ -467,8 +515,8 @@ pub mod doc_test { "README.md" => readme_md, "guide/src/advanced.md" => guide_advanced_md, "guide/src/async-await.md" => guide_async_await_md, - "guide/src/building_and_distribution.md" => guide_building_and_distribution_md, - "guide/src/building_and_distribution/multiple_python_versions.md" => guide_bnd_multiple_python_versions_md, + "guide/src/building-and-distribution.md" => guide_building_and_distribution_md, + "guide/src/building-and-distribution/multiple-python-versions.md" => guide_bnd_multiple_python_versions_md, "guide/src/class.md" => guide_class_md, "guide/src/class/call.md" => guide_class_call, "guide/src/class/object.md" => guide_class_object, @@ -486,16 +534,19 @@ pub mod doc_test { "guide/src/faq.md" => guide_faq_md, "guide/src/features.md" => guide_features_md, "guide/src/function.md" => guide_function_md, - "guide/src/function/error_handling.md" => guide_function_error_handling_md, + "guide/src/function/error-handling.md" => guide_function_error_handling_md, "guide/src/function/signature.md" => guide_function_signature_md, "guide/src/memory.md" => guide_memory_md, "guide/src/migration.md" => guide_migration_md, "guide/src/module.md" => guide_module_md, "guide/src/parallelism.md" => guide_parallelism_md, "guide/src/performance.md" => guide_performance_md, - "guide/src/python_from_rust.md" => guide_python_from_rust_md, - "guide/src/python_typing_hints.md" => guide_python_typing_hints_md, - "guide/src/trait_bounds.md" => guide_trait_bounds_md, + "guide/src/python-from-rust.md" => guide_python_from_rust_md, + "guide/src/python-from-rust/calling-existing-code.md" => guide_pfr_calling_existing_code_md, + "guide/src/python-from-rust/function-calls.md" => guide_pfr_function_calls_md, + "guide/src/python-typing-hints.md" => guide_python_typing_hints_md, + "guide/src/rust-from-python.md" => guide_rust_from_python_md, + "guide/src/trait-bounds.md" => guide_trait_bounds_md, "guide/src/types.md" => guide_types_md, } } diff --git a/src/macros.rs b/src/macros.rs index 560d43da1ca..6dde89e51a0 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -2,17 +2,17 @@ /// /// # Panics /// -/// This macro internally calls [`Python::run`](crate::Python::run) and panics +/// This macro internally calls [`Python::run_bound`](crate::Python::run_bound) and panics /// if it returns `Err`, after printing the error to stdout. /// -/// If you need to handle failures, please use [`Python::run`](crate::marker::Python::run) instead. +/// If you need to handle failures, please use [`Python::run_bound`](crate::marker::Python::run_bound) instead. /// /// # Examples /// ``` /// use pyo3::{prelude::*, py_run, types::PyList}; /// /// Python::with_gil(|py| { -/// let list = PyList::new(py, &[1, 2, 3]); +/// let list = PyList::new_bound(py, &[1, 2, 3]); /// py_run!(py, list, "assert list == [1, 2, 3]"); /// }); /// ``` @@ -45,7 +45,7 @@ /// } /// /// Python::with_gil(|py| { -/// let time = PyCell::new(py, Time {hour: 8, minute: 43, second: 16}).unwrap(); +/// let time = Py::new(py, Time {hour: 8, minute: 43, second: 16}).unwrap(); /// let time_as_tuple = (8, 43, 16); /// py_run!(py, time time_as_tuple, r#" /// assert time.hour == 8 @@ -73,7 +73,7 @@ /// } /// /// Python::with_gil(|py| { -/// let locals = [("C", py.get_type::())].into_py_dict(py); +/// let locals = [("C", py.get_type_bound::())].into_py_dict_bound(py); /// pyo3::py_run!(py, *locals, "c = C()"); /// }); /// ``` @@ -99,17 +99,20 @@ macro_rules! py_run_impl { ($py:expr, $($val:ident)+, $code:expr) => {{ use $crate::types::IntoPyDict; use $crate::ToPyObject; - let d = [$((stringify!($val), $val.to_object($py)),)+].into_py_dict($py); + let d = [$((stringify!($val), $val.to_object($py)),)+].into_py_dict_bound($py); $crate::py_run_impl!($py, *d, $code) }}; ($py:expr, *$dict:expr, $code:expr) => {{ use ::std::option::Option::*; - if let ::std::result::Result::Err(e) = $py.run($code, None, Some($dict)) { + #[allow(unused_imports)] + #[cfg(feature = "gil-refs")] + use $crate::PyNativeType; + if let ::std::result::Result::Err(e) = $py.run_bound($code, None, Some(&$dict.as_borrowed())) { e.print($py); // So when this c api function the last line called printed the error to stderr, // the output is only written into a buffer which is never flushed because we // panic before flushing. This is where this hack comes into place - $py.run("import sys; sys.stderr.flush()", None, None) + $py.run_bound("import sys; sys.stderr.flush()", None, None) .unwrap(); ::std::panic!("{}", $code) } @@ -118,19 +121,63 @@ macro_rules! py_run_impl { /// Wraps a Rust function annotated with [`#[pyfunction]`](macro@crate::pyfunction). /// -/// This can be used with [`PyModule::add_function`](crate::types::PyModule::add_function) to add free -/// functions to a [`PyModule`](crate::types::PyModule) - see its documentation for more information. +/// This can be used with [`PyModule::add_function`](crate::types::PyModuleMethods::add_function) to +/// add free functions to a [`PyModule`](crate::types::PyModule) - see its documentation for more +/// information. +/// +/// During the migration from the GIL Ref API to the Bound API, the return type of this macro will +/// be either the `&'py PyModule` GIL Ref or `Bound<'py, PyModule>` according to the second +/// argument. +/// +/// For backwards compatibility, if the second argument is `Python<'py>` then the return type will +/// be `&'py PyModule` GIL Ref. To get `Bound<'py, PyModule>`, use the [`crate::wrap_pyfunction_bound!`] +/// macro instead. #[macro_export] macro_rules! wrap_pyfunction { ($function:path) => { &|py_or_module| { use $function as wrapped_pyfunction; - $crate::impl_::pyfunction::_wrap_pyfunction(&wrapped_pyfunction::DEF, py_or_module) + $crate::impl_::pyfunction::WrapPyFunctionArg::wrap_pyfunction( + py_or_module, + &wrapped_pyfunction::_PYO3_DEF, + ) + } + }; + ($function:path, $py_or_module:expr) => {{ + use $function as wrapped_pyfunction; + let check_gil_refs = $crate::impl_::deprecations::GilRefs::new(); + let py_or_module = + $crate::impl_::deprecations::inspect_type($py_or_module, &check_gil_refs); + check_gil_refs.is_python(); + $crate::impl_::pyfunction::WrapPyFunctionArg::wrap_pyfunction( + py_or_module, + &wrapped_pyfunction::_PYO3_DEF, + ) + }}; +} + +/// Wraps a Rust function annotated with [`#[pyfunction]`](macro@crate::pyfunction). +/// +/// This can be used with [`PyModule::add_function`](crate::types::PyModuleMethods::add_function) to +/// add free functions to a [`PyModule`](crate::types::PyModule) - see its documentation for more +/// information. +#[macro_export] +macro_rules! wrap_pyfunction_bound { + ($function:path) => { + &|py_or_module| { + use $function as wrapped_pyfunction; + $crate::impl_::pyfunction::WrapPyFunctionArg::wrap_pyfunction( + $crate::impl_::pyfunction::OnlyBound(py_or_module), + &wrapped_pyfunction::_PYO3_DEF, + ) } }; ($function:path, $py_or_module:expr) => {{ use $function as wrapped_pyfunction; - $crate::impl_::pyfunction::_wrap_pyfunction(&wrapped_pyfunction::DEF, $py_or_module) + $crate::impl_::pyfunction::WrapPyFunctionArg::wrap_pyfunction( + $crate::impl_::pyfunction::OnlyBound($py_or_module), + &wrapped_pyfunction::_PYO3_DEF, + ) }}; } @@ -138,13 +185,13 @@ macro_rules! wrap_pyfunction { /// Python module. /// /// Use this together with [`#[pymodule]`](crate::pymodule) and -/// [`PyModule::add_wrapped`](crate::types::PyModule::add_wrapped). +/// [`PyModule::add_wrapped`](crate::types::PyModuleMethods::add_wrapped). #[macro_export] macro_rules! wrap_pymodule { ($module:path) => { &|py| { use $module as wrapped_pymodule; - wrapped_pymodule::DEF + wrapped_pymodule::_PYO3_DEF .make_module(py) .expect("failed to wrap pymodule") } @@ -156,7 +203,7 @@ macro_rules! wrap_pymodule { /// /// Use it before [`prepare_freethreaded_python`](crate::prepare_freethreaded_python) and /// leave feature `auto-initialize` off -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] #[macro_export] macro_rules! append_to_inittab { ($module:ident) => { @@ -167,8 +214,8 @@ macro_rules! append_to_inittab { ); } $crate::ffi::PyImport_AppendInittab( - $module::NAME.as_ptr() as *const ::std::os::raw::c_char, - ::std::option::Option::Some($module::init), + $module::__PYO3_NAME.as_ptr() as *const ::std::os::raw::c_char, + ::std::option::Option::Some($module::__pyo3_init), ); } }; diff --git a/src/marker.rs b/src/marker.rs index eb3be5c1870..d706c79fb3b 100644 --- a/src/marker.rs +++ b/src/marker.rs @@ -47,20 +47,20 @@ //! ```rust, no_run //! use pyo3::prelude::*; //! use pyo3::types::PyString; -//! use scoped_tls::scoped_thread_local; +//! use scoped_tls_hkt::scoped_thread_local; //! -//! scoped_thread_local!(static WRAPPED: PyString); +//! scoped_thread_local!(static WRAPPED: for<'py> &'py Bound<'py, PyString>); //! //! fn callback() { -//! WRAPPED.with(|smuggled: &PyString| { +//! WRAPPED.with(|smuggled: &Bound<'_, PyString>| { //! println!("{:?}", smuggled); //! }); //! } //! //! Python::with_gil(|py| { -//! let string = PyString::new(py, "foo"); +//! let string = PyString::new_bound(py, "foo"); //! -//! WRAPPED.set(string, || { +//! WRAPPED.set(&string, || { //! py.allow_threads().with(callback); //! }); //! }); @@ -86,21 +86,26 @@ //! which executes the closure directly after suspending the GIL. //! //! However, note establishing the required invariants to soundly call this function -//! requires highly non-local reasoning as thread-local storage allows "smuggling" GIL-bound references +//! requires highly non-local reasoning as thread-local storage allows "smuggling" GIL-bound data //! using what is essentially global state. //! //! [`Rc`]: std::rc::Rc //! [`Py`]: crate::Py -use crate::err::{self, PyDowncastError, PyErr, PyResult}; -use crate::gil::{GILGuard, GILPool}; +use crate::err::{self, PyErr, PyResult}; +use crate::ffi_ptr_ext::FfiPtrExt; +use crate::gil::GILGuard; use crate::impl_::not_send::NotSend; +use crate::py_result_ext::PyResultExt; use crate::sync::RemoteAllowThreads; -use crate::type_object::HasPyGilRef; +use crate::types::any::PyAnyMethods; use crate::types::{ PyAny, PyDict, PyEllipsis, PyModule, PyNone, PyNotImplemented, PyString, PyType, }; use crate::version::PythonVersionInfo; -use crate::{ffi, FromPyPointer, IntoPy, Py, PyObject, PyTypeCheck, PyTypeInfo}; +use crate::{ffi, Bound, IntoPy, Py, PyObject, PyTypeInfo}; +#[allow(deprecated)] +#[cfg(feature = "gil-refs")] +use crate::{gil::GILPool, FromPyPointer, PyNativeType}; use std::ffi::{CStr, CString}; use std::marker::PhantomData; use std::os::raw::c_int; @@ -108,7 +113,7 @@ use std::os::raw::c_int; /// A marker token that represents holding the GIL. /// /// It serves three main purposes: -/// - It provides a global API for the Python interpreter, such as [`Python::eval`]. +/// - It provides a global API for the Python interpreter, such as [`Python::eval_bound`]. /// - It can be passed to functions that require a proof of holding the GIL, such as /// [`Py::clone_ref`]. /// - Its lifetime represents the scope of holding the GIL which can be used to create Rust @@ -124,7 +129,7 @@ use std::os::raw::c_int; /// - In a function or method annotated with [`#[pyfunction]`](crate::pyfunction) or [`#[pymethods]`](crate::pymethods) you can declare it /// as a parameter, and PyO3 will pass in the token when Python code calls it. /// - If you already have something with a lifetime bound to the GIL, such as `&`[`PyAny`], you can -/// use its [`.py()`][PyAny::py] method to get a token. +/// use its `.py()` method to get a token. /// - When you need to acquire the GIL yourself, such as when calling Python code from Rust, you /// should call [`Python::with_gil`] to do that and pass your code as a closure to it. /// @@ -155,36 +160,9 @@ use std::os::raw::c_int; /// # Releasing and freeing memory /// /// The [`Python`] type can be used to create references to variables owned by the Python -/// interpreter, using functions such as [`Python::eval`] and [`PyModule::import`]. These -/// references are tied to a [`GILPool`] whose references are not cleared until it is dropped. -/// This can cause apparent "memory leaks" if it is kept around for a long time. +/// interpreter, using functions such as [`Python::eval_bound`] and [`PyModule::import_bound`]. /// -/// ```rust -/// use pyo3::prelude::*; -/// use pyo3::types::PyString; -/// -/// # fn main () -> PyResult<()> { -/// Python::with_gil(|py| -> PyResult<()> { -/// for _ in 0..10 { -/// let hello: &PyString = py.eval("\"Hello World!\"", None, None)?.extract()?; -/// println!("Python says: {}", hello.to_str()?); -/// // Normally variables in a loop scope are dropped here, but `hello` is a reference to -/// // something owned by the Python interpreter. Dropping this reference does nothing. -/// } -/// Ok(()) -/// }) -/// // This is where the `hello`'s reference counts start getting decremented. -/// # } -/// ``` -/// -/// The variable `hello` is dropped at the end of each loop iteration, but the lifetime of the -/// pointed-to memory is bound to [`Python::with_gil`]'s [`GILPool`] which will not be dropped until -/// the end of [`Python::with_gil`]'s scope. Only then is each `hello`'s Python reference count -/// decreased. This means that at the last line of the example there are 10 copies of `hello` in -/// Python's memory, not just one at a time as we might expect from Rust's [scoping rules]. -/// -/// See the [Memory Management] chapter of the guide for more information about how PyO3 uses -/// [`GILPool`] to manage memory. +/// See the [Memory Management] chapter of the guide for more information about how PyO3 manages memory. /// /// [scoping rules]: https://doc.rust-lang.org/stable/book/ch04-01-what-is-ownership.html#ownership-rules /// [`Py::clone_ref`]: crate::Py::clone_ref @@ -203,7 +181,7 @@ impl Python<'_> { /// If the [`auto-initialize`] feature is enabled and the Python runtime is not already /// initialized, this function will initialize it. See #[cfg_attr( - not(PyPy), + not(any(PyPy, GraalPy)), doc = "[`prepare_freethreaded_python`](crate::prepare_freethreaded_python)" )] #[cfg_attr(PyPy, doc = "`prepare_freethreaded_python`")] @@ -225,7 +203,7 @@ impl Python<'_> { /// /// # fn main() -> PyResult<()> { /// Python::with_gil(|py| -> PyResult<()> { - /// let x: i32 = py.eval("5", None, None)?.extract()?; + /// let x: i32 = py.eval_bound("5", None, None)?.extract()?; /// assert_eq!(x, 5); /// Ok(()) /// }) @@ -238,10 +216,10 @@ impl Python<'_> { where F: for<'py> FnOnce(Python<'py>) -> R, { - let _guard = GILGuard::acquire(); + let guard = GILGuard::acquire(); // SAFETY: Either the GIL was already acquired or we just created a new `GILGuard`. - f(unsafe { Python::assume_gil_acquired() }) + f(guard.python()) } /// Like [`Python::with_gil`] except Python interpreter state checking is skipped. @@ -272,10 +250,9 @@ impl Python<'_> { where F: for<'py> FnOnce(Python<'py>) -> R, { - let _guard = GILGuard::acquire_unchecked(); + let guard = GILGuard::acquire_unchecked(); - // SAFETY: Either the GIL was already acquired or we just created a new `GILGuard`. - f(Python::assume_gil_acquired()) + f(guard.python()) } } @@ -288,7 +265,7 @@ impl<'py> Python<'py> { /// /// Only types that implement [`Send`] can cross the closure /// because *it is executed on a dedicated runtime thread* - /// to prevent access to GIL-bound references based on thread identity. + /// to prevent access to GIL-bound data based on thread identity. /// /// If you need to pass Python objects into the closure you can use [`Py`]``to create a /// reference independent of the GIL lifetime. However, you cannot do much with those without a @@ -314,7 +291,7 @@ impl<'py> Python<'py> { /// # /// # fn main() -> PyResult<()> { /// # Python::with_gil(|py| -> PyResult<()> { - /// # let fun = pyo3::wrap_pyfunction!(sum_numbers, py)?; + /// # let fun = pyo3::wrap_pyfunction_bound!(sum_numbers, py)?; /// # let res = fun.call1((vec![1_u32, 2, 3],))?; /// # assert_eq!(res.extract::()?, 6_u32); /// # Ok(()) @@ -332,7 +309,7 @@ impl<'py> Python<'py> { /// use pyo3::types::PyString; /// /// fn parallel_print(py: Python<'_>) { - /// let s = PyString::new(py, "This object cannot be accessed without holding the GIL >_<"); + /// let s = PyString::new_bound(py, "This object cannot be accessed without holding the GIL >_<"); /// py.allow_threads().with(move || { /// println!("{:?}", s); // This causes a compile error. /// }); @@ -347,13 +324,13 @@ impl<'py> Python<'py> { /// use send_wrapper::SendWrapper; /// /// Python::with_gil(|py| { - /// let string = PyString::new(py, "foo"); + /// let string = PyString::new_bound(py, "foo"); /// /// let wrapped = SendWrapper::new(string); /// /// py.allow_threads().with(|| { /// // panicks because this is not the thread which created `wrapped` - /// let sneaky: &PyString = *wrapped; + /// let sneaky: &Bound<'_, PyString> = &*wrapped; /// println!("{:?}", sneaky); /// }); /// }); @@ -379,13 +356,13 @@ impl<'py> Python<'py> { /// use send_wrapper::SendWrapper; /// /// Python::with_gil(|py| { - /// let string = PyString::new(py, "foo"); + /// let string = PyString::new_bound(py, "foo"); /// /// let wrapped = SendWrapper::new(string); /// /// unsafe { py.allow_threads().local() }.with(|| { /// // 💥 Unsound! 💥 - /// let sneaky: &PyString = *wrapped; + /// let sneaky: &Bound<'_, PyString> = &*wrapped; /// println!("{:?}", sneaky); /// }); /// }); @@ -397,11 +374,31 @@ impl<'py> Python<'py> { /// /// The caller must ensure that no code within the closure accesses GIL-protected data /// bound to the current thread. Note that this property is highly non-local as for example - /// `scoped-tls` allows "smuggling" GIL-bound references using what is essentially global state. + /// `scoped-tls` allows "smuggling" GIL-bound data using what is essentially global state. pub fn allow_threads(self) -> RemoteAllowThreads<'py> { RemoteAllowThreads(self) } + /// Deprecated version of [`Python::eval_bound`] + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`Python::eval` will be replaced by `Python::eval_bound` in a future PyO3 version" + )] + pub fn eval( + self, + code: &str, + globals: Option<&'py PyDict>, + locals: Option<&'py PyDict>, + ) -> PyResult<&'py PyAny> { + self.eval_bound( + code, + globals.map(PyNativeType::as_borrowed).as_deref(), + locals.map(PyNativeType::as_borrowed).as_deref(), + ) + .map(Bound::into_gil_ref) + } + /// Evaluates a Python expression in the given context and returns the result. /// /// If `globals` is `None`, it defaults to Python module `__main__`. @@ -415,18 +412,37 @@ impl<'py> Python<'py> { /// ``` /// # use pyo3::prelude::*; /// # Python::with_gil(|py| { - /// let result = py.eval("[i * 10 for i in range(5)]", None, None).unwrap(); + /// let result = py.eval_bound("[i * 10 for i in range(5)]", None, None).unwrap(); /// let res: Vec = result.extract().unwrap(); /// assert_eq!(res, vec![0, 10, 20, 30, 40]) /// # }); /// ``` - pub fn eval( + pub fn eval_bound( + self, + code: &str, + globals: Option<&Bound<'py, PyDict>>, + locals: Option<&Bound<'py, PyDict>>, + ) -> PyResult> { + self.run_code(code, ffi::Py_eval_input, globals, locals) + } + + /// Deprecated version of [`Python::run_bound`] + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`Python::run` will be replaced by `Python::run_bound` in a future PyO3 version" + )] + pub fn run( self, code: &str, globals: Option<&PyDict>, locals: Option<&PyDict>, - ) -> PyResult<&'py PyAny> { - self.run_code(code, ffi::Py_eval_input, globals, locals) + ) -> PyResult<()> { + self.run_bound( + code, + globals.map(PyNativeType::as_borrowed).as_deref(), + locals.map(PyNativeType::as_borrowed).as_deref(), + ) } /// Executes one or more Python statements in the given context. @@ -444,30 +460,30 @@ impl<'py> Python<'py> { /// types::{PyBytes, PyDict}, /// }; /// Python::with_gil(|py| { - /// let locals = PyDict::new(py); - /// py.run( + /// let locals = PyDict::new_bound(py); + /// py.run_bound( /// r#" /// import base64 /// s = 'Hello Rust!' /// ret = base64.b64encode(s.encode('utf-8')) /// "#, /// None, - /// Some(locals), + /// Some(&locals), /// ) /// .unwrap(); /// let ret = locals.get_item("ret").unwrap().unwrap(); - /// let b64: &PyBytes = ret.downcast().unwrap(); + /// let b64 = ret.downcast::().unwrap(); /// assert_eq!(b64.as_bytes(), b"SGVsbG8gUnVzdCE="); /// }); /// ``` /// /// You can use [`py_run!`](macro.py_run.html) for a handy alternative of `run` /// if you don't need `globals` and unwrapping is OK. - pub fn run( + pub fn run_bound( self, code: &str, - globals: Option<&PyDict>, - locals: Option<&PyDict>, + globals: Option<&Bound<'py, PyDict>>, + locals: Option<&Bound<'py, PyDict>>, ) -> PyResult<()> { let res = self.run_code(code, ffi::Py_file_input, globals, locals); res.map(|obj| { @@ -486,9 +502,9 @@ impl<'py> Python<'py> { self, code: &str, start: c_int, - globals: Option<&PyDict>, - locals: Option<&PyDict>, - ) -> PyResult<&'py PyAny> { + globals: Option<&Bound<'py, PyDict>>, + locals: Option<&Bound<'py, PyDict>>, + ) -> PyResult> { let code = CString::new(code)?; unsafe { let mptr = ffi::PyImport_AddModule("__main__\0".as_ptr() as *const _); @@ -531,46 +547,73 @@ impl<'py> Python<'py> { let res_ptr = ffi::PyEval_EvalCode(code_obj, globals, locals); ffi::Py_DECREF(code_obj); - self.from_owned_ptr_or_err(res_ptr) + res_ptr.assume_owned_or_err(self).downcast_into_unchecked() } } /// Gets the Python type object for type `T`. + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`Python::get_type` will be replaced by `Python::get_type_bound` in a future PyO3 version" + )] #[inline] pub fn get_type(self) -> &'py PyType where T: PyTypeInfo, { - T::type_object(self) + self.get_type_bound::().into_gil_ref() } - /// Imports the Python module with the specified name. + /// Gets the Python type object for type `T`. + #[inline] + pub fn get_type_bound(self) -> Bound<'py, PyType> + where + T: PyTypeInfo, + { + T::type_object_bound(self) + } + + /// Deprecated form of [`Python::import_bound`] + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`Python::import` will be replaced by `Python::import_bound` in a future PyO3 version" + )] pub fn import(self, name: N) -> PyResult<&'py PyModule> where N: IntoPy>, { - PyModule::import(self, name) + Self::import_bound(self, name).map(Bound::into_gil_ref) + } + + /// Imports the Python module with the specified name. + pub fn import_bound(self, name: N) -> PyResult> + where + N: IntoPy>, + { + PyModule::import_bound(self, name) } /// Gets the Python builtin value `None`. #[allow(non_snake_case)] // the Python keyword starts with uppercase #[inline] - pub fn None(self) -> &'py PyNone { - PyNone::get(self) + pub fn None(self) -> PyObject { + PyNone::get_bound(self).into_py(self) } /// Gets the Python builtin value `Ellipsis`, or `...`. #[allow(non_snake_case)] // the Python keyword starts with uppercase #[inline] - pub fn Ellipsis(self) -> &'py PyEllipsis { - PyEllipsis::get(self) + pub fn Ellipsis(self) -> PyObject { + PyEllipsis::get_bound(self).into_py(self) } /// Gets the Python builtin value `NotImplemented`. #[allow(non_snake_case)] // the Python keyword starts with uppercase #[inline] - pub fn NotImplemented(self) -> &'py PyNotImplemented { - PyNotImplemented::get(self) + pub fn NotImplemented(self) -> PyObject { + PyNotImplemented::get_bound(self).into_py(self) } /// Gets the running Python interpreter version as a string. @@ -615,10 +658,19 @@ impl<'py> Python<'py> { } /// Registers the object in the release pool, and tries to downcast to specific type. - pub fn checked_cast_as(self, obj: PyObject) -> Result<&'py T, PyDowncastError<'py>> + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "use `obj.downcast_bound::(py)` instead of `py.checked_cast_as::(obj)`" + )] + pub fn checked_cast_as( + self, + obj: PyObject, + ) -> Result<&'py T, crate::err::PyDowncastError<'py>> where - T: PyTypeCheck, + T: crate::PyTypeCheck, { + #[allow(deprecated)] obj.into_ref(self).downcast() } @@ -628,10 +680,16 @@ impl<'py> Python<'py> { /// # Safety /// /// Callers must ensure that ensure that the cast is valid. + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "use `obj.downcast_bound_unchecked::(py)` instead of `py.cast_as::(obj)`" + )] pub unsafe fn cast_as(self, obj: PyObject) -> &'py T where - T: HasPyGilRef, + T: crate::type_object::HasPyGilRef, { + #[allow(deprecated)] obj.into_ref(self).downcast_unchecked() } @@ -641,7 +699,12 @@ impl<'py> Python<'py> { /// # Safety /// /// Callers must ensure that ensure that the cast is valid. - #[allow(clippy::wrong_self_convention)] + #[allow(clippy::wrong_self_convention, deprecated)] + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "use `Py::from_owned_ptr(py, ptr)` or `Bound::from_owned_ptr(py, ptr)` instead" + )] pub unsafe fn from_owned_ptr(self, ptr: *mut ffi::PyObject) -> &'py T where T: FromPyPointer<'py>, @@ -657,7 +720,12 @@ impl<'py> Python<'py> { /// # Safety /// /// Callers must ensure that ensure that the cast is valid. - #[allow(clippy::wrong_self_convention)] + #[allow(clippy::wrong_self_convention, deprecated)] + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "use `Py::from_owned_ptr_or_err(py, ptr)` or `Bound::from_owned_ptr_or_err(py, ptr)` instead" + )] pub unsafe fn from_owned_ptr_or_err(self, ptr: *mut ffi::PyObject) -> PyResult<&'py T> where T: FromPyPointer<'py>, @@ -673,7 +741,12 @@ impl<'py> Python<'py> { /// # Safety /// /// Callers must ensure that ensure that the cast is valid. - #[allow(clippy::wrong_self_convention)] + #[allow(clippy::wrong_self_convention, deprecated)] + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "use `Py::from_owned_ptr_or_opt(py, ptr)` or `Bound::from_owned_ptr_or_opt(py, ptr)` instead" + )] pub unsafe fn from_owned_ptr_or_opt(self, ptr: *mut ffi::PyObject) -> Option<&'py T> where T: FromPyPointer<'py>, @@ -688,7 +761,12 @@ impl<'py> Python<'py> { /// # Safety /// /// Callers must ensure that ensure that the cast is valid. - #[allow(clippy::wrong_self_convention)] + #[allow(clippy::wrong_self_convention, deprecated)] + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "use `Py::from_borrowed_ptr(py, ptr)` or `Bound::from_borrowed_ptr(py, ptr)` instead" + )] pub unsafe fn from_borrowed_ptr(self, ptr: *mut ffi::PyObject) -> &'py T where T: FromPyPointer<'py>, @@ -703,7 +781,12 @@ impl<'py> Python<'py> { /// # Safety /// /// Callers must ensure that ensure that the cast is valid. - #[allow(clippy::wrong_self_convention)] + #[allow(clippy::wrong_self_convention, deprecated)] + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "use `Py::from_borrowed_ptr_or_err(py, ptr)` or `Bound::from_borrowed_ptr_or_err(py, ptr)` instead" + )] pub unsafe fn from_borrowed_ptr_or_err(self, ptr: *mut ffi::PyObject) -> PyResult<&'py T> where T: FromPyPointer<'py>, @@ -718,7 +801,12 @@ impl<'py> Python<'py> { /// # Safety /// /// Callers must ensure that ensure that the cast is valid. - #[allow(clippy::wrong_self_convention)] + #[allow(clippy::wrong_self_convention, deprecated)] + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "use `Py::from_borrowed_ptr_or_opt(py, ptr)` or `Bound::from_borrowed_ptr_or_opt(py, ptr)` instead" + )] pub unsafe fn from_borrowed_ptr_or_opt(self, ptr: *mut ffi::PyObject) -> Option<&'py T> where T: FromPyPointer<'py>, @@ -772,9 +860,10 @@ impl<'py> Python<'py> { err::error_on_minusone(self, unsafe { ffi::PyErr_CheckSignals() }) } - /// Create a new pool for managing PyO3's owned references. + /// Create a new pool for managing PyO3's GIL Refs. This has no functional + /// use for code which does not use the deprecated GIL Refs API. /// - /// When this `GILPool` is dropped, all PyO3 owned references created after this `GILPool` will + /// When this `GILPool` is dropped, all GIL Refs created after this `GILPool` will /// all have their Python reference counts decremented, potentially allowing Python to drop /// the corresponding Python objects. /// @@ -793,6 +882,7 @@ impl<'py> Python<'py> { /// // Some long-running process like a webserver, which never releases the GIL. /// loop { /// // Create a new pool, so that PyO3 can clear memory at the end of the loop. + /// #[allow(deprecated)] // `new_pool` is not needed in code not using the GIL Refs API /// let pool = unsafe { py.new_pool() }; /// /// // It is recommended to *always* immediately set py to the pool's Python, to help @@ -827,6 +917,12 @@ impl<'py> Python<'py> { /// /// [`.python()`]: crate::GILPool::python #[inline] + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "code not using the GIL Refs API can safely remove use of `Python::new_pool`" + )] + #[allow(deprecated)] pub unsafe fn new_pool(self) -> GILPool { GILPool::new() } @@ -858,27 +954,25 @@ impl<'unbound> Python<'unbound> { #[cfg(test)] mod tests { use super::*; - use crate::types::{IntoPyDict, PyDict, PyList}; - use crate::Py; - use std::sync::Arc; + use crate::types::{IntoPyDict, PyList}; #[test] fn test_eval() { Python::with_gil(|py| { // Make sure builtin names are accessible let v: i32 = py - .eval("min(1, 2)", None, None) + .eval_bound("min(1, 2)", None, None) .map_err(|e| e.display(py)) .unwrap() .extract() .unwrap(); assert_eq!(v, 1); - let d = [("foo", 13)].into_py_dict(py); + let d = [("foo", 13)].into_py_dict_bound(py); // Inject our own global namespace let v: i32 = py - .eval("foo + 29", Some(d), None) + .eval_bound("foo + 29", Some(&d), None) .unwrap() .extract() .unwrap(); @@ -886,7 +980,7 @@ mod tests { // Inject our own local namespace let v: i32 = py - .eval("foo + 29", None, Some(d)) + .eval_bound("foo + 29", None, Some(&d)) .unwrap() .extract() .unwrap(); @@ -894,7 +988,7 @@ mod tests { // Make sure builtin names are still accessible when using a local namespace let v: i32 = py - .eval("min(foo, 2)", None, Some(d)) + .eval_bound("min(foo, 2)", None, Some(&d)) .unwrap() .extract() .unwrap(); @@ -941,19 +1035,17 @@ mod tests { // If allow_threads is implemented correctly, this thread still owns the GIL here // so the following Python calls should not cause crashes. - let list = PyList::new(py, [1, 2, 3, 4]); + let list = PyList::new_bound(py, [1, 2, 3, 4]); assert_eq!(list.extract::>().unwrap(), vec![1, 2, 3, 4]); }); } + #[cfg(not(pyo3_disable_reference_pool))] #[test] fn test_allow_threads_pass_stuff_in() { - let list: Py = Python::with_gil(|py| { - let list = PyList::new(py, vec!["foo", "bar"]); - list.into() - }); + let list = Python::with_gil(|py| PyList::new_bound(py, vec!["foo", "bar"]).unbind()); let mut v = vec![1, 2, 3]; - let a = Arc::new(String::from("foo")); + let a = std::sync::Arc::new(String::from("foo")); Python::with_gil(|py| { py.allow_threads().with(|| { @@ -986,7 +1078,7 @@ mod tests { assert_eq!(py.Ellipsis().to_string(), "Ellipsis"); let v = py - .eval("...", None, None) + .eval_bound("...", None, None) .map_err(|e| e.display(py)) .unwrap(); @@ -996,9 +1088,11 @@ mod tests { #[test] fn test_py_run_inserts_globals() { + use crate::types::dict::PyDictMethods; + Python::with_gil(|py| { - let namespace = PyDict::new(py); - py.run("class Foo: pass", Some(namespace), Some(namespace)) + let namespace = PyDict::new_bound(py); + py.run_bound("class Foo: pass", Some(&namespace), Some(&namespace)) .unwrap(); assert!(matches!(namespace.get_item("Foo"), Ok(Some(..)))); assert!(matches!(namespace.get_item("__builtins__"), Ok(Some(..)))); diff --git a/src/marshal.rs b/src/marshal.rs index 343b861deff..3978f4873e1 100644 --- a/src/marshal.rs +++ b/src/marshal.rs @@ -2,14 +2,30 @@ //! Support for the Python `marshal` format. -use crate::ffi; +use crate::ffi_ptr_ext::FfiPtrExt; +use crate::py_result_ext::PyResultExt; use crate::types::{PyAny, PyBytes}; -use crate::{AsPyPointer, FromPyPointer, PyResult, Python}; -use std::os::raw::{c_char, c_int}; +use crate::{ffi, Bound}; +use crate::{AsPyPointer, PyResult, Python}; +use std::os::raw::c_int; /// The current version of the marshal binary format. pub const VERSION: i32 = 4; +/// Deprecated form of [`dumps_bound`] +#[cfg(feature = "gil-refs")] +#[deprecated( + since = "0.21.0", + note = "`dumps` will be replaced by `dumps_bound` in a future PyO3 version" +)] +pub fn dumps<'py>( + py: Python<'py>, + object: &impl AsPyPointer, + version: i32, +) -> PyResult<&'py PyBytes> { + dumps_bound(py, object, version).map(Bound::into_gil_ref) +} + /// Serialize an object to bytes using the Python built-in marshal module. /// /// The built-in marshalling only supports a limited range of objects. @@ -20,55 +36,70 @@ pub const VERSION: i32 = 4; /// /// # Examples /// ``` -/// # use pyo3::{marshal, types::PyDict}; +/// # use pyo3::{marshal, types::PyDict, prelude::PyDictMethods}; /// # pyo3::Python::with_gil(|py| { -/// let dict = PyDict::new(py); +/// let dict = PyDict::new_bound(py); /// dict.set_item("aap", "noot").unwrap(); /// dict.set_item("mies", "wim").unwrap(); /// dict.set_item("zus", "jet").unwrap(); /// -/// let bytes = marshal::dumps(py, dict, marshal::VERSION); +/// let bytes = marshal::dumps_bound(py, &dict, marshal::VERSION); /// # }); /// ``` -pub fn dumps<'a>(py: Python<'a>, object: &impl AsPyPointer, version: i32) -> PyResult<&'a PyBytes> { +pub fn dumps_bound<'py>( + py: Python<'py>, + object: &impl AsPyPointer, + version: i32, +) -> PyResult> { unsafe { - let bytes = ffi::PyMarshal_WriteObjectToString(object.as_ptr(), version as c_int); - FromPyPointer::from_owned_ptr_or_err(py, bytes) + ffi::PyMarshal_WriteObjectToString(object.as_ptr(), version as c_int) + .assume_owned_or_err(py) + .downcast_into_unchecked() } } +/// Deprecated form of [`loads_bound`] +#[cfg(feature = "gil-refs")] +#[deprecated( + since = "0.21.0", + note = "`loads` will be replaced by `loads_bound` in a future PyO3 version" +)] +pub fn loads<'py, B>(py: Python<'py>, data: &B) -> PyResult<&'py PyAny> +where + B: AsRef<[u8]> + ?Sized, +{ + loads_bound(py, data).map(Bound::into_gil_ref) +} + /// Deserialize an object from bytes using the Python built-in marshal module. -pub fn loads<'a, B>(py: Python<'a>, data: &B) -> PyResult<&'a PyAny> +pub fn loads_bound<'py, B>(py: Python<'py>, data: &B) -> PyResult> where B: AsRef<[u8]> + ?Sized, { let data = data.as_ref(); unsafe { - let c_str = data.as_ptr() as *const c_char; - let object = ffi::PyMarshal_ReadObjectFromString(c_str, data.len() as isize); - FromPyPointer::from_owned_ptr_or_err(py, object) + ffi::PyMarshal_ReadObjectFromString(data.as_ptr().cast(), data.len() as isize) + .assume_owned_or_err(py) } } #[cfg(test)] mod tests { use super::*; - use crate::types::PyDict; + use crate::types::{bytes::PyBytesMethods, dict::PyDictMethods, PyDict}; #[test] fn marshal_roundtrip() { Python::with_gil(|py| { - let dict = PyDict::new(py); + let dict = PyDict::new_bound(py); dict.set_item("aap", "noot").unwrap(); dict.set_item("mies", "wim").unwrap(); dict.set_item("zus", "jet").unwrap(); - let bytes = dumps(py, dict, VERSION) - .expect("marshalling failed") - .as_bytes(); - let deserialized = loads(py, bytes).expect("unmarshalling failed"); + let pybytes = dumps_bound(py, &dict, VERSION).expect("marshalling failed"); + let deserialized = loads_bound(py, pybytes.as_bytes()).expect("unmarshalling failed"); - assert!(equal(py, dict, deserialized)); + assert!(equal(py, &dict, &deserialized)); }); } diff --git a/src/prelude.rs b/src/prelude.rs index 866b2226765..4052f7c2d0b 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -9,28 +9,43 @@ //! ``` pub use crate::conversion::{FromPyObject, IntoPy, ToPyObject}; +#[cfg(feature = "gil-refs")] #[allow(deprecated)] pub use crate::conversion::{PyTryFrom, PyTryInto}; pub use crate::err::{PyErr, PyResult}; pub use crate::instance::{Borrowed, Bound, Py, PyObject}; pub use crate::marker::Python; -pub use crate::pycell::{PyCell, PyRef, PyRefMut}; +#[cfg(feature = "gil-refs")] +#[allow(deprecated)] +pub use crate::pycell::PyCell; +pub use crate::pycell::{PyRef, PyRefMut}; pub use crate::pyclass_init::PyClassInitializer; pub use crate::types::{PyAny, PyModule}; +#[cfg(feature = "gil-refs")] +pub use crate::PyNativeType; #[cfg(feature = "macros")] pub use pyo3_macros::{pyclass, pyfunction, pymethods, pymodule, FromPyObject}; #[cfg(feature = "macros")] -pub use crate::wrap_pyfunction; +pub use crate::{wrap_pyfunction, wrap_pyfunction_bound}; pub use crate::types::any::PyAnyMethods; pub use crate::types::boolobject::PyBoolMethods; pub use crate::types::bytearray::PyByteArrayMethods; pub use crate::types::bytes::PyBytesMethods; +pub use crate::types::capsule::PyCapsuleMethods; +pub use crate::types::complex::PyComplexMethods; pub use crate::types::dict::PyDictMethods; pub use crate::types::float::PyFloatMethods; +pub use crate::types::frozenset::PyFrozenSetMethods; pub use crate::types::list::PyListMethods; pub use crate::types::mapping::PyMappingMethods; +pub use crate::types::module::PyModuleMethods; pub use crate::types::sequence::PySequenceMethods; +pub use crate::types::set::PySetMethods; +pub use crate::types::slice::PySliceMethods; pub use crate::types::string::PyStringMethods; +pub use crate::types::traceback::PyTracebackMethods; +pub use crate::types::tuple::PyTupleMethods; +pub use crate::types::typeobject::PyTypeMethods; diff --git a/src/py_result_ext.rs b/src/py_result_ext.rs index da0e5c2a5e0..2ad079ed7ac 100644 --- a/src/py_result_ext.rs +++ b/src/py_result_ext.rs @@ -1,20 +1,16 @@ -use crate::{types::any::PyAnyMethods, Bound, PyAny, PyResult}; +use crate::{types::any::PyAnyMethods, Bound, PyAny, PyResult, PyTypeCheck}; -mod sealed { - use super::*; - - pub trait Sealed {} - - impl Sealed for PyResult> {} -} - -use sealed::Sealed; - -pub(crate) trait PyResultExt<'py>: Sealed { +pub(crate) trait PyResultExt<'py>: crate::sealed::Sealed { + fn downcast_into(self) -> PyResult>; unsafe fn downcast_into_unchecked(self) -> PyResult>; } impl<'py> PyResultExt<'py> for PyResult> { + #[inline] + fn downcast_into(self) -> PyResult> where { + self.and_then(|instance| instance.downcast_into().map_err(Into::into)) + } + #[inline] unsafe fn downcast_into_unchecked(self) -> PyResult> { self.map(|instance| instance.downcast_into_unchecked()) diff --git a/src/pybacked.rs b/src/pybacked.rs new file mode 100644 index 00000000000..ed68ea52ec7 --- /dev/null +++ b/src/pybacked.rs @@ -0,0 +1,486 @@ +//! Contains types for working with Python objects that own the underlying data. + +use std::{ops::Deref, ptr::NonNull, sync::Arc}; + +use crate::{ + types::{ + any::PyAnyMethods, bytearray::PyByteArrayMethods, bytes::PyBytesMethods, + string::PyStringMethods, PyByteArray, PyBytes, PyString, + }, + Bound, DowncastError, FromPyObject, Py, PyAny, PyErr, PyResult, +}; + +/// A wrapper around `str` where the storage is owned by a Python `bytes` or `str` object. +/// +/// This type gives access to the underlying data via a `Deref` implementation. +#[cfg_attr(feature = "py-clone", derive(Clone))] +pub struct PyBackedStr { + #[allow(dead_code)] // only held so that the storage is not dropped + storage: Py, + data: NonNull, +} + +impl Deref for PyBackedStr { + type Target = str; + fn deref(&self) -> &str { + // Safety: `data` is known to be immutable and owned by self + unsafe { self.data.as_ref() } + } +} + +impl AsRef for PyBackedStr { + fn as_ref(&self) -> &str { + self + } +} + +impl AsRef<[u8]> for PyBackedStr { + fn as_ref(&self) -> &[u8] { + self.as_bytes() + } +} + +// Safety: the underlying Python str (or bytes) is immutable and +// safe to share between threads +unsafe impl Send for PyBackedStr {} +unsafe impl Sync for PyBackedStr {} + +impl std::fmt::Display for PyBackedStr { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.deref().fmt(f) + } +} + +impl_traits!(PyBackedStr, str); + +impl TryFrom> for PyBackedStr { + type Error = PyErr; + fn try_from(py_string: Bound<'_, PyString>) -> Result { + #[cfg(any(Py_3_10, not(Py_LIMITED_API)))] + { + let s = py_string.to_str()?; + let data = NonNull::from(s); + Ok(Self { + storage: py_string.as_any().to_owned().unbind(), + data, + }) + } + #[cfg(not(any(Py_3_10, not(Py_LIMITED_API))))] + { + let bytes = py_string.encode_utf8()?; + let s = unsafe { std::str::from_utf8_unchecked(bytes.as_bytes()) }; + let data = NonNull::from(s); + Ok(Self { + storage: bytes.into_any().unbind(), + data, + }) + } + } +} + +impl FromPyObject<'_> for PyBackedStr { + fn extract_bound(obj: &Bound<'_, PyAny>) -> PyResult { + let py_string = obj.downcast::()?.to_owned(); + Self::try_from(py_string) + } +} + +/// A wrapper around `[u8]` where the storage is either owned by a Python `bytes` object, or a Rust `Box<[u8]>`. +/// +/// This type gives access to the underlying data via a `Deref` implementation. +#[cfg_attr(feature = "py-clone", derive(Clone))] +pub struct PyBackedBytes { + #[allow(dead_code)] // only held so that the storage is not dropped + storage: PyBackedBytesStorage, + data: NonNull<[u8]>, +} + +#[allow(dead_code)] +#[cfg_attr(feature = "py-clone", derive(Clone))] +enum PyBackedBytesStorage { + Python(Py), + Rust(Arc<[u8]>), +} + +impl Deref for PyBackedBytes { + type Target = [u8]; + fn deref(&self) -> &[u8] { + // Safety: `data` is known to be immutable and owned by self + unsafe { self.data.as_ref() } + } +} + +impl AsRef<[u8]> for PyBackedBytes { + fn as_ref(&self) -> &[u8] { + self + } +} + +// Safety: the underlying Python bytes or Rust bytes is immutable and +// safe to share between threads +unsafe impl Send for PyBackedBytes {} +unsafe impl Sync for PyBackedBytes {} + +impl PartialEq<[u8; N]> for PyBackedBytes { + fn eq(&self, other: &[u8; N]) -> bool { + self.deref() == other + } +} + +impl PartialEq for [u8; N] { + fn eq(&self, other: &PyBackedBytes) -> bool { + self == other.deref() + } +} + +impl PartialEq<&[u8; N]> for PyBackedBytes { + fn eq(&self, other: &&[u8; N]) -> bool { + self.deref() == *other + } +} + +impl PartialEq for &[u8; N] { + fn eq(&self, other: &PyBackedBytes) -> bool { + self == &other.deref() + } +} + +impl_traits!(PyBackedBytes, [u8]); + +impl From> for PyBackedBytes { + fn from(py_bytes: Bound<'_, PyBytes>) -> Self { + let b = py_bytes.as_bytes(); + let data = NonNull::from(b); + Self { + storage: PyBackedBytesStorage::Python(py_bytes.to_owned().unbind()), + data, + } + } +} + +impl From> for PyBackedBytes { + fn from(py_bytearray: Bound<'_, PyByteArray>) -> Self { + let s = Arc::<[u8]>::from(py_bytearray.to_vec()); + let data = NonNull::from(s.as_ref()); + Self { + storage: PyBackedBytesStorage::Rust(s), + data, + } + } +} + +impl FromPyObject<'_> for PyBackedBytes { + fn extract_bound(obj: &Bound<'_, PyAny>) -> PyResult { + if let Ok(bytes) = obj.downcast::() { + Ok(Self::from(bytes.to_owned())) + } else if let Ok(bytearray) = obj.downcast::() { + Ok(Self::from(bytearray.to_owned())) + } else { + Err(DowncastError::new(obj, "`bytes` or `bytearray`").into()) + } + } +} + +macro_rules! impl_traits { + ($slf:ty, $equiv:ty) => { + impl std::fmt::Debug for $slf { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.deref().fmt(f) + } + } + + impl PartialEq for $slf { + fn eq(&self, other: &Self) -> bool { + self.deref() == other.deref() + } + } + + impl PartialEq<$equiv> for $slf { + fn eq(&self, other: &$equiv) -> bool { + self.deref() == other + } + } + + impl PartialEq<&$equiv> for $slf { + fn eq(&self, other: &&$equiv) -> bool { + self.deref() == *other + } + } + + impl PartialEq<$slf> for $equiv { + fn eq(&self, other: &$slf) -> bool { + self == other.deref() + } + } + + impl PartialEq<$slf> for &$equiv { + fn eq(&self, other: &$slf) -> bool { + self == &other.deref() + } + } + + impl Eq for $slf {} + + impl PartialOrd for $slf { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } + } + + impl PartialOrd<$equiv> for $slf { + fn partial_cmp(&self, other: &$equiv) -> Option { + self.deref().partial_cmp(other) + } + } + + impl PartialOrd<$slf> for $equiv { + fn partial_cmp(&self, other: &$slf) -> Option { + self.partial_cmp(other.deref()) + } + } + + impl Ord for $slf { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.deref().cmp(other.deref()) + } + } + + impl std::hash::Hash for $slf { + fn hash(&self, state: &mut H) { + self.deref().hash(state) + } + } + }; +} +use impl_traits; + +#[cfg(test)] +mod test { + use super::*; + use crate::Python; + use std::collections::hash_map::DefaultHasher; + use std::hash::{Hash, Hasher}; + + #[test] + fn py_backed_str_empty() { + Python::with_gil(|py| { + let s = PyString::new_bound(py, ""); + let py_backed_str = s.extract::().unwrap(); + assert_eq!(&*py_backed_str, ""); + }); + } + + #[test] + fn py_backed_str() { + Python::with_gil(|py| { + let s = PyString::new_bound(py, "hello"); + let py_backed_str = s.extract::().unwrap(); + assert_eq!(&*py_backed_str, "hello"); + }); + } + + #[test] + fn py_backed_str_try_from() { + Python::with_gil(|py| { + let s = PyString::new_bound(py, "hello"); + let py_backed_str = PyBackedStr::try_from(s).unwrap(); + assert_eq!(&*py_backed_str, "hello"); + }); + } + + #[test] + fn py_backed_bytes_empty() { + Python::with_gil(|py| { + let b = PyBytes::new_bound(py, b""); + let py_backed_bytes = b.extract::().unwrap(); + assert_eq!(&*py_backed_bytes, b""); + }); + } + + #[test] + fn py_backed_bytes() { + Python::with_gil(|py| { + let b = PyBytes::new_bound(py, b"abcde"); + let py_backed_bytes = b.extract::().unwrap(); + assert_eq!(&*py_backed_bytes, b"abcde"); + }); + } + + #[test] + fn py_backed_bytes_from_bytes() { + Python::with_gil(|py| { + let b = PyBytes::new_bound(py, b"abcde"); + let py_backed_bytes = PyBackedBytes::from(b); + assert_eq!(&*py_backed_bytes, b"abcde"); + }); + } + + #[test] + fn py_backed_bytes_from_bytearray() { + Python::with_gil(|py| { + let b = PyByteArray::new_bound(py, b"abcde"); + let py_backed_bytes = PyBackedBytes::from(b); + assert_eq!(&*py_backed_bytes, b"abcde"); + }); + } + + #[test] + fn test_backed_types_send_sync() { + fn is_send() {} + fn is_sync() {} + + is_send::(); + is_sync::(); + + is_send::(); + is_sync::(); + } + + #[cfg(feature = "py-clone")] + #[test] + fn test_backed_str_clone() { + Python::with_gil(|py| { + let s1: PyBackedStr = PyString::new_bound(py, "hello").try_into().unwrap(); + let s2 = s1.clone(); + assert_eq!(s1, s2); + + drop(s1); + assert_eq!(s2, "hello"); + }); + } + + #[test] + fn test_backed_str_eq() { + Python::with_gil(|py| { + let s1: PyBackedStr = PyString::new_bound(py, "hello").try_into().unwrap(); + let s2: PyBackedStr = PyString::new_bound(py, "hello").try_into().unwrap(); + assert_eq!(s1, "hello"); + assert_eq!(s1, s2); + + let s3: PyBackedStr = PyString::new_bound(py, "abcde").try_into().unwrap(); + assert_eq!("abcde", s3); + assert_ne!(s1, s3); + }); + } + + #[test] + fn test_backed_str_hash() { + Python::with_gil(|py| { + let h = { + let mut hasher = DefaultHasher::new(); + "abcde".hash(&mut hasher); + hasher.finish() + }; + + let s1: PyBackedStr = PyString::new_bound(py, "abcde").try_into().unwrap(); + let h1 = { + let mut hasher = DefaultHasher::new(); + s1.hash(&mut hasher); + hasher.finish() + }; + + assert_eq!(h, h1); + }); + } + + #[test] + fn test_backed_str_ord() { + Python::with_gil(|py| { + let mut a = vec!["a", "c", "d", "b", "f", "g", "e"]; + let mut b = a + .iter() + .map(|s| PyString::new_bound(py, s).try_into().unwrap()) + .collect::>(); + + a.sort(); + b.sort(); + + assert_eq!(a, b); + }) + } + + #[cfg(feature = "py-clone")] + #[test] + fn test_backed_bytes_from_bytes_clone() { + Python::with_gil(|py| { + let b1: PyBackedBytes = PyBytes::new_bound(py, b"abcde").into(); + let b2 = b1.clone(); + assert_eq!(b1, b2); + + drop(b1); + assert_eq!(b2, b"abcde"); + }); + } + + #[cfg(feature = "py-clone")] + #[test] + fn test_backed_bytes_from_bytearray_clone() { + Python::with_gil(|py| { + let b1: PyBackedBytes = PyByteArray::new_bound(py, b"abcde").into(); + let b2 = b1.clone(); + assert_eq!(b1, b2); + + drop(b1); + assert_eq!(b2, b"abcde"); + }); + } + + #[test] + fn test_backed_bytes_eq() { + Python::with_gil(|py| { + let b1: PyBackedBytes = PyBytes::new_bound(py, b"abcde").into(); + let b2: PyBackedBytes = PyByteArray::new_bound(py, b"abcde").into(); + + assert_eq!(b1, b"abcde"); + assert_eq!(b1, b2); + + let b3: PyBackedBytes = PyBytes::new_bound(py, b"hello").into(); + assert_eq!(b"hello", b3); + assert_ne!(b1, b3); + }); + } + + #[test] + fn test_backed_bytes_hash() { + Python::with_gil(|py| { + let h = { + let mut hasher = DefaultHasher::new(); + b"abcde".hash(&mut hasher); + hasher.finish() + }; + + let b1: PyBackedBytes = PyBytes::new_bound(py, b"abcde").into(); + let h1 = { + let mut hasher = DefaultHasher::new(); + b1.hash(&mut hasher); + hasher.finish() + }; + + let b2: PyBackedBytes = PyByteArray::new_bound(py, b"abcde").into(); + let h2 = { + let mut hasher = DefaultHasher::new(); + b2.hash(&mut hasher); + hasher.finish() + }; + + assert_eq!(h, h1); + assert_eq!(h, h2); + }); + } + + #[test] + fn test_backed_bytes_ord() { + Python::with_gil(|py| { + let mut a = vec![b"a", b"c", b"d", b"b", b"f", b"g", b"e"]; + let mut b = a + .iter() + .map(|&b| PyBytes::new_bound(py, b).into()) + .collect::>(); + + a.sort(); + b.sort(); + + assert_eq!(a, b); + }) + } +} diff --git a/src/pycell.rs b/src/pycell.rs index bde95ad8313..f15f5a54431 100644 --- a/src/pycell.rs +++ b/src/pycell.rs @@ -14,13 +14,13 @@ //! - However, methods and functions in Rust usually *do* need `&mut` references. While PyO3 can //! use the [`Python<'py>`](crate::Python) token to guarantee thread-safe access to them, it cannot //! statically guarantee uniqueness of `&mut` references. As such those references have to be tracked -//! dynamically at runtime, using [`PyCell`] and the other types defined in this module. This works +//! dynamically at runtime, using `PyCell` and the other types defined in this module. This works //! similar to std's [`RefCell`](std::cell::RefCell) type. //! //! # When *not* to use PyCell //! //! Usually you can use `&mut` references as method and function receivers and arguments, and you -//! won't need to use [`PyCell`] directly: +//! won't need to use `PyCell` directly: //! //! ```rust //! use pyo3::prelude::*; @@ -39,9 +39,9 @@ //! ``` //! //! The [`#[pymethods]`](crate::pymethods) proc macro will generate this wrapper function (and more), -//! using [`PyCell`] under the hood: +//! using `PyCell` under the hood: //! -//! ```rust +//! ```rust,ignore //! # use pyo3::prelude::*; //! # #[pyclass] //! # struct Number { @@ -62,6 +62,7 @@ //! ) -> *mut pyo3::ffi::PyObject { //! use :: pyo3 as _pyo3; //! _pyo3::impl_::trampoline::noargs(_slf, _args, |py, _slf| { +//! # #[allow(deprecated)] //! let _cell = py //! .from_borrowed_ptr::<_pyo3::PyAny>(_slf) //! .downcast::<_pyo3::PyCell>()?; @@ -75,7 +76,7 @@ //! # When to use PyCell //! ## Using pyclasses from Rust //! -//! However, we *do* need [`PyCell`] if we want to call its methods from Rust: +//! However, we *do* need `PyCell` if we want to call its methods from Rust: //! ```rust //! # use pyo3::prelude::*; //! # @@ -96,7 +97,7 @@ //! //! // We borrow the guard and then dereference //! // it to get a mutable reference to Number -//! let mut guard: PyRefMut<'_, Number> = n.as_ref(py).borrow_mut(); +//! let mut guard: PyRefMut<'_, Number> = n.bind(py).borrow_mut(); //! let n_mutable: &mut Number = &mut *guard; //! //! n_mutable.increment(); @@ -105,7 +106,7 @@ //! // `PyRefMut` before borrowing again. //! drop(guard); //! -//! let n_immutable: &Number = &n.as_ref(py).borrow(); +//! let n_immutable: &Number = &n.bind(py).borrow(); //! assert_eq!(n_immutable.inner, 1); //! //! Ok(()) @@ -114,7 +115,7 @@ //! ``` //! ## Dealing with possibly overlapping mutable references //! -//! It is also necessary to use [`PyCell`] if you can receive mutable arguments that may overlap. +//! It is also necessary to use `PyCell` if you can receive mutable arguments that may overlap. //! Suppose the following function that swaps the values of two `Number`s: //! ``` //! # use pyo3::prelude::*; @@ -131,7 +132,7 @@ //! # let n = Py::new(py, Number{inner: 35}).unwrap(); //! # let n2 = n.clone_ref(py); //! # assert!(n.is(&n2)); -//! # let fun = pyo3::wrap_pyfunction!(swap_numbers, py).unwrap(); +//! # let fun = pyo3::wrap_pyfunction_bound!(swap_numbers, py).unwrap(); //! # fun.call1((n, n2)).expect_err("Managed to create overlapping mutable references. Note: this is undefined behaviour."); //! # }); //! # } @@ -147,7 +148,8 @@ //! ``` //! //! It is better to write that function like this: -//! ```rust +//! ```rust,ignore +//! # #![allow(deprecated)] //! # use pyo3::prelude::*; //! # #[pyclass] //! # pub struct Number { @@ -168,7 +170,7 @@ //! # let n = Py::new(py, Number{inner: 35}).unwrap(); //! # let n2 = n.clone_ref(py); //! # assert!(n.is(&n2)); -//! # let fun = pyo3::wrap_pyfunction!(swap_numbers, py).unwrap(); +//! # let fun = pyo3::wrap_pyfunction_bound!(swap_numbers, py).unwrap(); //! # fun.call1((n, n2)).unwrap(); //! # }); //! # @@ -177,7 +179,7 @@ //! # let n = Py::new(py, Number{inner: 35}).unwrap(); //! # let n2 = Py::new(py, Number{inner: 42}).unwrap(); //! # assert!(!n.is(&n2)); -//! # let fun = pyo3::wrap_pyfunction!(swap_numbers, py).unwrap(); +//! # let fun = pyo3::wrap_pyfunction_bound!(swap_numbers, py).unwrap(); //! # fun.call1((&n, &n2)).unwrap(); //! # let n: u32 = n.borrow(py).inner; //! # let n2: u32 = n2.borrow(py).inner; @@ -191,39 +193,30 @@ //! [guide]: https://pyo3.rs/latest/class.html#pycell-and-interior-mutability "PyCell and interior mutability" //! [Interior Mutability]: https://doc.rust-lang.org/book/ch15-05-interior-mutability.html "RefCell and the Interior Mutability Pattern - The Rust Programming Language" +use crate::conversion::AsPyPointer; use crate::exceptions::PyRuntimeError; -use crate::impl_::pyclass::{ - PyClassBaseType, PyClassDict, PyClassImpl, PyClassThreadChecker, PyClassWeakRef, -}; -use crate::pyclass::{ - boolean_struct::{False, True}, - PyClass, -}; -use crate::pyclass_init::PyClassInitializer; -use crate::type_object::{PyLayout, PySizedLayout}; -use crate::types::PyAny; +use crate::ffi_ptr_ext::FfiPtrExt; +use crate::pyclass::{boolean_struct::False, PyClass}; +use crate::types::any::PyAnyMethods; +#[cfg(feature = "gil-refs")] use crate::{ - conversion::{AsPyPointer, FromPyPointer, ToPyObject}, - type_object::get_tp_free, - PyTypeInfo, + conversion::ToPyObject, + impl_::pyclass::PyClassImpl, + pyclass::boolean_struct::True, + pyclass_init::PyClassInitializer, + type_object::{PyLayout, PySizedLayout}, + types::PyAny, + PyNativeType, PyResult, PyTypeCheck, }; -use crate::{ffi, IntoPy, PyErr, PyNativeType, PyObject, PyResult, PyTypeCheck, Python}; -use std::cell::UnsafeCell; +use crate::{ffi, Bound, IntoPy, PyErr, PyObject, Python}; use std::fmt; use std::mem::ManuallyDrop; use std::ops::{Deref, DerefMut}; pub(crate) mod impl_; -use impl_::{GetBorrowChecker, PyClassBorrowChecker, PyClassMutability}; - -/// Base layout of PyCell. -#[doc(hidden)] -#[repr(C)] -pub struct PyCellBase { - ob_base: T, -} - -unsafe impl PyLayout for PyCellBase where U: PySizedLayout {} +#[cfg(feature = "gil-refs")] +use self::impl_::PyClassObject; +use impl_::{PyClassBorrowChecker, PyClassObjectLayout}; /// A container type for (mutably) accessing [`PyClass`] values /// @@ -249,6 +242,7 @@ unsafe impl PyLayout for PyCellBase where U: PySizedLayout {} /// /// # fn main() -> PyResult<()> { /// Python::with_gil(|py| { +/// # #[allow(deprecated)] /// let n = PyCell::new(py, Number { inner: 0 })?; /// /// let n_mutable: &mut Number = &mut n.borrow_mut(); @@ -260,36 +254,33 @@ unsafe impl PyLayout for PyCellBase where U: PySizedLayout {} /// ``` /// For more information on how, when and why (not) to use `PyCell` please see the /// [module-level documentation](self). -#[repr(C)] -pub struct PyCell { - ob_base: ::LayoutAsBase, - contents: PyCellContents, -} - -#[repr(C)] -pub(crate) struct PyCellContents { - pub(crate) value: ManuallyDrop>, - pub(crate) borrow_checker: ::Storage, - pub(crate) thread_checker: T::ThreadChecker, - pub(crate) dict: T::Dict, - pub(crate) weakref: T::WeakRef, -} - +#[cfg(feature = "gil-refs")] +#[deprecated( + since = "0.21.0", + note = "`PyCell` was merged into `Bound`, use that instead; see the migration guide for more info" +)] +#[repr(transparent)] +pub struct PyCell(PyClassObject); + +#[cfg(feature = "gil-refs")] +#[allow(deprecated)] unsafe impl PyNativeType for PyCell { type AsRefSource = T; } +#[cfg(feature = "gil-refs")] +#[allow(deprecated)] impl PyCell { /// Makes a new `PyCell` on the Python heap and return the reference to it. /// /// In cases where the value in the cell does not need to be accessed immediately after /// creation, consider [`Py::new`](crate::Py::new) as a more efficient alternative. + #[deprecated( + since = "0.21.0", + note = "use `Bound::new(py, value)` or `Py::new(py, value)` instead of `PyCell::new(py, value)`" + )] pub fn new(py: Python<'_>, value: impl Into>) -> PyResult<&Self> { - unsafe { - let initializer = value.into(); - let self_ = initializer.create_cell(py)?; - FromPyPointer::from_owned_ptr_or_err(py, self_ as _) - } + Bound::new(py, value).map(Bound::into_gil_ref) } /// Immutably borrows the value `T`. This borrow lasts as long as the returned `PyRef` exists. @@ -301,7 +292,7 @@ impl PyCell { /// Panics if the value is currently mutably borrowed. For a non-panicking variant, use /// [`try_borrow`](#method.try_borrow). pub fn borrow(&self) -> PyRef<'_, T> { - self.try_borrow().expect("Already mutably borrowed") + PyRef::borrow(&self.as_borrowed()) } /// Mutably borrows the value `T`. This borrow lasts as long as the returned `PyRefMut` exists. @@ -314,7 +305,7 @@ impl PyCell { where T: PyClass, { - self.try_borrow_mut().expect("Already borrowed") + PyRefMut::borrow(&self.as_borrowed()) } /// Immutably borrows the value `T`, returning an error if the value is currently @@ -332,6 +323,7 @@ impl PyCell { /// struct Class {} /// /// Python::with_gil(|py| { + /// # #[allow(deprecated)] /// let c = PyCell::new(py, Class {}).unwrap(); /// { /// let m = c.borrow_mut(); @@ -345,18 +337,7 @@ impl PyCell { /// }); /// ``` pub fn try_borrow(&self) -> Result, PyBorrowError> { - self.ensure_threadsafe(); - self.borrow_checker() - .try_borrow() - .map(|_| PyRef { inner: self }) - } - - /// Variant of [`try_borrow`][Self::try_borrow] which fails instead of panicking if called from the wrong thread - pub(crate) fn try_borrow_threadsafe(&self) -> Result, PyBorrowError> { - self.check_threadsafe()?; - self.borrow_checker() - .try_borrow() - .map(|_| PyRef { inner: self }) + PyRef::try_borrow(&self.as_borrowed()) } /// Mutably borrows the value `T`, returning an error if the value is currently borrowed. @@ -371,6 +352,7 @@ impl PyCell { /// #[pyclass] /// struct Class {} /// Python::with_gil(|py| { + /// # #[allow(deprecated)] /// let c = PyCell::new(py, Class {}).unwrap(); /// { /// let m = c.borrow(); @@ -384,10 +366,7 @@ impl PyCell { where T: PyClass, { - self.ensure_threadsafe(); - self.borrow_checker() - .try_borrow_mut() - .map(|_| PyRefMut { inner: self }) + PyRefMut::try_borrow(&self.as_borrowed()) } /// Immutably borrows the value `T`, returning an error if the value is @@ -406,6 +385,7 @@ impl PyCell { /// #[pyclass] /// struct Class {} /// Python::with_gil(|py| { + /// # #[allow(deprecated)] /// let c = PyCell::new(py, Class {}).unwrap(); /// /// { @@ -420,10 +400,11 @@ impl PyCell { /// }); /// ``` pub unsafe fn try_borrow_unguarded(&self) -> Result<&T, PyBorrowError> { - self.ensure_threadsafe(); - self.borrow_checker() + self.0.ensure_threadsafe(); + self.0 + .borrow_checker() .try_borrow_unguarded() - .map(|_: ()| &*self.contents.value.get()) + .map(|_: ()| &*self.0.get_ptr()) } /// Provide an immutable borrow of the value `T` without acquiring the GIL. @@ -448,6 +429,7 @@ impl PyCell { /// Python::with_gil(|py| { /// let counter = FrozenCounter { value: AtomicUsize::new(0) }; /// + /// # #[allow(deprecated)] /// let cell = PyCell::new(py, counter).unwrap(); /// /// cell.get().value.fetch_add(1, Ordering::Relaxed); @@ -502,88 +484,71 @@ impl PyCell { } pub(crate) fn get_ptr(&self) -> *mut T { - self.contents.value.get() - } - - /// Gets the offset of the dictionary from the start of the struct in bytes. - pub(crate) fn dict_offset() -> ffi::Py_ssize_t { - use memoffset::offset_of; - - let offset = offset_of!(PyCell, contents) + offset_of!(PyCellContents, dict); - - // Py_ssize_t may not be equal to isize on all platforms - #[allow(clippy::useless_conversion)] - offset.try_into().expect("offset should fit in Py_ssize_t") - } - - /// Gets the offset of the weakref list from the start of the struct in bytes. - pub(crate) fn weaklist_offset() -> ffi::Py_ssize_t { - use memoffset::offset_of; - - let offset = offset_of!(PyCell, contents) + offset_of!(PyCellContents, weakref); - - // Py_ssize_t may not be equal to isize on all platforms - #[allow(clippy::useless_conversion)] - offset.try_into().expect("offset should fit in Py_ssize_t") - } - - #[cfg(feature = "macros")] - pub(crate) fn release_ref(&self) { - self.borrow_checker().release_borrow(); - } - - #[cfg(feature = "macros")] - pub(crate) fn release_mut(&self) { - self.borrow_checker().release_borrow_mut(); - } -} - -impl PyCell { - fn borrow_checker(&self) -> &::Checker { - T::PyClassMutability::borrow_checker(self) + self.0.get_ptr() } } +#[cfg(feature = "gil-refs")] +#[allow(deprecated)] unsafe impl PyLayout for PyCell {} +#[cfg(feature = "gil-refs")] +#[allow(deprecated)] impl PySizedLayout for PyCell {} +#[cfg(feature = "gil-refs")] +#[allow(deprecated)] impl PyTypeCheck for PyCell where T: PyClass, { const NAME: &'static str = ::NAME; - fn type_check(object: &PyAny) -> bool { + fn type_check(object: &Bound<'_, PyAny>) -> bool { ::type_check(object) } } - +#[cfg(feature = "gil-refs")] +#[allow(deprecated)] unsafe impl AsPyPointer for PyCell { fn as_ptr(&self) -> *mut ffi::PyObject { (self as *const _) as *mut _ } } +#[cfg(feature = "gil-refs")] +#[allow(deprecated)] impl ToPyObject for &PyCell { fn to_object(&self, py: Python<'_>) -> PyObject { unsafe { PyObject::from_borrowed_ptr(py, self.as_ptr()) } } } +#[cfg(feature = "gil-refs")] +#[allow(deprecated)] impl AsRef for PyCell { fn as_ref(&self) -> &PyAny { - unsafe { self.py().from_borrowed_ptr(self.as_ptr()) } + #[allow(deprecated)] + unsafe { + self.py().from_borrowed_ptr(self.as_ptr()) + } } } +#[cfg(feature = "gil-refs")] +#[allow(deprecated)] impl Deref for PyCell { type Target = PyAny; fn deref(&self) -> &PyAny { - unsafe { self.py().from_borrowed_ptr(self.as_ptr()) } + #[allow(deprecated)] + unsafe { + self.py().from_borrowed_ptr(self.as_ptr()) + } } } +#[cfg(feature = "gil-refs")] +#[allow(deprecated)] impl fmt::Debug for PyCell { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self.try_borrow() { @@ -603,14 +568,14 @@ impl fmt::Debug for PyCell { } } -/// A wrapper type for an immutably borrowed value from a [`PyCell`]``. +/// A wrapper type for an immutably borrowed value from a [`Bound<'py, T>`]. /// -/// See the [`PyCell`] documentation for more information. +/// See the [`Bound`] documentation for more information. /// /// # Examples /// -/// You can use `PyRef` as an alternative to a `&self` receiver when -/// - you need to access the pointer of the `PyCell`, or +/// You can use [`PyRef`] as an alternative to a `&self` receiver when +/// - you need to access the pointer of the [`Bound`], or /// - you want to get a super class. /// ``` /// # use pyo3::prelude::*; @@ -640,14 +605,16 @@ impl fmt::Debug for PyCell { /// } /// } /// # Python::with_gil(|py| { -/// # let sub = PyCell::new(py, Child::new()).unwrap(); -/// # pyo3::py_run!(py, sub, "assert sub.format() == 'Caterpillar(base: Butterfly, cnt: 3)'"); +/// # let sub = Py::new(py, Child::new()).unwrap(); +/// # pyo3::py_run!(py, sub, "assert sub.format() == 'Caterpillar(base: Butterfly, cnt: 4)', sub.format()"); /// # }); /// ``` /// /// See the [module-level documentation](self) for more information. pub struct PyRef<'p, T: PyClass> { - inner: &'p PyCell, + // TODO: once the GIL Ref API is removed, consider adding a lifetime parameter to `PyRef` to + // store `Borrowed` here instead, avoiding reference counting overhead. + inner: Bound<'p, T>, } impl<'p, T: PyClass> PyRef<'p, T> { @@ -663,11 +630,11 @@ where U: PyClass, { fn as_ref(&self) -> &T::BaseType { - unsafe { &*self.inner.ob_base.get_ptr() } + unsafe { &*self.inner.get_class_object().ob_base.get_ptr() } } } -impl<'p, T: PyClass> PyRef<'p, T> { +impl<'py, T: PyClass> PyRef<'py, T> { /// Returns the raw FFI pointer represented by self. /// /// # Safety @@ -689,7 +656,28 @@ impl<'p, T: PyClass> PyRef<'p, T> { /// of the pointer or decrease the reference count (e.g. with [`pyo3::ffi::Py_DecRef`](crate::ffi::Py_DecRef)). #[inline] pub fn into_ptr(self) -> *mut ffi::PyObject { - self.inner.into_ptr() + self.inner.clone().into_ptr() + } + + #[track_caller] + pub(crate) fn borrow(obj: &Bound<'py, T>) -> Self { + Self::try_borrow(obj).expect("Already mutably borrowed") + } + + pub(crate) fn try_borrow(obj: &Bound<'py, T>) -> Result { + let cell = obj.get_class_object(); + cell.ensure_threadsafe(); + cell.borrow_checker() + .try_borrow() + .map(|_| Self { inner: obj.clone() }) + } + + pub(crate) fn try_borrow_threadsafe(obj: &Bound<'py, T>) -> Result { + let cell = obj.get_class_object(); + cell.check_threadsafe()?; + cell.borrow_checker() + .try_borrow() + .map(|_| Self { inner: obj.clone() }) } } @@ -739,15 +727,19 @@ where /// } /// } /// # Python::with_gil(|py| { - /// # let sub = PyCell::new(py, Sub::new()).unwrap(); + /// # let sub = Py::new(py, Sub::new()).unwrap(); /// # pyo3::py_run!(py, sub, "assert sub.name() == 'base1 base2 sub'") /// # }); /// ``` pub fn into_super(self) -> PyRef<'p, U> { - let PyRef { inner } = self; - std::mem::forget(self); + let py = self.py(); PyRef { - inner: &inner.ob_base, + inner: unsafe { + ManuallyDrop::new(self) + .as_ptr() + .assume_owned(py) + .downcast_into_unchecked() + }, } } } @@ -757,13 +749,16 @@ impl<'p, T: PyClass> Deref for PyRef<'p, T> { #[inline] fn deref(&self) -> &T { - unsafe { &*self.inner.get_ptr() } + unsafe { &*self.inner.get_class_object().get_ptr() } } } impl<'p, T: PyClass> Drop for PyRef<'p, T> { fn drop(&mut self) { - self.inner.borrow_checker().release_borrow() + self.inner + .get_class_object() + .borrow_checker() + .release_borrow() } } @@ -775,10 +770,12 @@ impl IntoPy for PyRef<'_, T> { impl IntoPy for &'_ PyRef<'_, T> { fn into_py(self, py: Python<'_>) -> PyObject { - self.inner.into_py(py) + unsafe { PyObject::from_borrowed_ptr(py, self.inner.as_ptr()) } } } +#[cfg(feature = "gil-refs")] +#[allow(deprecated)] impl<'a, T: PyClass> std::convert::TryFrom<&'a PyCell> for crate::PyRef<'a, T> { type Error = PyBorrowError; fn try_from(cell: &'a crate::PyCell) -> Result { @@ -798,11 +795,13 @@ impl fmt::Debug for PyRef<'_, T> { } } -/// A wrapper type for a mutably borrowed value from a[`PyCell`]``. +/// A wrapper type for a mutably borrowed value from a [`Bound<'py, T>`]. /// /// See the [module-level documentation](self) for more information. pub struct PyRefMut<'p, T: PyClass> { - inner: &'p PyCell, + // TODO: once the GIL Ref API is removed, consider adding a lifetime parameter to `PyRef` to + // store `Borrowed` here instead, avoiding reference counting overhead. + inner: Bound<'p, T>, } impl<'p, T: PyClass> PyRefMut<'p, T> { @@ -818,7 +817,7 @@ where U: PyClass, { fn as_ref(&self) -> &T::BaseType { - unsafe { &*self.inner.ob_base.get_ptr() } + unsafe { &*self.inner.get_class_object().ob_base.get_ptr() } } } @@ -828,11 +827,11 @@ where U: PyClass, { fn as_mut(&mut self) -> &mut T::BaseType { - unsafe { &mut *self.inner.ob_base.get_ptr() } + unsafe { &mut *self.inner.get_class_object().ob_base.get_ptr() } } } -impl<'p, T: PyClass> PyRefMut<'p, T> { +impl<'py, T: PyClass> PyRefMut<'py, T> { /// Returns the raw FFI pointer represented by self. /// /// # Safety @@ -854,7 +853,21 @@ impl<'p, T: PyClass> PyRefMut<'p, T> { /// of the pointer or decrease the reference count (e.g. with [`pyo3::ffi::Py_DecRef`](crate::ffi::Py_DecRef)). #[inline] pub fn into_ptr(self) -> *mut ffi::PyObject { - self.inner.into_ptr() + self.inner.clone().into_ptr() + } + + #[inline] + #[track_caller] + pub(crate) fn borrow(obj: &Bound<'py, T>) -> Self { + Self::try_borrow(obj).expect("Already borrowed") + } + + pub(crate) fn try_borrow(obj: &Bound<'py, T>) -> Result { + let cell = obj.get_class_object(); + cell.ensure_threadsafe(); + cell.borrow_checker() + .try_borrow_mut() + .map(|_| Self { inner: obj.clone() }) } } @@ -867,10 +880,14 @@ where /// /// See [`PyRef::into_super`] for more. pub fn into_super(self) -> PyRefMut<'p, U> { - let PyRefMut { inner } = self; - std::mem::forget(self); + let py = self.py(); PyRefMut { - inner: &inner.ob_base, + inner: unsafe { + ManuallyDrop::new(self) + .as_ptr() + .assume_owned(py) + .downcast_into_unchecked() + }, } } } @@ -880,20 +897,23 @@ impl<'p, T: PyClass> Deref for PyRefMut<'p, T> { #[inline] fn deref(&self) -> &T { - unsafe { &*self.inner.get_ptr() } + unsafe { &*self.inner.get_class_object().get_ptr() } } } impl<'p, T: PyClass> DerefMut for PyRefMut<'p, T> { #[inline] fn deref_mut(&mut self) -> &mut T { - unsafe { &mut *self.inner.get_ptr() } + unsafe { &mut *self.inner.get_class_object().get_ptr() } } } impl<'p, T: PyClass> Drop for PyRefMut<'p, T> { fn drop(&mut self) { - self.inner.borrow_checker().release_borrow_mut() + self.inner + .get_class_object() + .borrow_checker() + .release_borrow_mut() } } @@ -905,7 +925,7 @@ impl> IntoPy for PyRefMut<'_, T> { impl> IntoPy for &'_ PyRefMut<'_, T> { fn into_py(self, py: Python<'_>) -> PyObject { - self.inner.into_py(py) + self.inner.clone().into_py(py) } } @@ -915,6 +935,8 @@ unsafe impl<'a, T: PyClass> AsPyPointer for PyRefMut<'a, T> { } } +#[cfg(feature = "gil-refs")] +#[allow(deprecated)] impl<'a, T: PyClass> std::convert::TryFrom<&'a PyCell> for crate::PyRefMut<'a, T> { @@ -930,7 +952,7 @@ impl + fmt::Debug> fmt::Debug for PyRefMut<'_, T> { } } -/// An error type returned by [`PyCell::try_borrow`]. +/// An error type returned by [`Bound::try_borrow`]. /// /// If this error is allowed to bubble up into Python code it will raise a `RuntimeError`. pub struct PyBorrowError { @@ -955,7 +977,7 @@ impl From for PyErr { } } -/// An error type returned by [`PyCell::try_borrow_mut`]. +/// An error type returned by [`Bound::try_borrow_mut`]. /// /// If this error is allowed to bubble up into Python code it will raise a `RuntimeError`. pub struct PyBorrowMutError { @@ -980,81 +1002,6 @@ impl From for PyErr { } } -#[doc(hidden)] -pub trait PyCellLayout: PyLayout { - fn ensure_threadsafe(&self); - fn check_threadsafe(&self) -> Result<(), PyBorrowError>; - /// Implementation of tp_dealloc. - /// # Safety - /// - slf must be a valid pointer to an instance of a T or a subclass. - /// - slf must not be used after this call (as it will be freed). - unsafe fn tp_dealloc(py: Python<'_>, slf: *mut ffi::PyObject); -} - -impl PyCellLayout for PyCellBase -where - U: PySizedLayout, - T: PyTypeInfo, -{ - fn ensure_threadsafe(&self) {} - fn check_threadsafe(&self) -> Result<(), PyBorrowError> { - Ok(()) - } - unsafe fn tp_dealloc(py: Python<'_>, slf: *mut ffi::PyObject) { - let type_obj = T::type_object_raw(py); - // For `#[pyclass]` types which inherit from PyAny, we can just call tp_free - if type_obj == std::ptr::addr_of_mut!(ffi::PyBaseObject_Type) { - return get_tp_free(ffi::Py_TYPE(slf))(slf as _); - } - - // More complex native types (e.g. `extends=PyDict`) require calling the base's dealloc. - #[cfg(not(Py_LIMITED_API))] - { - if let Some(dealloc) = (*type_obj).tp_dealloc { - // Before CPython 3.11 BaseException_dealloc would use Py_GC_UNTRACK which - // assumes the exception is currently GC tracked, so we have to re-track - // before calling the dealloc so that it can safely call Py_GC_UNTRACK. - #[cfg(not(any(Py_3_11, PyPy)))] - if ffi::PyType_FastSubclass(type_obj, ffi::Py_TPFLAGS_BASE_EXC_SUBCLASS) == 1 { - ffi::PyObject_GC_Track(slf.cast()); - } - dealloc(slf as _); - } else { - get_tp_free(ffi::Py_TYPE(slf))(slf as _); - } - } - - #[cfg(Py_LIMITED_API)] - unreachable!("subclassing native types is not possible with the `abi3` feature"); - } -} - -impl PyCellLayout for PyCell -where - ::LayoutAsBase: PyCellLayout, -{ - fn ensure_threadsafe(&self) { - self.contents.thread_checker.ensure(); - self.ob_base.ensure_threadsafe(); - } - fn check_threadsafe(&self) -> Result<(), PyBorrowError> { - if !self.contents.thread_checker.check() { - return Err(PyBorrowError { _private: () }); - } - self.ob_base.check_threadsafe() - } - unsafe fn tp_dealloc(py: Python<'_>, slf: *mut ffi::PyObject) { - // Safety: Python only calls tp_dealloc when no references to the object remain. - let cell = &mut *(slf as *mut PyCell); - if cell.contents.thread_checker.can_drop(py) { - ManuallyDrop::drop(&mut cell.contents.value); - } - cell.contents.dict.clear_dict(py); - cell.contents.weakref.clear_weakrefs(slf, py); - ::LayoutAsBase::tp_dealloc(py, slf) - } -} - #[cfg(test)] #[cfg(feature = "macros")] mod tests { @@ -1066,97 +1013,112 @@ mod tests { #[derive(Copy, Clone, PartialEq, Eq, Debug)] struct SomeClass(i32); - #[test] - fn pycell_replace() { - Python::with_gil(|py| { - let cell = PyCell::new(py, SomeClass(0)).unwrap(); - assert_eq!(*cell.borrow(), SomeClass(0)); - - let previous = cell.replace(SomeClass(123)); - assert_eq!(previous, SomeClass(0)); - assert_eq!(*cell.borrow(), SomeClass(123)); - }) - } - - #[test] - #[should_panic(expected = "Already borrowed: PyBorrowMutError")] - fn pycell_replace_panic() { - Python::with_gil(|py| { - let cell = PyCell::new(py, SomeClass(0)).unwrap(); - let _guard = cell.borrow(); - - cell.replace(SomeClass(123)); - }) - } + #[cfg(feature = "gil-refs")] + mod deprecated { + use super::*; + + #[test] + fn pycell_replace() { + Python::with_gil(|py| { + #[allow(deprecated)] + let cell = PyCell::new(py, SomeClass(0)).unwrap(); + assert_eq!(*cell.borrow(), SomeClass(0)); + + let previous = cell.replace(SomeClass(123)); + assert_eq!(previous, SomeClass(0)); + assert_eq!(*cell.borrow(), SomeClass(123)); + }) + } - #[test] - fn pycell_replace_with() { - Python::with_gil(|py| { - let cell = PyCell::new(py, SomeClass(0)).unwrap(); - assert_eq!(*cell.borrow(), SomeClass(0)); - - let previous = cell.replace_with(|value| { - *value = SomeClass(2); - SomeClass(123) - }); - assert_eq!(previous, SomeClass(2)); - assert_eq!(*cell.borrow(), SomeClass(123)); - }) - } + #[test] + #[should_panic(expected = "Already borrowed: PyBorrowMutError")] + fn pycell_replace_panic() { + Python::with_gil(|py| { + #[allow(deprecated)] + let cell = PyCell::new(py, SomeClass(0)).unwrap(); + let _guard = cell.borrow(); - #[test] - #[should_panic(expected = "Already borrowed: PyBorrowMutError")] - fn pycell_replace_with_panic() { - Python::with_gil(|py| { - let cell = PyCell::new(py, SomeClass(0)).unwrap(); - let _guard = cell.borrow(); + cell.replace(SomeClass(123)); + }) + } - cell.replace_with(|_| SomeClass(123)); - }) - } + #[test] + fn pycell_replace_with() { + Python::with_gil(|py| { + #[allow(deprecated)] + let cell = PyCell::new(py, SomeClass(0)).unwrap(); + assert_eq!(*cell.borrow(), SomeClass(0)); + + let previous = cell.replace_with(|value| { + *value = SomeClass(2); + SomeClass(123) + }); + assert_eq!(previous, SomeClass(2)); + assert_eq!(*cell.borrow(), SomeClass(123)); + }) + } - #[test] - fn pycell_swap() { - Python::with_gil(|py| { - let cell = PyCell::new(py, SomeClass(0)).unwrap(); - let cell2 = PyCell::new(py, SomeClass(123)).unwrap(); - assert_eq!(*cell.borrow(), SomeClass(0)); - assert_eq!(*cell2.borrow(), SomeClass(123)); - - cell.swap(cell2); - assert_eq!(*cell.borrow(), SomeClass(123)); - assert_eq!(*cell2.borrow(), SomeClass(0)); - }) - } + #[test] + #[should_panic(expected = "Already borrowed: PyBorrowMutError")] + fn pycell_replace_with_panic() { + Python::with_gil(|py| { + #[allow(deprecated)] + let cell = PyCell::new(py, SomeClass(0)).unwrap(); + let _guard = cell.borrow(); - #[test] - #[should_panic(expected = "Already borrowed: PyBorrowMutError")] - fn pycell_swap_panic() { - Python::with_gil(|py| { - let cell = PyCell::new(py, SomeClass(0)).unwrap(); - let cell2 = PyCell::new(py, SomeClass(123)).unwrap(); + cell.replace_with(|_| SomeClass(123)); + }) + } - let _guard = cell.borrow(); - cell.swap(cell2); - }) - } + #[test] + fn pycell_swap() { + Python::with_gil(|py| { + #[allow(deprecated)] + let cell = PyCell::new(py, SomeClass(0)).unwrap(); + #[allow(deprecated)] + let cell2 = PyCell::new(py, SomeClass(123)).unwrap(); + assert_eq!(*cell.borrow(), SomeClass(0)); + assert_eq!(*cell2.borrow(), SomeClass(123)); + + cell.swap(cell2); + assert_eq!(*cell.borrow(), SomeClass(123)); + assert_eq!(*cell2.borrow(), SomeClass(0)); + }) + } - #[test] - #[should_panic(expected = "Already borrowed: PyBorrowMutError")] - fn pycell_swap_panic_other_borrowed() { - Python::with_gil(|py| { - let cell = PyCell::new(py, SomeClass(0)).unwrap(); - let cell2 = PyCell::new(py, SomeClass(123)).unwrap(); + #[test] + #[should_panic(expected = "Already borrowed: PyBorrowMutError")] + fn pycell_swap_panic() { + Python::with_gil(|py| { + #[allow(deprecated)] + let cell = PyCell::new(py, SomeClass(0)).unwrap(); + #[allow(deprecated)] + let cell2 = PyCell::new(py, SomeClass(123)).unwrap(); + + let _guard = cell.borrow(); + cell.swap(cell2); + }) + } - let _guard = cell2.borrow(); - cell.swap(cell2); - }) + #[test] + #[should_panic(expected = "Already borrowed: PyBorrowMutError")] + fn pycell_swap_panic_other_borrowed() { + Python::with_gil(|py| { + #[allow(deprecated)] + let cell = PyCell::new(py, SomeClass(0)).unwrap(); + #[allow(deprecated)] + let cell2 = PyCell::new(py, SomeClass(123)).unwrap(); + + let _guard = cell2.borrow(); + cell.swap(cell2); + }) + } } #[test] fn test_as_ptr() { Python::with_gil(|py| { - let cell = PyCell::new(py, SomeClass(0)).unwrap(); + let cell = Bound::new(py, SomeClass(0)).unwrap(); let ptr = cell.as_ptr(); assert_eq!(cell.borrow().as_ptr(), ptr); @@ -1167,7 +1129,7 @@ mod tests { #[test] fn test_into_ptr() { Python::with_gil(|py| { - let cell = PyCell::new(py, SomeClass(0)).unwrap(); + let cell = Bound::new(py, SomeClass(0)).unwrap(); let ptr = cell.as_ptr(); assert_eq!(cell.borrow().into_ptr(), ptr); diff --git a/src/pycell/impl_.rs b/src/pycell/impl_.rs index 62875e67ae4..5404464caba 100644 --- a/src/pycell/impl_.rs +++ b/src/pycell/impl_.rs @@ -1,11 +1,15 @@ #![allow(missing_docs)] -//! Crate-private implementation of pycell +//! Crate-private implementation of PyClassObject -use std::cell::Cell; +use std::cell::{Cell, UnsafeCell}; use std::marker::PhantomData; +use std::mem::ManuallyDrop; -use crate::impl_::pyclass::{PyClassBaseType, PyClassImpl}; -use crate::PyCell; +use crate::impl_::pyclass::{ + PyClassBaseType, PyClassDict, PyClassImpl, PyClassThreadChecker, PyClassWeakRef, +}; +use crate::type_object::{get_tp_free, PyLayout, PySizedLayout}; +use crate::{ffi, PyClass, PyTypeInfo, Python}; use super::{PyBorrowError, PyBorrowMutError}; @@ -51,7 +55,7 @@ struct BorrowFlag(usize); impl BorrowFlag { pub(crate) const UNUSED: BorrowFlag = BorrowFlag(0); - const HAS_MUTABLE_BORROW: BorrowFlag = BorrowFlag(usize::max_value()); + const HAS_MUTABLE_BORROW: BorrowFlag = BorrowFlag(usize::MAX); const fn increment(self) -> Self { Self(self.0 + 1) } @@ -70,6 +74,7 @@ pub trait PyClassBorrowChecker { /// Increments immutable borrow count, if possible fn try_borrow(&self) -> Result<(), PyBorrowError>; + #[cfg(feature = "gil-refs")] fn try_borrow_unguarded(&self) -> Result<(), PyBorrowError>; /// Decrements immutable borrow count @@ -92,6 +97,7 @@ impl PyClassBorrowChecker for EmptySlot { } #[inline] + #[cfg(feature = "gil-refs")] fn try_borrow_unguarded(&self) -> Result<(), PyBorrowError> { Ok(()) } @@ -126,6 +132,7 @@ impl PyClassBorrowChecker for BorrowChecker { } } + #[cfg(feature = "gil-refs")] fn try_borrow_unguarded(&self) -> Result<(), PyBorrowError> { let flag = self.0.get(); if flag != BorrowFlag::HAS_MUTABLE_BORROW { @@ -156,29 +163,170 @@ impl PyClassBorrowChecker for BorrowChecker { } pub trait GetBorrowChecker { - fn borrow_checker(cell: &PyCell) -> &::Checker; + fn borrow_checker( + class_object: &PyClassObject, + ) -> &::Checker; } impl> GetBorrowChecker for MutableClass { - fn borrow_checker(cell: &PyCell) -> &BorrowChecker { - &cell.contents.borrow_checker + fn borrow_checker(class_object: &PyClassObject) -> &BorrowChecker { + &class_object.contents.borrow_checker } } impl> GetBorrowChecker for ImmutableClass { - fn borrow_checker(cell: &PyCell) -> &EmptySlot { - &cell.contents.borrow_checker + fn borrow_checker(class_object: &PyClassObject) -> &EmptySlot { + &class_object.contents.borrow_checker } } impl, M: PyClassMutability> GetBorrowChecker for ExtendsMutableAncestor where - T::BaseType: PyClassImpl + PyClassBaseType>, + T::BaseType: PyClassImpl + PyClassBaseType>, ::PyClassMutability: PyClassMutability, { - fn borrow_checker(cell: &PyCell) -> &BorrowChecker { - <::PyClassMutability as GetBorrowChecker>::borrow_checker(&cell.ob_base) + fn borrow_checker(class_object: &PyClassObject) -> &BorrowChecker { + <::PyClassMutability as GetBorrowChecker>::borrow_checker(&class_object.ob_base) + } +} + +/// Base layout of PyClassObject. +#[doc(hidden)] +#[repr(C)] +pub struct PyClassObjectBase { + ob_base: T, +} + +unsafe impl PyLayout for PyClassObjectBase where U: PySizedLayout {} + +#[doc(hidden)] +pub trait PyClassObjectLayout: PyLayout { + fn ensure_threadsafe(&self); + fn check_threadsafe(&self) -> Result<(), PyBorrowError>; + /// Implementation of tp_dealloc. + /// # Safety + /// - slf must be a valid pointer to an instance of a T or a subclass. + /// - slf must not be used after this call (as it will be freed). + unsafe fn tp_dealloc(py: Python<'_>, slf: *mut ffi::PyObject); +} + +impl PyClassObjectLayout for PyClassObjectBase +where + U: PySizedLayout, + T: PyTypeInfo, +{ + fn ensure_threadsafe(&self) {} + fn check_threadsafe(&self) -> Result<(), PyBorrowError> { + Ok(()) + } + unsafe fn tp_dealloc(py: Python<'_>, slf: *mut ffi::PyObject) { + let type_obj = T::type_object_raw(py); + // For `#[pyclass]` types which inherit from PyAny, we can just call tp_free + if type_obj == std::ptr::addr_of_mut!(ffi::PyBaseObject_Type) { + return get_tp_free(ffi::Py_TYPE(slf))(slf.cast()); + } + + // More complex native types (e.g. `extends=PyDict`) require calling the base's dealloc. + #[cfg(not(Py_LIMITED_API))] + { + if let Some(dealloc) = (*type_obj).tp_dealloc { + // Before CPython 3.11 BaseException_dealloc would use Py_GC_UNTRACK which + // assumes the exception is currently GC tracked, so we have to re-track + // before calling the dealloc so that it can safely call Py_GC_UNTRACK. + #[cfg(not(any(Py_3_11, PyPy)))] + if ffi::PyType_FastSubclass(type_obj, ffi::Py_TPFLAGS_BASE_EXC_SUBCLASS) == 1 { + ffi::PyObject_GC_Track(slf.cast()); + } + dealloc(slf); + } else { + get_tp_free(ffi::Py_TYPE(slf))(slf.cast()); + } + } + + #[cfg(Py_LIMITED_API)] + unreachable!("subclassing native types is not possible with the `abi3` feature"); + } +} + +/// The layout of a PyClass as a Python object +#[repr(C)] +pub struct PyClassObject { + pub(crate) ob_base: ::LayoutAsBase, + contents: PyClassObjectContents, +} + +#[repr(C)] +pub(crate) struct PyClassObjectContents { + pub(crate) value: ManuallyDrop>, + pub(crate) borrow_checker: ::Storage, + pub(crate) thread_checker: T::ThreadChecker, + pub(crate) dict: T::Dict, + pub(crate) weakref: T::WeakRef, +} + +impl PyClassObject { + pub(crate) fn get_ptr(&self) -> *mut T { + self.contents.value.get() + } + + /// Gets the offset of the dictionary from the start of the struct in bytes. + pub(crate) fn dict_offset() -> ffi::Py_ssize_t { + use memoffset::offset_of; + + let offset = + offset_of!(PyClassObject, contents) + offset_of!(PyClassObjectContents, dict); + + // Py_ssize_t may not be equal to isize on all platforms + #[allow(clippy::useless_conversion)] + offset.try_into().expect("offset should fit in Py_ssize_t") + } + + /// Gets the offset of the weakref list from the start of the struct in bytes. + pub(crate) fn weaklist_offset() -> ffi::Py_ssize_t { + use memoffset::offset_of; + + let offset = + offset_of!(PyClassObject, contents) + offset_of!(PyClassObjectContents, weakref); + + // Py_ssize_t may not be equal to isize on all platforms + #[allow(clippy::useless_conversion)] + offset.try_into().expect("offset should fit in Py_ssize_t") + } +} + +impl PyClassObject { + pub(crate) fn borrow_checker(&self) -> &::Checker { + T::PyClassMutability::borrow_checker(self) + } +} + +unsafe impl PyLayout for PyClassObject {} +impl PySizedLayout for PyClassObject {} + +impl PyClassObjectLayout for PyClassObject +where + ::LayoutAsBase: PyClassObjectLayout, +{ + fn ensure_threadsafe(&self) { + self.contents.thread_checker.ensure(); + self.ob_base.ensure_threadsafe(); + } + fn check_threadsafe(&self) -> Result<(), PyBorrowError> { + if !self.contents.thread_checker.check() { + return Err(PyBorrowError { _private: () }); + } + self.ob_base.check_threadsafe() + } + unsafe fn tp_dealloc(py: Python<'_>, slf: *mut ffi::PyObject) { + // Safety: Python only calls tp_dealloc when no references to the object remain. + let class_object = &mut *(slf.cast::>()); + if class_object.contents.thread_checker.can_drop(py) { + ManuallyDrop::drop(&mut class_object.contents.value); + } + class_object.contents.dict.clear_dict(py); + class_object.contents.weakref.clear_weakrefs(slf, py); + ::LayoutAsBase::tp_dealloc(py, slf) } } @@ -189,7 +337,6 @@ mod tests { use crate::prelude::*; use crate::pyclass::boolean_struct::{False, True}; - use crate::PyClass; #[pyclass(crate = "crate", subclass)] struct MutableBase; @@ -284,43 +431,43 @@ mod tests { ) .unwrap(); - let mmm_cell: &PyCell = mmm.as_ref(py); + let mmm_bound: &Bound<'_, MutableChildOfMutableChildOfMutableBase> = mmm.bind(py); - let mmm_refmut = mmm_cell.borrow_mut(); + let mmm_refmut = mmm_bound.borrow_mut(); // Cannot take any other mutable or immutable borrows whilst the object is borrowed mutably - assert!(mmm_cell + assert!(mmm_bound .extract::>() .is_err()); - assert!(mmm_cell + assert!(mmm_bound .extract::>() .is_err()); - assert!(mmm_cell.extract::>().is_err()); - assert!(mmm_cell + assert!(mmm_bound.extract::>().is_err()); + assert!(mmm_bound .extract::>() .is_err()); - assert!(mmm_cell + assert!(mmm_bound .extract::>() .is_err()); - assert!(mmm_cell.extract::>().is_err()); + assert!(mmm_bound.extract::>().is_err()); // With the borrow dropped, all other borrow attempts will succeed drop(mmm_refmut); - assert!(mmm_cell + assert!(mmm_bound .extract::>() .is_ok()); - assert!(mmm_cell + assert!(mmm_bound .extract::>() .is_ok()); - assert!(mmm_cell.extract::>().is_ok()); - assert!(mmm_cell + assert!(mmm_bound.extract::>().is_ok()); + assert!(mmm_bound .extract::>() .is_ok()); - assert!(mmm_cell + assert!(mmm_bound .extract::>() .is_ok()); - assert!(mmm_cell.extract::>().is_ok()); + assert!(mmm_bound.extract::>().is_ok()); }) } @@ -335,38 +482,38 @@ mod tests { ) .unwrap(); - let mmm_cell: &PyCell = mmm.as_ref(py); + let mmm_bound: &Bound<'_, MutableChildOfMutableChildOfMutableBase> = mmm.bind(py); - let mmm_refmut = mmm_cell.borrow(); + let mmm_refmut = mmm_bound.borrow(); // Further immutable borrows are ok - assert!(mmm_cell + assert!(mmm_bound .extract::>() .is_ok()); - assert!(mmm_cell + assert!(mmm_bound .extract::>() .is_ok()); - assert!(mmm_cell.extract::>().is_ok()); + assert!(mmm_bound.extract::>().is_ok()); // Further mutable borrows are not ok - assert!(mmm_cell + assert!(mmm_bound .extract::>() .is_err()); - assert!(mmm_cell + assert!(mmm_bound .extract::>() .is_err()); - assert!(mmm_cell.extract::>().is_err()); + assert!(mmm_bound.extract::>().is_err()); // With the borrow dropped, all mutable borrow attempts will succeed drop(mmm_refmut); - assert!(mmm_cell + assert!(mmm_bound .extract::>() .is_ok()); - assert!(mmm_cell + assert!(mmm_bound .extract::>() .is_ok()); - assert!(mmm_cell.extract::>().is_ok()); + assert!(mmm_bound.extract::>().is_ok()); }) } } diff --git a/src/pyclass.rs b/src/pyclass.rs index eb4a5595ca9..162ae0d3119 100644 --- a/src/pyclass.rs +++ b/src/pyclass.rs @@ -1,7 +1,7 @@ //! `PyClass` and related traits. use crate::{ - callback::IntoPyCallbackOutput, ffi, impl_::pyclass::PyClassImpl, IntoPy, PyCell, PyObject, - PyResult, PyTypeInfo, Python, + callback::IntoPyCallbackOutput, ffi, impl_::pyclass::PyClassImpl, IntoPy, PyObject, PyResult, + PyTypeInfo, Python, }; use std::{cmp::Ordering, os::raw::c_int}; @@ -15,7 +15,21 @@ pub use self::gc::{PyTraverseError, PyVisit}; /// /// The `#[pyclass]` attribute implements this trait for your Rust struct - /// you shouldn't implement this trait directly. -pub trait PyClass: PyTypeInfo> + PyClassImpl { +#[allow(deprecated)] +#[cfg(feature = "gil-refs")] +pub trait PyClass: PyTypeInfo> + PyClassImpl { + /// Whether the pyclass is frozen. + /// + /// This can be enabled via `#[pyclass(frozen)]`. + type Frozen: Frozen; +} + +/// Types that can be used as Python classes. +/// +/// The `#[pyclass]` attribute implements this trait for your Rust struct - +/// you shouldn't implement this trait directly. +#[cfg(not(feature = "gil-refs"))] +pub trait PyClass: PyTypeInfo + PyClassImpl { /// Whether the pyclass is frozen. /// /// This can be enabled via `#[pyclass(frozen)]`. diff --git a/src/pyclass/create_type_object.rs b/src/pyclass/create_type_object.rs index a7196f30288..1b3a9fb1296 100644 --- a/src/pyclass/create_type_object.rs +++ b/src/pyclass/create_type_object.rs @@ -3,6 +3,7 @@ use pyo3_ffi::PyType_IS_GC; use crate::{ exceptions::PyTypeError, ffi, + impl_::pycell::PyClassObject, impl_::pyclass::{ assign_sequence_item_from_mapping, get_sequence_item_from_mapping, tp_dealloc, tp_dealloc_with_gc, PyClassItemsIter, @@ -11,13 +12,13 @@ use crate::{ pymethods::{get_doc, get_name, Getter, Setter}, trampoline::trampoline, }, + types::typeobject::PyTypeMethods, types::PyType, - Py, PyCell, PyClass, PyGetterDef, PyMethodDefType, PyResult, PySetterDef, PyTypeInfo, Python, + Py, PyClass, PyGetterDef, PyMethodDefType, PyResult, PySetterDef, PyTypeInfo, Python, }; use std::{ borrow::Cow, collections::HashMap, - convert::TryInto, ffi::{CStr, CString}, os::raw::{c_char, c_int, c_ulong, c_void}, ptr, @@ -94,7 +95,7 @@ where T::items_iter(), T::NAME, T::MODULE, - std::mem::size_of::>(), + std::mem::size_of::>(), ) } } @@ -144,12 +145,14 @@ impl PyTypeBuilder { #[cfg(all(not(Py_3_9), not(Py_LIMITED_API)))] ffi::Py_bf_getbuffer => { // Safety: slot.pfunc is a valid function pointer - self.buffer_procs.bf_getbuffer = Some(std::mem::transmute(pfunc)); + self.buffer_procs.bf_getbuffer = + Some(std::mem::transmute::<*mut T, ffi::getbufferproc>(pfunc)); } #[cfg(all(not(Py_3_9), not(Py_LIMITED_API)))] ffi::Py_bf_releasebuffer => { // Safety: slot.pfunc is a valid function pointer - self.buffer_procs.bf_releasebuffer = Some(std::mem::transmute(pfunc)); + self.buffer_procs.bf_releasebuffer = + Some(std::mem::transmute::<*mut T, ffi::releasebufferproc>(pfunc)); } _ => {} } @@ -435,7 +438,7 @@ impl PyTypeBuilder { bpo_45315_workaround(py, class_name); for cleanup in std::mem::take(&mut self.cleanup) { - cleanup(&self, type_object.as_ref(py).as_type_ptr()); + cleanup(&self, type_object.bind(py).as_type_ptr()); } Ok(PyClassTypeObject { @@ -505,7 +508,7 @@ impl GetSetDefBuilder { self.doc = Some(getter.doc); } // TODO: return an error if getter already defined? - self.getter = Some(getter.meth.0) + self.getter = Some(getter.meth) } fn add_setter(&mut self, setter: &PySetterDef) { @@ -514,7 +517,7 @@ impl GetSetDefBuilder { self.doc = Some(setter.doc); } // TODO: return an error if setter already defined? - self.setter = Some(setter.meth.0) + self.setter = Some(setter.meth) } fn as_get_set_def( diff --git a/src/pyclass_init.rs b/src/pyclass_init.rs index 63761f435bb..923bc5b7c5a 100644 --- a/src/pyclass_init.rs +++ b/src/pyclass_init.rs @@ -1,13 +1,12 @@ //! Contains initialization utilities for `#[pyclass]`. use crate::callback::IntoPyCallbackOutput; +use crate::ffi_ptr_ext::FfiPtrExt; use crate::impl_::pyclass::{PyClassBaseType, PyClassDict, PyClassThreadChecker, PyClassWeakRef}; -use crate::{ffi, Py, PyCell, PyClass, PyErr, PyResult, Python}; +use crate::types::PyAnyMethods; +use crate::{ffi, Bound, Py, PyClass, PyErr, PyResult, Python}; use crate::{ ffi::PyTypeObject, - pycell::{ - impl_::{PyClassBorrowChecker, PyClassMutability}, - PyCellContents, - }, + pycell::impl_::{PyClassBorrowChecker, PyClassMutability, PyClassObjectContents}, type_object::{get_tp_alloc, PyTypeInfo}, }; use std::{ @@ -122,7 +121,7 @@ impl PyObjectInit for PyNativeTypeInitializer { /// } /// } /// Python::with_gil(|py| { -/// let typeobj = py.get_type::(); +/// let typeobj = py.get_type_bound::(); /// let sub_sub_class = typeobj.call((), None).unwrap(); /// py_run!( /// py, @@ -184,7 +183,7 @@ impl PyClassInitializer { /// /// fn main() -> PyResult<()> { /// Python::with_gil(|py| { - /// let m = PyModule::new(py, "example")?; + /// let m = PyModule::new_bound(py, "example")?; /// m.add_class::()?; /// m.add_class::()?; /// @@ -207,57 +206,44 @@ impl PyClassInitializer { } /// Creates a new PyCell and initializes it. - #[doc(hidden)] - pub fn create_cell(self, py: Python<'_>) -> PyResult<*mut PyCell> + pub(crate) fn create_class_object(self, py: Python<'_>) -> PyResult> where T: PyClass, { - unsafe { self.create_cell_from_subtype(py, T::type_object_raw(py)) } + unsafe { self.create_class_object_of_type(py, T::type_object_raw(py)) } } - /// Creates a new PyCell and initializes it given a typeobject `subtype`. - /// Called by the Python `tp_new` implementation generated by a `#[new]` function in a `#[pymethods]` block. + /// Creates a new class object and initializes it given a typeobject `subtype`. /// /// # Safety /// `subtype` must be a valid pointer to the type object of T or a subclass. - #[doc(hidden)] - pub unsafe fn create_cell_from_subtype( + pub(crate) unsafe fn create_class_object_of_type( self, py: Python<'_>, - subtype: *mut crate::ffi::PyTypeObject, - ) -> PyResult<*mut PyCell> + target_type: *mut crate::ffi::PyTypeObject, + ) -> PyResult> where T: PyClass, { - self.into_new_object(py, subtype).map(|obj| obj as _) - } -} - -impl PyObjectInit for PyClassInitializer { - unsafe fn into_new_object( - self, - py: Python<'_>, - subtype: *mut PyTypeObject, - ) -> PyResult<*mut ffi::PyObject> { - /// Layout of a PyCell after base new has been called, but the contents have not yet been + /// Layout of a PyClassObject after base new has been called, but the contents have not yet been /// written. #[repr(C)] - struct PartiallyInitializedPyCell { + struct PartiallyInitializedClassObject { _ob_base: ::LayoutAsBase, - contents: MaybeUninit>, + contents: MaybeUninit>, } let (init, super_init) = match self.0 { - PyClassInitializerImpl::Existing(value) => return Ok(value.into_ptr()), + PyClassInitializerImpl::Existing(value) => return Ok(value.into_bound(py)), PyClassInitializerImpl::New { init, super_init } => (init, super_init), }; - let obj = super_init.into_new_object(py, subtype)?; + let obj = super_init.into_new_object(py, target_type)?; - let cell: *mut PartiallyInitializedPyCell = obj as _; + let part_init: *mut PartiallyInitializedClassObject = obj.cast(); std::ptr::write( - (*cell).contents.as_mut_ptr(), - PyCellContents { + (*part_init).contents.as_mut_ptr(), + PyClassObjectContents { value: ManuallyDrop::new(UnsafeCell::new(init)), borrow_checker: ::Storage::new(), thread_checker: T::ThreadChecker::new(), @@ -265,7 +251,21 @@ impl PyObjectInit for PyClassInitializer { weakref: T::WeakRef::INIT, }, ); - Ok(obj) + + // Safety: obj is a valid pointer to an object of type `target_type`, which` is a known + // subclass of `T` + Ok(obj.assume_owned(py).downcast_into_unchecked()) + } +} + +impl PyObjectInit for PyClassInitializer { + unsafe fn into_new_object( + self, + py: Python<'_>, + subtype: *mut PyTypeObject, + ) -> PyResult<*mut ffi::PyObject> { + self.create_class_object_of_type(py, subtype) + .map(Bound::into_ptr) } private_impl! {} diff --git a/src/sealed.rs b/src/sealed.rs new file mode 100644 index 00000000000..e2d5c5ccfed --- /dev/null +++ b/src/sealed.rs @@ -0,0 +1,34 @@ +use crate::types::{ + PyBool, PyByteArray, PyBytes, PyCapsule, PyComplex, PyDict, PyFloat, PyFrozenSet, PyList, + PyMapping, PyModule, PySequence, PySet, PySlice, PyString, PyTraceback, PyTuple, PyType, +}; +use crate::{ffi, Bound, PyAny, PyResult}; + +pub trait Sealed {} + +// for FfiPtrExt +impl Sealed for *mut ffi::PyObject {} + +// for PyResultExt +impl Sealed for PyResult> {} + +// for Py(...)Methods +impl Sealed for Bound<'_, PyAny> {} +impl Sealed for Bound<'_, PyBool> {} +impl Sealed for Bound<'_, PyByteArray> {} +impl Sealed for Bound<'_, PyBytes> {} +impl Sealed for Bound<'_, PyCapsule> {} +impl Sealed for Bound<'_, PyComplex> {} +impl Sealed for Bound<'_, PyDict> {} +impl Sealed for Bound<'_, PyFloat> {} +impl Sealed for Bound<'_, PyFrozenSet> {} +impl Sealed for Bound<'_, PyList> {} +impl Sealed for Bound<'_, PyMapping> {} +impl Sealed for Bound<'_, PyModule> {} +impl Sealed for Bound<'_, PySequence> {} +impl Sealed for Bound<'_, PySet> {} +impl Sealed for Bound<'_, PySlice> {} +impl Sealed for Bound<'_, PyString> {} +impl Sealed for Bound<'_, PyTraceback> {} +impl Sealed for Bound<'_, PyTuple> {} +impl Sealed for Bound<'_, PyType> {} diff --git a/src/sync.rs b/src/sync.rs index c7d8f1e1831..ccd0d19c152 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -1,20 +1,23 @@ //! Synchronization mechanisms based on the Python GIL. +//! +//! With the acceptance of [PEP 703] (aka a "freethreaded Python") for Python 3.13, these +//! are likely to undergo significant developments in the future. +//! +//! [PEP 703]: https://peps.python.org/pep-703/ use std::{ cell::UnsafeCell, mem::{replace, transmute}, panic::{catch_unwind, resume_unwind, AssertUnwindSafe}, - sync::Arc, + sync::{Arc, Condvar, Mutex}, thread::Builder, time::Duration, }; -use parking_lot::{Condvar, Mutex}; - use crate::{ gil::SuspendGIL, impl_::panic::PanicTrap, - types::{PyString, PyType}, - Py, PyResult, PyVisit, Python, + types::{PyAnyMethods, PyString, PyType}, + Bound, Py, PyResult, PyVisit, Python, }; /// Value with concurrent access protected by the GIL. @@ -90,13 +93,14 @@ unsafe impl Sync for GILProtected where T: Send {} /// /// static LIST_CELL: GILOnceCell> = GILOnceCell::new(); /// -/// pub fn get_shared_list(py: Python<'_>) -> &PyList { +/// pub fn get_shared_list(py: Python<'_>) -> &Bound<'_, PyList> { /// LIST_CELL -/// .get_or_init(py, || PyList::empty(py).into()) -/// .as_ref(py) +/// .get_or_init(py, || PyList::empty_bound(py).unbind()) +/// .bind(py) /// } /// # Python::with_gil(|py| assert_eq!(get_shared_list(py).len(), 0)); /// ``` +#[derive(Default)] pub struct GILOnceCell(UnsafeCell>); // T: Send is needed for Sync because the thread which drops the GILOnceCell can be different @@ -207,13 +211,19 @@ impl GILOnceCell> { /// /// This is a shorthand method for `get_or_init` which imports the type from Python on init. pub(crate) fn get_or_try_init_type_ref<'py>( - &'py self, + &self, py: Python<'py>, module_name: &str, attr_name: &str, - ) -> PyResult<&'py PyType> { - self.get_or_try_init(py, || py.import(module_name)?.getattr(attr_name)?.extract()) - .map(|ty| ty.as_ref(py)) + ) -> PyResult<&Bound<'py, PyType>> { + self.get_or_try_init(py, || { + let type_object = py + .import_bound(module_name)? + .getattr(attr_name)? + .downcast_into()?; + Ok(type_object.unbind()) + }) + .map(|ty| ty.bind(py)) } } @@ -225,11 +235,11 @@ impl GILOnceCell> { /// /// ``` /// use pyo3::intern; -/// # use pyo3::{pyfunction, types::PyDict, wrap_pyfunction, PyResult, Python}; +/// # use pyo3::{prelude::*, types::PyDict}; /// /// #[pyfunction] -/// fn create_dict(py: Python<'_>) -> PyResult<&PyDict> { -/// let dict = PyDict::new(py); +/// fn create_dict(py: Python<'_>) -> PyResult> { +/// let dict = PyDict::new_bound(py); /// // 👇 A new `PyString` is created /// // for every call of this function. /// dict.set_item("foo", 42)?; @@ -237,8 +247,8 @@ impl GILOnceCell> { /// } /// /// #[pyfunction] -/// fn create_dict_faster(py: Python<'_>) -> PyResult<&PyDict> { -/// let dict = PyDict::new(py); +/// fn create_dict_faster(py: Python<'_>) -> PyResult> { +/// let dict = PyDict::new_bound(py); /// // 👇 A `PyString` is created once and reused /// // for the lifetime of the program. /// dict.set_item(intern!(py, "foo"), 42)?; @@ -246,10 +256,10 @@ impl GILOnceCell> { /// } /// # /// # Python::with_gil(|py| { -/// # let fun_slow = wrap_pyfunction!(create_dict, py).unwrap(); +/// # let fun_slow = wrap_pyfunction_bound!(create_dict, py).unwrap(); /// # let dict = fun_slow.call0().unwrap(); /// # assert!(dict.contains("foo").unwrap()); -/// # let fun = wrap_pyfunction!(create_dict_faster, py).unwrap(); +/// # let fun = wrap_pyfunction_bound!(create_dict_faster, py).unwrap(); /// # let dict = fun.call0().unwrap(); /// # assert!(dict.contains("foo").unwrap()); /// # }); @@ -274,10 +284,10 @@ impl Interned { /// Gets or creates the interned `str` value. #[inline] - pub fn get<'py>(&'py self, py: Python<'py>) -> &'py PyString { + pub fn get<'py>(&self, py: Python<'py>) -> &Bound<'py, PyString> { self.1 - .get_or_init(py, || PyString::intern(py, self.0).into()) - .as_ref(py) + .get_or_init(py, || PyString::intern_bound(py, self.0).into()) + .bind(py) } } @@ -439,7 +449,7 @@ impl Mailbox { fn init(&self, task: Task) { use MailboxInner::*; - let mut inner = self.inner.lock(); + let mut inner = self.inner.lock().unwrap(); match &*inner { Abandoned => *inner = MailboxInner::Task(task), Empty | Task(_) | Working | Done => { @@ -450,7 +460,7 @@ impl Mailbox { fn send_task(&self, task: Task) -> Option { use MailboxInner::*; - let mut inner = self.inner.lock(); + let mut inner = self.inner.lock().unwrap(); match &*inner { Empty => { *inner = Task(task); @@ -465,15 +475,16 @@ impl Mailbox { fn recv_task(&self) -> Option { use MailboxInner::*; - let mut inner = self.inner.lock(); + let mut inner = self.inner.lock().unwrap(); loop { match &*inner { Empty | Done => { - if self + let (guard, result) = self .flag - .wait_for(&mut inner, Duration::from_secs(60)) - .timed_out() - { + .wait_timeout(inner, Duration::from_secs(60)) + .unwrap(); + inner = guard; + if result.timed_out() { *inner = Abandoned; return None; } @@ -491,7 +502,7 @@ impl Mailbox { fn signal_done(&self) { use MailboxInner::*; - let mut inner = self.inner.lock(); + let mut inner = self.inner.lock().unwrap(); match &*inner { Working => { *inner = Done; @@ -506,14 +517,14 @@ impl Mailbox { fn await_done(&self) { use MailboxInner::*; - let mut inner = self.inner.lock(); + let mut inner = self.inner.lock().unwrap(); loop { match &*inner { Done => { *inner = Empty; return; } - Task(_) | Working => self.flag.wait(&mut inner), + Task(_) | Working => inner = self.flag.wait(inner).unwrap(), Empty | Abandoned => { unreachable!("awaited completion from inactive worker") } @@ -526,7 +537,7 @@ impl Mailbox { mod tests { use super::*; - use crate::types::PyDict; + use crate::types::{dict::PyDictMethods, PyDict}; #[test] fn test_intern() { @@ -535,7 +546,7 @@ mod tests { let foo2 = intern!(py, "foo"); let foo3 = intern!(py, stringify!(foo)); - let dict = PyDict::new(py); + let dict = PyDict::new_bound(py); dict.set_item(foo1, 42_usize).unwrap(); assert!(dict.contains(foo2).unwrap()); assert_eq!( diff --git a/src/tests/common.rs b/src/tests/common.rs index 89b4a83fa1d..e1f2e7dfc28 100644 --- a/src/tests/common.rs +++ b/src/tests/common.rs @@ -23,19 +23,26 @@ mod inner { }; } + #[macro_export] + macro_rules! assert_py_eq { + ($val:expr, $expected:expr) => { + assert!($val.eq($expected).unwrap()); + }; + } + #[macro_export] macro_rules! py_expect_exception { // Case1: idents & no err_msg ($py:expr, $($val:ident)+, $code:expr, $err:ident) => {{ use pyo3::types::IntoPyDict; - let d = [$((stringify!($val), $val.to_object($py)),)+].into_py_dict($py); + let d = [$((stringify!($val), $val.to_object($py)),)+].into_py_dict_bound($py); py_expect_exception!($py, *d, $code, $err) }}; // Case2: dict & no err_msg ($py:expr, *$dict:expr, $code:expr, $err:ident) => {{ - let res = $py.run($code, None, Some($dict)); + let res = $py.run_bound($code, None, Some(&$dict.as_borrowed())); let err = res.expect_err(&format!("Did not raise {}", stringify!($err))); - if !err.matches($py, $py.get_type::()) { + if !err.matches($py, $py.get_type_bound::()) { panic!("Expected {} but got {:?}", stringify!($err), err) } err @@ -66,8 +73,8 @@ mod inner { #[cfg(all(feature = "macros", Py_3_8))] #[pymethods(crate = "pyo3")] impl UnraisableCapture { - pub fn hook(&mut self, unraisable: &PyAny) { - let err = PyErr::from_value(unraisable.getattr("exc_value").unwrap()); + pub fn hook(&mut self, unraisable: Bound<'_, PyAny>) { + let err = PyErr::from_value_bound(unraisable.getattr("exc_value").unwrap()); let instance = unraisable.getattr("object").unwrap(); self.capture = Some((err, instance.into())); } @@ -76,7 +83,7 @@ mod inner { #[cfg(all(feature = "macros", Py_3_8))] impl UnraisableCapture { pub fn install(py: Python<'_>) -> Py { - let sys = py.import("sys").unwrap(); + let sys = py.import_bound("sys").unwrap(); let old_hook = sys.getattr("unraisablehook").unwrap().into(); let capture = Py::new( @@ -97,23 +104,28 @@ mod inner { pub fn uninstall(&mut self, py: Python<'_>) { let old_hook = self.old_hook.take().unwrap(); - let sys = py.import("sys").unwrap(); + let sys = py.import_bound("sys").unwrap(); sys.setattr("unraisablehook", old_hook).unwrap(); } } pub struct CatchWarnings<'py> { - catch_warnings: &'py PyAny, + catch_warnings: Bound<'py, PyAny>, } impl<'py> CatchWarnings<'py> { - pub fn enter(py: Python<'py>, f: impl FnOnce(&PyList) -> PyResult) -> PyResult { - let warnings = py.import("warnings")?; - let kwargs = [("record", true)].into_py_dict(py); - let catch_warnings = warnings.getattr("catch_warnings")?.call((), Some(kwargs))?; - let list = catch_warnings.call_method0("__enter__")?.extract()?; + pub fn enter( + py: Python<'py>, + f: impl FnOnce(&Bound<'py, PyList>) -> PyResult, + ) -> PyResult { + let warnings = py.import_bound("warnings")?; + let kwargs = [("record", true)].into_py_dict_bound(py); + let catch_warnings = warnings + .getattr("catch_warnings")? + .call((), Some(&kwargs))?; + let list = catch_warnings.call_method0("__enter__")?.downcast_into()?; let _guard = Self { catch_warnings }; - f(list) + f(&list) } } @@ -130,12 +142,13 @@ mod inner { macro_rules! assert_warnings { ($py:expr, $body:expr, [$(($category:ty, $message:literal)),+] $(,)? ) => {{ $crate::tests::common::CatchWarnings::enter($py, |w| { + use $crate::types::{PyListMethods, PyStringMethods}; $body; - let expected_warnings = [$((<$category as $crate::type_object::PyTypeInfo>::type_object($py), $message)),+]; + let expected_warnings = [$((<$category as $crate::type_object::PyTypeInfo>::type_object_bound($py), $message)),+]; assert_eq!(w.len(), expected_warnings.len()); for (warning, (category, message)) in w.iter().zip(expected_warnings) { - assert!(warning.getattr("category").unwrap().is(category)); + assert!(warning.getattr("category").unwrap().is(&category)); assert_eq!( warning.getattr("message").unwrap().str().unwrap().to_string_lossy(), message diff --git a/src/tests/hygiene/misc.rs b/src/tests/hygiene/misc.rs index 2e7d3e6f9ee..7a2f58818a1 100644 --- a/src/tests/hygiene/misc.rs +++ b/src/tests/hygiene/misc.rs @@ -2,7 +2,7 @@ #[derive(crate::FromPyObject)] #[pyo3(crate = "crate")] -struct Derive1(i32); // newtype case +struct Derive1(#[allow(dead_code)] i32); // newtype case #[derive(crate::FromPyObject)] #[pyo3(crate = "crate")] @@ -14,6 +14,7 @@ struct Derive2(i32, i32); // tuple case #[allow(dead_code)] struct Derive3 { f: i32, + #[pyo3(item(42))] g: i32, } // struct case @@ -40,7 +41,10 @@ fn append_to_inittab() { #[crate::pymodule] #[pyo3(crate = "crate")] #[allow(clippy::unnecessary_wraps)] - fn module_for_inittab(_: crate::Python<'_>, _: &crate::types::PyModule) -> crate::PyResult<()> { + fn module_for_inittab( + _: crate::Python<'_>, + _: &crate::Bound<'_, crate::types::PyModule>, + ) -> crate::PyResult<()> { ::std::result::Result::Ok(()) } crate::append_to_inittab!(module_for_inittab); diff --git a/src/tests/hygiene/pyclass.rs b/src/tests/hygiene/pyclass.rs index 4d07009cad6..34b30a8c6f4 100644 --- a/src/tests/hygiene/pyclass.rs +++ b/src/tests/hygiene/pyclass.rs @@ -25,7 +25,7 @@ pub struct Bar { a: u8, #[pyo3(get, set)] b: Foo, - #[pyo3(get, set)] + #[pyo3(set)] c: ::std::option::Option>, } @@ -39,11 +39,11 @@ pub enum Enum { #[pyo3(crate = "crate")] pub struct Foo3 { #[pyo3(get, set)] - #[cfg(FALSE)] + #[cfg(any())] field: i32, #[pyo3(get, set)] - #[cfg(not(FALSE))] + #[cfg(not(any()))] field: u32, } @@ -51,11 +51,11 @@ pub struct Foo3 { #[pyo3(crate = "crate")] pub struct Foo4 { #[pyo3(get, set)] - #[cfg(FALSE)] - #[cfg(not(FALSE))] + #[cfg(any())] + #[cfg(not(any()))] field: i32, #[pyo3(get, set)] - #[cfg(not(FALSE))] + #[cfg(not(any()))] field: u32, } diff --git a/src/tests/hygiene/pyfunction.rs b/src/tests/hygiene/pyfunction.rs index 19fe2739407..c1bca213933 100644 --- a/src/tests/hygiene/pyfunction.rs +++ b/src/tests/hygiene/pyfunction.rs @@ -8,9 +8,19 @@ fn do_something(x: i32) -> crate::PyResult { } #[test] +#[cfg(feature = "gil-refs")] fn invoke_wrap_pyfunction() { crate::Python::with_gil(|py| { + #[allow(deprecated)] let func = crate::wrap_pyfunction!(do_something)(py).unwrap(); crate::py_run!(py, func, r#"func(5)"#); }); } + +#[test] +fn invoke_wrap_pyfunction_bound() { + crate::Python::with_gil(|py| { + let func = crate::wrap_pyfunction_bound!(do_something, py).unwrap(); + crate::py_run!(py, func, r#"func(5)"#); + }); +} diff --git a/src/tests/hygiene/pymethods.rs b/src/tests/hygiene/pymethods.rs index 8e5bce8eefe..95d670c63a6 100644 --- a/src/tests/hygiene/pymethods.rs +++ b/src/tests/hygiene/pymethods.rs @@ -9,7 +9,6 @@ pub struct Dummy; #[pyo3(crate = "crate")] pub struct DummyIter; -#[cfg(Py_3_8)] #[crate::pymethods] #[pyo3(crate = "crate")] impl Dummy { @@ -24,12 +23,12 @@ impl Dummy { "Dummy" } - fn __bytes__<'py>(&self, py: crate::Python<'py>) -> &'py crate::types::PyBytes { - crate::types::PyBytes::new(py, &[0]) + fn __bytes__<'py>(&self, py: crate::Python<'py>) -> crate::Bound<'py, crate::types::PyBytes> { + crate::types::PyBytes::new_bound(py, &[0]) } fn __format__(&self, format_spec: ::std::string::String) -> ::std::string::String { - ::std::panic!("unimplemented isn't hygienic before 1.50") + ::std::unimplemented!() } fn __lt__(&self, other: &Self) -> bool { @@ -64,20 +63,20 @@ impl Dummy { // Customizing attribute access ////////////////////// - fn __getattr__(&self, name: ::std::string::String) -> &crate::PyAny { - ::std::panic!("unimplemented isn't hygienic before 1.50") + fn __getattr__(&self, name: ::std::string::String) -> &crate::Bound<'_, crate::PyAny> { + ::std::unimplemented!() } - fn __getattribute__(&self, name: ::std::string::String) -> &crate::PyAny { - ::std::panic!("unimplemented isn't hygienic before 1.50") + fn __getattribute__(&self, name: ::std::string::String) -> &crate::Bound<'_, crate::PyAny> { + ::std::unimplemented!() } fn __setattr__(&mut self, name: ::std::string::String, value: ::std::string::String) {} fn __delattr__(&mut self, name: ::std::string::String) {} - fn __dir__<'py>(&self, py: crate::Python<'py>) -> &'py crate::types::PyList { - crate::types::PyList::new(py, ::std::vec![0_u8]) + fn __dir__<'py>(&self, py: crate::Python<'py>) -> crate::Bound<'py, crate::types::PyList> { + crate::types::PyList::new_bound(py, ::std::vec![0_u8]) } ////////////////////// @@ -86,411 +85,28 @@ impl Dummy { fn __get__( &self, - instance: &crate::PyAny, - owner: &crate::PyAny, - ) -> crate::PyResult<&crate::PyAny> { - ::std::panic!("unimplemented isn't hygienic before 1.50") + instance: &crate::Bound<'_, crate::PyAny>, + owner: &crate::Bound<'_, crate::PyAny>, + ) -> crate::PyResult<&crate::Bound<'_, crate::PyAny>> { + ::std::unimplemented!() } - fn __set__(&self, instance: &crate::PyAny, owner: &crate::PyAny) {} - - fn __delete__(&self, instance: &crate::PyAny) {} - - fn __set_name__(&self, owner: &crate::PyAny, name: &crate::PyAny) {} - - ////////////////////// - // Implementing Descriptors - ////////////////////// - - fn __len__(&self) -> usize { - 0 - } - - fn __getitem__(&self, key: u32) -> crate::PyResult { - ::std::result::Result::Err(crate::exceptions::PyKeyError::new_err("boo")) - } - - fn __setitem__(&self, key: u32, value: u32) {} - - fn __delitem__(&self, key: u32) {} - - fn __iter__(_: crate::pycell::PyRef<'_, Self>, py: crate::Python<'_>) -> crate::Py { - crate::Py::new(py, DummyIter {}).unwrap() - } - - fn __next__(&mut self) -> ::std::option::Option<()> { - ::std::option::Option::None - } - - fn __reversed__( - slf: crate::pycell::PyRef<'_, Self>, - py: crate::Python<'_>, - ) -> crate::Py { - crate::Py::new(py, DummyIter {}).unwrap() - } - - fn __contains__(&self, item: u32) -> bool { - false - } - - ////////////////////// - // Emulating numeric types - ////////////////////// - - fn __add__(&self, other: &Self) -> Dummy { - Dummy {} - } - - fn __sub__(&self, other: &Self) -> Dummy { - Dummy {} - } - - fn __mul__(&self, other: &Self) -> Dummy { - Dummy {} - } - - fn __truediv__(&self, _other: &Self) -> crate::PyResult<()> { - ::std::result::Result::Err(crate::exceptions::PyZeroDivisionError::new_err("boo")) - } - - fn __floordiv__(&self, _other: &Self) -> crate::PyResult<()> { - ::std::result::Result::Err(crate::exceptions::PyZeroDivisionError::new_err("boo")) - } - - fn __mod__(&self, _other: &Self) -> u32 { - 0 - } - - fn __divmod__(&self, _other: &Self) -> (u32, u32) { - (0, 0) - } - - fn __pow__(&self, _other: &Self, modulo: ::std::option::Option) -> Dummy { - Dummy {} - } - - fn __lshift__(&self, other: &Self) -> Dummy { - Dummy {} - } - - fn __rshift__(&self, other: &Self) -> Dummy { - Dummy {} - } - - fn __and__(&self, other: &Self) -> Dummy { - Dummy {} - } - - fn __xor__(&self, other: &Self) -> Dummy { - Dummy {} - } - - fn __or__(&self, other: &Self) -> Dummy { - Dummy {} - } - - fn __radd__(&self, other: &Self) -> Dummy { - Dummy {} - } - - fn __rrsub__(&self, other: &Self) -> Dummy { - Dummy {} - } - - fn __rmul__(&self, other: &Self) -> Dummy { - Dummy {} - } - - fn __rtruediv__(&self, _other: &Self) -> crate::PyResult<()> { - ::std::result::Result::Err(crate::exceptions::PyZeroDivisionError::new_err("boo")) - } - - fn __rfloordiv__(&self, _other: &Self) -> crate::PyResult<()> { - ::std::result::Result::Err(crate::exceptions::PyZeroDivisionError::new_err("boo")) - } - - fn __rmod__(&self, _other: &Self) -> u32 { - 0 - } - - fn __rdivmod__(&self, _other: &Self) -> (u32, u32) { - (0, 0) - } - - fn __rpow__(&self, _other: &Self, modulo: ::std::option::Option) -> Dummy { - Dummy {} - } - - fn __rlshift__(&self, other: &Self) -> Dummy { - Dummy {} - } - - fn __rrshift__(&self, other: &Self) -> Dummy { - Dummy {} - } - - fn __rand__(&self, other: &Self) -> Dummy { - Dummy {} - } - - fn __rxor__(&self, other: &Self) -> Dummy { - Dummy {} - } - - fn __ror__(&self, other: &Self) -> Dummy { - Dummy {} - } - - fn __iadd__(&mut self, other: &Self) {} - - fn __irsub__(&mut self, other: &Self) {} - - fn __imul__(&mut self, other: &Self) {} - - fn __itruediv__(&mut self, _other: &Self) {} - - fn __ifloordiv__(&mut self, _other: &Self) {} - - fn __imod__(&mut self, _other: &Self) {} - - fn __ipow__(&mut self, _other: &Self, modulo: ::std::option::Option) {} - - fn __ilshift__(&mut self, other: &Self) {} - - fn __irshift__(&mut self, other: &Self) {} - - fn __iand__(&mut self, other: &Self) {} - - fn __ixor__(&mut self, other: &Self) {} - - fn __ior__(&mut self, other: &Self) {} - - fn __neg__(slf: crate::pycell::PyRef<'_, Self>) -> crate::pycell::PyRef<'_, Self> { - slf - } - - fn __pos__(slf: crate::pycell::PyRef<'_, Self>) -> crate::pycell::PyRef<'_, Self> { - slf - } - - fn __abs__(slf: crate::pycell::PyRef<'_, Self>) -> crate::pycell::PyRef<'_, Self> { - slf - } - - fn __invert__(slf: crate::pycell::PyRef<'_, Self>) -> crate::pycell::PyRef<'_, Self> { - slf - } - - fn __complex__<'py>(&self, py: crate::Python<'py>) -> &'py crate::types::PyComplex { - crate::types::PyComplex::from_doubles(py, 0.0, 0.0) - } - - fn __int__(&self) -> u32 { - 0 - } - - fn __float__(&self) -> f64 { - 0.0 - } - - fn __index__(&self) -> u32 { - 0 - } - - fn __round__(&self, ndigits: ::std::option::Option) -> u32 { - 0 - } - - fn __trunc__(&self) -> u32 { - 0 - } - - fn __floor__(&self) -> u32 { - 0 - } - - fn __ceil__(&self) -> u32 { - 0 - } - - ////////////////////// - // With Statement Context Managers - ////////////////////// - - fn __enter__(&mut self) {} - - fn __exit__( - &mut self, - exc_type: &crate::PyAny, - exc_value: &crate::PyAny, - traceback: &crate::PyAny, - ) { - } - - ////////////////////// - // Awaitable Objects - ////////////////////// - - fn __await__(slf: crate::pycell::PyRef<'_, Self>) -> crate::pycell::PyRef<'_, Self> { - slf - } - - ////////////////////// - - // Asynchronous Iterators - ////////////////////// - - fn __aiter__( - slf: crate::pycell::PyRef<'_, Self>, - py: crate::Python<'_>, - ) -> crate::Py { - crate::Py::new(py, DummyIter {}).unwrap() - } - - fn __anext__(&mut self) -> ::std::option::Option<()> { - ::std::option::Option::None - } - - ////////////////////// - // Asynchronous Context Managers - ////////////////////// - - fn __aenter__(&mut self) {} - - fn __aexit__( - &mut self, - exc_type: &crate::PyAny, - exc_value: &crate::PyAny, - traceback: &crate::PyAny, - ) { - } - - // Things with attributes - - #[pyo3(signature = (_y, *, _z=2))] - fn test(&self, _y: &Dummy, _z: i32) {} - #[staticmethod] - fn staticmethod() {} - #[classmethod] - fn clsmethod(_: &crate::types::PyType) {} - #[pyo3(signature = (*_args, **_kwds))] - fn __call__( + fn __set__( &self, - _args: &crate::types::PyTuple, - _kwds: ::std::option::Option<&crate::types::PyDict>, - ) -> crate::PyResult { - ::std::panic!("unimplemented isn't hygienic before 1.50") - } - #[new] - fn new(a: u8) -> Self { - Dummy {} - } - #[getter] - fn get(&self) -> i32 { - 0 - } - #[setter] - fn set(&mut self, _v: i32) {} - #[classattr] - fn class_attr() -> i32 { - 0 - } - - // Dunder methods invented for protocols - - // PyGcProtocol - // Buffer protocol? -} - -#[cfg(not(Py_3_8))] -#[crate::pymethods] -#[pyo3(crate = "crate")] -impl Dummy { - ////////////////////// - // Basic customization - ////////////////////// - fn __repr__(&self) -> &'static str { - "Dummy" - } - - fn __str__(&self) -> &'static str { - "Dummy" - } - - fn __bytes__<'py>(&self, py: crate::Python<'py>) -> &'py crate::types::PyBytes { - crate::types::PyBytes::new(py, &[0]) - } - - fn __format__(&self, format_spec: ::std::string::String) -> ::std::string::String { - ::std::panic!("unimplemented isn't hygienic before 1.50") - } - - fn __lt__(&self, other: &Self) -> bool { - false - } - - fn __le__(&self, other: &Self) -> bool { - false - } - fn __eq__(&self, other: &Self) -> bool { - false - } - fn __ne__(&self, other: &Self) -> bool { - false - } - fn __gt__(&self, other: &Self) -> bool { - false - } - fn __ge__(&self, other: &Self) -> bool { - false - } - - fn __hash__(&self) -> u64 { - 42 - } - - fn __bool__(&self) -> bool { - true - } - - ////////////////////// - // Customizing attribute access - ////////////////////// - - fn __getattr__(&self, name: ::std::string::String) -> &crate::PyAny { - ::std::panic!("unimplemented isn't hygienic before 1.50") - } - - fn __getattribute__(&self, name: ::std::string::String) -> &crate::PyAny { - ::std::panic!("unimplemented isn't hygienic before 1.50") - } - - fn __setattr__(&mut self, name: ::std::string::String, value: ::std::string::String) {} - - fn __delattr__(&mut self, name: ::std::string::String) {} - - fn __dir__<'py>(&self, py: crate::Python<'py>) -> &'py crate::types::PyList { - crate::types::PyList::new(py, ::std::vec![0_u8]) + instance: &crate::Bound<'_, crate::PyAny>, + owner: &crate::Bound<'_, crate::PyAny>, + ) { } - ////////////////////// - // Implementing Descriptors - ////////////////////// + fn __delete__(&self, instance: &crate::Bound<'_, crate::PyAny>) {} - fn __get__( + fn __set_name__( &self, - instance: &crate::PyAny, - owner: &crate::PyAny, - ) -> crate::PyResult<&crate::PyAny> { - ::std::panic!("unimplemented isn't hygienic before 1.50") + owner: &crate::Bound<'_, crate::PyAny>, + name: &crate::Bound<'_, crate::PyAny>, + ) { } - fn __set__(&self, instance: &crate::PyAny, owner: &crate::PyAny) {} - - fn __delete__(&self, instance: &crate::PyAny) {} - - fn __set_name__(&self, owner: &crate::PyAny, name: &crate::PyAny) {} - ////////////////////// // Implementing Descriptors ////////////////////// @@ -646,7 +262,8 @@ impl Dummy { fn __imod__(&mut self, _other: &Self) {} - fn __ipow__(&mut self, _other: &Self, _modulo: ::std::option::Option) {} + fn __ipow__(&mut self, _other: &Self, modulo: ::std::option::Option) {} + fn __ilshift__(&mut self, other: &Self) {} fn __irshift__(&mut self, other: &Self) {} @@ -673,8 +290,11 @@ impl Dummy { slf } - fn __complex__<'py>(&self, py: crate::Python<'py>) -> &'py crate::types::PyComplex { - crate::types::PyComplex::from_doubles(py, 0.0, 0.0) + fn __complex__<'py>( + &self, + py: crate::Python<'py>, + ) -> crate::Bound<'py, crate::types::PyComplex> { + crate::types::PyComplex::from_doubles_bound(py, 0.0, 0.0) } fn __int__(&self) -> u32 { @@ -689,6 +309,7 @@ impl Dummy { 0 } + #[pyo3(signature=(ndigits=::std::option::Option::None))] fn __round__(&self, ndigits: ::std::option::Option) -> u32 { 0 } @@ -713,9 +334,9 @@ impl Dummy { fn __exit__( &mut self, - exc_type: &crate::PyAny, - exc_value: &crate::PyAny, - traceback: &crate::PyAny, + exc_type: &crate::Bound<'_, crate::PyAny>, + exc_value: &crate::Bound<'_, crate::PyAny>, + traceback: &crate::Bound<'_, crate::PyAny>, ) { } @@ -751,9 +372,9 @@ impl Dummy { fn __aexit__( &mut self, - exc_type: &crate::PyAny, - exc_value: &crate::PyAny, - traceback: &crate::PyAny, + exc_type: &crate::Bound<'_, crate::PyAny>, + exc_value: &crate::Bound<'_, crate::PyAny>, + traceback: &crate::Bound<'_, crate::PyAny>, ) { } @@ -764,14 +385,14 @@ impl Dummy { #[staticmethod] fn staticmethod() {} #[classmethod] - fn clsmethod(_: &crate::types::PyType) {} + fn clsmethod(_: &crate::Bound<'_, crate::types::PyType>) {} #[pyo3(signature = (*_args, **_kwds))] fn __call__( &self, - _args: &crate::types::PyTuple, - _kwds: ::std::option::Option<&crate::types::PyDict>, + _args: &crate::Bound<'_, crate::types::PyTuple>, + _kwds: ::std::option::Option<&crate::Bound<'_, crate::types::PyDict>>, ) -> crate::PyResult { - ::std::panic!("unimplemented isn't hygienic before 1.50") + ::std::unimplemented!() } #[new] fn new(a: u8) -> Self { diff --git a/src/tests/hygiene/pymodule.rs b/src/tests/hygiene/pymodule.rs index 0b37c440325..91f9808bcc4 100644 --- a/src/tests/hygiene/pymodule.rs +++ b/src/tests/hygiene/pymodule.rs @@ -7,12 +7,25 @@ fn do_something(x: i32) -> crate::PyResult { ::std::result::Result::Ok(x) } +#[cfg(feature = "gil-refs")] +#[allow(deprecated)] #[crate::pymodule] #[pyo3(crate = "crate")] fn foo(_py: crate::Python<'_>, _m: &crate::types::PyModule) -> crate::PyResult<()> { ::std::result::Result::Ok(()) } +#[crate::pymodule] +#[pyo3(crate = "crate")] +fn foo_bound( + _py: crate::Python<'_>, + _m: &crate::Bound<'_, crate::types::PyModule>, +) -> crate::PyResult<()> { + ::std::result::Result::Ok(()) +} + +#[cfg(feature = "gil-refs")] +#[allow(deprecated)] #[crate::pymodule] #[pyo3(crate = "crate")] fn my_module(_py: crate::Python<'_>, m: &crate::types::PyModule) -> crate::PyResult<()> { @@ -21,3 +34,18 @@ fn my_module(_py: crate::Python<'_>, m: &crate::types::PyModule) -> crate::PyRes ::std::result::Result::Ok(()) } + +#[crate::pymodule] +#[pyo3(crate = "crate")] +fn my_module_bound(m: &crate::Bound<'_, crate::types::PyModule>) -> crate::PyResult<()> { + as crate::types::PyModuleMethods>::add_function( + m, + crate::wrap_pyfunction_bound!(do_something, m)?, + )?; + as crate::types::PyModuleMethods>::add_wrapped( + m, + crate::wrap_pymodule!(foo_bound), + )?; + + ::std::result::Result::Ok(()) +} diff --git a/src/type_object.rs b/src/type_object.rs index e7d88f76c18..871e8366865 100644 --- a/src/type_object.rs +++ b/src/type_object.rs @@ -1,10 +1,14 @@ //! Python type object information +use crate::ffi_ptr_ext::FfiPtrExt; +use crate::types::any::PyAnyMethods; use crate::types::{PyAny, PyType}; -use crate::{ffi, PyNativeType, Python}; +#[cfg(feature = "gil-refs")] +use crate::PyNativeType; +use crate::{ffi, Bound, Python}; /// `T: PyLayout` represents that `T` is a concrete representation of `U` in the Python heap. -/// E.g., `PyCell` is a concrete representation of all `pyclass`es, and `ffi::PyObject` +/// E.g., `PyClassObject` is a concrete representation of all `pyclass`es, and `ffi::PyObject` /// is of `PyAny`. /// /// This trait is intended to be used internally. @@ -27,11 +31,13 @@ pub trait PySizedLayout: PyLayout + Sized {} /// /// - `Py::as_ref` will hand out references to `Self::AsRefTarget`. /// - `Self::AsRefTarget` must have the same layout as `UnsafeCell`. +#[cfg(feature = "gil-refs")] pub unsafe trait HasPyGilRef { /// Utility type to make Py::as_ref work. type AsRefTarget: PyNativeType; } +#[cfg(feature = "gil-refs")] unsafe impl HasPyGilRef for T where T: PyNativeType, @@ -52,6 +58,7 @@ where /// /// Implementations must provide an implementation for `type_object_raw` which infallibly produces a /// non-null pointer to the corresponding Python type object. +#[cfg(feature = "gil-refs")] pub unsafe trait PyTypeInfo: Sized + HasPyGilRef { /// Class name. const NAME: &'static str; @@ -64,24 +71,128 @@ pub unsafe trait PyTypeInfo: Sized + HasPyGilRef { /// Returns the safe abstraction over the type object. #[inline] + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`PyTypeInfo::type_object` will be replaced by `PyTypeInfo::type_object_bound` in a future PyO3 version" + )] fn type_object(py: Python<'_>) -> &PyType { - unsafe { py.from_borrowed_ptr(Self::type_object_raw(py) as _) } + // This isn't implemented in terms of `type_object_bound` because this just borrowed the + // object, for legacy reasons. + #[allow(deprecated)] + unsafe { + py.from_borrowed_ptr(Self::type_object_raw(py) as _) + } + } + + /// Returns the safe abstraction over the type object. + #[inline] + fn type_object_bound(py: Python<'_>) -> Bound<'_, PyType> { + // Making the borrowed object `Bound` is necessary for soundness reasons. It's an extreme + // edge case, but arbitrary Python code _could_ change the __class__ of an object and cause + // the type object to be freed. + // + // By making `Bound` we assume ownership which is then safe against races. + unsafe { + Self::type_object_raw(py) + .cast::() + .assume_borrowed_unchecked(py) + .to_owned() + .downcast_into_unchecked() + } } /// Checks if `object` is an instance of this type or a subclass of this type. #[inline] + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`PyTypeInfo::is_type_of` will be replaced by `PyTypeInfo::is_type_of_bound` in a future PyO3 version" + )] fn is_type_of(object: &PyAny) -> bool { + Self::is_type_of_bound(&object.as_borrowed()) + } + + /// Checks if `object` is an instance of this type or a subclass of this type. + #[inline] + fn is_type_of_bound(object: &Bound<'_, PyAny>) -> bool { unsafe { ffi::PyObject_TypeCheck(object.as_ptr(), Self::type_object_raw(object.py())) != 0 } } /// Checks if `object` is an instance of this type. #[inline] + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`PyTypeInfo::is_exact_type_of` will be replaced by `PyTypeInfo::is_exact_type_of_bound` in a future PyO3 version" + )] fn is_exact_type_of(object: &PyAny) -> bool { + Self::is_exact_type_of_bound(&object.as_borrowed()) + } + + /// Checks if `object` is an instance of this type. + #[inline] + fn is_exact_type_of_bound(object: &Bound<'_, PyAny>) -> bool { + unsafe { ffi::Py_TYPE(object.as_ptr()) == Self::type_object_raw(object.py()) } + } +} + +/// Python type information. +/// All Python native types (e.g., `PyDict`) and `#[pyclass]` structs implement this trait. +/// +/// This trait is marked unsafe because: +/// - specifying the incorrect layout can lead to memory errors +/// - the return value of type_object must always point to the same PyTypeObject instance +/// +/// It is safely implemented by the `pyclass` macro. +/// +/// # Safety +/// +/// Implementations must provide an implementation for `type_object_raw` which infallibly produces a +/// non-null pointer to the corresponding Python type object. +#[cfg(not(feature = "gil-refs"))] +pub unsafe trait PyTypeInfo: Sized { + /// Class name. + const NAME: &'static str; + + /// Module name, if any. + const MODULE: Option<&'static str>; + + /// Returns the PyTypeObject instance for this type. + fn type_object_raw(py: Python<'_>) -> *mut ffi::PyTypeObject; + + /// Returns the safe abstraction over the type object. + #[inline] + fn type_object_bound(py: Python<'_>) -> Bound<'_, PyType> { + // Making the borrowed object `Bound` is necessary for soundness reasons. It's an extreme + // edge case, but arbitrary Python code _could_ change the __class__ of an object and cause + // the type object to be freed. + // + // By making `Bound` we assume ownership which is then safe against races. + unsafe { + Self::type_object_raw(py) + .cast::() + .assume_borrowed_unchecked(py) + .to_owned() + .downcast_into_unchecked() + } + } + + /// Checks if `object` is an instance of this type or a subclass of this type. + #[inline] + fn is_type_of_bound(object: &Bound<'_, PyAny>) -> bool { + unsafe { ffi::PyObject_TypeCheck(object.as_ptr(), Self::type_object_raw(object.py())) != 0 } + } + + /// Checks if `object` is an instance of this type. + #[inline] + fn is_exact_type_of_bound(object: &Bound<'_, PyAny>) -> bool { unsafe { ffi::Py_TYPE(object.as_ptr()) == Self::type_object_raw(object.py()) } } } /// Implemented by types which can be used as a concrete Python type inside `Py` smart pointers. +#[cfg(feature = "gil-refs")] pub trait PyTypeCheck: HasPyGilRef { /// Name of self. This is used in error messages, for example. const NAME: &'static str; @@ -89,7 +200,19 @@ pub trait PyTypeCheck: HasPyGilRef { /// Checks if `object` is an instance of `Self`, which may include a subtype. /// /// This should be equivalent to the Python expression `isinstance(object, Self)`. - fn type_check(object: &PyAny) -> bool; + fn type_check(object: &Bound<'_, PyAny>) -> bool; +} + +/// Implemented by types which can be used as a concrete Python type inside `Py` smart pointers. +#[cfg(not(feature = "gil-refs"))] +pub trait PyTypeCheck { + /// Name of self. This is used in error messages, for example. + const NAME: &'static str; + + /// Checks if `object` is an instance of `Self`, which may include a subtype. + /// + /// This should be equivalent to the Python expression `isinstance(object, Self)`. + fn type_check(object: &Bound<'_, PyAny>) -> bool; } impl PyTypeCheck for T @@ -99,8 +222,8 @@ where const NAME: &'static str = ::NAME; #[inline] - fn type_check(object: &PyAny) -> bool { - ::is_type_of(object) + fn type_check(object: &Bound<'_, PyAny>) -> bool { + T::is_type_of_bound(object) } } diff --git a/src/types/any.rs b/src/types/any.rs index 2e8a518fad4..ba5ea01b1a3 100644 --- a/src/types/any.rs +++ b/src/types/any.rs @@ -1,15 +1,17 @@ use crate::class::basic::CompareOp; -use crate::conversion::{AsPyPointer, FromPyObject, IntoPy, ToPyObject}; -use crate::err::{DowncastError, DowncastIntoError, PyDowncastError, PyErr, PyResult}; +use crate::conversion::{AsPyPointer, FromPyObjectBound, IntoPy, ToPyObject}; +use crate::err::{DowncastError, DowncastIntoError, PyErr, PyResult}; use crate::exceptions::{PyAttributeError, PyTypeError}; use crate::ffi_ptr_ext::FfiPtrExt; use crate::instance::Bound; use crate::py_result_ext::PyResultExt; -use crate::type_object::{HasPyGilRef, PyTypeCheck, PyTypeInfo}; -#[cfg(not(PyPy))] +use crate::type_object::{PyTypeCheck, PyTypeInfo}; +#[cfg(not(any(PyPy, GraalPy)))] use crate::types::PySuper; use crate::types::{PyDict, PyIterator, PyList, PyString, PyTuple, PyType}; -use crate::{err, ffi, Py, PyNativeType, Python}; +use crate::{err, ffi, Py, Python}; +#[cfg(feature = "gil-refs")] +use crate::{err::PyDowncastError, type_object::HasPyGilRef, PyNativeType}; use std::cell::UnsafeCell; use std::cmp::Ordering; use std::os::raw::c_int; @@ -66,6 +68,7 @@ pyobject_native_type_extract!(PyAny); pyobject_native_type_sized!(PyAny, ffi::PyObject); +#[cfg(feature = "gil-refs")] impl PyAny { /// Returns whether `self` and `other` point to the same object. To compare /// the equality of two objects (the `==` operator), use [`eq`](PyAny::eq). @@ -73,7 +76,7 @@ impl PyAny { /// This is equivalent to the Python expression `self is other`. #[inline] pub fn is(&self, other: &T) -> bool { - Bound::borrowed_from_gil_ref(&self).is(other) + self.as_borrowed().is(other) } /// Determines whether this object has the given attribute. @@ -86,23 +89,23 @@ impl PyAny { /// # Example: `intern!`ing the attribute name /// /// ``` - /// # use pyo3::{intern, pyfunction, types::PyModule, Python, PyResult}; + /// # use pyo3::{prelude::*, intern}; /// # /// #[pyfunction] - /// fn has_version(sys: &PyModule) -> PyResult { + /// fn has_version(sys: &Bound<'_, PyModule>) -> PyResult { /// sys.hasattr(intern!(sys.py(), "version")) /// } /// # /// # Python::with_gil(|py| { - /// # let sys = py.import("sys").unwrap(); - /// # has_version(sys).unwrap(); + /// # let sys = py.import_bound("sys").unwrap(); + /// # has_version(&sys).unwrap(); /// # }); /// ``` pub fn hasattr(&self, attr_name: N) -> PyResult where N: IntoPy>, { - Bound::borrowed_from_gil_ref(&self).hasattr(attr_name) + self.as_borrowed().hasattr(attr_name) } /// Retrieves an attribute value. @@ -115,72 +118,27 @@ impl PyAny { /// # Example: `intern!`ing the attribute name /// /// ``` - /// # use pyo3::{intern, pyfunction, types::PyModule, PyAny, Python, PyResult}; + /// # use pyo3::{prelude::*, intern}; /// # /// #[pyfunction] - /// fn version(sys: &PyModule) -> PyResult<&PyAny> { + /// fn version<'py>(sys: &Bound<'py, PyModule>) -> PyResult> { /// sys.getattr(intern!(sys.py(), "version")) /// } /// # /// # Python::with_gil(|py| { - /// # let sys = py.import("sys").unwrap(); - /// # version(sys).unwrap(); + /// # let sys = py.import_bound("sys").unwrap(); + /// # version(&sys).unwrap(); /// # }); /// ``` pub fn getattr(&self, attr_name: N) -> PyResult<&PyAny> where N: IntoPy>, { - Bound::borrowed_from_gil_ref(&self) + self.as_borrowed() .getattr(attr_name) .map(Bound::into_gil_ref) } - /// Retrieve an attribute value, skipping the instance dictionary during the lookup but still - /// binding the object to the instance. - /// - /// This is useful when trying to resolve Python's "magic" methods like `__getitem__`, which - /// are looked up starting from the type object. This returns an `Option` as it is not - /// typically a direct error for the special lookup to fail, as magic methods are optional in - /// many situations in which they might be called. - /// - /// To avoid repeated temporary allocations of Python strings, the [`intern!`] macro can be used - /// to intern `attr_name`. - #[allow(dead_code)] // Currently only used with num-complex+abi3, so dead without that. - pub(crate) fn lookup_special(&self, attr_name: N) -> PyResult> - where - N: IntoPy>, - { - let py = self.py(); - let self_type = self.get_type(); - let attr = if let Ok(attr) = self_type.getattr(attr_name) { - attr - } else { - return Ok(None); - }; - - // Manually resolve descriptor protocol. - if cfg!(Py_3_10) - || unsafe { ffi::PyType_HasFeature(attr.get_type_ptr(), ffi::Py_TPFLAGS_HEAPTYPE) } != 0 - { - // This is the preferred faster path, but does not work on static types (generally, - // types defined in extension modules) before Python 3.10. - unsafe { - let descr_get_ptr = ffi::PyType_GetSlot(attr.get_type_ptr(), ffi::Py_tp_descr_get); - if descr_get_ptr.is_null() { - return Ok(Some(attr)); - } - let descr_get: ffi::descrgetfunc = std::mem::transmute(descr_get_ptr); - let ret = descr_get(attr.as_ptr(), self.as_ptr(), self_type.as_ptr()); - py.from_owned_ptr_or_err(ret).map(Some) - } - } else if let Ok(descr_get) = attr.get_type().getattr(crate::intern!(py, "__get__")) { - descr_get.call1((attr, self, self_type)).map(Some) - } else { - Ok(Some(attr)) - } - } - /// Sets an attribute value. /// /// This is equivalent to the Python expression `self.attr_name = value`. @@ -191,16 +149,16 @@ impl PyAny { /// # Example: `intern!`ing the attribute name /// /// ``` - /// # use pyo3::{intern, pyfunction, types::PyModule, PyAny, Python, PyResult}; + /// # use pyo3::{prelude::*, intern}; /// # /// #[pyfunction] - /// fn set_answer(ob: &PyAny) -> PyResult<()> { + /// fn set_answer(ob: &Bound<'_, PyAny>) -> PyResult<()> { /// ob.setattr(intern!(ob.py(), "answer"), 42) /// } /// # /// # Python::with_gil(|py| { - /// # let ob = PyModule::new(py, "empty").unwrap(); - /// # set_answer(ob).unwrap(); + /// # let ob = PyModule::new_bound(py, "empty").unwrap(); + /// # set_answer(&ob).unwrap(); /// # }); /// ``` pub fn setattr(&self, attr_name: N, value: V) -> PyResult<()> @@ -208,7 +166,7 @@ impl PyAny { N: IntoPy>, V: ToPyObject, { - Bound::borrowed_from_gil_ref(&self).setattr(attr_name, value) + self.as_borrowed().setattr(attr_name, value) } /// Deletes an attribute. @@ -221,7 +179,7 @@ impl PyAny { where N: IntoPy>, { - Bound::borrowed_from_gil_ref(&self).delattr(attr_name) + self.as_borrowed().delattr(attr_name) } /// Returns an [`Ordering`] between `self` and `other`. @@ -247,8 +205,8 @@ impl PyAny { /// /// # fn main() -> PyResult<()> { /// Python::with_gil(|py| -> PyResult<()> { - /// let a = PyFloat::new(py, 0_f64); - /// let b = PyFloat::new(py, 42_f64); + /// let a = PyFloat::new_bound(py, 0_f64); + /// let b = PyFloat::new_bound(py, 42_f64); /// assert_eq!(a.compare(b)?, Ordering::Less); /// Ok(()) /// })?; @@ -263,8 +221,8 @@ impl PyAny { /// /// # fn main() -> PyResult<()> { /// Python::with_gil(|py| -> PyResult<()> { - /// let a = PyFloat::new(py, 0_f64); - /// let b = PyString::new(py, "zero"); + /// let a = PyFloat::new_bound(py, 0_f64); + /// let b = PyString::new_bound(py, "zero"); /// assert!(a.compare(b).is_err()); /// Ok(()) /// })?; @@ -274,7 +232,7 @@ impl PyAny { where O: ToPyObject, { - Bound::borrowed_from_gil_ref(&self).compare(other) + self.as_borrowed().compare(other) } /// Tests whether two Python objects obey a given [`CompareOp`]. @@ -304,8 +262,8 @@ impl PyAny { /// /// # fn main() -> PyResult<()> { /// Python::with_gil(|py| -> PyResult<()> { - /// let a: &PyInt = 0_u8.into_py(py).into_ref(py).downcast()?; - /// let b: &PyInt = 42_u8.into_py(py).into_ref(py).downcast()?; + /// let a: Bound<'_, PyInt> = 0_u8.into_py(py).into_bound(py).downcast_into()?; + /// let b: Bound<'_, PyInt> = 42_u8.into_py(py).into_bound(py).downcast_into()?; /// assert!(a.rich_compare(b, CompareOp::Le)?.is_truthy()?); /// Ok(()) /// })?; @@ -315,7 +273,7 @@ impl PyAny { where O: ToPyObject, { - Bound::borrowed_from_gil_ref(&self) + self.as_borrowed() .rich_compare(other, compare_op) .map(Bound::into_gil_ref) } @@ -327,7 +285,7 @@ impl PyAny { where O: ToPyObject, { - Bound::borrowed_from_gil_ref(&self).lt(other) + self.as_borrowed().lt(other) } /// Tests whether this object is less than or equal to another. @@ -337,7 +295,7 @@ impl PyAny { where O: ToPyObject, { - Bound::borrowed_from_gil_ref(&self).le(other) + self.as_borrowed().le(other) } /// Tests whether this object is equal to another. @@ -347,7 +305,7 @@ impl PyAny { where O: ToPyObject, { - Bound::borrowed_from_gil_ref(&self).eq(other) + self.as_borrowed().eq(other) } /// Tests whether this object is not equal to another. @@ -357,7 +315,7 @@ impl PyAny { where O: ToPyObject, { - Bound::borrowed_from_gil_ref(&self).ne(other) + self.as_borrowed().ne(other) } /// Tests whether this object is greater than another. @@ -367,7 +325,7 @@ impl PyAny { where O: ToPyObject, { - Bound::borrowed_from_gil_ref(&self).gt(other) + self.as_borrowed().gt(other) } /// Tests whether this object is greater than or equal to another. @@ -377,7 +335,7 @@ impl PyAny { where O: ToPyObject, { - Bound::borrowed_from_gil_ref(&self).ge(other) + self.as_borrowed().ge(other) } /// Determines whether this object appears callable. @@ -391,7 +349,7 @@ impl PyAny { /// /// # fn main() -> PyResult<()> { /// Python::with_gil(|py| -> PyResult<()> { - /// let builtins = PyModule::import(py, "builtins")?; + /// let builtins = PyModule::import_bound(py, "builtins")?; /// let print = builtins.getattr("print")?; /// assert!(print.is_callable()); /// Ok(()) @@ -408,7 +366,7 @@ impl PyAny { /// /// [1]: https://docs.python.org/3/library/functions.html#callable pub fn is_callable(&self) -> bool { - Bound::borrowed_from_gil_ref(&self).is_callable() + self.as_borrowed().is_callable() } /// Calls the object. @@ -430,13 +388,13 @@ impl PyAny { /// /// # fn main() -> PyResult<()> { /// Python::with_gil(|py| { - /// let module = PyModule::from_code(py, CODE, "", "")?; + /// let module = PyModule::from_code_bound(py, CODE, "", "")?; /// let fun = module.getattr("function")?; /// let args = ("hello",); - /// let kwargs = PyDict::new(py); + /// let kwargs = PyDict::new_bound(py); /// kwargs.set_item("cruel", "world")?; - /// let result = fun.call(args, Some(kwargs))?; - /// assert_eq!(result.extract::<&str>()?, "called with args and kwargs"); + /// let result = fun.call(args, Some(&kwargs))?; + /// assert_eq!(result.extract::()?, "called with args and kwargs"); /// Ok(()) /// }) /// # } @@ -446,8 +404,8 @@ impl PyAny { args: impl IntoPy>, kwargs: Option<&PyDict>, ) -> PyResult<&PyAny> { - Bound::borrowed_from_gil_ref(&self) - .call(args, kwargs) + self.as_borrowed() + .call(args, kwargs.map(PyDict::as_borrowed).as_deref()) .map(Bound::into_gil_ref) } @@ -462,7 +420,7 @@ impl PyAny { /// /// # fn main() -> PyResult<()> { /// Python::with_gil(|py| -> PyResult<()> { - /// let module = PyModule::import(py, "builtins")?; + /// let module = PyModule::import_bound(py, "builtins")?; /// let help = module.getattr("help")?; /// help.call0()?; /// Ok(()) @@ -472,9 +430,7 @@ impl PyAny { /// /// This is equivalent to the Python expression `help()`. pub fn call0(&self) -> PyResult<&PyAny> { - Bound::borrowed_from_gil_ref(&self) - .call0() - .map(Bound::into_gil_ref) + self.as_borrowed().call0().map(Bound::into_gil_ref) } /// Calls the object with only positional arguments. @@ -495,19 +451,17 @@ impl PyAny { /// /// # fn main() -> PyResult<()> { /// Python::with_gil(|py| { - /// let module = PyModule::from_code(py, CODE, "", "")?; + /// let module = PyModule::from_code_bound(py, CODE, "", "")?; /// let fun = module.getattr("function")?; /// let args = ("hello",); /// let result = fun.call1(args)?; - /// assert_eq!(result.extract::<&str>()?, "called with args"); + /// assert_eq!(result.extract::()?, "called with args"); /// Ok(()) /// }) /// # } /// ``` pub fn call1(&self, args: impl IntoPy>) -> PyResult<&PyAny> { - Bound::borrowed_from_gil_ref(&self) - .call1(args) - .map(Bound::into_gil_ref) + self.as_borrowed().call1(args).map(Bound::into_gil_ref) } /// Calls a method on the object. @@ -534,13 +488,13 @@ impl PyAny { /// /// # fn main() -> PyResult<()> { /// Python::with_gil(|py| { - /// let module = PyModule::from_code(py, CODE, "", "")?; + /// let module = PyModule::from_code_bound(py, CODE, "", "")?; /// let instance = module.getattr("a")?; /// let args = ("hello",); - /// let kwargs = PyDict::new(py); + /// let kwargs = PyDict::new_bound(py); /// kwargs.set_item("cruel", "world")?; - /// let result = instance.call_method("method", args, Some(kwargs))?; - /// assert_eq!(result.extract::<&str>()?, "called with args and kwargs"); + /// let result = instance.call_method("method", args, Some(&kwargs))?; + /// assert_eq!(result.extract::()?, "called with args and kwargs"); /// Ok(()) /// }) /// # } @@ -550,8 +504,8 @@ impl PyAny { N: IntoPy>, A: IntoPy>, { - Bound::borrowed_from_gil_ref(&self) - .call_method(name, args, kwargs) + self.as_borrowed() + .call_method(name, args, kwargs.map(PyDict::as_borrowed).as_deref()) .map(Bound::into_gil_ref) } @@ -578,10 +532,10 @@ impl PyAny { /// /// # fn main() -> PyResult<()> { /// Python::with_gil(|py| { - /// let module = PyModule::from_code(py, CODE, "", "")?; + /// let module = PyModule::from_code_bound(py, CODE, "", "")?; /// let instance = module.getattr("a")?; /// let result = instance.call_method0("method")?; - /// assert_eq!(result.extract::<&str>()?, "called with no arguments"); + /// assert_eq!(result.extract::()?, "called with no arguments"); /// Ok(()) /// }) /// # } @@ -590,7 +544,7 @@ impl PyAny { where N: IntoPy>, { - Bound::borrowed_from_gil_ref(&self) + self.as_borrowed() .call_method0(name) .map(Bound::into_gil_ref) } @@ -618,11 +572,11 @@ impl PyAny { /// /// # fn main() -> PyResult<()> { /// Python::with_gil(|py| { - /// let module = PyModule::from_code(py, CODE, "", "")?; + /// let module = PyModule::from_code_bound(py, CODE, "", "")?; /// let instance = module.getattr("a")?; /// let args = ("hello",); /// let result = instance.call_method1("method", args)?; - /// assert_eq!(result.extract::<&str>()?, "called with args"); + /// assert_eq!(result.extract::()?, "called with args"); /// Ok(()) /// }) /// # } @@ -632,7 +586,7 @@ impl PyAny { N: IntoPy>, A: IntoPy>, { - Bound::borrowed_from_gil_ref(&self) + self.as_borrowed() .call_method1(name, args) .map(Bound::into_gil_ref) } @@ -649,7 +603,7 @@ impl PyAny { /// /// This applies truth value testing equivalent to the Python expression `bool(self)`. pub fn is_truthy(&self) -> PyResult { - Bound::borrowed_from_gil_ref(&self).is_truthy() + self.as_borrowed().is_truthy() } /// Returns whether the object is considered to be None. @@ -657,7 +611,7 @@ impl PyAny { /// This is equivalent to the Python expression `self is None`. #[inline] pub fn is_none(&self) -> bool { - Bound::borrowed_from_gil_ref(&self).is_none() + self.as_borrowed().is_none() } /// Returns whether the object is Ellipsis, e.g. `...`. @@ -665,14 +619,14 @@ impl PyAny { /// This is equivalent to the Python expression `self is ...`. #[deprecated(since = "0.20.0", note = "use `.is(py.Ellipsis())` instead")] pub fn is_ellipsis(&self) -> bool { - Bound::borrowed_from_gil_ref(&self).is_ellipsis() + self.as_borrowed().is_ellipsis() } /// Returns true if the sequence or mapping has a length of 0. /// /// This is equivalent to the Python expression `len(self) == 0`. pub fn is_empty(&self) -> PyResult { - Bound::borrowed_from_gil_ref(&self).is_empty() + self.as_borrowed().is_empty() } /// Gets an item from the collection. @@ -682,9 +636,7 @@ impl PyAny { where K: ToPyObject, { - Bound::borrowed_from_gil_ref(&self) - .get_item(key) - .map(Bound::into_gil_ref) + self.as_borrowed().get_item(key).map(Bound::into_gil_ref) } /// Sets a collection item value. @@ -695,7 +647,7 @@ impl PyAny { K: ToPyObject, V: ToPyObject, { - Bound::borrowed_from_gil_ref(&self).set_item(key, value) + self.as_borrowed().set_item(key, value) } /// Deletes an item from the collection. @@ -705,7 +657,7 @@ impl PyAny { where K: ToPyObject, { - Bound::borrowed_from_gil_ref(&self).del_item(key) + self.as_borrowed().del_item(key) } /// Takes an object and returns an iterator for it. @@ -713,20 +665,18 @@ impl PyAny { /// This is typically a new iterator but if the argument is an iterator, /// this returns itself. pub fn iter(&self) -> PyResult<&PyIterator> { - Bound::borrowed_from_gil_ref(&self) - .iter() - .map(Bound::into_gil_ref) + self.as_borrowed().iter().map(Bound::into_gil_ref) } /// Returns the Python type object for this object's type. pub fn get_type(&self) -> &PyType { - Bound::borrowed_from_gil_ref(&self).get_type() + self.as_borrowed().get_type().into_gil_ref() } /// Returns the Python type pointer for this object. #[inline] pub fn get_type_ptr(&self) -> *mut ffi::PyTypeObject { - Bound::borrowed_from_gil_ref(&self).get_type_ptr() + self.as_borrowed().get_type_ptr() } /// Downcast this `PyAny` to a concrete Python type or pyclass. @@ -744,9 +694,9 @@ impl PyAny { /// use pyo3::types::{PyDict, PyList}; /// /// Python::with_gil(|py| { - /// let dict = PyDict::new(py); + /// let dict = PyDict::new_bound(py); /// assert!(dict.is_instance_of::()); - /// let any: &PyAny = dict.as_ref(); + /// let any = dict.as_any(); /// /// assert!(any.downcast::().is_ok()); /// assert!(any.downcast::().is_err()); @@ -768,11 +718,11 @@ impl PyAny { /// } /// /// Python::with_gil(|py| { - /// let class: &PyAny = Py::new(py, Class { i: 0 }).unwrap().into_ref(py); + /// let class = Py::new(py, Class { i: 0 }).unwrap().into_bound(py).into_any(); /// - /// let class_cell: &PyCell = class.downcast()?; + /// let class_bound: &Bound<'_, Class> = class.downcast()?; /// - /// class_cell.borrow_mut().i += 1; + /// class_bound.borrow_mut().i += 1; /// /// // Alternatively you can get a `PyRefMut` directly /// let class_ref: PyRefMut<'_, Class> = class.extract()?; @@ -786,7 +736,7 @@ impl PyAny { where T: PyTypeCheck, { - if T::type_check(self) { + if T::type_check(&self.as_borrowed()) { // Safety: type_check is responsible for ensuring that the type is correct Ok(unsafe { self.downcast_unchecked() }) } else { @@ -812,9 +762,9 @@ impl PyAny { /// use pyo3::types::{PyBool, PyLong}; /// /// Python::with_gil(|py| { - /// let b = PyBool::new(py, true); + /// let b = PyBool::new_bound(py, true); /// assert!(b.is_instance_of::()); - /// let any: &PyAny = b.as_ref(); + /// let any: &Bound<'_, PyAny> = b.as_any(); /// /// // `bool` is a subtype of `int`, so `downcast` will accept a `bool` as an `int` /// // but `downcast_exact` will not. @@ -829,7 +779,7 @@ impl PyAny { where T: PyTypeInfo, { - if T::is_exact_type_of(self) { + if T::is_exact_type_of_bound(&self.as_borrowed()) { // Safety: type_check is responsible for ensuring that the type is correct Ok(unsafe { self.downcast_unchecked() }) } else { @@ -852,57 +802,54 @@ impl PyAny { /// Extracts some type from the Python object. /// - /// This is a wrapper function around [`FromPyObject::extract()`]. + /// This is a wrapper function around + /// [`FromPyObject::extract()`](crate::FromPyObject::extract). #[inline] - pub fn extract<'a, D>(&'a self) -> PyResult + pub fn extract<'py, D>(&'py self) -> PyResult where - D: FromPyObject<'a>, + D: FromPyObjectBound<'py, 'py>, { - FromPyObject::extract(self) + FromPyObjectBound::from_py_object_bound(self.as_borrowed()) } /// Returns the reference count for the Python object. pub fn get_refcnt(&self) -> isize { - Bound::borrowed_from_gil_ref(&self).get_refcnt() + self.as_borrowed().get_refcnt() } /// Computes the "repr" representation of self. /// /// This is equivalent to the Python expression `repr(self)`. pub fn repr(&self) -> PyResult<&PyString> { - Bound::borrowed_from_gil_ref(&self) - .repr() - .map(Bound::into_gil_ref) + self.as_borrowed().repr().map(Bound::into_gil_ref) } /// Computes the "str" representation of self. /// /// This is equivalent to the Python expression `str(self)`. pub fn str(&self) -> PyResult<&PyString> { - Bound::borrowed_from_gil_ref(&self) - .str() - .map(Bound::into_gil_ref) + self.as_borrowed().str().map(Bound::into_gil_ref) } /// Retrieves the hash code of self. /// /// This is equivalent to the Python expression `hash(self)`. pub fn hash(&self) -> PyResult { - Bound::borrowed_from_gil_ref(&self).hash() + self.as_borrowed().hash() } /// Returns the length of the sequence or mapping. /// /// This is equivalent to the Python expression `len(self)`. pub fn len(&self) -> PyResult { - Bound::borrowed_from_gil_ref(&self).len() + self.as_borrowed().len() } /// Returns the list of attributes of this object. /// /// This is equivalent to the Python expression `dir(self)`. - pub fn dir(&self) -> &PyList { - Bound::borrowed_from_gil_ref(&self).dir().into_gil_ref() + pub fn dir(&self) -> PyResult<&PyList> { + self.as_borrowed().dir().map(Bound::into_gil_ref) } /// Checks whether this object is an instance of type `ty`. @@ -910,7 +857,7 @@ impl PyAny { /// This is equivalent to the Python expression `isinstance(self, ty)`. #[inline] pub fn is_instance(&self, ty: &PyAny) -> PyResult { - Bound::borrowed_from_gil_ref(&self).is_instance(Bound::borrowed_from_gil_ref(&ty)) + self.as_borrowed().is_instance(&ty.as_borrowed()) } /// Checks whether this object is an instance of exactly type `ty` (not a subclass). @@ -918,7 +865,7 @@ impl PyAny { /// This is equivalent to the Python expression `type(self) is ty`. #[inline] pub fn is_exact_instance(&self, ty: &PyAny) -> bool { - Bound::borrowed_from_gil_ref(&self).is_exact_instance(Bound::borrowed_from_gil_ref(&ty)) + self.as_borrowed().is_exact_instance(&ty.as_borrowed()) } /// Checks whether this object is an instance of type `T`. @@ -927,7 +874,7 @@ impl PyAny { /// if the type `T` is known at compile time. #[inline] pub fn is_instance_of(&self) -> bool { - Bound::borrowed_from_gil_ref(&self).is_instance_of::() + self.as_borrowed().is_instance_of::() } /// Checks whether this object is an instance of exactly type `T`. @@ -936,7 +883,7 @@ impl PyAny { /// if the type `T` is known at compile time. #[inline] pub fn is_exact_instance_of(&self) -> bool { - Bound::borrowed_from_gil_ref(&self).is_exact_instance_of::() + self.as_borrowed().is_exact_instance_of::() } /// Determines if self contains `value`. @@ -946,7 +893,7 @@ impl PyAny { where V: ToPyObject, { - Bound::borrowed_from_gil_ref(&self).contains(value) + self.as_borrowed().contains(value) } /// Returns a GIL marker constrained to the lifetime of this type. @@ -977,17 +924,17 @@ impl PyAny { #[inline] pub fn into_ptr(&self) -> *mut ffi::PyObject { // Safety: self.as_ptr() returns a valid non-null pointer - unsafe { ffi::_Py_NewRef(self.as_ptr()) } + let ptr = self.as_ptr(); + unsafe { ffi::Py_INCREF(ptr) }; + ptr } /// Return a proxy object that delegates method calls to a parent or sibling class of type. /// /// This is equivalent to the Python expression `super()` - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] pub fn py_super(&self) -> PyResult<&PySuper> { - Bound::borrowed_from_gil_ref(&self) - .py_super() - .map(Bound::into_gil_ref) + self.as_borrowed().py_super().map(Bound::into_gil_ref) } } @@ -996,9 +943,9 @@ impl PyAny { /// It is recommended you import this trait via `use pyo3::prelude::*` rather than /// by importing this trait directly. #[doc(alias = "PyAny")] -pub trait PyAnyMethods<'py> { +pub trait PyAnyMethods<'py>: crate::sealed::Sealed { /// Returns whether `self` and `other` point to the same object. To compare - /// the equality of two objects (the `==` operator), use [`eq`](PyAny::eq). + /// the equality of two objects (the `==` operator), use [`eq`](PyAnyMethods::eq). /// /// This is equivalent to the Python expression `self is other`. fn is(&self, other: &T) -> bool; @@ -1013,16 +960,16 @@ pub trait PyAnyMethods<'py> { /// # Example: `intern!`ing the attribute name /// /// ``` - /// # use pyo3::{intern, pyfunction, types::PyModule, Python, PyResult}; + /// # use pyo3::{prelude::*, intern}; /// # /// #[pyfunction] - /// fn has_version(sys: &PyModule) -> PyResult { + /// fn has_version(sys: &Bound<'_, PyModule>) -> PyResult { /// sys.hasattr(intern!(sys.py(), "version")) /// } /// # /// # Python::with_gil(|py| { - /// # let sys = py.import("sys").unwrap(); - /// # has_version(sys).unwrap(); + /// # let sys = py.import_bound("sys").unwrap(); + /// # has_version(&sys).unwrap(); /// # }); /// ``` fn hasattr(&self, attr_name: N) -> PyResult @@ -1039,16 +986,16 @@ pub trait PyAnyMethods<'py> { /// # Example: `intern!`ing the attribute name /// /// ``` - /// # use pyo3::{intern, pyfunction, types::PyModule, PyAny, Python, PyResult}; + /// # use pyo3::{prelude::*, intern}; /// # /// #[pyfunction] - /// fn version(sys: &PyModule) -> PyResult<&PyAny> { + /// fn version<'py>(sys: &Bound<'py, PyModule>) -> PyResult> { /// sys.getattr(intern!(sys.py(), "version")) /// } /// # /// # Python::with_gil(|py| { - /// # let sys = py.import("sys").unwrap(); - /// # version(sys).unwrap(); + /// # let sys = py.import_bound("sys").unwrap(); + /// # version(&sys).unwrap(); /// # }); /// ``` fn getattr(&self, attr_name: N) -> PyResult> @@ -1065,16 +1012,16 @@ pub trait PyAnyMethods<'py> { /// # Example: `intern!`ing the attribute name /// /// ``` - /// # use pyo3::{intern, pyfunction, types::PyModule, PyAny, Python, PyResult}; + /// # use pyo3::{prelude::*, intern}; /// # /// #[pyfunction] - /// fn set_answer(ob: &PyAny) -> PyResult<()> { + /// fn set_answer(ob: &Bound<'_, PyAny>) -> PyResult<()> { /// ob.setattr(intern!(ob.py(), "answer"), 42) /// } /// # /// # Python::with_gil(|py| { - /// # let ob = PyModule::new(py, "empty").unwrap(); - /// # set_answer(ob).unwrap(); + /// # let ob = PyModule::new_bound(py, "empty").unwrap(); + /// # set_answer(&ob).unwrap(); /// # }); /// ``` fn setattr(&self, attr_name: N, value: V) -> PyResult<()> @@ -1115,8 +1062,8 @@ pub trait PyAnyMethods<'py> { /// /// # fn main() -> PyResult<()> { /// Python::with_gil(|py| -> PyResult<()> { - /// let a = PyFloat::new(py, 0_f64); - /// let b = PyFloat::new(py, 42_f64); + /// let a = PyFloat::new_bound(py, 0_f64); + /// let b = PyFloat::new_bound(py, 42_f64); /// assert_eq!(a.compare(b)?, Ordering::Less); /// Ok(()) /// })?; @@ -1131,8 +1078,8 @@ pub trait PyAnyMethods<'py> { /// /// # fn main() -> PyResult<()> { /// Python::with_gil(|py| -> PyResult<()> { - /// let a = PyFloat::new(py, 0_f64); - /// let b = PyString::new(py, "zero"); + /// let a = PyFloat::new_bound(py, 0_f64); + /// let b = PyString::new_bound(py, "zero"); /// assert!(a.compare(b).is_err()); /// Ok(()) /// })?; @@ -1169,8 +1116,8 @@ pub trait PyAnyMethods<'py> { /// /// # fn main() -> PyResult<()> { /// Python::with_gil(|py| -> PyResult<()> { - /// let a: &PyInt = 0_u8.into_py(py).into_ref(py).downcast()?; - /// let b: &PyInt = 42_u8.into_py(py).into_ref(py).downcast()?; + /// let a: Bound<'_, PyInt> = 0_u8.into_py(py).into_bound(py).downcast_into()?; + /// let b: Bound<'_, PyInt> = 42_u8.into_py(py).into_bound(py).downcast_into()?; /// assert!(a.rich_compare(b, CompareOp::Le)?.is_truthy()?); /// Ok(()) /// })?; @@ -1222,6 +1169,58 @@ pub trait PyAnyMethods<'py> { where O: ToPyObject; + /// Computes `self + other`. + fn add(&self, other: O) -> PyResult> + where + O: ToPyObject; + + /// Computes `self - other`. + fn sub(&self, other: O) -> PyResult> + where + O: ToPyObject; + + /// Computes `self * other`. + fn mul(&self, other: O) -> PyResult> + where + O: ToPyObject; + + /// Computes `self / other`. + fn div(&self, other: O) -> PyResult> + where + O: ToPyObject; + + /// Computes `self << other`. + fn lshift(&self, other: O) -> PyResult> + where + O: ToPyObject; + + /// Computes `self >> other`. + fn rshift(&self, other: O) -> PyResult> + where + O: ToPyObject; + + /// Computes `self ** other % modulus` (`pow(self, other, modulus)`). + /// `py.None()` may be passed for the `modulus`. + fn pow(&self, other: O1, modulus: O2) -> PyResult> + where + O1: ToPyObject, + O2: ToPyObject; + + /// Computes `self & other`. + fn bitand(&self, other: O) -> PyResult> + where + O: ToPyObject; + + /// Computes `self | other`. + fn bitor(&self, other: O) -> PyResult> + where + O: ToPyObject; + + /// Computes `self ^ other`. + fn bitxor(&self, other: O) -> PyResult> + where + O: ToPyObject; + /// Determines whether this object appears callable. /// /// This is equivalent to Python's [`callable()`][1] function. @@ -1233,7 +1232,7 @@ pub trait PyAnyMethods<'py> { /// /// # fn main() -> PyResult<()> { /// Python::with_gil(|py| -> PyResult<()> { - /// let builtins = PyModule::import(py, "builtins")?; + /// let builtins = PyModule::import_bound(py, "builtins")?; /// let print = builtins.getattr("print")?; /// assert!(print.is_callable()); /// Ok(()) @@ -1270,13 +1269,13 @@ pub trait PyAnyMethods<'py> { /// /// # fn main() -> PyResult<()> { /// Python::with_gil(|py| { - /// let module = PyModule::from_code(py, CODE, "", "")?; + /// let module = PyModule::from_code_bound(py, CODE, "", "")?; /// let fun = module.getattr("function")?; /// let args = ("hello",); - /// let kwargs = PyDict::new(py); + /// let kwargs = PyDict::new_bound(py); /// kwargs.set_item("cruel", "world")?; - /// let result = fun.call(args, Some(kwargs))?; - /// assert_eq!(result.extract::<&str>()?, "called with args and kwargs"); + /// let result = fun.call(args, Some(&kwargs))?; + /// assert_eq!(result.extract::()?, "called with args and kwargs"); /// Ok(()) /// }) /// # } @@ -1284,7 +1283,7 @@ pub trait PyAnyMethods<'py> { fn call( &self, args: impl IntoPy>, - kwargs: Option<&PyDict>, + kwargs: Option<&Bound<'_, PyDict>>, ) -> PyResult>; /// Calls the object without arguments. @@ -1298,7 +1297,7 @@ pub trait PyAnyMethods<'py> { /// /// # fn main() -> PyResult<()> { /// Python::with_gil(|py| -> PyResult<()> { - /// let module = PyModule::import(py, "builtins")?; + /// let module = PyModule::import_bound(py, "builtins")?; /// let help = module.getattr("help")?; /// help.call0()?; /// Ok(()) @@ -1327,11 +1326,11 @@ pub trait PyAnyMethods<'py> { /// /// # fn main() -> PyResult<()> { /// Python::with_gil(|py| { - /// let module = PyModule::from_code(py, CODE, "", "")?; + /// let module = PyModule::from_code_bound(py, CODE, "", "")?; /// let fun = module.getattr("function")?; /// let args = ("hello",); /// let result = fun.call1(args)?; - /// assert_eq!(result.extract::<&str>()?, "called with args"); + /// assert_eq!(result.extract::()?, "called with args"); /// Ok(()) /// }) /// # } @@ -1362,13 +1361,13 @@ pub trait PyAnyMethods<'py> { /// /// # fn main() -> PyResult<()> { /// Python::with_gil(|py| { - /// let module = PyModule::from_code(py, CODE, "", "")?; + /// let module = PyModule::from_code_bound(py, CODE, "", "")?; /// let instance = module.getattr("a")?; /// let args = ("hello",); - /// let kwargs = PyDict::new(py); + /// let kwargs = PyDict::new_bound(py); /// kwargs.set_item("cruel", "world")?; - /// let result = instance.call_method("method", args, Some(kwargs))?; - /// assert_eq!(result.extract::<&str>()?, "called with args and kwargs"); + /// let result = instance.call_method("method", args, Some(&kwargs))?; + /// assert_eq!(result.extract::()?, "called with args and kwargs"); /// Ok(()) /// }) /// # } @@ -1377,7 +1376,7 @@ pub trait PyAnyMethods<'py> { &self, name: N, args: A, - kwargs: Option<&PyDict>, + kwargs: Option<&Bound<'_, PyDict>>, ) -> PyResult> where N: IntoPy>, @@ -1406,10 +1405,10 @@ pub trait PyAnyMethods<'py> { /// /// # fn main() -> PyResult<()> { /// Python::with_gil(|py| { - /// let module = PyModule::from_code(py, CODE, "", "")?; + /// let module = PyModule::from_code_bound(py, CODE, "", "")?; /// let instance = module.getattr("a")?; /// let result = instance.call_method0("method")?; - /// assert_eq!(result.extract::<&str>()?, "called with no arguments"); + /// assert_eq!(result.extract::()?, "called with no arguments"); /// Ok(()) /// }) /// # } @@ -1441,11 +1440,11 @@ pub trait PyAnyMethods<'py> { /// /// # fn main() -> PyResult<()> { /// Python::with_gil(|py| { - /// let module = PyModule::from_code(py, CODE, "", "")?; + /// let module = PyModule::from_code_bound(py, CODE, "", "")?; /// let instance = module.getattr("a")?; /// let args = ("hello",); /// let result = instance.call_method1("method", args)?; - /// assert_eq!(result.extract::<&str>()?, "called with args"); + /// assert_eq!(result.extract::()?, "called with args"); /// Ok(()) /// }) /// # } @@ -1504,7 +1503,7 @@ pub trait PyAnyMethods<'py> { fn iter(&self) -> PyResult>; /// Returns the Python type object for this object's type. - fn get_type(&self) -> &'py PyType; + fn get_type(&self) -> Bound<'py, PyType>; /// Returns the Python type pointer for this object. fn get_type_ptr(&self) -> *mut ffi::PyTypeObject; @@ -1524,9 +1523,9 @@ pub trait PyAnyMethods<'py> { /// use pyo3::types::{PyDict, PyList}; /// /// Python::with_gil(|py| { - /// let dict = PyDict::new(py); + /// let dict = PyDict::new_bound(py); /// assert!(dict.is_instance_of::()); - /// let any: &PyAny = dict.as_ref(); + /// let any = dict.as_any(); /// /// assert!(any.downcast::().is_ok()); /// assert!(any.downcast::().is_err()); @@ -1548,11 +1547,11 @@ pub trait PyAnyMethods<'py> { /// } /// /// Python::with_gil(|py| { - /// let class: &PyAny = Py::new(py, Class { i: 0 }).unwrap().into_ref(py); + /// let class = Py::new(py, Class { i: 0 }).unwrap().into_bound(py).into_any(); /// - /// let class_cell: &PyCell = class.downcast()?; + /// let class_bound: &Bound<'_, Class> = class.downcast()?; /// - /// class_cell.borrow_mut().i += 1; + /// class_bound.borrow_mut().i += 1; /// /// // Alternatively you can get a `PyRefMut` directly /// let class_ref: PyRefMut<'_, Class> = class.extract()?; @@ -1566,16 +1565,37 @@ pub trait PyAnyMethods<'py> { T: PyTypeCheck; /// Like `downcast` but takes ownership of `self`. + /// + /// In case of an error, it is possible to retrieve `self` again via [`DowncastIntoError::into_inner`]. + /// + /// # Example + /// + /// ```rust + /// use pyo3::prelude::*; + /// use pyo3::types::{PyDict, PyList}; + /// + /// Python::with_gil(|py| { + /// let obj: Bound<'_, PyAny> = PyDict::new_bound(py).into_any(); + /// + /// let obj: Bound<'_, PyAny> = match obj.downcast_into::() { + /// Ok(_) => panic!("obj should not be a list"), + /// Err(err) => err.into_inner(), + /// }; + /// + /// // obj is a dictionary + /// assert!(obj.downcast_into::().is_ok()); + /// }) + /// ``` fn downcast_into(self) -> Result, DowncastIntoError<'py>> where T: PyTypeCheck; /// Downcast this `PyAny` to a concrete Python type or pyclass (but not a subclass of it). /// - /// It is almost always better to use [`PyAny::downcast`] because it accounts for Python + /// It is almost always better to use [`PyAnyMethods::downcast`] because it accounts for Python /// subtyping. Use this method only when you do not want to allow subtypes. /// - /// The advantage of this method over [`PyAny::downcast`] is that it is faster. The implementation + /// The advantage of this method over [`PyAnyMethods::downcast`] is that it is faster. The implementation /// of `downcast_exact` uses the equivalent of the Python expression `type(self) is T`, whereas /// `downcast` uses `isinstance(self, T)`. /// @@ -1588,9 +1608,9 @@ pub trait PyAnyMethods<'py> { /// use pyo3::types::{PyBool, PyLong}; /// /// Python::with_gil(|py| { - /// let b = PyBool::new(py, true); + /// let b = PyBool::new_bound(py, true); /// assert!(b.is_instance_of::()); - /// let any: &PyAny = b.as_ref(); + /// let any: &Bound<'_, PyAny> = b.as_any(); /// /// // `bool` is a subtype of `int`, so `downcast` will accept a `bool` as an `int` /// // but `downcast_exact` will not. @@ -1625,10 +1645,11 @@ pub trait PyAnyMethods<'py> { /// Extracts some type from the Python object. /// - /// This is a wrapper function around [`FromPyObject::extract()`]. - fn extract<'a, D>(&'a self) -> PyResult + /// This is a wrapper function around + /// [`FromPyObject::extract_bound()`](crate::FromPyObject::extract_bound). + fn extract<'a, T>(&'a self) -> PyResult where - D: FromPyObject<'a>; + T: FromPyObjectBound<'a, 'py>; /// Returns the reference count for the Python object. fn get_refcnt(&self) -> isize; @@ -1656,7 +1677,7 @@ pub trait PyAnyMethods<'py> { /// Returns the list of attributes of this object. /// /// This is equivalent to the Python expression `dir(self)`. - fn dir(&self) -> Bound<'py, PyList>; + fn dir(&self) -> PyResult>; /// Checks whether this object is an instance of type `ty`. /// @@ -1690,10 +1711,30 @@ pub trait PyAnyMethods<'py> { /// Return a proxy object that delegates method calls to a parent or sibling class of type. /// /// This is equivalent to the Python expression `super()` - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] fn py_super(&self) -> PyResult>; } +macro_rules! implement_binop { + ($name:ident, $c_api:ident, $op:expr) => { + #[doc = concat!("Computes `self ", $op, " other`.")] + fn $name(&self, other: O) -> PyResult> + where + O: ToPyObject, + { + fn inner<'py>( + any: &Bound<'py, PyAny>, + other: Bound<'_, PyAny>, + ) -> PyResult> { + unsafe { ffi::$c_api(any.as_ptr(), other.as_ptr()).assume_owned_or_err(any.py()) } + } + + let py = self.py(); + inner(self, other.to_object(py).into_bound(py)) + } + }; +} + impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { #[inline] fn is(&self, other: &T) -> bool { @@ -1869,6 +1910,42 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { .and_then(|any| any.is_truthy()) } + implement_binop!(add, PyNumber_Add, "+"); + implement_binop!(sub, PyNumber_Subtract, "-"); + implement_binop!(mul, PyNumber_Multiply, "*"); + implement_binop!(div, PyNumber_TrueDivide, "/"); + implement_binop!(lshift, PyNumber_Lshift, "<<"); + implement_binop!(rshift, PyNumber_Rshift, ">>"); + implement_binop!(bitand, PyNumber_And, "&"); + implement_binop!(bitor, PyNumber_Or, "|"); + implement_binop!(bitxor, PyNumber_Xor, "^"); + + /// Computes `self ** other % modulus` (`pow(self, other, modulus)`). + /// `py.None()` may be passed for the `modulus`. + fn pow(&self, other: O1, modulus: O2) -> PyResult> + where + O1: ToPyObject, + O2: ToPyObject, + { + fn inner<'py>( + any: &Bound<'py, PyAny>, + other: Bound<'_, PyAny>, + modulus: Bound<'_, PyAny>, + ) -> PyResult> { + unsafe { + ffi::PyNumber_Power(any.as_ptr(), other.as_ptr(), modulus.as_ptr()) + .assume_owned_or_err(any.py()) + } + } + + let py = self.py(); + inner( + self, + other.to_object(py).into_bound(py), + modulus.to_object(py).into_bound(py), + ) + } + fn is_callable(&self) -> bool { unsafe { ffi::PyCallable_Check(self.as_ptr()) != 0 } } @@ -1876,12 +1953,12 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { fn call( &self, args: impl IntoPy>, - kwargs: Option<&PyDict>, + kwargs: Option<&Bound<'_, PyDict>>, ) -> PyResult> { fn inner<'py>( any: &Bound<'py, PyAny>, args: Bound<'_, PyTuple>, - kwargs: Option<&PyDict>, + kwargs: Option<&Bound<'_, PyDict>>, ) -> PyResult> { unsafe { ffi::PyObject_Call( @@ -1901,6 +1978,7 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { cfg_if::cfg_if! { if #[cfg(all( not(PyPy), + not(GraalPy), any(Py_3_10, all(not(Py_LIMITED_API), Py_3_9)) // PyObject_CallNoArgs was added to python in 3.9 but to limited API in 3.10 ))] { // Optimized path on python 3.9+ @@ -1921,7 +1999,7 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { &self, name: N, args: A, - kwargs: Option<&PyDict>, + kwargs: Option<&Bound<'_, PyDict>>, ) -> PyResult> where N: IntoPy>, @@ -1936,7 +2014,7 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { N: IntoPy>, { cfg_if::cfg_if! { - if #[cfg(all(Py_3_9, not(any(Py_LIMITED_API, PyPy))))] { + if #[cfg(all(Py_3_9, not(any(Py_LIMITED_API, PyPy, GraalPy))))] { let py = self.py(); // Optimized path on python 3.9+ @@ -2032,11 +2110,11 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { } fn iter(&self) -> PyResult> { - PyIterator::from_object2(self) + PyIterator::from_bound_object(self) } - fn get_type(&self) -> &'py PyType { - unsafe { PyType::from_type_ptr(self.py(), ffi::Py_TYPE(self.as_ptr())) } + fn get_type(&self) -> Bound<'py, PyType> { + unsafe { PyType::from_borrowed_type_ptr(self.py(), ffi::Py_TYPE(self.as_ptr())) } } #[inline] @@ -2049,7 +2127,7 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { where T: PyTypeCheck, { - if T::type_check(self.as_gil_ref()) { + if T::type_check(self) { // Safety: type_check is responsible for ensuring that the type is correct Ok(unsafe { self.downcast_unchecked() }) } else { @@ -2062,7 +2140,7 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { where T: PyTypeCheck, { - if T::type_check(self.as_gil_ref()) { + if T::type_check(&self) { // Safety: type_check is responsible for ensuring that the type is correct Ok(unsafe { self.downcast_into_unchecked() }) } else { @@ -2106,11 +2184,11 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { std::mem::transmute(self) } - fn extract<'a, D>(&'a self) -> PyResult + fn extract<'a, T>(&'a self) -> PyResult where - D: FromPyObject<'a>, + T: FromPyObjectBound<'a, 'py>, { - FromPyObject::extract(self.as_gil_ref()) + FromPyObjectBound::from_py_object_bound(self.as_borrowed()) } fn get_refcnt(&self) -> isize { @@ -2145,10 +2223,10 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { Ok(v as usize) } - fn dir(&self) -> Bound<'py, PyList> { + fn dir(&self) -> PyResult> { unsafe { ffi::PyObject_Dir(self.as_ptr()) - .assume_owned(self.py()) + .assume_owned_or_err(self.py()) .downcast_into_unchecked() } } @@ -2167,12 +2245,12 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { #[inline] fn is_instance_of(&self) -> bool { - T::is_type_of(self.as_gil_ref()) + T::is_type_of_bound(self) } #[inline] fn is_exact_instance_of(&self) -> bool { - T::is_exact_type_of(self.as_gil_ref()) + T::is_exact_type_of_bound(self) } fn contains(&self, value: V) -> PyResult @@ -2191,9 +2269,60 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { inner(self, value.to_object(py).into_bound(py)) } - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] fn py_super(&self) -> PyResult> { - PySuper::new2(Bound::borrowed_from_gil_ref(&self.get_type()), self) + PySuper::new_bound(&self.get_type(), self) + } +} + +impl<'py> Bound<'py, PyAny> { + /// Retrieve an attribute value, skipping the instance dictionary during the lookup but still + /// binding the object to the instance. + /// + /// This is useful when trying to resolve Python's "magic" methods like `__getitem__`, which + /// are looked up starting from the type object. This returns an `Option` as it is not + /// typically a direct error for the special lookup to fail, as magic methods are optional in + /// many situations in which they might be called. + /// + /// To avoid repeated temporary allocations of Python strings, the [`intern!`] macro can be used + /// to intern `attr_name`. + #[allow(dead_code)] // Currently only used with num-complex+abi3, so dead without that. + pub(crate) fn lookup_special(&self, attr_name: N) -> PyResult>> + where + N: IntoPy>, + { + let py = self.py(); + let self_type = self.get_type(); + let attr = if let Ok(attr) = self_type.getattr(attr_name) { + attr + } else { + return Ok(None); + }; + + // Manually resolve descriptor protocol. + if cfg!(Py_3_10) + || unsafe { ffi::PyType_HasFeature(attr.get_type_ptr(), ffi::Py_TPFLAGS_HEAPTYPE) } != 0 + { + // This is the preferred faster path, but does not work on static types (generally, + // types defined in extension modules) before Python 3.10. + unsafe { + let descr_get_ptr = ffi::PyType_GetSlot(attr.get_type_ptr(), ffi::Py_tp_descr_get); + if descr_get_ptr.is_null() { + return Ok(Some(attr)); + } + let descr_get: ffi::descrgetfunc = std::mem::transmute(descr_get_ptr); + let ret = descr_get(attr.as_ptr(), self.as_ptr(), self_type.as_ptr()); + ret.assume_owned_or_err(py).map(Some) + } + } else if let Ok(descr_get) = attr + .get_type() + .as_borrowed() + .getattr(crate::intern!(py, "__get__")) + { + descr_get.call1((attr, self, self_type)).map(Some) + } else { + Ok(Some(attr)) + } } } @@ -2201,14 +2330,14 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { mod tests { use crate::{ basic::CompareOp, - types::{IntoPyDict, PyAny, PyBool, PyList, PyLong, PyModule}, - PyTypeInfo, Python, ToPyObject, + types::{IntoPyDict, PyAny, PyAnyMethods, PyBool, PyList, PyLong, PyModule, PyTypeMethods}, + Bound, PyTypeInfo, Python, ToPyObject, }; #[test] fn test_lookup_special() { Python::with_gil(|py| { - let module = PyModule::from_code( + let module = PyModule::from_code_bound( py, r#" class CustomCallable: @@ -2248,7 +2377,7 @@ class NonHeapNonDescriptorInt: let int = crate::intern!(py, "__int__"); let eval_int = - |obj: &PyAny| obj.lookup_special(int)?.unwrap().call0()?.extract::(); + |obj: Bound<'_, PyAny>| obj.lookup_special(int)?.unwrap().call0()?.extract::(); let simple = module.getattr("SimpleInt").unwrap().call0().unwrap(); assert_eq!(eval_int(simple).unwrap(), 1); @@ -2257,7 +2386,7 @@ class NonHeapNonDescriptorInt: let no_descriptor = module.getattr("NoDescriptorInt").unwrap().call0().unwrap(); assert_eq!(eval_int(no_descriptor).unwrap(), 1); let missing = module.getattr("NoInt").unwrap().call0().unwrap(); - assert!(missing.lookup_special(int).unwrap().is_none()); + assert!(missing.as_borrowed().lookup_special(int).unwrap().is_none()); // Note the instance override should _not_ call the instance method that returns 2, // because that's not how special lookups are meant to work. let instance_override = module.getattr("instance_override").unwrap(); @@ -2267,7 +2396,7 @@ class NonHeapNonDescriptorInt: .unwrap() .call0() .unwrap(); - assert!(descriptor_error.lookup_special(int).is_err()); + assert!(descriptor_error.as_borrowed().lookup_special(int).is_err()); let nonheap_nondescriptor = module .getattr("NonHeapNonDescriptorInt") .unwrap() @@ -2280,7 +2409,7 @@ class NonHeapNonDescriptorInt: #[test] fn test_call_for_non_existing_method() { Python::with_gil(|py| { - let a = py.eval("42", None, None).unwrap(); + let a = py.eval_bound("42", None, None).unwrap(); a.call_method0("__str__").unwrap(); // ok assert!(a.call_method("nonexistent_method", (1,), None).is_err()); assert!(a.call_method0("nonexistent_method").is_err()); @@ -2292,8 +2421,8 @@ class NonHeapNonDescriptorInt: fn test_call_with_kwargs() { Python::with_gil(|py| { let list = vec![3, 6, 5, 4, 7].to_object(py); - let dict = vec![("reverse", true)].into_py_dict(py); - list.call_method(py, "sort", (), Some(dict)).unwrap(); + let dict = vec![("reverse", true)].into_py_dict_bound(py); + list.call_method_bound(py, "sort", (), Some(&dict)).unwrap(); assert_eq!(list.extract::>(py).unwrap(), vec![7, 6, 5, 4, 3]); }); } @@ -2301,7 +2430,7 @@ class NonHeapNonDescriptorInt: #[test] fn test_call_method0() { Python::with_gil(|py| { - let module = PyModule::from_code( + let module = PyModule::from_code_bound( py, r#" class SimpleClass: @@ -2328,7 +2457,7 @@ class SimpleClass: #[test] fn test_type() { Python::with_gil(|py| { - let obj = py.eval("42", None, None).unwrap(); + let obj = py.eval_bound("42", None, None).unwrap(); assert_eq!(obj.get_type().as_type_ptr(), obj.get_type_ptr()); }); } @@ -2336,14 +2465,15 @@ class SimpleClass: #[test] fn test_dir() { Python::with_gil(|py| { - let obj = py.eval("42", None, None).unwrap(); + let obj = py.eval_bound("42", None, None).unwrap(); let dir = py - .eval("dir(42)", None, None) + .eval_bound("dir(42)", None, None) .unwrap() - .downcast::() + .downcast_into::() .unwrap(); let a = obj .dir() + .unwrap() .into_iter() .map(|x| x.extract::().unwrap()); let b = dir.into_iter().map(|x| x.extract::().unwrap()); @@ -2354,7 +2484,7 @@ class SimpleClass: #[test] fn test_hasattr() { Python::with_gil(|py| { - let x = 5.to_object(py).into_ref(py); + let x = 5.to_object(py).into_bound(py); assert!(x.is_instance_of::()); assert!(x.hasattr("to_bytes").unwrap()); @@ -2364,6 +2494,7 @@ class SimpleClass: #[cfg(feature = "macros")] #[test] + #[allow(unknown_lints, non_local_definitions)] fn test_hasattr_error() { use crate::exceptions::PyValueError; use crate::prelude::*; @@ -2380,7 +2511,7 @@ class SimpleClass: Python::with_gil(|py| { let obj = Py::new(py, GetattrFail).unwrap(); - let obj = obj.as_ref(py).as_ref(); + let obj = obj.bind(py).as_ref(); assert!(obj .hasattr("foo") @@ -2392,18 +2523,18 @@ class SimpleClass: #[test] fn test_nan_eq() { Python::with_gil(|py| { - let nan = py.eval("float('nan')", None, None).unwrap(); - assert!(nan.compare(nan).is_err()); + let nan = py.eval_bound("float('nan')", None, None).unwrap(); + assert!(nan.compare(&nan).is_err()); }); } #[test] fn test_any_is_instance_of() { Python::with_gil(|py| { - let x = 5.to_object(py).into_ref(py); + let x = 5.to_object(py).into_bound(py); assert!(x.is_instance_of::()); - let l = vec![x, x].to_object(py).into_ref(py); + let l = vec![&x, &x].to_object(py).into_bound(py); assert!(l.is_instance_of::()); }); } @@ -2411,23 +2542,23 @@ class SimpleClass: #[test] fn test_any_is_instance() { Python::with_gil(|py| { - let l = vec![1u8, 2].to_object(py).into_ref(py); - assert!(l.is_instance(py.get_type::()).unwrap()); + let l = vec![1u8, 2].to_object(py).into_bound(py); + assert!(l.is_instance(&py.get_type_bound::()).unwrap()); }); } #[test] fn test_any_is_exact_instance_of() { Python::with_gil(|py| { - let x = 5.to_object(py).into_ref(py); + let x = 5.to_object(py).into_bound(py); assert!(x.is_exact_instance_of::()); - let t = PyBool::new(py, true); + let t = PyBool::new_bound(py, true); assert!(t.is_instance_of::()); assert!(!t.is_exact_instance_of::()); assert!(t.is_exact_instance_of::()); - let l = vec![x, x].to_object(py).into_ref(py); + let l = vec![&x, &x].to_object(py).into_bound(py); assert!(l.is_exact_instance_of::()); }); } @@ -2435,10 +2566,10 @@ class SimpleClass: #[test] fn test_any_is_exact_instance() { Python::with_gil(|py| { - let t = PyBool::new(py, true); - assert!(t.is_instance(py.get_type::()).unwrap()); - assert!(!t.is_exact_instance(py.get_type::())); - assert!(t.is_exact_instance(py.get_type::())); + let t = PyBool::new_bound(py, true); + assert!(t.is_instance(&py.get_type_bound::()).unwrap()); + assert!(!t.is_exact_instance(&py.get_type_bound::())); + assert!(t.is_exact_instance(&py.get_type_bound::())); }); } @@ -2446,7 +2577,7 @@ class SimpleClass: fn test_any_contains() { Python::with_gil(|py| { let v: Vec = vec![1, 1, 2, 3, 5, 8]; - let ob = v.to_object(py).into_ref(py); + let ob = v.to_object(py).into_bound(py); let bad_needle = 7i32.to_object(py); assert!(!ob.contains(&bad_needle).unwrap()); @@ -2458,7 +2589,7 @@ class SimpleClass: assert!(ob.contains(&type_coerced_needle).unwrap()); let n: u32 = 42; - let bad_haystack = n.to_object(py).into_ref(py); + let bad_haystack = n.to_object(py).into_bound(py); let irrelevant_needle = 0i32.to_object(py); assert!(bad_haystack.contains(&irrelevant_needle).is_err()); }); @@ -2472,12 +2603,12 @@ class SimpleClass: Python::with_gil(|py| { for a in list { for b in list { - let a_py = a.to_object(py).into_ref(py); - let b_py = b.to_object(py).into_ref(py); + let a_py = a.to_object(py).into_bound(py); + let b_py = b.to_object(py).into_bound(py); assert_eq!( a.lt(b), - a_py.lt(b_py).unwrap(), + a_py.lt(&b_py).unwrap(), "{} < {} should be {}.", a_py, b_py, @@ -2485,7 +2616,7 @@ class SimpleClass: ); assert_eq!( a.le(b), - a_py.le(b_py).unwrap(), + a_py.le(&b_py).unwrap(), "{} <= {} should be {}.", a_py, b_py, @@ -2493,7 +2624,7 @@ class SimpleClass: ); assert_eq!( a.eq(b), - a_py.eq(b_py).unwrap(), + a_py.eq(&b_py).unwrap(), "{} == {} should be {}.", a_py, b_py, @@ -2501,7 +2632,7 @@ class SimpleClass: ); assert_eq!( a.ne(b), - a_py.ne(b_py).unwrap(), + a_py.ne(&b_py).unwrap(), "{} != {} should be {}.", a_py, b_py, @@ -2509,7 +2640,7 @@ class SimpleClass: ); assert_eq!( a.gt(b), - a_py.gt(b_py).unwrap(), + a_py.gt(&b_py).unwrap(), "{} > {} should be {}.", a_py, b_py, @@ -2517,7 +2648,7 @@ class SimpleClass: ); assert_eq!( a.ge(b), - a_py.ge(b_py).unwrap(), + a_py.ge(&b_py).unwrap(), "{} >= {} should be {}.", a_py, b_py, @@ -2564,10 +2695,10 @@ class SimpleClass: #[test] fn test_rich_compare_type_error() { Python::with_gil(|py| { - let py_int = 1.to_object(py).into_ref(py); - let py_str = "1".to_object(py).into_ref(py); + let py_int = 1.to_object(py).into_bound(py); + let py_str = "1".to_object(py).into_bound(py); - assert!(py_int.rich_compare(py_str, CompareOp::Lt).is_err()); + assert!(py_int.rich_compare(&py_str, CompareOp::Lt).is_err()); assert!(!py_int .rich_compare(py_str, CompareOp::Eq) .unwrap() @@ -2577,17 +2708,16 @@ class SimpleClass: } #[test] - #[allow(deprecated)] fn test_is_ellipsis() { Python::with_gil(|py| { let v = py - .eval("...", None, None) + .eval_bound("...", None, None) .map_err(|e| e.display(py)) .unwrap(); assert!(v.is_ellipsis()); - let not_ellipsis = 5.to_object(py).into_ref(py); + let not_ellipsis = 5.to_object(py).into_bound(py); assert!(!not_ellipsis.is_ellipsis()); }); } @@ -2595,9 +2725,9 @@ class SimpleClass: #[test] fn test_is_callable() { Python::with_gil(|py| { - assert!(PyList::type_object(py).is_callable()); + assert!(PyList::type_object_bound(py).is_callable()); - let not_callable = 5.to_object(py).into_ref(py); + let not_callable = 5.to_object(py).into_bound(py); assert!(!not_callable.is_callable()); }); } @@ -2605,14 +2735,37 @@ class SimpleClass: #[test] fn test_is_empty() { Python::with_gil(|py| { - let empty_list: &PyAny = PyList::empty(py); + let empty_list = PyList::empty_bound(py).into_any(); assert!(empty_list.is_empty().unwrap()); - let list: &PyAny = PyList::new(py, vec![1, 2, 3]); + let list = PyList::new_bound(py, vec![1, 2, 3]).into_any(); assert!(!list.is_empty().unwrap()); - let not_container = 5.to_object(py).into_ref(py); + let not_container = 5.to_object(py).into_bound(py); assert!(not_container.is_empty().is_err()); }); } + + #[cfg(feature = "macros")] + #[test] + #[allow(unknown_lints, non_local_definitions)] + fn test_fallible_dir() { + use crate::exceptions::PyValueError; + use crate::prelude::*; + + #[pyclass(crate = "crate")] + struct DirFail; + + #[pymethods(crate = "crate")] + impl DirFail { + fn __dir__(&self) -> PyResult { + Err(PyValueError::new_err("uh-oh!")) + } + } + + Python::with_gil(|py| { + let obj = Bound::new(py, DirFail).unwrap(); + assert!(obj.dir().unwrap_err().is_instance_of::(py)); + }) + } } diff --git a/src/types/boolobject.rs b/src/types/boolobject.rs index 55896f877c5..9b5aa659fdf 100644 --- a/src/types/boolobject.rs +++ b/src/types/boolobject.rs @@ -1,10 +1,15 @@ #[cfg(feature = "experimental-inspect")] use crate::inspect::types::TypeInfo; +#[cfg(feature = "gil-refs")] +use crate::PyNativeType; use crate::{ - exceptions::PyTypeError, ffi, instance::Bound, FromPyObject, IntoPy, PyAny, PyObject, PyResult, + exceptions::PyTypeError, ffi, ffi_ptr_ext::FfiPtrExt, instance::Bound, + types::typeobject::PyTypeMethods, Borrowed, FromPyObject, IntoPy, PyAny, PyObject, PyResult, Python, ToPyObject, }; +use super::any::PyAnyMethods; + /// Represents a Python `bool`. #[repr(transparent)] pub struct PyBool(PyAny); @@ -13,15 +18,39 @@ pyobject_native_type!(PyBool, ffi::PyObject, pyobject_native_static_type_object! impl PyBool { /// Depending on `val`, returns `true` or `false`. + /// + /// # Note + /// This returns a [`Borrowed`] reference to one of Pythons `True` or + /// `False` singletons + #[inline] + pub fn new_bound(py: Python<'_>, val: bool) -> Borrowed<'_, '_, Self> { + unsafe { + if val { ffi::Py_True() } else { ffi::Py_False() } + .assume_borrowed(py) + .downcast_unchecked() + } + } +} + +#[cfg(feature = "gil-refs")] +impl PyBool { + /// Deprecated form of [`PyBool::new_bound`] + #[deprecated( + since = "0.21.0", + note = "`PyBool::new` will be replaced by `PyBool::new_bound` in a future PyO3 version" + )] #[inline] pub fn new(py: Python<'_>, val: bool) -> &PyBool { - unsafe { py.from_borrowed_ptr(if val { ffi::Py_True() } else { ffi::Py_False() }) } + #[allow(deprecated)] + unsafe { + py.from_borrowed_ptr(if val { ffi::Py_True() } else { ffi::Py_False() }) + } } /// Gets whether this boolean is `true`. #[inline] pub fn is_true(&self) -> bool { - Bound::borrowed_from_gil_ref(&self).is_true() + self.as_borrowed().is_true() } } @@ -31,7 +60,7 @@ impl PyBool { /// syntax these methods are separated into a trait, because stable Rust does not yet support /// `arbitrary_self_types`. #[doc(alias = "PyBool")] -pub trait PyBoolMethods<'py> { +pub trait PyBoolMethods<'py>: crate::sealed::Sealed { /// Gets whether this boolean is `true`. fn is_true(&self) -> bool; } @@ -63,7 +92,7 @@ impl ToPyObject for bool { impl IntoPy for bool { #[inline] fn into_py(self, py: Python<'_>) -> PyObject { - PyBool::new(py, self).into() + PyBool::new_bound(py, self).into_py(py) } #[cfg(feature = "experimental-inspect")] @@ -75,8 +104,8 @@ impl IntoPy for bool { /// Converts a Python `bool` to a Rust `bool`. /// /// Fails with `TypeError` if the input is not a Python `bool`. -impl<'source> FromPyObject<'source> for bool { - fn extract(obj: &'source PyAny) -> PyResult { +impl FromPyObject<'_> for bool { + fn extract_bound(obj: &Bound<'_, PyAny>) -> PyResult { let err = match obj.downcast::() { Ok(obj) => return Ok(obj.is_true()), Err(err) => err, @@ -87,7 +116,7 @@ impl<'source> FromPyObject<'source> for bool { .name() .map_or(false, |name| name == "numpy.bool_") { - let missing_conversion = |obj: &PyAny| { + let missing_conversion = |obj: &Bound<'_, PyAny>| { PyTypeError::new_err(format!( "object of type '{}' does not define a '__bool__' conversion", obj.get_type() @@ -117,7 +146,7 @@ impl<'source> FromPyObject<'source> for bool { .lookup_special(crate::intern!(obj.py(), "__bool__"))? .ok_or_else(|| missing_conversion(obj))?; - let obj = meth.call0()?.downcast::()?; + let obj = meth.call0()?.downcast_into::()?; return Ok(obj.is_true()); } } @@ -133,27 +162,29 @@ impl<'source> FromPyObject<'source> for bool { #[cfg(test)] mod tests { - use crate::types::{PyAny, PyBool}; + use crate::types::any::PyAnyMethods; + use crate::types::boolobject::PyBoolMethods; + use crate::types::PyBool; use crate::Python; use crate::ToPyObject; #[test] fn test_true() { Python::with_gil(|py| { - assert!(PyBool::new(py, true).is_true()); - let t: &PyAny = PyBool::new(py, true).into(); + assert!(PyBool::new_bound(py, true).is_true()); + let t = PyBool::new_bound(py, true); assert!(t.extract::().unwrap()); - assert!(true.to_object(py).is(PyBool::new(py, true))); + assert!(true.to_object(py).is(&*PyBool::new_bound(py, true))); }); } #[test] fn test_false() { Python::with_gil(|py| { - assert!(!PyBool::new(py, false).is_true()); - let t: &PyAny = PyBool::new(py, false).into(); + assert!(!PyBool::new_bound(py, false).is_true()); + let t = PyBool::new_bound(py, false); assert!(!t.extract::().unwrap()); - assert!(false.to_object(py).is(PyBool::new(py, false))); + assert!(false.to_object(py).is(&*PyBool::new_bound(py, false))); }); } } diff --git a/src/types/bytearray.rs b/src/types/bytearray.rs index 3418876e4a6..bec0120136d 100644 --- a/src/types/bytearray.rs +++ b/src/types/bytearray.rs @@ -1,6 +1,11 @@ use crate::err::{PyErr, PyResult}; +use crate::ffi_ptr_ext::FfiPtrExt; use crate::instance::{Borrowed, Bound}; -use crate::{ffi, AsPyPointer, Py, PyAny, Python}; +use crate::py_result_ext::PyResultExt; +use crate::types::any::PyAnyMethods; +use crate::{ffi, PyAny, Python}; +#[cfg(feature = "gil-refs")] +use crate::{AsPyPointer, PyNativeType}; use std::os::raw::c_char; use std::slice; @@ -14,10 +19,14 @@ impl PyByteArray { /// Creates a new Python bytearray object. /// /// The byte string is initialized by copying the data from the `&[u8]`. - pub fn new<'p>(py: Python<'p>, src: &[u8]) -> &'p PyByteArray { + pub fn new_bound<'py>(py: Python<'py>, src: &[u8]) -> Bound<'py, PyByteArray> { let ptr = src.as_ptr() as *const c_char; let len = src.len() as ffi::Py_ssize_t; - unsafe { py.from_owned_ptr::(ffi::PyByteArray_FromStringAndSize(ptr, len)) } + unsafe { + ffi::PyByteArray_FromStringAndSize(ptr, len) + .assume_owned(py) + .downcast_into_unchecked() + } } /// Creates a new Python `bytearray` object with an `init` closure to write its contents. @@ -34,7 +43,7 @@ impl PyByteArray { /// /// # fn main() -> PyResult<()> { /// Python::with_gil(|py| -> PyResult<()> { - /// let py_bytearray = PyByteArray::new_with(py, 10, |bytes: &mut [u8]| { + /// let py_bytearray = PyByteArray::new_bound_with(py, 10, |bytes: &mut [u8]| { /// bytes.copy_from_slice(b"Hello Rust"); /// Ok(()) /// })?; @@ -44,43 +53,83 @@ impl PyByteArray { /// }) /// # } /// ``` - pub fn new_with(py: Python<'_>, len: usize, init: F) -> PyResult<&PyByteArray> + pub fn new_bound_with( + py: Python<'_>, + len: usize, + init: F, + ) -> PyResult> where F: FnOnce(&mut [u8]) -> PyResult<()>, { unsafe { - let pyptr = - ffi::PyByteArray_FromStringAndSize(std::ptr::null(), len as ffi::Py_ssize_t); - // Check for an allocation error and return it - let pypybytearray: Py = Py::from_owned_ptr_or_err(py, pyptr)?; - let buffer: *mut u8 = ffi::PyByteArray_AsString(pyptr).cast(); + // Allocate buffer and check for an error + let pybytearray: Bound<'_, Self> = + ffi::PyByteArray_FromStringAndSize(std::ptr::null(), len as ffi::Py_ssize_t) + .assume_owned_or_err(py)? + .downcast_into_unchecked(); + + let buffer: *mut u8 = ffi::PyByteArray_AsString(pybytearray.as_ptr()).cast(); debug_assert!(!buffer.is_null()); // Zero-initialise the uninitialised bytearray std::ptr::write_bytes(buffer, 0u8, len); // (Further) Initialise the bytearray in init // If init returns an Err, pypybytearray will automatically deallocate the buffer - init(std::slice::from_raw_parts_mut(buffer, len)).map(|_| pypybytearray.into_ref(py)) + init(std::slice::from_raw_parts_mut(buffer, len)).map(|_| pybytearray) } } /// Creates a new Python `bytearray` object from another Python object that /// implements the buffer protocol. - pub fn from(src: &PyAny) -> PyResult<&PyByteArray> { + pub fn from_bound<'py>(src: &Bound<'py, PyAny>) -> PyResult> { unsafe { - src.py() - .from_owned_ptr_or_err(ffi::PyByteArray_FromObject(src.as_ptr())) + ffi::PyByteArray_FromObject(src.as_ptr()) + .assume_owned_or_err(src.py()) + .downcast_into_unchecked() } } +} + +#[cfg(feature = "gil-refs")] +impl PyByteArray { + /// Deprecated form of [`PyByteArray::new_bound`] + #[deprecated( + since = "0.21.0", + note = "`PyByteArray::new` will be replaced by `PyByteArray::new_bound` in a future PyO3 version" + )] + pub fn new<'py>(py: Python<'py>, src: &[u8]) -> &'py PyByteArray { + Self::new_bound(py, src).into_gil_ref() + } + + /// Deprecated form of [`PyByteArray::new_bound_with`] + #[deprecated( + since = "0.21.0", + note = "`PyByteArray::new_with` will be replaced by `PyByteArray::new_bound_with` in a future PyO3 version" + )] + pub fn new_with(py: Python<'_>, len: usize, init: F) -> PyResult<&PyByteArray> + where + F: FnOnce(&mut [u8]) -> PyResult<()>, + { + Self::new_bound_with(py, len, init).map(Bound::into_gil_ref) + } + + /// Deprecated form of [`PyByteArray::from_bound`] + #[deprecated( + since = "0.21.0", + note = "`PyByteArray::from` will be replaced by `PyByteArray::from_bound` in a future PyO3 version" + )] + pub fn from(src: &PyAny) -> PyResult<&PyByteArray> { + PyByteArray::from_bound(&src.as_borrowed()).map(Bound::into_gil_ref) + } /// Gets the length of the bytearray. #[inline] pub fn len(&self) -> usize { - Bound::borrowed_from_gil_ref(&self).len() + self.as_borrowed().len() } /// Checks if the bytearray is empty. pub fn is_empty(&self) -> bool { - Bound::borrowed_from_gil_ref(&self).is_empty() + self.as_borrowed().is_empty() } /// Gets the start of the buffer containing the contents of the bytearray. @@ -89,7 +138,7 @@ impl PyByteArray { /// /// See the safety requirements of [`PyByteArray::as_bytes`] and [`PyByteArray::as_bytes_mut`]. pub fn data(&self) -> *mut u8 { - Bound::borrowed_from_gil_ref(&self).data() + self.as_borrowed().data() } /// Extracts a slice of the `ByteArray`'s entire buffer. @@ -118,7 +167,7 @@ impl PyByteArray { /// use pyo3::types::PyByteArray; /// /// #[pyfunction] - /// fn a_valid_function(bytes: &PyByteArray) -> PyResult<()> { + /// fn a_valid_function(bytes: &Bound<'_, PyByteArray>) -> PyResult<()> { /// let section = { /// // SAFETY: We promise to not let the interpreter regain control /// // or invoke any PyO3 APIs while using the slice. @@ -140,11 +189,11 @@ impl PyByteArray { /// } /// # fn main() -> PyResult<()> { /// # Python::with_gil(|py| -> PyResult<()> { - /// # let fun = wrap_pyfunction!(a_valid_function, py)?; - /// # let locals = pyo3::types::PyDict::new(py); + /// # let fun = wrap_pyfunction_bound!(a_valid_function, py)?; + /// # let locals = pyo3::types::PyDict::new_bound(py); /// # locals.set_item("a_valid_function", fun)?; /// # - /// # py.run( + /// # py.run_bound( /// # r#"b = bytearray(b"hello world") /// # a_valid_function(b) /// # @@ -153,7 +202,7 @@ impl PyByteArray { /// # except RuntimeError as e: /// # assert str(e) == 'input is not long enough'"#, /// # None, - /// # Some(locals), + /// # Some(&locals), /// # )?; /// # /// # Ok(()) @@ -171,7 +220,7 @@ impl PyByteArray { /// /// # #[allow(dead_code)] /// #[pyfunction] - /// fn bug(py: Python<'_>, bytes: &PyByteArray) { + /// fn bug(py: Python<'_>, bytes: &Bound<'_, PyByteArray>) { /// let slice = unsafe { bytes.as_bytes() }; /// /// // This explicitly yields control back to the Python interpreter... @@ -188,7 +237,7 @@ impl PyByteArray { /// } /// ``` pub unsafe fn as_bytes(&self) -> &[u8] { - Borrowed::from_gil_ref(self).as_bytes() + self.as_borrowed().as_bytes() } /// Extracts a mutable slice of the `ByteArray`'s entire buffer. @@ -200,7 +249,7 @@ impl PyByteArray { /// apply to this function as well. #[allow(clippy::mut_from_ref)] pub unsafe fn as_bytes_mut(&self) -> &mut [u8] { - Borrowed::from_gil_ref(self).as_bytes_mut() + self.as_borrowed().as_bytes_mut() } /// Copies the contents of the bytearray to a Rust vector. @@ -211,7 +260,7 @@ impl PyByteArray { /// # use pyo3::prelude::*; /// # use pyo3::types::PyByteArray; /// # Python::with_gil(|py| { - /// let bytearray = PyByteArray::new(py, b"Hello World."); + /// let bytearray = PyByteArray::new_bound(py, b"Hello World."); /// let mut copied_message = bytearray.to_vec(); /// assert_eq!(b"Hello World.", copied_message.as_slice()); /// @@ -222,7 +271,7 @@ impl PyByteArray { /// # }); /// ``` pub fn to_vec(&self) -> Vec { - Bound::borrowed_from_gil_ref(&self).to_vec() + self.as_borrowed().to_vec() } /// Resizes the bytearray object to the new length `len`. @@ -230,7 +279,7 @@ impl PyByteArray { /// Note that this will invalidate any pointers obtained by [PyByteArray::data], as well as /// any (unsafe) slices obtained from [PyByteArray::as_bytes] and [PyByteArray::as_bytes_mut]. pub fn resize(&self, len: usize) -> PyResult<()> { - Bound::borrowed_from_gil_ref(&self).resize(len) + self.as_borrowed().resize(len) } } @@ -240,7 +289,7 @@ impl PyByteArray { /// syntax these methods are separated into a trait, because stable Rust does not yet support /// `arbitrary_self_types`. #[doc(alias = "PyByteArray")] -pub trait PyByteArrayMethods<'py> { +pub trait PyByteArrayMethods<'py>: crate::sealed::Sealed { /// Gets the length of the bytearray. fn len(&self) -> usize; @@ -251,7 +300,7 @@ pub trait PyByteArrayMethods<'py> { /// /// # Safety /// - /// See the safety requirements of [`PyByteArray::as_bytes`] and [`PyByteArray::as_bytes_mut`]. + /// See the safety requirements of [`PyByteArrayMethods::as_bytes`] and [`PyByteArrayMethods::as_bytes_mut`]. fn data(&self) -> *mut u8; /// Extracts a slice of the `ByteArray`'s entire buffer. @@ -262,7 +311,7 @@ pub trait PyByteArrayMethods<'py> { /// undefined. /// /// These mutations may occur in Python code as well as from Rust: - /// - Calling methods like [`PyByteArray::as_bytes_mut`] and [`PyByteArray::resize`] will + /// - Calling methods like [`PyByteArrayMethods::as_bytes_mut`] and [`PyByteArrayMethods::resize`] will /// invalidate the slice. /// - Actions like dropping objects or raising exceptions can invoke `__del__`methods or signal /// handlers, which may execute arbitrary Python code. This means that if Python code has a @@ -280,7 +329,7 @@ pub trait PyByteArrayMethods<'py> { /// use pyo3::types::PyByteArray; /// /// #[pyfunction] - /// fn a_valid_function(bytes: &PyByteArray) -> PyResult<()> { + /// fn a_valid_function(bytes: &Bound<'_, PyByteArray>) -> PyResult<()> { /// let section = { /// // SAFETY: We promise to not let the interpreter regain control /// // or invoke any PyO3 APIs while using the slice. @@ -302,11 +351,11 @@ pub trait PyByteArrayMethods<'py> { /// } /// # fn main() -> PyResult<()> { /// # Python::with_gil(|py| -> PyResult<()> { - /// # let fun = wrap_pyfunction!(a_valid_function, py)?; - /// # let locals = pyo3::types::PyDict::new(py); + /// # let fun = wrap_pyfunction_bound!(a_valid_function, py)?; + /// # let locals = pyo3::types::PyDict::new_bound(py); /// # locals.set_item("a_valid_function", fun)?; /// # - /// # py.run( + /// # py.run_bound( /// # r#"b = bytearray(b"hello world") /// # a_valid_function(b) /// # @@ -315,7 +364,7 @@ pub trait PyByteArrayMethods<'py> { /// # except RuntimeError as e: /// # assert str(e) == 'input is not long enough'"#, /// # None, - /// # Some(locals), + /// # Some(&locals), /// # )?; /// # /// # Ok(()) @@ -333,7 +382,7 @@ pub trait PyByteArrayMethods<'py> { /// /// # #[allow(dead_code)] /// #[pyfunction] - /// fn bug(py: Python<'_>, bytes: &PyByteArray) { + /// fn bug(py: Python<'_>, bytes: &Bound<'_, PyByteArray>) { /// let slice = unsafe { bytes.as_bytes() }; /// /// // This explicitly yields control back to the Python interpreter... @@ -356,7 +405,7 @@ pub trait PyByteArrayMethods<'py> { /// # Safety /// /// Any other accesses of the `bytearray`'s buffer invalidate the slice. If it is used - /// afterwards, the behavior is undefined. The safety requirements of [`PyByteArray::as_bytes`] + /// afterwards, the behavior is undefined. The safety requirements of [`PyByteArrayMethods::as_bytes`] /// apply to this function as well. #[allow(clippy::mut_from_ref)] unsafe fn as_bytes_mut(&self) -> &mut [u8]; @@ -369,7 +418,7 @@ pub trait PyByteArrayMethods<'py> { /// # use pyo3::prelude::*; /// # use pyo3::types::PyByteArray; /// # Python::with_gil(|py| { - /// let bytearray = PyByteArray::new(py, b"Hello World."); + /// let bytearray = PyByteArray::new_bound(py, b"Hello World."); /// let mut copied_message = bytearray.to_vec(); /// assert_eq!(b"Hello World.", copied_message.as_slice()); /// @@ -383,8 +432,8 @@ pub trait PyByteArrayMethods<'py> { /// Resizes the bytearray object to the new length `len`. /// - /// Note that this will invalidate any pointers obtained by [PyByteArray::data], as well as - /// any (unsafe) slices obtained from [PyByteArray::as_bytes] and [PyByteArray::as_bytes_mut]. + /// Note that this will invalidate any pointers obtained by [PyByteArrayMethods::data], as well as + /// any (unsafe) slices obtained from [PyByteArrayMethods::as_bytes] and [PyByteArrayMethods::as_bytes_mut]. fn resize(&self, len: usize) -> PyResult<()>; } @@ -400,16 +449,16 @@ impl<'py> PyByteArrayMethods<'py> for Bound<'py, PyByteArray> { } fn data(&self) -> *mut u8 { - Borrowed::from(self).data() + self.as_borrowed().data() } unsafe fn as_bytes(&self) -> &[u8] { - Borrowed::from(self).as_bytes() + self.as_borrowed().as_bytes() } #[allow(clippy::mut_from_ref)] unsafe fn as_bytes_mut(&self) -> &mut [u8] { - Borrowed::from(self).as_bytes_mut() + self.as_borrowed().as_bytes_mut() } fn to_vec(&self) -> Vec { @@ -444,27 +493,41 @@ impl<'a> Borrowed<'a, '_, PyByteArray> { } } +#[cfg(feature = "gil-refs")] impl<'py> TryFrom<&'py PyAny> for &'py PyByteArray { type Error = crate::PyErr; /// Creates a new Python `bytearray` object from another Python object that /// implements the buffer protocol. fn try_from(value: &'py PyAny) -> Result { - PyByteArray::from(value) + PyByteArray::from_bound(&value.as_borrowed()).map(Bound::into_gil_ref) + } +} + +impl<'py> TryFrom<&Bound<'py, PyAny>> for Bound<'py, PyByteArray> { + type Error = crate::PyErr; + + /// Creates a new Python `bytearray` object from another Python object that + /// implements the buffer protocol. + fn try_from(value: &Bound<'py, PyAny>) -> Result { + PyByteArray::from_bound(value) } } #[cfg(test)] mod tests { + use crate::types::any::PyAnyMethods; + use crate::types::bytearray::PyByteArrayMethods; + use crate::types::string::PyStringMethods; use crate::types::PyByteArray; - use crate::{exceptions, PyAny}; + use crate::{exceptions, Bound, PyAny}; use crate::{PyObject, Python}; #[test] fn test_len() { Python::with_gil(|py| { let src = b"Hello Python"; - let bytearray = PyByteArray::new(py, src); + let bytearray = PyByteArray::new_bound(py, src); assert_eq!(src.len(), bytearray.len()); }); } @@ -473,7 +536,7 @@ mod tests { fn test_as_bytes() { Python::with_gil(|py| { let src = b"Hello Python"; - let bytearray = PyByteArray::new(py, src); + let bytearray = PyByteArray::new_bound(py, src); let slice = unsafe { bytearray.as_bytes() }; assert_eq!(src, slice); @@ -485,7 +548,7 @@ mod tests { fn test_as_bytes_mut() { Python::with_gil(|py| { let src = b"Hello Python"; - let bytearray = PyByteArray::new(py, src); + let bytearray = PyByteArray::new_bound(py, src); let slice = unsafe { bytearray.as_bytes_mut() }; assert_eq!(src, slice); @@ -494,7 +557,7 @@ mod tests { slice[0..5].copy_from_slice(b"Hi..."); assert_eq!( - bytearray.str().unwrap().to_str().unwrap(), + bytearray.str().unwrap().to_cow().unwrap(), "bytearray(b'Hi... Python')" ); }); @@ -504,7 +567,7 @@ mod tests { fn test_to_vec() { Python::with_gil(|py| { let src = b"Hello Python"; - let bytearray = PyByteArray::new(py, src); + let bytearray = PyByteArray::new_bound(py, src); let vec = bytearray.to_vec(); assert_eq!(src, vec.as_slice()); @@ -515,10 +578,10 @@ mod tests { fn test_from() { Python::with_gil(|py| { let src = b"Hello Python"; - let bytearray = PyByteArray::new(py, src); + let bytearray = PyByteArray::new_bound(py, src); let ba: PyObject = bytearray.into(); - let bytearray = PyByteArray::from(ba.as_ref(py)).unwrap(); + let bytearray = PyByteArray::from_bound(ba.bind(py)).unwrap(); assert_eq!(src, unsafe { bytearray.as_bytes() }); }); @@ -527,7 +590,7 @@ mod tests { #[test] fn test_from_err() { Python::with_gil(|py| { - if let Err(err) = PyByteArray::from(py.None()) { + if let Err(err) = PyByteArray::from_bound(py.None().bind(py)) { assert!(err.is_instance_of::(py)); } else { panic!("error"); @@ -539,8 +602,8 @@ mod tests { fn test_try_from() { Python::with_gil(|py| { let src = b"Hello Python"; - let bytearray: &PyAny = PyByteArray::new(py, src).into(); - let bytearray: &PyByteArray = TryInto::try_into(bytearray).unwrap(); + let bytearray: &Bound<'_, PyAny> = &PyByteArray::new_bound(py, src); + let bytearray: Bound<'_, PyByteArray> = TryInto::try_into(bytearray).unwrap(); assert_eq!(src, unsafe { bytearray.as_bytes() }); }); @@ -550,7 +613,7 @@ mod tests { fn test_resize() { Python::with_gil(|py| { let src = b"Hello Python"; - let bytearray = PyByteArray::new(py, src); + let bytearray = PyByteArray::new_bound(py, src); bytearray.resize(20).unwrap(); assert_eq!(20, bytearray.len()); @@ -560,7 +623,7 @@ mod tests { #[test] fn test_byte_array_new_with() -> super::PyResult<()> { Python::with_gil(|py| -> super::PyResult<()> { - let py_bytearray = PyByteArray::new_with(py, 10, |b: &mut [u8]| { + let py_bytearray = PyByteArray::new_bound_with(py, 10, |b: &mut [u8]| { b.copy_from_slice(b"Hello Rust"); Ok(()) })?; @@ -573,7 +636,7 @@ mod tests { #[test] fn test_byte_array_new_with_zero_initialised() -> super::PyResult<()> { Python::with_gil(|py| -> super::PyResult<()> { - let py_bytearray = PyByteArray::new_with(py, 10, |_b: &mut [u8]| Ok(()))?; + let py_bytearray = PyByteArray::new_bound_with(py, 10, |_b: &mut [u8]| Ok(()))?; let bytearray: &[u8] = unsafe { py_bytearray.as_bytes() }; assert_eq!(bytearray, &[0; 10]); Ok(()) @@ -584,7 +647,7 @@ mod tests { fn test_byte_array_new_with_error() { use crate::exceptions::PyValueError; Python::with_gil(|py| { - let py_bytearray_result = PyByteArray::new_with(py, 10, |_b: &mut [u8]| { + let py_bytearray_result = PyByteArray::new_bound_with(py, 10, |_b: &mut [u8]| { Err(PyValueError::new_err("Hello Crustaceans!")) }); assert!(py_bytearray_result.is_err()); diff --git a/src/types/bytes.rs b/src/types/bytes.rs index 4db098c6bb6..1d6a2f8ec7d 100644 --- a/src/types/bytes.rs +++ b/src/types/bytes.rs @@ -1,13 +1,14 @@ +use crate::ffi_ptr_ext::FfiPtrExt; use crate::instance::{Borrowed, Bound}; -use crate::{ffi, FromPyObject, IntoPy, Py, PyAny, PyResult, Python, ToPyObject}; -use std::borrow::Cow; +use crate::types::any::PyAnyMethods; +#[cfg(feature = "gil-refs")] +use crate::PyNativeType; +use crate::{ffi, Py, PyAny, PyResult, Python}; use std::ops::Index; use std::os::raw::c_char; use std::slice::SliceIndex; use std::str; -use super::bytearray::PyByteArray; - /// Represents a Python `bytes` object. /// /// This type is immutable. @@ -21,10 +22,14 @@ impl PyBytes { /// The bytestring is initialized by copying the data from the `&[u8]`. /// /// Panics if out of memory. - pub fn new<'p>(py: Python<'p>, s: &[u8]) -> &'p PyBytes { + pub fn new_bound<'p>(py: Python<'p>, s: &[u8]) -> Bound<'p, PyBytes> { let ptr = s.as_ptr() as *const c_char; let len = s.len() as ffi::Py_ssize_t; - unsafe { py.from_owned_ptr(ffi::PyBytes_FromStringAndSize(ptr, len)) } + unsafe { + ffi::PyBytes_FromStringAndSize(ptr, len) + .assume_owned(py) + .downcast_into_unchecked() + } } /// Creates a new Python `bytes` object with an `init` closure to write its contents. @@ -41,31 +46,31 @@ impl PyBytes { /// /// # fn main() -> PyResult<()> { /// Python::with_gil(|py| -> PyResult<()> { - /// let py_bytes = PyBytes::new_with(py, 10, |bytes: &mut [u8]| { + /// let py_bytes = PyBytes::new_bound_with(py, 10, |bytes: &mut [u8]| { /// bytes.copy_from_slice(b"Hello Rust"); /// Ok(()) /// })?; - /// let bytes: &[u8] = FromPyObject::extract(py_bytes)?; + /// let bytes: &[u8] = py_bytes.extract()?; /// assert_eq!(bytes, b"Hello Rust"); /// Ok(()) /// }) /// # } /// ``` - pub fn new_with(py: Python<'_>, len: usize, init: F) -> PyResult<&PyBytes> + pub fn new_bound_with(py: Python<'_>, len: usize, init: F) -> PyResult> where F: FnOnce(&mut [u8]) -> PyResult<()>, { unsafe { let pyptr = ffi::PyBytes_FromStringAndSize(std::ptr::null(), len as ffi::Py_ssize_t); // Check for an allocation error and return it - let pypybytes: Py = Py::from_owned_ptr_or_err(py, pyptr)?; + let pybytes = pyptr.assume_owned_or_err(py)?.downcast_into_unchecked(); let buffer: *mut u8 = ffi::PyBytes_AsString(pyptr).cast(); debug_assert!(!buffer.is_null()); // Zero-initialise the uninitialised bytestring std::ptr::write_bytes(buffer, 0u8, len); // (Further) Initialise the bytestring in init // If init returns an Err, pypybytearray will automatically deallocate the buffer - init(std::slice::from_raw_parts_mut(buffer, len)).map(|_| pypybytes.into_ref(py)) + init(std::slice::from_raw_parts_mut(buffer, len)).map(|_| pybytes) } } @@ -79,17 +84,52 @@ impl PyBytes { /// leading pointer of a slice of length `len`. [As with /// `std::slice::from_raw_parts`, this is /// unsafe](https://doc.rust-lang.org/std/slice/fn.from_raw_parts.html#safety). + pub unsafe fn bound_from_ptr(py: Python<'_>, ptr: *const u8, len: usize) -> Bound<'_, PyBytes> { + ffi::PyBytes_FromStringAndSize(ptr as *const _, len as isize) + .assume_owned(py) + .downcast_into_unchecked() + } +} + +#[cfg(feature = "gil-refs")] +impl PyBytes { + /// Deprecated form of [`PyBytes::new_bound`]. + #[deprecated( + since = "0.21.0", + note = "`PyBytes::new` will be replaced by `PyBytes::new_bound` in a future PyO3 version" + )] + pub fn new<'p>(py: Python<'p>, s: &[u8]) -> &'p PyBytes { + Self::new_bound(py, s).into_gil_ref() + } + + /// Deprecated form of [`PyBytes::new_bound_with`]. + #[deprecated( + since = "0.21.0", + note = "`PyBytes::new_with` will be replaced by `PyBytes::new_bound_with` in a future PyO3 version" + )] + pub fn new_with(py: Python<'_>, len: usize, init: F) -> PyResult<&PyBytes> + where + F: FnOnce(&mut [u8]) -> PyResult<()>, + { + Self::new_bound_with(py, len, init).map(Bound::into_gil_ref) + } + + /// Deprecated form of [`PyBytes::bound_from_ptr`]. + /// + /// # Safety + /// See [`PyBytes::bound_from_ptr`]. + #[deprecated( + since = "0.21.0", + note = "`PyBytes::from_ptr` will be replaced by `PyBytes::bound_from_ptr` in a future PyO3 version" + )] pub unsafe fn from_ptr(py: Python<'_>, ptr: *const u8, len: usize) -> &PyBytes { - py.from_owned_ptr(ffi::PyBytes_FromStringAndSize( - ptr as *const _, - len as isize, - )) + Self::bound_from_ptr(py, ptr, len).into_gil_ref() } /// Gets the Python string as a byte slice. #[inline] pub fn as_bytes(&self) -> &[u8] { - Borrowed::from_gil_ref(self).as_bytes() + self.as_borrowed().as_bytes() } } @@ -99,7 +139,7 @@ impl PyBytes { /// syntax these methods are separated into a trait, because stable Rust does not yet support /// `arbitrary_self_types`. #[doc(alias = "PyBytes")] -pub trait PyBytesMethods<'py> { +pub trait PyBytesMethods<'py>: crate::sealed::Sealed { /// Gets the Python string as a byte slice. fn as_bytes(&self) -> &[u8]; } @@ -107,14 +147,14 @@ pub trait PyBytesMethods<'py> { impl<'py> PyBytesMethods<'py> for Bound<'py, PyBytes> { #[inline] fn as_bytes(&self) -> &[u8] { - Borrowed::from(self).as_bytes() + self.as_borrowed().as_bytes() } } impl<'a> Borrowed<'a, '_, PyBytes> { /// Gets the Python string as a byte slice. #[allow(clippy::wrong_self_convention)] - fn as_bytes(self) -> &'a [u8] { + pub(crate) fn as_bytes(self) -> &'a [u8] { unsafe { let buffer = ffi::PyBytes_AsString(self.as_ptr()) as *const u8; let length = ffi::PyBytes_Size(self.as_ptr()) as usize; @@ -134,6 +174,7 @@ impl Py { } /// This is the same way [Vec] is indexed. +#[cfg(feature = "gil-refs")] impl> Index for PyBytes { type Output = I::Output; @@ -142,31 +183,12 @@ impl> Index for PyBytes { } } -/// Special-purpose trait impl to efficiently handle both `bytes` and `bytearray` -/// -/// If the source object is a `bytes` object, the `Cow` will be borrowed and -/// pointing into the source object, and no copying or heap allocations will happen. -/// If it is a `bytearray`, its contents will be copied to an owned `Cow`. -impl<'source> FromPyObject<'source> for Cow<'source, [u8]> { - fn extract(ob: &'source PyAny) -> PyResult { - if let Ok(bytes) = ob.downcast::() { - return Ok(Cow::Borrowed(bytes.as_bytes())); - } - - let byte_array = ob.downcast::()?; - Ok(Cow::Owned(byte_array.to_vec())) - } -} - -impl ToPyObject for Cow<'_, [u8]> { - fn to_object(&self, py: Python<'_>) -> Py { - PyBytes::new(py, self.as_ref()).into() - } -} +/// This is the same way [Vec] is indexed. +impl> Index for Bound<'_, PyBytes> { + type Output = I::Output; -impl IntoPy> for Cow<'_, [u8]> { - fn into_py(self, py: Python<'_>) -> Py { - self.to_object(py) + fn index(&self, index: I) -> &Self::Output { + &self.as_bytes()[index] } } @@ -177,7 +199,18 @@ mod tests { #[test] fn test_bytes_index() { Python::with_gil(|py| { - let bytes = PyBytes::new(py, b"Hello World"); + let bytes = PyBytes::new_bound(py, b"Hello World"); + assert_eq!(bytes[1], b'e'); + }); + } + + #[test] + fn test_bound_bytes_index() { + Python::with_gil(|py| { + let bytes = PyBytes::new_bound(py, b"Hello World"); + assert_eq!(bytes[1], b'e'); + + let bytes = &bytes; assert_eq!(bytes[1], b'e'); }); } @@ -185,11 +218,11 @@ mod tests { #[test] fn test_bytes_new_with() -> super::PyResult<()> { Python::with_gil(|py| -> super::PyResult<()> { - let py_bytes = PyBytes::new_with(py, 10, |b: &mut [u8]| { + let py_bytes = PyBytes::new_bound_with(py, 10, |b: &mut [u8]| { b.copy_from_slice(b"Hello Rust"); Ok(()) })?; - let bytes: &[u8] = FromPyObject::extract(py_bytes)?; + let bytes: &[u8] = py_bytes.extract()?; assert_eq!(bytes, b"Hello Rust"); Ok(()) }) @@ -198,8 +231,8 @@ mod tests { #[test] fn test_bytes_new_with_zero_initialised() -> super::PyResult<()> { Python::with_gil(|py| -> super::PyResult<()> { - let py_bytes = PyBytes::new_with(py, 10, |_b: &mut [u8]| Ok(()))?; - let bytes: &[u8] = FromPyObject::extract(py_bytes)?; + let py_bytes = PyBytes::new_bound_with(py, 10, |_b: &mut [u8]| Ok(()))?; + let bytes: &[u8] = py_bytes.extract()?; assert_eq!(bytes, &[0; 10]); Ok(()) }) @@ -209,7 +242,7 @@ mod tests { fn test_bytes_new_with_error() { use crate::exceptions::PyValueError; Python::with_gil(|py| { - let py_bytes_result = PyBytes::new_with(py, 10, |_b: &mut [u8]| { + let py_bytes_result = PyBytes::new_bound_with(py, 10, |_b: &mut [u8]| { Err(PyValueError::new_err("Hello Crustaceans!")) }); assert!(py_bytes_result.is_err()); @@ -219,28 +252,4 @@ mod tests { .is_instance_of::(py)); }); } - - #[test] - fn test_cow_impl() { - Python::with_gil(|py| { - let bytes = py.eval(r#"b"foobar""#, None, None).unwrap(); - let cow = bytes.extract::>().unwrap(); - assert_eq!(cow, Cow::<[u8]>::Borrowed(b"foobar")); - - let byte_array = py.eval(r#"bytearray(b"foobar")"#, None, None).unwrap(); - let cow = byte_array.extract::>().unwrap(); - assert_eq!(cow, Cow::<[u8]>::Owned(b"foobar".to_vec())); - - let something_else_entirely = py.eval("42", None, None).unwrap(); - something_else_entirely - .extract::>() - .unwrap_err(); - - let cow = Cow::<[u8]>::Borrowed(b"foobar").to_object(py); - assert!(cow.as_ref(py).is_instance_of::()); - - let cow = Cow::<[u8]>::Owned(b"foobar".to_vec()).to_object(py); - assert!(cow.as_ref(py).is_instance_of::()); - }); - } } diff --git a/src/types/capsule.rs b/src/types/capsule.rs index f97320eb474..9b9445cdffe 100644 --- a/src/types/capsule.rs +++ b/src/types/capsule.rs @@ -1,9 +1,12 @@ -use crate::Python; +use crate::ffi_ptr_ext::FfiPtrExt; +use crate::py_result_ext::PyResultExt; +#[cfg(feature = "gil-refs")] +use crate::PyNativeType; use crate::{ffi, PyAny}; -use crate::{pyobject_native_type_core, PyErr, PyResult}; +use crate::{Bound, Python}; +use crate::{PyErr, PyResult}; use std::ffi::{CStr, CString}; use std::os::raw::{c_char, c_int, c_void}; - /// Represents a Python Capsule /// as described in [Capsules](https://docs.python.org/3/c-api/capsule.html#capsules): /// > This subtype of PyObject represents an opaque value, useful for C extension @@ -27,9 +30,9 @@ use std::os::raw::{c_char, c_int, c_void}; /// let foo = Foo { val: 123 }; /// let name = CString::new("builtins.capsule").unwrap(); /// -/// let capsule = PyCapsule::new(py, foo, Some(name.clone()))?; +/// let capsule = PyCapsule::new_bound(py, foo, Some(name.clone()))?; /// -/// let module = PyModule::import(py, "builtins")?; +/// let module = PyModule::import_bound(py, "builtins")?; /// module.add("capsule", capsule)?; /// /// let cap: &Foo = unsafe { PyCapsule::import(py, name.as_ref())? }; @@ -59,7 +62,7 @@ impl PyCapsule { /// /// Python::with_gil(|py| { /// let name = CString::new("foo").unwrap(); - /// let capsule = PyCapsule::new(py, 123_u32, Some(name)).unwrap(); + /// let capsule = PyCapsule::new_bound(py, 123_u32, Some(name)).unwrap(); /// let val = unsafe { capsule.reference::() }; /// assert_eq!(*val, 123); /// }); @@ -72,15 +75,15 @@ impl PyCapsule { /// use std::ffi::CString; /// /// Python::with_gil(|py| { - /// let capsule = PyCapsule::new(py, (), None).unwrap(); // Oops! `()` is zero sized! + /// let capsule = PyCapsule::new_bound(py, (), None).unwrap(); // Oops! `()` is zero sized! /// }); /// ``` - pub fn new( + pub fn new_bound( py: Python<'_>, value: T, name: Option, - ) -> PyResult<&Self> { - Self::new_with_destructor(py, value, name, |_, _| {}) + ) -> PyResult> { + Self::new_bound_with_destructor(py, value, name, |_, _| {}) } /// Constructs a new capsule whose contents are `value`, associated with `name`. @@ -90,7 +93,7 @@ impl PyCapsule { /// /// The `destructor` must be `Send`, because there is no guarantee which thread it will eventually /// be called from. - pub fn new_with_destructor< + pub fn new_bound_with_destructor< T: 'static + Send + AssertNotZeroSized, F: FnOnce(T, *mut c_void) + Send, >( @@ -98,7 +101,7 @@ impl PyCapsule { value: T, name: Option, destructor: F, - ) -> PyResult<&'_ Self> { + ) -> PyResult> { AssertNotZeroSized::assert_not_zero_sized(&value); // Sanity check for capsule layout @@ -112,12 +115,13 @@ impl PyCapsule { }); unsafe { - let cap_ptr = ffi::PyCapsule_New( - Box::into_raw(val) as *mut c_void, + ffi::PyCapsule_New( + Box::into_raw(val).cast(), name_ptr, Some(capsule_destructor::), - ); - py.from_owned_ptr_or_err(cap_ptr) + ) + .assume_owned_or_err(py) + .downcast_into_unchecked() } } @@ -134,9 +138,42 @@ impl PyCapsule { if ptr.is_null() { Err(PyErr::fetch(py)) } else { - Ok(&*(ptr as *const T)) + Ok(&*ptr.cast::()) } } +} + +#[cfg(feature = "gil-refs")] +impl PyCapsule { + /// Deprecated form of [`PyCapsule::new_bound`]. + #[deprecated( + since = "0.21.0", + note = "`PyCapsule::new` will be replaced by `PyCapsule::new_bound` in a future PyO3 version" + )] + pub fn new( + py: Python<'_>, + value: T, + name: Option, + ) -> PyResult<&Self> { + Self::new_bound(py, value, name).map(Bound::into_gil_ref) + } + + /// Deprecated form of [`PyCapsule::new_bound_with_destructor`]. + #[deprecated( + since = "0.21.0", + note = "`PyCapsule::new_with_destructor` will be replaced by `PyCapsule::new_bound_with_destructor` in a future PyO3 version" + )] + pub fn new_with_destructor< + T: 'static + Send + AssertNotZeroSized, + F: FnOnce(T, *mut c_void) + Send, + >( + py: Python<'_>, + value: T, + name: Option, + destructor: F, + ) -> PyResult<&'_ Self> { + Self::new_bound_with_destructor(py, value, name, destructor).map(Bound::into_gil_ref) + } /// Sets the context pointer in the capsule. /// @@ -160,29 +197,23 @@ impl PyCapsule { /// let (tx, rx) = channel::(); /// /// fn destructor(val: u32, context: *mut c_void) { - /// let ctx = unsafe { *Box::from_raw(context as *mut Sender) }; + /// let ctx = unsafe { *Box::from_raw(context.cast::>()) }; /// ctx.send("Destructor called!".to_string()).unwrap(); /// } /// /// Python::with_gil(|py| { /// let capsule = - /// PyCapsule::new_with_destructor(py, 123, None, destructor as fn(u32, *mut c_void)) + /// PyCapsule::new_bound_with_destructor(py, 123, None, destructor as fn(u32, *mut c_void)) /// .unwrap(); /// let context = Box::new(tx); // `Sender` is our context, box it up and ship it! - /// capsule.set_context(Box::into_raw(context) as *mut c_void).unwrap(); + /// capsule.set_context(Box::into_raw(context).cast()).unwrap(); /// // This scope will end, causing our destructor to be called... /// }); /// /// assert_eq!(rx.recv(), Ok("Destructor called!".to_string())); /// ``` - #[allow(clippy::not_unsafe_ptr_arg_deref)] pub fn set_context(&self, context: *mut c_void) -> PyResult<()> { - let result = unsafe { ffi::PyCapsule_SetContext(self.as_ptr(), context) }; - if result != 0 { - Err(PyErr::fetch(self.py())) - } else { - Ok(()) - } + self.as_borrowed().set_context(context) } /// Gets the current context stored in the capsule. If there is no context, the pointer @@ -190,11 +221,7 @@ impl PyCapsule { /// /// Returns an error if this capsule is not valid. pub fn context(&self) -> PyResult<*mut c_void> { - let ctx = unsafe { ffi::PyCapsule_GetContext(self.as_ptr()) }; - if ctx.is_null() { - ensure_no_error(self.py())? - } - Ok(ctx) + self.as_borrowed().context() } /// Obtains a reference to the value of this capsule. @@ -203,15 +230,132 @@ impl PyCapsule { /// /// It must be known that this capsule is valid and its pointer is to an item of type `T`. pub unsafe fn reference(&self) -> &T { - &*(self.pointer() as *const T) + self.as_borrowed().reference() } /// Gets the raw `c_void` pointer to the value in this capsule. /// /// Returns null if this capsule is not valid. pub fn pointer(&self) -> *mut c_void { + self.as_borrowed().pointer() + } + + /// Checks if this is a valid capsule. + /// + /// Returns true if the stored `pointer()` is non-null. + pub fn is_valid(&self) -> bool { + self.as_borrowed().is_valid() + } + + /// Retrieves the name of this capsule, if set. + /// + /// Returns an error if this capsule is not valid. + pub fn name(&self) -> PyResult> { + self.as_borrowed().name() + } +} + +/// Implementation of functionality for [`PyCapsule`]. +/// +/// These methods are defined for the `Bound<'py, PyCapsule>` smart pointer, so to use method call +/// syntax these methods are separated into a trait, because stable Rust does not yet support +/// `arbitrary_self_types`. +#[doc(alias = "PyCapsule")] +pub trait PyCapsuleMethods<'py>: crate::sealed::Sealed { + /// Sets the context pointer in the capsule. + /// + /// Returns an error if this capsule is not valid. + /// + /// # Notes + /// + /// The context is treated much like the value of the capsule, but should likely act as + /// a place to store any state management when using the capsule. + /// + /// If you want to store a Rust value as the context, and drop it from the destructor, use + /// `Box::into_raw` to convert it into a pointer, see the example. + /// + /// # Example + /// + /// ``` + /// use std::sync::mpsc::{channel, Sender}; + /// use libc::c_void; + /// use pyo3::{prelude::*, types::PyCapsule}; + /// + /// let (tx, rx) = channel::(); + /// + /// fn destructor(val: u32, context: *mut c_void) { + /// let ctx = unsafe { *Box::from_raw(context.cast::>()) }; + /// ctx.send("Destructor called!".to_string()).unwrap(); + /// } + /// + /// Python::with_gil(|py| { + /// let capsule = + /// PyCapsule::new_bound_with_destructor(py, 123, None, destructor as fn(u32, *mut c_void)) + /// .unwrap(); + /// let context = Box::new(tx); // `Sender` is our context, box it up and ship it! + /// capsule.set_context(Box::into_raw(context).cast()).unwrap(); + /// // This scope will end, causing our destructor to be called... + /// }); + /// + /// assert_eq!(rx.recv(), Ok("Destructor called!".to_string())); + /// ``` + fn set_context(&self, context: *mut c_void) -> PyResult<()>; + + /// Gets the current context stored in the capsule. If there is no context, the pointer + /// will be null. + /// + /// Returns an error if this capsule is not valid. + fn context(&self) -> PyResult<*mut c_void>; + + /// Obtains a reference to the value of this capsule. + /// + /// # Safety + /// + /// It must be known that this capsule is valid and its pointer is to an item of type `T`. + unsafe fn reference(&self) -> &'py T; + + /// Gets the raw `c_void` pointer to the value in this capsule. + /// + /// Returns null if this capsule is not valid. + fn pointer(&self) -> *mut c_void; + + /// Checks if this is a valid capsule. + /// + /// Returns true if the stored `pointer()` is non-null. + fn is_valid(&self) -> bool; + + /// Retrieves the name of this capsule, if set. + /// + /// Returns an error if this capsule is not valid. + fn name(&self) -> PyResult>; +} + +impl<'py> PyCapsuleMethods<'py> for Bound<'py, PyCapsule> { + #[allow(clippy::not_unsafe_ptr_arg_deref)] + fn set_context(&self, context: *mut c_void) -> PyResult<()> { + let result = unsafe { ffi::PyCapsule_SetContext(self.as_ptr(), context) }; + if result != 0 { + Err(PyErr::fetch(self.py())) + } else { + Ok(()) + } + } + + fn context(&self) -> PyResult<*mut c_void> { + let ctx = unsafe { ffi::PyCapsule_GetContext(self.as_ptr()) }; + if ctx.is_null() { + ensure_no_error(self.py())? + } + Ok(ctx) + } + + unsafe fn reference(&self) -> &'py T { + &*self.pointer().cast() + } + + fn pointer(&self) -> *mut c_void { unsafe { - let ptr = ffi::PyCapsule_GetPointer(self.0.as_ptr(), self.name_ptr_ignore_error()); + let ptr = ffi::PyCapsule_GetPointer(self.as_ptr(), name_ptr_ignore_error(self)); if ptr.is_null() { ffi::PyErr_Clear(); } @@ -219,21 +363,15 @@ impl PyCapsule { } } - /// Checks if this is a valid capsule. - /// - /// Returns true if the stored `pointer()` is non-null. - pub fn is_valid(&self) -> bool { + fn is_valid(&self) -> bool { // As well as if the stored pointer is null, PyCapsule_IsValid also returns false if // self.as_ptr() is null or not a ptr to a PyCapsule object. Both of these are guaranteed // to not be the case thanks to invariants of this PyCapsule struct. - let r = unsafe { ffi::PyCapsule_IsValid(self.as_ptr(), self.name_ptr_ignore_error()) }; + let r = unsafe { ffi::PyCapsule_IsValid(self.as_ptr(), name_ptr_ignore_error(self)) }; r != 0 } - /// Retrieves the name of this capsule, if set. - /// - /// Returns an error if this capsule is not valid. - pub fn name(&self) -> PyResult> { + fn name(&self) -> PyResult> { unsafe { let ptr = ffi::PyCapsule_GetName(self.as_ptr()); if ptr.is_null() { @@ -244,18 +382,6 @@ impl PyCapsule { } } } - - /// Attempts to retrieve the raw name pointer of this capsule. - /// - /// On error, clears the error indicator and returns NULL. This is a private function and next - /// use of this capsule will error anyway. - fn name_ptr_ignore_error(&self) -> *const c_char { - let ptr = unsafe { ffi::PyCapsule_GetName(self.as_ptr()) }; - if ptr.is_null() { - unsafe { ffi::PyErr_Clear() }; - } - ptr - } } // C layout, as PyCapsule::get_reference depends on `T` being first. @@ -277,7 +403,7 @@ unsafe extern "C" fn capsule_destructor); + } = *Box::from_raw(ptr.cast::>()); destructor(value, ctx) } @@ -304,11 +430,21 @@ fn ensure_no_error(py: Python<'_>) -> PyResult<()> { } } +fn name_ptr_ignore_error(slf: &Bound<'_, PyCapsule>) -> *const c_char { + let ptr = unsafe { ffi::PyCapsule_GetName(slf.as_ptr()) }; + if ptr.is_null() { + unsafe { ffi::PyErr_Clear() }; + } + ptr +} + #[cfg(test)] mod tests { use libc::c_void; use crate::prelude::PyModule; + use crate::types::capsule::PyCapsuleMethods; + use crate::types::module::PyModuleMethods; use crate::{types::PyCapsule, Py, PyResult, Python}; use std::ffi::CString; use std::sync::mpsc::{channel, Sender}; @@ -330,7 +466,7 @@ mod tests { let foo = Foo { val: 123 }; let name = CString::new("foo").unwrap(); - let cap = PyCapsule::new(py, foo, Some(name.clone()))?; + let cap = PyCapsule::new_bound(py, foo, Some(name.clone()))?; assert!(cap.is_valid()); let foo_capi = unsafe { cap.reference::() }; @@ -349,12 +485,12 @@ mod tests { let cap: Py = Python::with_gil(|py| { let name = CString::new("foo").unwrap(); - let cap = PyCapsule::new(py, foo as fn(u32) -> u32, Some(name)).unwrap(); + let cap = PyCapsule::new_bound(py, foo as fn(u32) -> u32, Some(name)).unwrap(); cap.into() }); - Python::with_gil(|py| { - let f = unsafe { cap.as_ref(py).reference:: u32>() }; + Python::with_gil(move |py| { + let f = unsafe { cap.bind(py).reference:: u32>() }; assert_eq!(f(123), 123); }); } @@ -363,16 +499,16 @@ mod tests { fn test_pycapsule_context() -> PyResult<()> { Python::with_gil(|py| { let name = CString::new("foo").unwrap(); - let cap = PyCapsule::new(py, 0, Some(name))?; + let cap = PyCapsule::new_bound(py, 0, Some(name))?; let c = cap.context()?; assert!(c.is_null()); let ctx = Box::new(123_u32); - cap.set_context(Box::into_raw(ctx) as _)?; + cap.set_context(Box::into_raw(ctx).cast())?; let ctx_ptr: *mut c_void = cap.context()?; - let ctx = unsafe { *Box::from_raw(ctx_ptr as *mut u32) }; + let ctx = unsafe { *Box::from_raw(ctx_ptr.cast::()) }; assert_eq!(ctx, 123); Ok(()) }) @@ -389,9 +525,9 @@ mod tests { let foo = Foo { val: 123 }; let name = CString::new("builtins.capsule").unwrap(); - let capsule = PyCapsule::new(py, foo, Some(name.clone()))?; + let capsule = PyCapsule::new_bound(py, foo, Some(name.clone()))?; - let module = PyModule::import(py, "builtins")?; + let module = PyModule::import_bound(py, "builtins")?; module.add("capsule", capsule)?; // check error when wrong named passed for capsule. @@ -412,13 +548,13 @@ mod tests { let name = CString::new("foo").unwrap(); let stuff: Vec = vec![1, 2, 3, 4]; - let cap = PyCapsule::new(py, stuff, Some(name)).unwrap(); + let cap = PyCapsule::new_bound(py, stuff, Some(name)).unwrap(); cap.into() }); - Python::with_gil(|py| { - let ctx: &Vec = unsafe { cap.as_ref(py).reference() }; + Python::with_gil(move |py| { + let ctx: &Vec = unsafe { cap.bind(py).reference() }; assert_eq!(ctx, &[1, 2, 3, 4]); }) } @@ -429,16 +565,16 @@ mod tests { let cap: Py = Python::with_gil(|py| { let name = CString::new("foo").unwrap(); - let cap = PyCapsule::new(py, 0, Some(name)).unwrap(); - cap.set_context(Box::into_raw(Box::new(&context)) as _) + let cap = PyCapsule::new_bound(py, 0, Some(name)).unwrap(); + cap.set_context(Box::into_raw(Box::new(&context)).cast()) .unwrap(); cap.into() }); - Python::with_gil(|py| { - let ctx_ptr: *mut c_void = cap.as_ref(py).context().unwrap(); - let ctx = unsafe { *Box::from_raw(ctx_ptr as *mut &Vec) }; + Python::with_gil(move |py| { + let ctx_ptr: *mut c_void = cap.bind(py).context().unwrap(); + let ctx = unsafe { *Box::from_raw(ctx_ptr.cast::<&Vec>()) }; assert_eq!(ctx, &vec![1_u8, 2, 3, 4]); }) } @@ -449,14 +585,14 @@ mod tests { fn destructor(_val: u32, ctx: *mut c_void) { assert!(!ctx.is_null()); - let context = unsafe { *Box::from_raw(ctx as *mut Sender) }; + let context = unsafe { *Box::from_raw(ctx.cast::>()) }; context.send(true).unwrap(); } - Python::with_gil(|py| { + Python::with_gil(move |py| { let name = CString::new("foo").unwrap(); - let cap = PyCapsule::new_with_destructor(py, 0, Some(name), destructor).unwrap(); - cap.set_context(Box::into_raw(Box::new(tx)) as _).unwrap(); + let cap = PyCapsule::new_bound_with_destructor(py, 0, Some(name), destructor).unwrap(); + cap.set_context(Box::into_raw(Box::new(tx)).cast()).unwrap(); }); // the destructor was called. @@ -466,7 +602,7 @@ mod tests { #[test] fn test_pycapsule_no_name() { Python::with_gil(|py| { - let cap = PyCapsule::new(py, 0usize, None).unwrap(); + let cap = PyCapsule::new_bound(py, 0usize, None).unwrap(); assert_eq!(unsafe { cap.reference::() }, &0usize); assert_eq!(cap.name().unwrap(), None); diff --git a/src/types/code.rs b/src/types/code.rs index 8956deb1a71..f60e7783aa4 100644 --- a/src/types/code.rs +++ b/src/types/code.rs @@ -10,3 +10,17 @@ pyobject_native_type_core!( pyobject_native_static_type_object!(ffi::PyCode_Type), #checkfunction=ffi::PyCode_Check ); + +#[cfg(test)] +mod tests { + use super::*; + use crate::types::PyTypeMethods; + use crate::{PyTypeInfo, Python}; + + #[test] + fn test_type_object() { + Python::with_gil(|py| { + assert_eq!(PyCode::type_object_bound(py).name().unwrap(), "code"); + }) + } +} diff --git a/src/types/complex.rs b/src/types/complex.rs index b722508befa..65b08cc9c5c 100644 --- a/src/types/complex.rs +++ b/src/types/complex.rs @@ -1,4 +1,6 @@ -use crate::{ffi, PyAny, Python}; +#[cfg(feature = "gil-refs")] +use crate::PyNativeType; +use crate::{ffi, types::any::PyAnyMethods, Bound, PyAny, Python}; use std::os::raw::c_double; /// Represents a Python [`complex`](https://docs.python.org/3/library/functions.html#complex) object. @@ -20,117 +22,173 @@ pyobject_native_type!( impl PyComplex { /// Creates a new `PyComplex` from the given real and imaginary values. - pub fn from_doubles(py: Python<'_>, real: c_double, imag: c_double) -> &PyComplex { + pub fn from_doubles_bound( + py: Python<'_>, + real: c_double, + imag: c_double, + ) -> Bound<'_, PyComplex> { + use crate::ffi_ptr_ext::FfiPtrExt; unsafe { - let ptr = ffi::PyComplex_FromDoubles(real, imag); - py.from_owned_ptr(ptr) + ffi::PyComplex_FromDoubles(real, imag) + .assume_owned(py) + .downcast_into_unchecked() } } +} + +#[cfg(feature = "gil-refs")] +impl PyComplex { + /// Deprecated form of [`PyComplex::from_doubles_bound`] + #[deprecated( + since = "0.21.0", + note = "`PyComplex::from_doubles` will be replaced by `PyComplex::from_doubles_bound` in a future PyO3 version" + )] + pub fn from_doubles(py: Python<'_>, real: c_double, imag: c_double) -> &PyComplex { + Self::from_doubles_bound(py, real, imag).into_gil_ref() + } + /// Returns the real part of the complex number. pub fn real(&self) -> c_double { - unsafe { ffi::PyComplex_RealAsDouble(self.as_ptr()) } + self.as_borrowed().real() } /// Returns the imaginary part of the complex number. pub fn imag(&self) -> c_double { - unsafe { ffi::PyComplex_ImagAsDouble(self.as_ptr()) } + self.as_borrowed().imag() } } -#[cfg(not(any(Py_LIMITED_API, PyPy)))] +#[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))] mod not_limited_impls { + use crate::ffi_ptr_ext::FfiPtrExt; + use crate::Borrowed; + use super::*; use std::ops::{Add, Div, Mul, Neg, Sub}; + #[cfg(feature = "gil-refs")] impl PyComplex { /// Returns `|self|`. pub fn abs(&self) -> c_double { - unsafe { - let val = (*(self.as_ptr() as *mut ffi::PyComplexObject)).cval; - ffi::_Py_c_abs(val) - } + self.as_borrowed().abs() } /// Returns `self` raised to the power of `other`. - pub fn pow(&self, other: &PyComplex) -> &PyComplex { - unsafe { - self.py() - .from_owned_ptr(complex_operation(self, other, ffi::_Py_c_pow)) - } + pub fn pow<'py>(&'py self, other: &'py PyComplex) -> &'py PyComplex { + self.as_borrowed().pow(&other.as_borrowed()).into_gil_ref() } } #[inline(always)] - unsafe fn complex_operation( - l: &PyComplex, - r: &PyComplex, + pub(super) unsafe fn complex_operation<'py>( + l: Borrowed<'_, 'py, PyComplex>, + r: Borrowed<'_, 'py, PyComplex>, operation: unsafe extern "C" fn(ffi::Py_complex, ffi::Py_complex) -> ffi::Py_complex, ) -> *mut ffi::PyObject { - let l_val = (*(l.as_ptr() as *mut ffi::PyComplexObject)).cval; - let r_val = (*(r.as_ptr() as *mut ffi::PyComplexObject)).cval; + let l_val = (*l.as_ptr().cast::()).cval; + let r_val = (*r.as_ptr().cast::()).cval; ffi::PyComplex_FromCComplex(operation(l_val, r_val)) } - impl<'py> Add for &'py PyComplex { - type Output = &'py PyComplex; - fn add(self, other: &'py PyComplex) -> &'py PyComplex { - unsafe { - self.py() - .from_owned_ptr(complex_operation(self, other, ffi::_Py_c_sum)) + macro_rules! bin_ops { + ($trait:ident, $fn:ident, $op:tt, $ffi:path) => { + impl<'py> $trait for Borrowed<'_, 'py, PyComplex> { + type Output = Bound<'py, PyComplex>; + fn $fn(self, other: Self) -> Self::Output { + unsafe { + complex_operation(self, other, $ffi) + .assume_owned(self.py()) + .downcast_into_unchecked() + } + } } - } + + #[cfg(feature = "gil-refs")] + impl<'py> $trait for &'py PyComplex { + type Output = &'py PyComplex; + fn $fn(self, other: &'py PyComplex) -> &'py PyComplex { + (self.as_borrowed() $op other.as_borrowed()).into_gil_ref() + } + } + + impl<'py> $trait for &Bound<'py, PyComplex> { + type Output = Bound<'py, PyComplex>; + fn $fn(self, other: &Bound<'py, PyComplex>) -> Bound<'py, PyComplex> { + self.as_borrowed() $op other.as_borrowed() + } + } + + impl<'py> $trait> for &Bound<'py, PyComplex> { + type Output = Bound<'py, PyComplex>; + fn $fn(self, other: Bound<'py, PyComplex>) -> Bound<'py, PyComplex> { + self.as_borrowed() $op other.as_borrowed() + } + } + + impl<'py> $trait for Bound<'py, PyComplex> { + type Output = Bound<'py, PyComplex>; + fn $fn(self, other: Bound<'py, PyComplex>) -> Bound<'py, PyComplex> { + self.as_borrowed() $op other.as_borrowed() + } + } + + impl<'py> $trait<&Self> for Bound<'py, PyComplex> { + type Output = Bound<'py, PyComplex>; + fn $fn(self, other: &Bound<'py, PyComplex>) -> Bound<'py, PyComplex> { + self.as_borrowed() $op other.as_borrowed() + } + } + }; } - impl<'py> Sub for &'py PyComplex { + bin_ops!(Add, add, +, ffi::_Py_c_sum); + bin_ops!(Sub, sub, -, ffi::_Py_c_diff); + bin_ops!(Mul, mul, *, ffi::_Py_c_prod); + bin_ops!(Div, div, /, ffi::_Py_c_quot); + + #[cfg(feature = "gil-refs")] + impl<'py> Neg for &'py PyComplex { type Output = &'py PyComplex; - fn sub(self, other: &'py PyComplex) -> &'py PyComplex { - unsafe { - self.py() - .from_owned_ptr(complex_operation(self, other, ffi::_Py_c_diff)) - } + fn neg(self) -> &'py PyComplex { + (-self.as_borrowed()).into_gil_ref() } } - impl<'py> Mul for &'py PyComplex { - type Output = &'py PyComplex; - fn mul(self, other: &'py PyComplex) -> &'py PyComplex { + impl<'py> Neg for Borrowed<'_, 'py, PyComplex> { + type Output = Bound<'py, PyComplex>; + fn neg(self) -> Self::Output { unsafe { - self.py() - .from_owned_ptr(complex_operation(self, other, ffi::_Py_c_prod)) + let val = (*self.as_ptr().cast::()).cval; + ffi::PyComplex_FromCComplex(ffi::_Py_c_neg(val)) + .assume_owned(self.py()) + .downcast_into_unchecked() } } } - impl<'py> Div for &'py PyComplex { - type Output = &'py PyComplex; - fn div(self, other: &'py PyComplex) -> &'py PyComplex { - unsafe { - self.py() - .from_owned_ptr(complex_operation(self, other, ffi::_Py_c_quot)) - } + impl<'py> Neg for &Bound<'py, PyComplex> { + type Output = Bound<'py, PyComplex>; + fn neg(self) -> Bound<'py, PyComplex> { + -self.as_borrowed() } } - impl<'py> Neg for &'py PyComplex { - type Output = &'py PyComplex; - fn neg(self) -> &'py PyComplex { - unsafe { - let val = (*(self.as_ptr() as *mut ffi::PyComplexObject)).cval; - self.py() - .from_owned_ptr(ffi::PyComplex_FromCComplex(ffi::_Py_c_neg(val))) - } + impl<'py> Neg for Bound<'py, PyComplex> { + type Output = Bound<'py, PyComplex>; + fn neg(self) -> Bound<'py, PyComplex> { + -self.as_borrowed() } } #[cfg(test)] mod tests { use super::PyComplex; - use crate::Python; + use crate::{types::complex::PyComplexMethods, Python}; use assert_approx_eq::assert_approx_eq; #[test] fn test_add() { Python::with_gil(|py| { - let l = PyComplex::from_doubles(py, 3.0, 1.2); - let r = PyComplex::from_doubles(py, 1.0, 2.6); + let l = PyComplex::from_doubles_bound(py, 3.0, 1.2); + let r = PyComplex::from_doubles_bound(py, 1.0, 2.6); let res = l + r; assert_approx_eq!(res.real(), 4.0); assert_approx_eq!(res.imag(), 3.8); @@ -140,8 +198,8 @@ mod not_limited_impls { #[test] fn test_sub() { Python::with_gil(|py| { - let l = PyComplex::from_doubles(py, 3.0, 1.2); - let r = PyComplex::from_doubles(py, 1.0, 2.6); + let l = PyComplex::from_doubles_bound(py, 3.0, 1.2); + let r = PyComplex::from_doubles_bound(py, 1.0, 2.6); let res = l - r; assert_approx_eq!(res.real(), 2.0); assert_approx_eq!(res.imag(), -1.4); @@ -151,8 +209,8 @@ mod not_limited_impls { #[test] fn test_mul() { Python::with_gil(|py| { - let l = PyComplex::from_doubles(py, 3.0, 1.2); - let r = PyComplex::from_doubles(py, 1.0, 2.6); + let l = PyComplex::from_doubles_bound(py, 3.0, 1.2); + let r = PyComplex::from_doubles_bound(py, 1.0, 2.6); let res = l * r; assert_approx_eq!(res.real(), -0.12); assert_approx_eq!(res.imag(), 9.0); @@ -162,8 +220,8 @@ mod not_limited_impls { #[test] fn test_div() { Python::with_gil(|py| { - let l = PyComplex::from_doubles(py, 3.0, 1.2); - let r = PyComplex::from_doubles(py, 1.0, 2.6); + let l = PyComplex::from_doubles_bound(py, 3.0, 1.2); + let r = PyComplex::from_doubles_bound(py, 1.0, 2.6); let res = l / r; assert_approx_eq!(res.real(), 0.788_659_793_814_432_9); assert_approx_eq!(res.imag(), -0.850_515_463_917_525_7); @@ -173,7 +231,7 @@ mod not_limited_impls { #[test] fn test_neg() { Python::with_gil(|py| { - let val = PyComplex::from_doubles(py, 3.0, 1.2); + let val = PyComplex::from_doubles_bound(py, 3.0, 1.2); let res = -val; assert_approx_eq!(res.real(), -3.0); assert_approx_eq!(res.imag(), -1.2); @@ -183,7 +241,7 @@ mod not_limited_impls { #[test] fn test_abs() { Python::with_gil(|py| { - let val = PyComplex::from_doubles(py, 3.0, 1.2); + let val = PyComplex::from_doubles_bound(py, 3.0, 1.2); assert_approx_eq!(val.abs(), 3.231_098_884_280_702_2); }); } @@ -191,9 +249,9 @@ mod not_limited_impls { #[test] fn test_pow() { Python::with_gil(|py| { - let l = PyComplex::from_doubles(py, 3.0, 1.2); - let r = PyComplex::from_doubles(py, 1.2, 2.6); - let val = l.pow(r); + let l = PyComplex::from_doubles_bound(py, 3.0, 1.2); + let r = PyComplex::from_doubles_bound(py, 1.2, 2.6); + let val = l.pow(&r); assert_approx_eq!(val.real(), -1.419_309_997_016_603_7); assert_approx_eq!(val.imag(), -0.541_297_466_033_544_6); }); @@ -201,10 +259,61 @@ mod not_limited_impls { } } +/// Implementation of functionality for [`PyComplex`]. +/// +/// These methods are defined for the `Bound<'py, PyComplex>` smart pointer, so to use method call +/// syntax these methods are separated into a trait, because stable Rust does not yet support +/// `arbitrary_self_types`. +#[doc(alias = "PyComplex")] +pub trait PyComplexMethods<'py>: crate::sealed::Sealed { + /// Returns the real part of the complex number. + fn real(&self) -> c_double; + /// Returns the imaginary part of the complex number. + fn imag(&self) -> c_double; + /// Returns `|self|`. + #[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))] + fn abs(&self) -> c_double; + /// Returns `self` raised to the power of `other`. + #[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))] + fn pow(&self, other: &Bound<'py, PyComplex>) -> Bound<'py, PyComplex>; +} + +impl<'py> PyComplexMethods<'py> for Bound<'py, PyComplex> { + fn real(&self) -> c_double { + unsafe { ffi::PyComplex_RealAsDouble(self.as_ptr()) } + } + + fn imag(&self) -> c_double { + unsafe { ffi::PyComplex_ImagAsDouble(self.as_ptr()) } + } + + #[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))] + fn abs(&self) -> c_double { + unsafe { + let val = (*self.as_ptr().cast::()).cval; + ffi::_Py_c_abs(val) + } + } + + #[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))] + fn pow(&self, other: &Bound<'py, PyComplex>) -> Bound<'py, PyComplex> { + use crate::ffi_ptr_ext::FfiPtrExt; + unsafe { + not_limited_impls::complex_operation( + self.as_borrowed(), + other.as_borrowed(), + ffi::_Py_c_pow, + ) + .assume_owned(self.py()) + .downcast_into_unchecked() + } + } +} + #[cfg(test)] mod tests { use super::PyComplex; - use crate::Python; + use crate::{types::complex::PyComplexMethods, Python}; use assert_approx_eq::assert_approx_eq; #[test] @@ -212,7 +321,7 @@ mod tests { use assert_approx_eq::assert_approx_eq; Python::with_gil(|py| { - let complex = PyComplex::from_doubles(py, 3.0, 1.2); + let complex = PyComplex::from_doubles_bound(py, 3.0, 1.2); assert_approx_eq!(complex.real(), 3.0); assert_approx_eq!(complex.imag(), 1.2); }); diff --git a/src/types/datetime.rs b/src/types/datetime.rs index 94802fe14ef..cdf3b011e6c 100644 --- a/src/types/datetime.rs +++ b/src/types/datetime.rs @@ -11,6 +11,8 @@ use crate::ffi::{ PyDateTime_DATE_GET_FOLD, PyDateTime_DATE_GET_HOUR, PyDateTime_DATE_GET_MICROSECOND, PyDateTime_DATE_GET_MINUTE, PyDateTime_DATE_GET_SECOND, }; +#[cfg(GraalPy)] +use crate::ffi::{PyDateTime_DATE_GET_TZINFO, PyDateTime_TIME_GET_TZINFO, Py_IsNone}; use crate::ffi::{ PyDateTime_DELTA_GET_DAYS, PyDateTime_DELTA_GET_MICROSECONDS, PyDateTime_DELTA_GET_SECONDS, }; @@ -20,24 +22,32 @@ use crate::ffi::{ PyDateTime_TIME_GET_MINUTE, PyDateTime_TIME_GET_SECOND, }; use crate::ffi_ptr_ext::FfiPtrExt; +#[cfg(feature = "gil-refs")] use crate::instance::PyNativeType; +use crate::py_result_ext::PyResultExt; use crate::types::any::PyAnyMethods; use crate::types::PyTuple; -use crate::{Bound, IntoPy, Py, PyAny, Python}; +use crate::{Bound, IntoPy, Py, PyAny, PyErr, Python}; use std::os::raw::c_int; #[cfg(feature = "chrono")] use std::ptr; -fn ensure_datetime_api(_py: Python<'_>) -> &'static PyDateTime_CAPI { - unsafe { - if pyo3_ffi::PyDateTimeAPI().is_null() { - PyDateTime_IMPORT() +fn ensure_datetime_api(py: Python<'_>) -> PyResult<&'static PyDateTime_CAPI> { + if let Some(api) = unsafe { pyo3_ffi::PyDateTimeAPI().as_ref() } { + Ok(api) + } else { + unsafe { + PyDateTime_IMPORT(); + pyo3_ffi::PyDateTimeAPI().as_ref() } - - &*pyo3_ffi::PyDateTimeAPI() + .ok_or_else(|| PyErr::fetch(py)) } } +fn expect_datetime_api(py: Python<'_>) -> &'static PyDateTime_CAPI { + ensure_datetime_api(py).expect("failed to import `datetime` C API") +} + // Type Check macros // // These are bindings around the C API typecheck macros, all of them return @@ -163,12 +173,22 @@ pub trait PyTimeAccess { /// Trait for accessing the components of a struct containing a tzinfo. pub trait PyTzInfoAccess<'py> { + /// Deprecated form of `get_tzinfo_bound`. + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`get_tzinfo` will be replaced by `get_tzinfo_bound` in a future PyO3 version" + )] + fn get_tzinfo(&self) -> Option<&'py PyTzInfo> { + self.get_tzinfo_bound().map(Bound::into_gil_ref) + } + /// Returns the tzinfo (which may be None). /// /// Implementations should conform to the upstream documentation: /// /// - fn get_tzinfo(&self) -> Option>; + fn get_tzinfo_bound(&self) -> Option>; } /// Bindings around `datetime.date` @@ -177,52 +197,71 @@ pub struct PyDate(PyAny); pyobject_native_type!( PyDate, crate::ffi::PyDateTime_Date, - |py| ensure_datetime_api(py).DateType, + |py| expect_datetime_api(py).DateType, #module=Some("datetime"), #checkfunction=PyDate_Check ); impl PyDate { - /// Creates a new `datetime.date`. + /// Deprecated form of [`PyDate::new_bound`]. + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`PyDate::new` will be replaced by `PyDate::new_bound` in a future PyO3 version" + )] pub fn new(py: Python<'_>, year: i32, month: u8, day: u8) -> PyResult<&PyDate> { + Self::new_bound(py, year, month, day).map(Bound::into_gil_ref) + } + + /// Creates a new `datetime.date`. + pub fn new_bound(py: Python<'_>, year: i32, month: u8, day: u8) -> PyResult> { + let api = ensure_datetime_api(py)?; unsafe { - let ptr = (ensure_datetime_api(py).Date_FromDate)( - year, - c_int::from(month), - c_int::from(day), - ensure_datetime_api(py).DateType, - ); - py.from_owned_ptr_or_err(ptr) + (api.Date_FromDate)(year, c_int::from(month), c_int::from(day), api.DateType) + .assume_owned_or_err(py) + .downcast_into_unchecked() } } + /// Deprecated form of [`PyDate::from_timestamp_bound`]. + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`PyDate::from_timestamp` will be replaced by `PyDate::from_timestamp_bound` in a future PyO3 version" + )] + pub fn from_timestamp(py: Python<'_>, timestamp: i64) -> PyResult<&PyDate> { + Self::from_timestamp_bound(py, timestamp).map(Bound::into_gil_ref) + } + /// Construct a `datetime.date` from a POSIX timestamp /// /// This is equivalent to `datetime.date.fromtimestamp` - pub fn from_timestamp(py: Python<'_>, timestamp: i64) -> PyResult<&PyDate> { - let time_tuple = PyTuple::new(py, [timestamp]); + pub fn from_timestamp_bound(py: Python<'_>, timestamp: i64) -> PyResult> { + let time_tuple = PyTuple::new_bound(py, [timestamp]); // safety ensure that the API is loaded - let _api = ensure_datetime_api(py); + let _api = ensure_datetime_api(py)?; unsafe { - let ptr = PyDate_FromTimestamp(time_tuple.as_ptr()); - py.from_owned_ptr_or_err(ptr) + PyDate_FromTimestamp(time_tuple.as_ptr()) + .assume_owned_or_err(py) + .downcast_into_unchecked() } } } +#[cfg(feature = "gil-refs")] impl PyDateAccess for PyDate { fn get_year(&self) -> i32 { - Bound::borrowed_from_gil_ref(&self).get_year() + self.as_borrowed().get_year() } fn get_month(&self) -> u8 { - Bound::borrowed_from_gil_ref(&self).get_month() + self.as_borrowed().get_month() } fn get_day(&self) -> u8 { - Bound::borrowed_from_gil_ref(&self).get_day() + self.as_borrowed().get_day() } } @@ -246,16 +285,48 @@ pub struct PyDateTime(PyAny); pyobject_native_type!( PyDateTime, crate::ffi::PyDateTime_DateTime, - |py| ensure_datetime_api(py).DateTimeType, + |py| expect_datetime_api(py).DateTimeType, #module=Some("datetime"), #checkfunction=PyDateTime_Check ); impl PyDateTime { + /// Deprecated form of [`PyDateTime::new_bound`]. + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`PyDateTime::new` will be replaced by `PyDateTime::new_bound` in a future PyO3 version" + )] + #[allow(clippy::too_many_arguments)] + pub fn new<'py>( + py: Python<'py>, + year: i32, + month: u8, + day: u8, + hour: u8, + minute: u8, + second: u8, + microsecond: u32, + tzinfo: Option<&'py PyTzInfo>, + ) -> PyResult<&'py PyDateTime> { + Self::new_bound( + py, + year, + month, + day, + hour, + minute, + second, + microsecond, + tzinfo.map(PyTzInfo::as_borrowed).as_deref(), + ) + .map(Bound::into_gil_ref) + } + /// Creates a new `datetime.datetime` object. #[allow(clippy::too_many_arguments)] - pub fn new<'p>( - py: Python<'p>, + pub fn new_bound<'py>( + py: Python<'py>, year: i32, month: u8, day: u8, @@ -263,11 +334,11 @@ impl PyDateTime { minute: u8, second: u8, microsecond: u32, - tzinfo: Option<&PyTzInfo>, - ) -> PyResult<&'p PyDateTime> { - let api = ensure_datetime_api(py); + tzinfo: Option<&Bound<'py, PyTzInfo>>, + ) -> PyResult> { + let api = ensure_datetime_api(py)?; unsafe { - let ptr = (api.DateTime_FromDateAndTime)( + (api.DateTime_FromDateAndTime)( year, c_int::from(month), c_int::from(day), @@ -277,11 +348,46 @@ impl PyDateTime { microsecond as c_int, opt_to_pyobj(tzinfo), api.DateTimeType, - ); - py.from_owned_ptr_or_err(ptr) + ) + .assume_owned_or_err(py) + .downcast_into_unchecked() } } + /// Deprecated form of [`PyDateTime::new_bound_with_fold`]. + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`PyDateTime::new_with_fold` will be replaced by `PyDateTime::new_bound_with_fold` in a future PyO3 version" + )] + #[allow(clippy::too_many_arguments)] + pub fn new_with_fold<'py>( + py: Python<'py>, + year: i32, + month: u8, + day: u8, + hour: u8, + minute: u8, + second: u8, + microsecond: u32, + tzinfo: Option<&'py PyTzInfo>, + fold: bool, + ) -> PyResult<&'py PyDateTime> { + Self::new_bound_with_fold( + py, + year, + month, + day, + hour, + minute, + second, + microsecond, + tzinfo.map(PyTzInfo::as_borrowed).as_deref(), + fold, + ) + .map(Bound::into_gil_ref) + } + /// Alternate constructor that takes a `fold` parameter. A `true` value for this parameter /// signifies this this datetime is the later of two moments with the same representation, /// during a repeated interval. @@ -290,8 +396,8 @@ impl PyDateTime { /// represented time is ambiguous. /// See [PEP 495](https://www.python.org/dev/peps/pep-0495/) for more detail. #[allow(clippy::too_many_arguments)] - pub fn new_with_fold<'p>( - py: Python<'p>, + pub fn new_bound_with_fold<'py>( + py: Python<'py>, year: i32, month: u8, day: u8, @@ -299,12 +405,12 @@ impl PyDateTime { minute: u8, second: u8, microsecond: u32, - tzinfo: Option<&PyTzInfo>, + tzinfo: Option<&Bound<'py, PyTzInfo>>, fold: bool, - ) -> PyResult<&'p PyDateTime> { - let api = ensure_datetime_api(py); + ) -> PyResult> { + let api = ensure_datetime_api(py)?; unsafe { - let ptr = (api.DateTime_FromDateAndTimeAndFold)( + (api.DateTime_FromDateAndTimeAndFold)( year, c_int::from(month), c_int::from(day), @@ -315,42 +421,60 @@ impl PyDateTime { opt_to_pyobj(tzinfo), c_int::from(fold), api.DateTimeType, - ); - py.from_owned_ptr_or_err(ptr) + ) + .assume_owned_or_err(py) + .downcast_into_unchecked() } } + /// Deprecated form of [`PyDateTime::from_timestamp_bound`]. + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`PyDateTime::from_timestamp` will be replaced by `PyDateTime::from_timestamp_bound` in a future PyO3 version" + )] + pub fn from_timestamp<'py>( + py: Python<'py>, + timestamp: f64, + tzinfo: Option<&'py PyTzInfo>, + ) -> PyResult<&'py PyDateTime> { + Self::from_timestamp_bound(py, timestamp, tzinfo.map(PyTzInfo::as_borrowed).as_deref()) + .map(Bound::into_gil_ref) + } + /// Construct a `datetime` object from a POSIX timestamp /// /// This is equivalent to `datetime.datetime.fromtimestamp` - pub fn from_timestamp<'p>( - py: Python<'p>, + pub fn from_timestamp_bound<'py>( + py: Python<'py>, timestamp: f64, - tzinfo: Option<&PyTzInfo>, - ) -> PyResult<&'p PyDateTime> { - let args: Py = (timestamp, tzinfo).into_py(py); + tzinfo: Option<&Bound<'py, PyTzInfo>>, + ) -> PyResult> { + let args = IntoPy::>::into_py((timestamp, tzinfo), py).into_bound(py); // safety ensure API is loaded - let _api = ensure_datetime_api(py); + let _api = ensure_datetime_api(py)?; unsafe { - let ptr = PyDateTime_FromTimestamp(args.as_ptr()); - py.from_owned_ptr_or_err(ptr) + PyDateTime_FromTimestamp(args.as_ptr()) + .assume_owned_or_err(py) + .downcast_into_unchecked() } } } +#[cfg(feature = "gil-refs")] impl PyDateAccess for PyDateTime { fn get_year(&self) -> i32 { - Bound::borrowed_from_gil_ref(&self).get_year() + self.as_borrowed().get_year() } fn get_month(&self) -> u8 { - Bound::borrowed_from_gil_ref(&self).get_month() + self.as_borrowed().get_month() } fn get_day(&self) -> u8 { - Bound::borrowed_from_gil_ref(&self).get_day() + self.as_borrowed().get_day() } } @@ -368,25 +492,26 @@ impl PyDateAccess for Bound<'_, PyDateTime> { } } +#[cfg(feature = "gil-refs")] impl PyTimeAccess for PyDateTime { fn get_hour(&self) -> u8 { - Bound::borrowed_from_gil_ref(&self).get_hour() + self.as_borrowed().get_hour() } fn get_minute(&self) -> u8 { - Bound::borrowed_from_gil_ref(&self).get_minute() + self.as_borrowed().get_minute() } fn get_second(&self) -> u8 { - Bound::borrowed_from_gil_ref(&self).get_second() + self.as_borrowed().get_second() } fn get_microsecond(&self) -> u32 { - Bound::borrowed_from_gil_ref(&self).get_microsecond() + self.as_borrowed().get_microsecond() } fn get_fold(&self) -> bool { - Bound::borrowed_from_gil_ref(&self).get_fold() + self.as_borrowed().get_fold() } } @@ -412,15 +537,17 @@ impl PyTimeAccess for Bound<'_, PyDateTime> { } } +#[cfg(feature = "gil-refs")] impl<'py> PyTzInfoAccess<'py> for &'py PyDateTime { - fn get_tzinfo(&self) -> Option> { - Bound::borrowed_from_gil_ref(self).get_tzinfo() + fn get_tzinfo_bound(&self) -> Option> { + self.as_borrowed().get_tzinfo_bound() } } impl<'py> PyTzInfoAccess<'py> for Bound<'py, PyDateTime> { - fn get_tzinfo(&self) -> Option> { + fn get_tzinfo_bound(&self) -> Option> { let ptr = self.as_ptr() as *mut ffi::PyDateTime_DateTime; + #[cfg(not(GraalPy))] unsafe { if (*ptr).hastzinfo != 0 { Some( @@ -434,6 +561,20 @@ impl<'py> PyTzInfoAccess<'py> for Bound<'py, PyDateTime> { None } } + + #[cfg(GraalPy)] + unsafe { + let res = PyDateTime_DATE_GET_TZINFO(ptr as *mut ffi::PyObject); + if Py_IsNone(res) == 1 { + None + } else { + Some( + res.assume_borrowed(self.py()) + .to_owned() + .downcast_into_unchecked(), + ) + } + } } } @@ -443,48 +584,101 @@ pub struct PyTime(PyAny); pyobject_native_type!( PyTime, crate::ffi::PyDateTime_Time, - |py| ensure_datetime_api(py).TimeType, + |py| expect_datetime_api(py).TimeType, #module=Some("datetime"), #checkfunction=PyTime_Check ); impl PyTime { + /// Deprecated form of [`PyTime::new_bound`]. + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`PyTime::new` will be replaced by `PyTime::new_bound` in a future PyO3 version" + )] + pub fn new<'py>( + py: Python<'py>, + hour: u8, + minute: u8, + second: u8, + microsecond: u32, + tzinfo: Option<&'py PyTzInfo>, + ) -> PyResult<&'py PyTime> { + Self::new_bound( + py, + hour, + minute, + second, + microsecond, + tzinfo.map(PyTzInfo::as_borrowed).as_deref(), + ) + .map(Bound::into_gil_ref) + } + /// Creates a new `datetime.time` object. - pub fn new<'p>( - py: Python<'p>, + pub fn new_bound<'py>( + py: Python<'py>, hour: u8, minute: u8, second: u8, microsecond: u32, - tzinfo: Option<&PyTzInfo>, - ) -> PyResult<&'p PyTime> { - let api = ensure_datetime_api(py); + tzinfo: Option<&Bound<'py, PyTzInfo>>, + ) -> PyResult> { + let api = ensure_datetime_api(py)?; unsafe { - let ptr = (api.Time_FromTime)( + (api.Time_FromTime)( c_int::from(hour), c_int::from(minute), c_int::from(second), microsecond as c_int, opt_to_pyobj(tzinfo), api.TimeType, - ); - py.from_owned_ptr_or_err(ptr) + ) + .assume_owned_or_err(py) + .downcast_into_unchecked() } } - /// Alternate constructor that takes a `fold` argument. See [`PyDateTime::new_with_fold`]. - pub fn new_with_fold<'p>( - py: Python<'p>, + /// Deprecated form of [`PyTime::new_bound_with_fold`]. + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`PyTime::new_with_fold` will be replaced by `PyTime::new_bound_with_fold` in a future PyO3 version" + )] + pub fn new_with_fold<'py>( + py: Python<'py>, + hour: u8, + minute: u8, + second: u8, + microsecond: u32, + tzinfo: Option<&'py PyTzInfo>, + fold: bool, + ) -> PyResult<&'py PyTime> { + Self::new_bound_with_fold( + py, + hour, + minute, + second, + microsecond, + tzinfo.map(PyTzInfo::as_borrowed).as_deref(), + fold, + ) + .map(Bound::into_gil_ref) + } + + /// Alternate constructor that takes a `fold` argument. See [`PyDateTime::new_bound_with_fold`]. + pub fn new_bound_with_fold<'py>( + py: Python<'py>, hour: u8, minute: u8, second: u8, microsecond: u32, - tzinfo: Option<&PyTzInfo>, + tzinfo: Option<&Bound<'py, PyTzInfo>>, fold: bool, - ) -> PyResult<&'p PyTime> { - let api = ensure_datetime_api(py); + ) -> PyResult> { + let api = ensure_datetime_api(py)?; unsafe { - let ptr = (api.Time_FromTimeAndFold)( + (api.Time_FromTimeAndFold)( c_int::from(hour), c_int::from(minute), c_int::from(second), @@ -492,31 +686,33 @@ impl PyTime { opt_to_pyobj(tzinfo), fold as c_int, api.TimeType, - ); - py.from_owned_ptr_or_err(ptr) + ) + .assume_owned_or_err(py) + .downcast_into_unchecked() } } } +#[cfg(feature = "gil-refs")] impl PyTimeAccess for PyTime { fn get_hour(&self) -> u8 { - Bound::borrowed_from_gil_ref(&self).get_hour() + self.as_borrowed().get_hour() } fn get_minute(&self) -> u8 { - Bound::borrowed_from_gil_ref(&self).get_minute() + self.as_borrowed().get_minute() } fn get_second(&self) -> u8 { - Bound::borrowed_from_gil_ref(&self).get_second() + self.as_borrowed().get_second() } fn get_microsecond(&self) -> u32 { - Bound::borrowed_from_gil_ref(&self).get_microsecond() + self.as_borrowed().get_microsecond() } fn get_fold(&self) -> bool { - Bound::borrowed_from_gil_ref(&self).get_fold() + self.as_borrowed().get_fold() } } @@ -542,15 +738,17 @@ impl PyTimeAccess for Bound<'_, PyTime> { } } +#[cfg(feature = "gil-refs")] impl<'py> PyTzInfoAccess<'py> for &'py PyTime { - fn get_tzinfo(&self) -> Option> { - Bound::borrowed_from_gil_ref(self).get_tzinfo() + fn get_tzinfo_bound(&self) -> Option> { + self.as_borrowed().get_tzinfo_bound() } } impl<'py> PyTzInfoAccess<'py> for Bound<'py, PyTime> { - fn get_tzinfo(&self) -> Option> { + fn get_tzinfo_bound(&self) -> Option> { let ptr = self.as_ptr() as *mut ffi::PyDateTime_Time; + #[cfg(not(GraalPy))] unsafe { if (*ptr).hastzinfo != 0 { Some( @@ -564,38 +762,75 @@ impl<'py> PyTzInfoAccess<'py> for Bound<'py, PyTime> { None } } + + #[cfg(GraalPy)] + unsafe { + let res = PyDateTime_TIME_GET_TZINFO(ptr as *mut ffi::PyObject); + if Py_IsNone(res) == 1 { + None + } else { + Some( + res.assume_borrowed(self.py()) + .to_owned() + .downcast_into_unchecked(), + ) + } + } } } /// Bindings for `datetime.tzinfo`. /// /// This is an abstract base class and cannot be constructed directly. -/// For concrete time zone implementations, see [`timezone_utc`] and +/// For concrete time zone implementations, see [`timezone_utc_bound`] and /// the [`zoneinfo` module](https://docs.python.org/3/library/zoneinfo.html). #[repr(transparent)] pub struct PyTzInfo(PyAny); pyobject_native_type!( PyTzInfo, crate::ffi::PyObject, - |py| ensure_datetime_api(py).TZInfoType, + |py| expect_datetime_api(py).TZInfoType, #module=Some("datetime"), #checkfunction=PyTZInfo_Check ); -/// Equivalent to `datetime.timezone.utc` +/// Deprecated form of [`timezone_utc_bound`]. +#[cfg(feature = "gil-refs")] +#[deprecated( + since = "0.21.0", + note = "`timezone_utc` will be replaced by `timezone_utc_bound` in a future PyO3 version" +)] pub fn timezone_utc(py: Python<'_>) -> &PyTzInfo { - unsafe { &*(ensure_datetime_api(py).TimeZone_UTC as *const PyTzInfo) } + timezone_utc_bound(py).into_gil_ref() +} + +/// Equivalent to `datetime.timezone.utc` +pub fn timezone_utc_bound(py: Python<'_>) -> Bound<'_, PyTzInfo> { + // TODO: this _could_ have a borrowed form `timezone_utc_borrowed`, but that seems + // like an edge case optimization and we'd prefer in PyO3 0.21 to use `Bound` as + // much as possible + unsafe { + expect_datetime_api(py) + .TimeZone_UTC + .assume_borrowed(py) + .to_owned() + .downcast_into_unchecked() + } } /// Equivalent to `datetime.timezone` constructor /// /// Only used internally #[cfg(feature = "chrono")] -pub fn timezone_from_offset<'a>(py: Python<'a>, offset: &PyDelta) -> PyResult<&'a PyTzInfo> { - let api = ensure_datetime_api(py); +pub(crate) fn timezone_from_offset<'py>( + offset: &Bound<'py, PyDelta>, +) -> PyResult> { + let py = offset.py(); + let api = ensure_datetime_api(py)?; unsafe { - let ptr = (api.TimeZone_FromTimeZone)(offset.as_ptr(), ptr::null_mut()); - py.from_owned_ptr_or_err(ptr) + (api.TimeZone_FromTimeZone)(offset.as_ptr(), ptr::null_mut()) + .assume_owned_or_err(py) + .downcast_into_unchecked() } } @@ -605,13 +840,18 @@ pub struct PyDelta(PyAny); pyobject_native_type!( PyDelta, crate::ffi::PyDateTime_Delta, - |py| ensure_datetime_api(py).DeltaType, + |py| expect_datetime_api(py).DeltaType, #module=Some("datetime"), #checkfunction=PyDelta_Check ); impl PyDelta { - /// Creates a new `timedelta`. + /// Deprecated form of [`PyDelta::new_bound`]. + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`PyDelta::new` will be replaced by `PyDelta::new_bound` in a future PyO3 version" + )] pub fn new( py: Python<'_>, days: i32, @@ -619,31 +859,44 @@ impl PyDelta { microseconds: i32, normalize: bool, ) -> PyResult<&PyDelta> { - let api = ensure_datetime_api(py); + Self::new_bound(py, days, seconds, microseconds, normalize).map(Bound::into_gil_ref) + } + + /// Creates a new `timedelta`. + pub fn new_bound( + py: Python<'_>, + days: i32, + seconds: i32, + microseconds: i32, + normalize: bool, + ) -> PyResult> { + let api = ensure_datetime_api(py)?; unsafe { - let ptr = (api.Delta_FromDelta)( + (api.Delta_FromDelta)( days as c_int, seconds as c_int, microseconds as c_int, normalize as c_int, api.DeltaType, - ); - py.from_owned_ptr_or_err(ptr) + ) + .assume_owned_or_err(py) + .downcast_into_unchecked() } } } +#[cfg(feature = "gil-refs")] impl PyDeltaAccess for PyDelta { fn get_days(&self) -> i32 { - Bound::borrowed_from_gil_ref(&self).get_days() + self.as_borrowed().get_days() } fn get_seconds(&self) -> i32 { - Bound::borrowed_from_gil_ref(&self).get_seconds() + self.as_borrowed().get_seconds() } fn get_microseconds(&self) -> i32 { - Bound::borrowed_from_gil_ref(&self).get_microseconds() + self.as_borrowed().get_microseconds() } } @@ -663,7 +916,7 @@ impl PyDeltaAccess for Bound<'_, PyDelta> { // Utility function which returns a borrowed reference to either // the underlying tzinfo or None. -fn opt_to_pyobj(opt: Option<&PyTzInfo>) -> *mut ffi::PyObject { +fn opt_to_pyobj(opt: Option<&Bound<'_, PyTzInfo>>) -> *mut ffi::PyObject { match opt { Some(tzi) => tzi.as_ptr(), None => unsafe { ffi::Py_None() }, @@ -681,14 +934,15 @@ mod tests { #[cfg_attr(target_arch = "wasm32", ignore)] // DateTime import fails on wasm for mysterious reasons fn test_datetime_fromtimestamp() { Python::with_gil(|py| { - let dt = PyDateTime::from_timestamp(py, 100.0, None).unwrap(); + let dt = PyDateTime::from_timestamp_bound(py, 100.0, None).unwrap(); py_run!( py, dt, "import datetime; assert dt == datetime.datetime.fromtimestamp(100)" ); - let dt = PyDateTime::from_timestamp(py, 100.0, Some(timezone_utc(py))).unwrap(); + let dt = + PyDateTime::from_timestamp_bound(py, 100.0, Some(&timezone_utc_bound(py))).unwrap(); py_run!( py, dt, @@ -702,7 +956,7 @@ mod tests { #[cfg_attr(target_arch = "wasm32", ignore)] // DateTime import fails on wasm for mysterious reasons fn test_date_fromtimestamp() { Python::with_gil(|py| { - let dt = PyDate::from_timestamp(py, 100).unwrap(); + let dt = PyDate::from_timestamp_bound(py, 100).unwrap(); py_run!( py, dt, @@ -715,8 +969,10 @@ mod tests { #[cfg_attr(target_arch = "wasm32", ignore)] // DateTime import fails on wasm for mysterious reasons fn test_new_with_fold() { Python::with_gil(|py| { - let a = PyDateTime::new_with_fold(py, 2021, 1, 23, 20, 32, 40, 341516, None, false); - let b = PyDateTime::new_with_fold(py, 2021, 1, 23, 20, 32, 40, 341516, None, true); + let a = + PyDateTime::new_bound_with_fold(py, 2021, 1, 23, 20, 32, 40, 341516, None, false); + let b = + PyDateTime::new_bound_with_fold(py, 2021, 1, 23, 20, 32, 40, 341516, None, true); assert!(!a.unwrap().get_fold()); assert!(b.unwrap().get_fold()); @@ -727,23 +983,23 @@ mod tests { #[cfg_attr(target_arch = "wasm32", ignore)] // DateTime import fails on wasm for mysterious reasons fn test_get_tzinfo() { crate::Python::with_gil(|py| { - let utc = timezone_utc(py); + let utc = timezone_utc_bound(py); - let dt = PyDateTime::new(py, 2018, 1, 1, 0, 0, 0, 0, Some(utc)).unwrap(); + let dt = PyDateTime::new_bound(py, 2018, 1, 1, 0, 0, 0, 0, Some(&utc)).unwrap(); - assert!(dt.get_tzinfo().unwrap().eq(utc).unwrap()); + assert!(dt.get_tzinfo_bound().unwrap().eq(&utc).unwrap()); - let dt = PyDateTime::new(py, 2018, 1, 1, 0, 0, 0, 0, None).unwrap(); + let dt = PyDateTime::new_bound(py, 2018, 1, 1, 0, 0, 0, 0, None).unwrap(); - assert!(dt.get_tzinfo().is_none()); + assert!(dt.get_tzinfo_bound().is_none()); - let t = PyTime::new(py, 0, 0, 0, 0, Some(utc)).unwrap(); + let t = PyTime::new_bound(py, 0, 0, 0, 0, Some(&utc)).unwrap(); - assert!(t.get_tzinfo().unwrap().eq(utc).unwrap()); + assert!(t.get_tzinfo_bound().unwrap().eq(utc).unwrap()); - let t = PyTime::new(py, 0, 0, 0, 0, None).unwrap(); + let t = PyTime::new_bound(py, 0, 0, 0, 0, None).unwrap(); - assert!(t.get_tzinfo().is_none()); + assert!(t.get_tzinfo_bound().is_none()); }); } @@ -753,28 +1009,28 @@ mod tests { fn test_timezone_from_offset() { Python::with_gil(|py| { assert!( - timezone_from_offset(py, PyDelta::new(py, 0, -3600, 0, true).unwrap()) + timezone_from_offset(&PyDelta::new_bound(py, 0, -3600, 0, true).unwrap()) .unwrap() .call_method1("utcoffset", ((),)) .unwrap() - .extract::<&PyDelta>() + .downcast_into::() .unwrap() - .eq(PyDelta::new(py, 0, -3600, 0, true).unwrap()) + .eq(PyDelta::new_bound(py, 0, -3600, 0, true).unwrap()) .unwrap() ); assert!( - timezone_from_offset(py, PyDelta::new(py, 0, 3600, 0, true).unwrap()) + timezone_from_offset(&PyDelta::new_bound(py, 0, 3600, 0, true).unwrap()) .unwrap() .call_method1("utcoffset", ((),)) .unwrap() - .extract::<&PyDelta>() + .downcast_into::() .unwrap() - .eq(PyDelta::new(py, 0, 3600, 0, true).unwrap()) + .eq(PyDelta::new_bound(py, 0, 3600, 0, true).unwrap()) .unwrap() ); - timezone_from_offset(py, PyDelta::new(py, 1, 0, 0, true).unwrap()).unwrap_err(); + timezone_from_offset(&PyDelta::new_bound(py, 1, 0, 0, true).unwrap()).unwrap_err(); }) } } diff --git a/src/types/dict.rs b/src/types/dict.rs index 89e0df96e63..cab6d68124b 100644 --- a/src/types/dict.rs +++ b/src/types/dict.rs @@ -6,6 +6,8 @@ use crate::instance::{Borrowed, Bound}; use crate::py_result_ext::PyResultExt; use crate::types::any::PyAnyMethods; use crate::types::{PyAny, PyList}; +#[cfg(feature = "gil-refs")] +use crate::PyNativeType; use crate::{ffi, Python, ToPyObject}; /// Represents a Python `dict`. @@ -20,11 +22,11 @@ pyobject_native_type!( ); /// Represents a Python `dict_keys`. -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] #[repr(transparent)] pub struct PyDictKeys(PyAny); -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] pyobject_native_type_core!( PyDictKeys, pyobject_native_static_type_object!(ffi::PyDictKeys_Type), @@ -32,11 +34,11 @@ pyobject_native_type_core!( ); /// Represents a Python `dict_values`. -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] #[repr(transparent)] pub struct PyDictValues(PyAny); -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] pyobject_native_type_core!( PyDictValues, pyobject_native_static_type_object!(ffi::PyDictValues_Type), @@ -44,11 +46,11 @@ pyobject_native_type_core!( ); /// Represents a Python `dict_items`. -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] #[repr(transparent)] pub struct PyDictItems(PyAny); -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] pyobject_native_type_core!( PyDictItems, pyobject_native_static_type_object!(ffi::PyDictItems_Type), @@ -57,8 +59,8 @@ pyobject_native_type_core!( impl PyDict { /// Creates a new empty dictionary. - pub fn new(py: Python<'_>) -> &PyDict { - unsafe { py.from_owned_ptr(ffi::PyDict_New()) } + pub fn new_bound(py: Python<'_>) -> Bound<'_, PyDict> { + unsafe { ffi::PyDict_New().assume_owned(py).downcast_into_unchecked() } } /// Creates a new dictionary from the sequence given. @@ -68,40 +70,62 @@ impl PyDict { /// /// Returns an error on invalid input. In the case of key collisions, /// this keeps the last entry seen. - #[cfg(not(PyPy))] - pub fn from_sequence(seq: &PyAny) -> PyResult<&PyDict> { + #[cfg(not(any(PyPy, GraalPy)))] + pub fn from_sequence_bound<'py>(seq: &Bound<'py, PyAny>) -> PyResult> { let py = seq.py(); - let dict = Self::new(py); + let dict = Self::new_bound(py); err::error_on_minusone(py, unsafe { - ffi::PyDict_MergeFromSeq2(dict.into_ptr(), seq.into_ptr(), 1) + ffi::PyDict_MergeFromSeq2(dict.as_ptr(), seq.as_ptr(), 1) })?; Ok(dict) } +} + +#[cfg(feature = "gil-refs")] +impl PyDict { + /// Deprecated form of [`new_bound`][PyDict::new_bound]. + #[deprecated( + since = "0.21.0", + note = "`PyDict::new` will be replaced by `PyDict::new_bound` in a future PyO3 version" + )] + #[inline] + pub fn new(py: Python<'_>) -> &PyDict { + Self::new_bound(py).into_gil_ref() + } + + /// Deprecated form of [`from_sequence_bound`][PyDict::from_sequence_bound]. + #[deprecated( + since = "0.21.0", + note = "`PyDict::from_sequence` will be replaced by `PyDict::from_sequence_bound` in a future PyO3 version" + )] + #[inline] + #[cfg(not(any(PyPy, GraalPy)))] + pub fn from_sequence(seq: &PyAny) -> PyResult<&PyDict> { + Self::from_sequence_bound(&seq.as_borrowed()).map(Bound::into_gil_ref) + } /// Returns a new dictionary that contains the same key-value pairs as self. /// /// This is equivalent to the Python expression `self.copy()`. pub fn copy(&self) -> PyResult<&PyDict> { - Bound::borrowed_from_gil_ref(&self) - .copy() - .map(Bound::into_gil_ref) + self.as_borrowed().copy().map(Bound::into_gil_ref) } /// Empties an existing dictionary of all key-value pairs. pub fn clear(&self) { - Bound::borrowed_from_gil_ref(&self).clear() + self.as_borrowed().clear() } /// Return the number of items in the dictionary. /// /// This is equivalent to the Python expression `len(self)`. pub fn len(&self) -> usize { - Bound::borrowed_from_gil_ref(&self).len() + self.as_borrowed().len() } /// Checks if the dict is empty, i.e. `len(self) == 0`. pub fn is_empty(&self) -> bool { - Bound::borrowed_from_gil_ref(&self).is_empty() + self.as_borrowed().is_empty() } /// Determines if the dictionary contains the specified key. @@ -111,7 +135,7 @@ impl PyDict { where K: ToPyObject, { - Bound::borrowed_from_gil_ref(&self).contains(key) + self.as_borrowed().contains(key) } /// Gets an item from the dictionary. @@ -135,13 +159,13 @@ impl PyDict { /// /// ```rust /// use pyo3::prelude::*; - /// use pyo3::types::{PyDict, IntoPyDict}; + /// use pyo3::types::{IntoPyDict}; /// use pyo3::exceptions::{PyTypeError, PyKeyError}; /// /// # fn main() { /// # let _ = /// Python::with_gil(|py| -> PyResult<()> { - /// let dict: &PyDict = [("a", 1)].into_py_dict(py); + /// let dict = &[("a", 1)].into_py_dict_bound(py); /// // `a` is in the dictionary, with value 1 /// assert!(dict.get_item("a")?.map_or(Ok(false), |x| x.eq(1))?); /// // `b` is not in the dictionary @@ -150,7 +174,7 @@ impl PyDict { /// assert!(dict.get_item(dict).unwrap_err().is_instance_of::(py)); /// /// // `PyAny::get_item("b")` will raise a `KeyError` instead of returning `None` - /// let any: &PyAny = dict.as_ref(); + /// let any = dict.as_any(); /// assert!(any.get_item("b").unwrap_err().is_instance_of::(py)); /// Ok(()) /// }); @@ -160,7 +184,7 @@ impl PyDict { where K: ToPyObject, { - match Bound::borrowed_from_gil_ref(&self).get_item(key) { + match self.as_borrowed().get_item(key) { Ok(Some(item)) => Ok(Some(item.into_gil_ref())), Ok(None) => Ok(None), Err(e) => Err(e), @@ -188,7 +212,7 @@ impl PyDict { K: ToPyObject, V: ToPyObject, { - Bound::borrowed_from_gil_ref(&self).set_item(key, value) + self.as_borrowed().set_item(key, value) } /// Deletes an item. @@ -198,28 +222,28 @@ impl PyDict { where K: ToPyObject, { - Bound::borrowed_from_gil_ref(&self).del_item(key) + self.as_borrowed().del_item(key) } /// Returns a list of dict keys. /// /// This is equivalent to the Python expression `list(dict.keys())`. pub fn keys(&self) -> &PyList { - Bound::borrowed_from_gil_ref(&self).keys().into_gil_ref() + self.as_borrowed().keys().into_gil_ref() } /// Returns a list of dict values. /// /// This is equivalent to the Python expression `list(dict.values())`. pub fn values(&self) -> &PyList { - Bound::borrowed_from_gil_ref(&self).values().into_gil_ref() + self.as_borrowed().values().into_gil_ref() } /// Returns a list of dict items. /// /// This is equivalent to the Python expression `list(dict.items())`. pub fn items(&self) -> &PyList { - Bound::borrowed_from_gil_ref(&self).items().into_gil_ref() + self.as_borrowed().items().into_gil_ref() } /// Returns an iterator of `(key, value)` pairs in this dictionary. @@ -230,7 +254,7 @@ impl PyDict { /// It is allowed to modify values as you iterate over the dictionary, but only /// so long as the set of keys does not change. pub fn iter(&self) -> PyDictIterator<'_> { - PyDictIterator(Bound::borrowed_from_gil_ref(&self).iter()) + PyDictIterator(self.as_borrowed().iter()) } /// Returns `self` cast as a `PyMapping`. @@ -243,7 +267,7 @@ impl PyDict { /// This is equivalent to the Python expression `self.update(other)`. If `other` is a `PyDict`, you may want /// to use `self.update(other.as_mapping())`, note: `PyDict::as_mapping` is a zero-cost conversion. pub fn update(&self, other: &PyMapping) -> PyResult<()> { - Bound::borrowed_from_gil_ref(&self).update(Bound::borrowed_from_gil_ref(&other)) + self.as_borrowed().update(&other.as_borrowed()) } /// Add key/value pairs from another dictionary to this one only when they do not exist in this. @@ -255,7 +279,7 @@ impl PyDict { /// This method uses [`PyDict_Merge`](https://docs.python.org/3/c-api/dict.html#c.PyDict_Merge) internally, /// so should have the same performance as `update`. pub fn update_if_missing(&self, other: &PyMapping) -> PyResult<()> { - Bound::borrowed_from_gil_ref(&self).update_if_missing(Bound::borrowed_from_gil_ref(&other)) + self.as_borrowed().update_if_missing(&other.as_borrowed()) } } @@ -265,7 +289,7 @@ impl PyDict { /// syntax these methods are separated into a trait, because stable Rust does not yet support /// `arbitrary_self_types`. #[doc(alias = "PyDict")] -pub trait PyDictMethods<'py> { +pub trait PyDictMethods<'py>: crate::sealed::Sealed { /// Returns a new dictionary that contains the same key-value pairs as self. /// /// This is equivalent to the Python expression `self.copy()`. @@ -340,6 +364,9 @@ pub trait PyDictMethods<'py> { /// Returns `self` cast as a `PyMapping`. fn as_mapping(&self) -> &Bound<'py, PyMapping>; + /// Returns `self` cast as a `PyMapping`. + fn into_mapping(self) -> Bound<'py, PyMapping>; + /// Update this dictionary with the key/value pairs from another. /// /// This is equivalent to the Python expression `self.update(other)`. If `other` is a `PyDict`, you may want @@ -486,6 +513,10 @@ impl<'py> PyDictMethods<'py> for Bound<'py, PyDict> { unsafe { self.downcast_unchecked() } } + fn into_mapping(self) -> Bound<'py, PyMapping> { + unsafe { self.into_any().downcast_into_unchecked() } + } + fn update(&self, other: &Bound<'_, PyMapping>) -> PyResult<()> { err::error_on_minusone(self.py(), unsafe { ffi::PyDict_Update(self.as_ptr(), other.as_ptr()) @@ -499,21 +530,33 @@ impl<'py> PyDictMethods<'py> for Bound<'py, PyDict> { } } +impl<'a, 'py> Borrowed<'a, 'py, PyDict> { + /// Iterates over the contents of this dictionary without incrementing reference counts. + /// + /// # Safety + /// It must be known that this dictionary will not be modified during iteration. + pub(crate) unsafe fn iter_borrowed(self) -> BorrowedDictIter<'a, 'py> { + BorrowedDictIter::new(self) + } +} + fn dict_len(dict: &Bound<'_, PyDict>) -> Py_ssize_t { - #[cfg(any(not(Py_3_8), PyPy, Py_LIMITED_API))] + #[cfg(any(not(Py_3_8), PyPy, GraalPy, Py_LIMITED_API))] unsafe { ffi::PyDict_Size(dict.as_ptr()) } - #[cfg(all(Py_3_8, not(PyPy), not(Py_LIMITED_API)))] + #[cfg(all(Py_3_8, not(PyPy), not(GraalPy), not(Py_LIMITED_API)))] unsafe { (*dict.as_ptr().cast::()).ma_used } } /// PyO3 implementation of an iterator for a Python `dict` object. +#[cfg(feature = "gil-refs")] pub struct PyDictIterator<'py>(BoundDictIterator<'py>); +#[cfg(feature = "gil-refs")] impl<'py> Iterator for PyDictIterator<'py> { type Item = (&'py PyAny, &'py PyAny); @@ -529,12 +572,14 @@ impl<'py> Iterator for PyDictIterator<'py> { } } +#[cfg(feature = "gil-refs")] impl<'py> ExactSizeIterator for PyDictIterator<'py> { fn len(&self) -> usize { self.0.len() } } +#[cfg(feature = "gil-refs")] impl<'a> IntoIterator for &'a PyDict { type Item = (&'a PyAny, &'a PyAny); type IntoIter = PyDictIterator<'a>; @@ -628,30 +673,99 @@ impl<'py> BoundDictIterator<'py> { } } -impl<'py> IntoIterator for &'_ Bound<'py, PyDict> { +impl<'py> IntoIterator for Bound<'py, PyDict> { type Item = (Bound<'py, PyAny>, Bound<'py, PyAny>); type IntoIter = BoundDictIterator<'py>; fn into_iter(self) -> Self::IntoIter { - self.iter() + BoundDictIterator::new(self) } } -impl<'py> IntoIterator for Bound<'py, PyDict> { +impl<'py> IntoIterator for &Bound<'py, PyDict> { type Item = (Bound<'py, PyAny>, Bound<'py, PyAny>); type IntoIter = BoundDictIterator<'py>; fn into_iter(self) -> Self::IntoIter { - BoundDictIterator::new(self) + self.iter() + } +} + +mod borrowed_iter { + use super::*; + + /// Variant of the above which is used to iterate the items of the dictionary + /// without incrementing reference counts. This is only safe if it's known + /// that the dictionary will not be modified during iteration. + pub struct BorrowedDictIter<'a, 'py> { + dict: Borrowed<'a, 'py, PyDict>, + ppos: ffi::Py_ssize_t, + len: ffi::Py_ssize_t, + } + + impl<'a, 'py> Iterator for BorrowedDictIter<'a, 'py> { + type Item = (Borrowed<'a, 'py, PyAny>, Borrowed<'a, 'py, PyAny>); + + #[inline] + fn next(&mut self) -> Option { + let mut key: *mut ffi::PyObject = std::ptr::null_mut(); + let mut value: *mut ffi::PyObject = std::ptr::null_mut(); + + // Safety: self.dict lives sufficiently long that the pointer is not dangling + if unsafe { ffi::PyDict_Next(self.dict.as_ptr(), &mut self.ppos, &mut key, &mut value) } + != 0 + { + let py = self.dict.py(); + self.len -= 1; + // Safety: + // - PyDict_Next returns borrowed values + // - we have already checked that `PyDict_Next` succeeded, so we can assume these to be non-null + Some(unsafe { (key.assume_borrowed(py), value.assume_borrowed(py)) }) + } else { + None + } + } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + let len = self.len(); + (len, Some(len)) + } + } + + impl ExactSizeIterator for BorrowedDictIter<'_, '_> { + fn len(&self) -> usize { + self.len as usize + } + } + + impl<'a, 'py> BorrowedDictIter<'a, 'py> { + pub(super) fn new(dict: Borrowed<'a, 'py, PyDict>) -> Self { + let len = dict_len(&dict); + BorrowedDictIter { dict, ppos: 0, len } + } } } +pub(crate) use borrowed_iter::BorrowedDictIter; + /// Conversion trait that allows a sequence of tuples to be converted into `PyDict` /// Primary use case for this trait is `call` and `call_method` methods as keywords argument. -pub trait IntoPyDict { +pub trait IntoPyDict: Sized { + /// Converts self into a `PyDict` object pointer. Whether pointer owned or borrowed + /// depends on implementation. + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`IntoPyDict::into_py_dict` will be replaced by `IntoPyDict::into_py_dict_bound` in a future PyO3 version" + )] + fn into_py_dict(self, py: Python<'_>) -> &PyDict { + Self::into_py_dict_bound(self, py).into_gil_ref() + } + /// Converts self into a `PyDict` object pointer. Whether pointer owned or borrowed /// depends on implementation. - fn into_py_dict(self, py: Python<'_>) -> &PyDict; + fn into_py_dict_bound(self, py: Python<'_>) -> Bound<'_, PyDict>; } impl IntoPyDict for I @@ -659,8 +773,8 @@ where T: PyDictItem, I: IntoIterator, { - fn into_py_dict(self, py: Python<'_>) -> &PyDict { - let dict = PyDict::new(py); + fn into_py_dict_bound(self, py: Python<'_>) -> Bound<'_, PyDict> { + let dict = PyDict::new_bound(py); for item in self { dict.set_item(item.key(), item.value()) .expect("Failed to set_item on dict"); @@ -710,17 +824,13 @@ where #[cfg(test)] mod tests { use super::*; - #[cfg(not(PyPy))] - use crate::exceptions; - #[cfg(not(PyPy))] - use crate::types::PyList; - use crate::{types::PyTuple, Python, ToPyObject}; + use crate::types::PyTuple; use std::collections::{BTreeMap, HashMap}; #[test] fn test_new() { Python::with_gil(|py| { - let dict = [(7, 32)].into_py_dict(py); + let dict = [(7, 32)].into_py_dict_bound(py); assert_eq!( 32, dict.get_item(7i32) @@ -738,11 +848,11 @@ mod tests { } #[test] - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] fn test_from_sequence() { Python::with_gil(|py| { - let items = PyList::new(py, &vec![("a", 1), ("b", 2)]); - let dict = PyDict::from_sequence(items).unwrap(); + let items = PyList::new_bound(py, &vec![("a", 1), ("b", 2)]); + let dict = PyDict::from_sequence_bound(&items).unwrap(); assert_eq!( 1, dict.get_item("a") @@ -769,18 +879,18 @@ mod tests { } #[test] - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] fn test_from_sequence_err() { Python::with_gil(|py| { - let items = PyList::new(py, &vec!["a", "b"]); - assert!(PyDict::from_sequence(items).is_err()); + let items = PyList::new_bound(py, &vec!["a", "b"]); + assert!(PyDict::from_sequence_bound(&items).is_err()); }); } #[test] fn test_copy() { Python::with_gil(|py| { - let dict = [(7, 32)].into_py_dict(py); + let dict = [(7, 32)].into_py_dict_bound(py); let ndict = dict.copy().unwrap(); assert_eq!( @@ -801,11 +911,11 @@ mod tests { Python::with_gil(|py| { let mut v = HashMap::new(); let ob = v.to_object(py); - let dict: &PyDict = ob.downcast(py).unwrap(); + let dict = ob.downcast_bound::(py).unwrap(); assert_eq!(0, dict.len()); v.insert(7, 32); let ob = v.to_object(py); - let dict2: &PyDict = ob.downcast(py).unwrap(); + let dict2 = ob.downcast_bound::(py).unwrap(); assert_eq!(1, dict2.len()); }); } @@ -816,7 +926,7 @@ mod tests { let mut v = HashMap::new(); v.insert(7, 32); let ob = v.to_object(py); - let dict: &PyDict = ob.downcast(py).unwrap(); + let dict = ob.downcast_bound::(py).unwrap(); assert!(dict.contains(7i32).unwrap()); assert!(!dict.contains(8i32).unwrap()); }); @@ -828,7 +938,7 @@ mod tests { let mut v = HashMap::new(); v.insert(7, 32); let ob = v.to_object(py); - let dict: &PyDict = ob.downcast(py).unwrap(); + let dict = ob.downcast_bound::(py).unwrap(); assert_eq!( 32, dict.get_item(7i32) @@ -843,13 +953,13 @@ mod tests { #[test] #[allow(deprecated)] - #[cfg(not(PyPy))] + #[cfg(all(not(any(PyPy, GraalPy)), feature = "gil-refs"))] fn test_get_item_with_error() { Python::with_gil(|py| { let mut v = HashMap::new(); v.insert(7, 32); let ob = v.to_object(py); - let dict: &PyDict = ob.downcast(py).unwrap(); + let dict = ob.downcast::(py).unwrap(); assert_eq!( 32, dict.get_item_with_error(7i32) @@ -862,7 +972,7 @@ mod tests { assert!(dict .get_item_with_error(dict) .unwrap_err() - .is_instance_of::(py)); + .is_instance_of::(py)); }); } @@ -872,7 +982,7 @@ mod tests { let mut v = HashMap::new(); v.insert(7, 32); let ob = v.to_object(py); - let dict: &PyDict = ob.downcast(py).unwrap(); + let dict = ob.downcast_bound::(py).unwrap(); assert!(dict.set_item(7i32, 42i32).is_ok()); // change assert!(dict.set_item(8i32, 123i32).is_ok()); // insert assert_eq!( @@ -898,11 +1008,10 @@ mod tests { fn test_set_item_refcnt() { Python::with_gil(|py| { let cnt; - let obj = py.eval("object()", None, None).unwrap(); + let obj = py.eval_bound("object()", None, None).unwrap(); { - let _pool = unsafe { crate::GILPool::new() }; cnt = obj.get_refcnt(); - let _dict = [(10, obj)].into_py_dict(py); + let _dict = [(10, &obj)].into_py_dict_bound(py); } { assert_eq!(cnt, obj.get_refcnt()); @@ -916,7 +1025,7 @@ mod tests { let mut v = HashMap::new(); v.insert(7, 32); let ob = v.to_object(py); - let dict: &PyDict = ob.downcast(py).unwrap(); + let dict = ob.downcast_bound::(py).unwrap(); assert!(dict.set_item(7i32, 42i32).is_ok()); // change assert!(dict.set_item(8i32, 123i32).is_ok()); // insert assert_eq!(32i32, v[&7i32]); // not updated! @@ -930,7 +1039,7 @@ mod tests { let mut v = HashMap::new(); v.insert(7, 32); let ob = v.to_object(py); - let dict: &PyDict = ob.downcast(py).unwrap(); + let dict = ob.downcast_bound::(py).unwrap(); assert!(dict.del_item(7i32).is_ok()); assert_eq!(0, dict.len()); assert!(dict.get_item(7i32).unwrap().is_none()); @@ -943,7 +1052,7 @@ mod tests { let mut v = HashMap::new(); v.insert(7, 32); let ob = v.to_object(py); - let dict: &PyDict = ob.downcast(py).unwrap(); + let dict = ob.downcast_bound::(py).unwrap(); assert!(dict.del_item(7i32).is_ok()); // change assert_eq!(32i32, *v.get(&7i32).unwrap()); // not updated! }); @@ -957,7 +1066,7 @@ mod tests { v.insert(8, 42); v.insert(9, 123); let ob = v.to_object(py); - let dict: &PyDict = ob.downcast(py).unwrap(); + let dict = ob.downcast_bound::(py).unwrap(); // Can't just compare against a vector of tuples since we don't have a guaranteed ordering. let mut key_sum = 0; let mut value_sum = 0; @@ -979,7 +1088,7 @@ mod tests { v.insert(8, 42); v.insert(9, 123); let ob = v.to_object(py); - let dict: &PyDict = ob.downcast(py).unwrap(); + let dict = ob.downcast_bound::(py).unwrap(); // Can't just compare against a vector of tuples since we don't have a guaranteed ordering. let mut key_sum = 0; for el in dict.keys() { @@ -997,7 +1106,7 @@ mod tests { v.insert(8, 42); v.insert(9, 123); let ob = v.to_object(py); - let dict: &PyDict = ob.downcast(py).unwrap(); + let dict = ob.downcast_bound::(py).unwrap(); // Can't just compare against a vector of tuples since we don't have a guaranteed ordering. let mut values_sum = 0; for el in dict.values() { @@ -1015,7 +1124,27 @@ mod tests { v.insert(8, 42); v.insert(9, 123); let ob = v.to_object(py); - let dict: &PyDict = ob.downcast(py).unwrap(); + let dict = ob.downcast_bound::(py).unwrap(); + let mut key_sum = 0; + let mut value_sum = 0; + for (key, value) in dict { + key_sum += key.extract::().unwrap(); + value_sum += value.extract::().unwrap(); + } + assert_eq!(7 + 8 + 9, key_sum); + assert_eq!(32 + 42 + 123, value_sum); + }); + } + + #[test] + fn test_iter_bound() { + Python::with_gil(|py| { + let mut v = HashMap::new(); + v.insert(7, 32); + v.insert(8, 42); + v.insert(9, 123); + let ob = v.to_object(py); + let dict: &Bound<'_, PyDict> = ob.downcast_bound(py).unwrap(); let mut key_sum = 0; let mut value_sum = 0; for (key, value) in dict { @@ -1036,7 +1165,7 @@ mod tests { v.insert(9, 123); let ob = v.to_object(py); - let dict: &PyDict = ob.downcast(py).unwrap(); + let dict = ob.downcast_bound::(py).unwrap(); for (key, value) in dict { dict.set_item(key, value.extract::().unwrap() + 7) @@ -1054,7 +1183,7 @@ mod tests { v.insert(i * 2, i * 2); } let ob = v.to_object(py); - let dict: &PyDict = ob.downcast(py).unwrap(); + let dict = ob.downcast_bound::(py).unwrap(); for (i, (key, value)) in dict.iter().enumerate() { let key = key.extract::().unwrap(); @@ -1079,7 +1208,7 @@ mod tests { v.insert(i * 2, i * 2); } let ob = v.to_object(py); - let dict: &PyDict = ob.downcast(py).unwrap(); + let dict = ob.downcast_bound::(py).unwrap(); for (i, (key, value)) in dict.iter().enumerate() { let key = key.extract::().unwrap(); @@ -1103,7 +1232,7 @@ mod tests { v.insert(8, 42); v.insert(9, 123); let ob = v.to_object(py); - let dict: &PyDict = ob.downcast(py).unwrap(); + let dict = ob.downcast_bound::(py).unwrap(); let mut iter = dict.iter(); assert_eq!(iter.size_hint(), (v.len(), Some(v.len()))); @@ -1129,7 +1258,7 @@ mod tests { v.insert(8, 42); v.insert(9, 123); let ob = v.to_object(py); - let dict: &PyDict = ob.downcast(py).unwrap(); + let dict = ob.downcast_bound::(py).unwrap(); let mut key_sum = 0; let mut value_sum = 0; for (key, value) in dict { @@ -1147,7 +1276,7 @@ mod tests { let mut map = HashMap::::new(); map.insert(1, 1); - let py_map = map.into_py_dict(py); + let py_map = map.into_py_dict_bound(py); assert_eq!(py_map.len(), 1); assert_eq!( @@ -1168,7 +1297,7 @@ mod tests { let mut map = BTreeMap::::new(); map.insert(1, 1); - let py_map = map.into_py_dict(py); + let py_map = map.into_py_dict_bound(py); assert_eq!(py_map.len(), 1); assert_eq!( @@ -1187,7 +1316,7 @@ mod tests { fn test_vec_into_dict() { Python::with_gil(|py| { let vec = vec![("a", 1), ("b", 2), ("c", 3)]; - let py_map = vec.into_py_dict(py); + let py_map = vec.into_py_dict_bound(py); assert_eq!(py_map.len(), 3); assert_eq!( @@ -1206,7 +1335,7 @@ mod tests { fn test_slice_into_dict() { Python::with_gil(|py| { let arr = [("a", 1), ("b", 2), ("c", 3)]; - let py_map = arr.into_py_dict(py); + let py_map = arr.into_py_dict_bound(py); assert_eq!(py_map.len(), 3); assert_eq!( @@ -1227,7 +1356,7 @@ mod tests { let mut map = HashMap::::new(); map.insert(1, 1); - let py_map = map.into_py_dict(py); + let py_map = map.into_py_dict_bound(py); assert_eq!(py_map.as_mapping().len().unwrap(), 1); assert_eq!( @@ -1242,50 +1371,70 @@ mod tests { }); } - #[cfg(not(PyPy))] - fn abc_dict(py: Python<'_>) -> &PyDict { + #[test] + fn dict_into_mapping() { + Python::with_gil(|py| { + let mut map = HashMap::::new(); + map.insert(1, 1); + + let py_map = map.into_py_dict_bound(py); + + let py_mapping = py_map.into_mapping(); + assert_eq!(py_mapping.len().unwrap(), 1); + assert_eq!(py_mapping.get_item(1).unwrap().extract::().unwrap(), 1); + }); + } + + #[cfg(not(any(PyPy, GraalPy)))] + fn abc_dict(py: Python<'_>) -> Bound<'_, PyDict> { let mut map = HashMap::<&'static str, i32>::new(); map.insert("a", 1); map.insert("b", 2); map.insert("c", 3); - map.into_py_dict(py) + map.into_py_dict_bound(py) } #[test] - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] fn dict_keys_view() { Python::with_gil(|py| { let dict = abc_dict(py); let keys = dict.call_method0("keys").unwrap(); - assert!(keys.is_instance(py.get_type::()).unwrap()); + assert!(keys + .is_instance(&py.get_type_bound::().as_borrowed()) + .unwrap()); }) } #[test] - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] fn dict_values_view() { Python::with_gil(|py| { let dict = abc_dict(py); let values = dict.call_method0("values").unwrap(); - assert!(values.is_instance(py.get_type::()).unwrap()); + assert!(values + .is_instance(&py.get_type_bound::().as_borrowed()) + .unwrap()); }) } #[test] - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] fn dict_items_view() { Python::with_gil(|py| { let dict = abc_dict(py); let items = dict.call_method0("items").unwrap(); - assert!(items.is_instance(py.get_type::()).unwrap()); + assert!(items + .is_instance(&py.get_type_bound::().as_borrowed()) + .unwrap()); }) } #[test] fn dict_update() { Python::with_gil(|py| { - let dict = [("a", 1), ("b", 2), ("c", 3)].into_py_dict(py); - let other = [("b", 4), ("c", 5), ("d", 6)].into_py_dict(py); + let dict = [("a", 1), ("b", 2), ("c", 3)].into_py_dict_bound(py); + let other = [("b", 4), ("c", 5), ("d", 6)].into_py_dict_bound(py); dict.update(other.as_mapping()).unwrap(); assert_eq!(dict.len(), 4); assert_eq!( @@ -1355,8 +1504,8 @@ mod tests { #[test] fn dict_update_if_missing() { Python::with_gil(|py| { - let dict = [("a", 1), ("b", 2), ("c", 3)].into_py_dict(py); - let other = [("b", 4), ("c", 5), ("d", 6)].into_py_dict(py); + let dict = [("a", 1), ("b", 2), ("c", 3)].into_py_dict_bound(py); + let other = [("b", 4), ("c", 5), ("d", 6)].into_py_dict_bound(py); dict.update_if_missing(other.as_mapping()).unwrap(); assert_eq!(dict.len(), 4); assert_eq!( diff --git a/src/types/ellipsis.rs b/src/types/ellipsis.rs index e31f9d4183b..cbeaf489c17 100644 --- a/src/types/ellipsis.rs +++ b/src/types/ellipsis.rs @@ -1,4 +1,7 @@ -use crate::{ffi, PyAny, PyTypeInfo, Python}; +use crate::{ + ffi, ffi_ptr_ext::FfiPtrExt, types::any::PyAnyMethods, Borrowed, Bound, PyAny, PyTypeInfo, + Python, +}; /// Represents the Python `Ellipsis` object. #[repr(transparent)] @@ -9,9 +12,20 @@ pyobject_native_type_extract!(PyEllipsis); impl PyEllipsis { /// Returns the `Ellipsis` object. + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`PyEllipsis::get` will be replaced by `PyEllipsis::get_bound` in a future PyO3 version" + )] #[inline] pub fn get(py: Python<'_>) -> &PyEllipsis { - unsafe { py.from_borrowed_ptr(ffi::Py_Ellipsis()) } + Self::get_bound(py).into_gil_ref() + } + + /// Returns the `Ellipsis` object. + #[inline] + pub fn get_bound(py: Python<'_>) -> Borrowed<'_, '_, PyEllipsis> { + unsafe { ffi::Py_Ellipsis().assume_borrowed(py).downcast_unchecked() } } } @@ -25,43 +39,44 @@ unsafe impl PyTypeInfo for PyEllipsis { } #[inline] - fn is_type_of(object: &PyAny) -> bool { + fn is_type_of_bound(object: &Bound<'_, PyAny>) -> bool { // ellipsis is not usable as a base type - Self::is_exact_type_of(object) + Self::is_exact_type_of_bound(object) } #[inline] - fn is_exact_type_of(object: &PyAny) -> bool { - object.is(Self::get(object.py())) + fn is_exact_type_of_bound(object: &Bound<'_, PyAny>) -> bool { + object.is(&**Self::get_bound(object.py())) } } #[cfg(test)] mod tests { + use crate::types::any::PyAnyMethods; use crate::types::{PyDict, PyEllipsis}; use crate::{PyTypeInfo, Python}; #[test] fn test_ellipsis_is_itself() { Python::with_gil(|py| { - assert!(PyEllipsis::get(py).is_instance_of::()); - assert!(PyEllipsis::get(py).is_exact_instance_of::()); + assert!(PyEllipsis::get_bound(py).is_instance_of::()); + assert!(PyEllipsis::get_bound(py).is_exact_instance_of::()); }) } #[test] fn test_ellipsis_type_object_consistent() { Python::with_gil(|py| { - assert!(PyEllipsis::get(py) + assert!(PyEllipsis::get_bound(py) .get_type() - .is(PyEllipsis::type_object(py))); + .is(&PyEllipsis::type_object_bound(py))); }) } #[test] fn test_dict_is_not_ellipsis() { Python::with_gil(|py| { - assert!(PyDict::new(py).downcast::().is_err()); + assert!(PyDict::new_bound(py).downcast::().is_err()); }) } } diff --git a/src/types/float.rs b/src/types/float.rs index bb55d7ed5f5..8499d1e54aa 100644 --- a/src/types/float.rs +++ b/src/types/float.rs @@ -1,15 +1,18 @@ +use super::any::PyAnyMethods; #[cfg(feature = "experimental-inspect")] use crate::inspect::types::TypeInfo; +#[cfg(feature = "gil-refs")] +use crate::PyNativeType; use crate::{ - ffi, instance::Bound, FromPyObject, IntoPy, PyAny, PyErr, PyNativeType, PyObject, PyResult, - Python, ToPyObject, + ffi, ffi_ptr_ext::FfiPtrExt, instance::Bound, FromPyObject, IntoPy, PyAny, PyErr, PyObject, + PyResult, Python, ToPyObject, }; use std::os::raw::c_double; /// Represents a Python `float` object. /// /// You can usually avoid directly working with this type -/// by using [`ToPyObject`] and [`extract`](PyAny::extract) +/// by using [`ToPyObject`] and [`extract`](PyAnyMethods::extract) /// with `f32`/`f64`. #[repr(transparent)] pub struct PyFloat(PyAny); @@ -23,13 +26,30 @@ pyobject_native_type!( impl PyFloat { /// Creates a new Python `float` object. - pub fn new(py: Python<'_>, val: c_double) -> &PyFloat { - unsafe { py.from_owned_ptr(ffi::PyFloat_FromDouble(val)) } + pub fn new_bound(py: Python<'_>, val: c_double) -> Bound<'_, PyFloat> { + unsafe { + ffi::PyFloat_FromDouble(val) + .assume_owned(py) + .downcast_into_unchecked() + } + } +} + +#[cfg(feature = "gil-refs")] +impl PyFloat { + /// Deprecated form of [`PyFloat::new_bound`]. + #[inline] + #[deprecated( + since = "0.21.0", + note = "`PyFloat::new` will be replaced by `PyFloat::new_bound` in a future PyO3 version" + )] + pub fn new(py: Python<'_>, val: f64) -> &'_ Self { + Self::new_bound(py, val).into_gil_ref() } /// Gets the value of this float. pub fn value(&self) -> c_double { - Bound::borrowed_from_gil_ref(&self).value() + self.as_borrowed().value() } } @@ -39,7 +59,7 @@ impl PyFloat { /// syntax these methods are separated into a trait, because stable Rust does not yet support /// `arbitrary_self_types`. #[doc(alias = "PyFloat")] -pub trait PyFloatMethods<'py> { +pub trait PyFloatMethods<'py>: crate::sealed::Sealed { /// Gets the value of this float. fn value(&self) -> c_double; } @@ -61,13 +81,13 @@ impl<'py> PyFloatMethods<'py> for Bound<'py, PyFloat> { impl ToPyObject for f64 { fn to_object(&self, py: Python<'_>) -> PyObject { - PyFloat::new(py, *self).into() + PyFloat::new_bound(py, *self).into() } } impl IntoPy for f64 { fn into_py(self, py: Python<'_>) -> PyObject { - PyFloat::new(py, self).into() + PyFloat::new_bound(py, self).into() } #[cfg(feature = "experimental-inspect")] @@ -76,10 +96,10 @@ impl IntoPy for f64 { } } -impl<'source> FromPyObject<'source> for f64 { +impl<'py> FromPyObject<'py> for f64 { // PyFloat_AsDouble returns -1.0 upon failure #![allow(clippy::float_cmp)] - fn extract(obj: &'source PyAny) -> PyResult { + fn extract_bound(obj: &Bound<'py, PyAny>) -> PyResult { // On non-limited API, .value() uses PyFloat_AS_DOUBLE which // allows us to have an optimized fast path for the case when // we have exactly a `float` object (it's not worth going through @@ -108,13 +128,13 @@ impl<'source> FromPyObject<'source> for f64 { impl ToPyObject for f32 { fn to_object(&self, py: Python<'_>) -> PyObject { - PyFloat::new(py, f64::from(*self)).into() + PyFloat::new_bound(py, f64::from(*self)).into() } } impl IntoPy for f32 { fn into_py(self, py: Python<'_>) -> PyObject { - PyFloat::new(py, f64::from(self)).into() + PyFloat::new_bound(py, f64::from(self)).into() } #[cfg(feature = "experimental-inspect")] @@ -123,8 +143,8 @@ impl IntoPy for f32 { } } -impl<'source> FromPyObject<'source> for f32 { - fn extract(obj: &'source PyAny) -> PyResult { +impl<'py> FromPyObject<'py> for f32 { + fn extract_bound(obj: &Bound<'py, PyAny>) -> PyResult { Ok(obj.extract::()? as f32) } @@ -136,7 +156,10 @@ impl<'source> FromPyObject<'source> for f32 { #[cfg(test)] mod tests { - use crate::{types::PyFloat, Python, ToPyObject}; + use crate::{ + types::{PyFloat, PyFloatMethods}, + Python, ToPyObject, + }; macro_rules! num_to_py_object_and_back ( ($func_name:ident, $t1:ty, $t2:ty) => ( @@ -164,7 +187,7 @@ mod tests { Python::with_gil(|py| { let v = 1.23f64; - let obj = PyFloat::new(py, 1.23); + let obj = PyFloat::new_bound(py, 1.23); assert_approx_eq!(v, obj.value()); }); } diff --git a/src/types/frozenset.rs b/src/types/frozenset.rs index bbcd7102fac..78cbf01df67 100644 --- a/src/types/frozenset.rs +++ b/src/types/frozenset.rs @@ -1,16 +1,19 @@ -#[cfg(Py_LIMITED_API)] use crate::types::PyIterator; +#[cfg(feature = "gil-refs")] +use crate::PyNativeType; use crate::{ err::{self, PyErr, PyResult}, - Py, PyObject, + ffi, + ffi_ptr_ext::FfiPtrExt, + py_result_ext::PyResultExt, + types::any::PyAnyMethods, + Bound, PyAny, PyObject, Python, ToPyObject, }; -use crate::{ffi, PyAny, Python, ToPyObject}; - use std::ptr; /// Allows building a Python `frozenset` one item at a time pub struct PyFrozenSetBuilder<'py> { - py_frozen_set: &'py PyFrozenSet, + py_frozen_set: Bound<'py, PyFrozenSet>, } impl<'py> PyFrozenSetBuilder<'py> { @@ -19,7 +22,7 @@ impl<'py> PyFrozenSetBuilder<'py> { /// panic when running out of memory. pub fn new(py: Python<'py>) -> PyResult> { Ok(PyFrozenSetBuilder { - py_frozen_set: PyFrozenSet::empty(py)?, + py_frozen_set: PyFrozenSet::empty_bound(py)?, }) } @@ -28,17 +31,27 @@ impl<'py> PyFrozenSetBuilder<'py> { where K: ToPyObject, { - fn inner(frozenset: &PyFrozenSet, key: PyObject) -> PyResult<()> { + fn inner(frozenset: &Bound<'_, PyFrozenSet>, key: PyObject) -> PyResult<()> { err::error_on_minusone(frozenset.py(), unsafe { ffi::PySet_Add(frozenset.as_ptr(), key.as_ptr()) }) } - inner(self.py_frozen_set, key.to_object(self.py_frozen_set.py())) + inner(&self.py_frozen_set, key.to_object(self.py_frozen_set.py())) } - /// Finish building the set and take ownership of its current value + /// Deprecated form of [`PyFrozenSetBuilder::finalize_bound`] + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`PyFrozenSetBuilder::finalize` will be replaced by `PyFrozenSetBuilder::finalize_bound` in a future PyO3 version" + )] pub fn finalize(self) -> &'py PyFrozenSet { + self.finalize_bound().into_gil_ref() + } + + /// Finish building the set and take ownership of its current value + pub fn finalize_bound(self) -> Bound<'py, PyFrozenSet> { self.py_frozen_set } } @@ -47,7 +60,7 @@ impl<'py> PyFrozenSetBuilder<'py> { #[repr(transparent)] pub struct PyFrozenSet(PyAny); -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] pyobject_native_type!( PyFrozenSet, ffi::PySetObject, @@ -55,7 +68,7 @@ pyobject_native_type!( #checkfunction=ffi::PyFrozenSet_Check ); -#[cfg(PyPy)] +#[cfg(any(PyPy, GraalPy))] pyobject_native_type_core!( PyFrozenSet, pyobject_native_static_type_object!(ffi::PyFrozenSet_Type), @@ -67,28 +80,57 @@ impl PyFrozenSet { /// /// May panic when running out of memory. #[inline] + pub fn new_bound<'a, 'p, T: ToPyObject + 'a>( + py: Python<'p>, + elements: impl IntoIterator, + ) -> PyResult> { + new_from_iter(py, elements) + } + + /// Creates a new empty frozen set + pub fn empty_bound(py: Python<'_>) -> PyResult> { + unsafe { + ffi::PyFrozenSet_New(ptr::null_mut()) + .assume_owned_or_err(py) + .downcast_into_unchecked() + } + } +} + +#[cfg(feature = "gil-refs")] +impl PyFrozenSet { + /// Deprecated form of [`PyFrozenSet::new_bound`]. + #[inline] + #[deprecated( + since = "0.21.0", + note = "`PyFrozenSet::new` will be replaced by `PyFrozenSet::new_bound` in a future PyO3 version" + )] pub fn new<'a, 'p, T: ToPyObject + 'a>( py: Python<'p>, elements: impl IntoIterator, ) -> PyResult<&'p PyFrozenSet> { - new_from_iter(py, elements).map(|set| set.into_ref(py)) + Self::new_bound(py, elements).map(Bound::into_gil_ref) } - /// Creates a new empty frozen set - pub fn empty(py: Python<'_>) -> PyResult<&PyFrozenSet> { - unsafe { py.from_owned_ptr_or_err(ffi::PyFrozenSet_New(ptr::null_mut())) } + /// Deprecated form of [`PyFrozenSet::empty_bound`]. + #[deprecated( + since = "0.21.0", + note = "`PyFrozenSet::empty` will be replaced by `PyFrozenSet::empty_bound` in a future PyO3 version" + )] + pub fn empty(py: Python<'_>) -> PyResult<&'_ PyFrozenSet> { + Self::empty_bound(py).map(Bound::into_gil_ref) } /// Return the number of items in the set. /// This is equivalent to len(p) on a set. #[inline] pub fn len(&self) -> usize { - unsafe { ffi::PySet_Size(self.as_ptr()) as usize } + self.as_borrowed().len() } /// Check if set is empty. pub fn is_empty(&self) -> bool { - self.len() == 0 + self.as_borrowed().is_empty() } /// Determine if the set contains the specified key. @@ -97,7 +139,54 @@ impl PyFrozenSet { where K: ToPyObject, { - fn inner(frozenset: &PyFrozenSet, key: PyObject) -> PyResult { + self.as_borrowed().contains(key) + } + + /// Returns an iterator of values in this frozen set. + pub fn iter(&self) -> PyFrozenSetIterator<'_> { + PyFrozenSetIterator(BoundFrozenSetIterator::new(self.as_borrowed().to_owned())) + } +} + +/// Implementation of functionality for [`PyFrozenSet`]. +/// +/// These methods are defined for the `Bound<'py, PyFrozenSet>` smart pointer, so to use method call +/// syntax these methods are separated into a trait, because stable Rust does not yet support +/// `arbitrary_self_types`. +#[doc(alias = "PyFrozenSet")] +pub trait PyFrozenSetMethods<'py>: crate::sealed::Sealed { + /// Returns the number of items in the set. + /// + /// This is equivalent to the Python expression `len(self)`. + fn len(&self) -> usize; + + /// Checks if set is empty. + fn is_empty(&self) -> bool { + self.len() == 0 + } + + /// Determines if the set contains the specified key. + /// + /// This is equivalent to the Python expression `key in self`. + fn contains(&self, key: K) -> PyResult + where + K: ToPyObject; + + /// Returns an iterator of values in this set. + fn iter(&self) -> BoundFrozenSetIterator<'py>; +} + +impl<'py> PyFrozenSetMethods<'py> for Bound<'py, PyFrozenSet> { + #[inline] + fn len(&self) -> usize { + unsafe { ffi::PySet_Size(self.as_ptr()) as usize } + } + + fn contains(&self, key: K) -> PyResult + where + K: ToPyObject, + { + fn inner(frozenset: &Bound<'_, PyFrozenSet>, key: Bound<'_, PyAny>) -> PyResult { match unsafe { ffi::PySet_Contains(frozenset.as_ptr(), key.as_ptr()) } { 1 => Ok(true), 0 => Ok(false), @@ -105,110 +194,122 @@ impl PyFrozenSet { } } - inner(self, key.to_object(self.py())) + let py = self.py(); + inner(self, key.to_object(py).into_bound(py)) } - /// Returns an iterator of values in this frozen set. - pub fn iter(&self) -> PyFrozenSetIterator<'_> { - IntoIterator::into_iter(self) + fn iter(&self) -> BoundFrozenSetIterator<'py> { + BoundFrozenSetIterator::new(self.clone()) } } -#[cfg(Py_LIMITED_API)] -mod impl_ { - use super::*; +/// PyO3 implementation of an iterator for a Python `frozenset` object. +#[cfg(feature = "gil-refs")] +pub struct PyFrozenSetIterator<'py>(BoundFrozenSetIterator<'py>); - impl<'a> std::iter::IntoIterator for &'a PyFrozenSet { - type Item = &'a PyAny; - type IntoIter = PyFrozenSetIterator<'a>; +#[cfg(feature = "gil-refs")] +impl<'py> Iterator for PyFrozenSetIterator<'py> { + type Item = &'py super::PyAny; - fn into_iter(self) -> Self::IntoIter { - PyFrozenSetIterator { - it: PyIterator::from_object(self).unwrap(), - } - } + /// Advances the iterator and returns the next value. + #[inline] + fn next(&mut self) -> Option { + self.0.next().map(Bound::into_gil_ref) } - /// PyO3 implementation of an iterator for a Python `frozenset` object. - pub struct PyFrozenSetIterator<'p> { - it: &'p PyIterator, + fn size_hint(&self) -> (usize, Option) { + self.0.size_hint() } +} - impl<'py> Iterator for PyFrozenSetIterator<'py> { - type Item = &'py super::PyAny; - - #[inline] - fn next(&mut self) -> Option { - self.it.next().map(Result::unwrap) - } +#[cfg(feature = "gil-refs")] +impl ExactSizeIterator for PyFrozenSetIterator<'_> { + #[inline] + fn len(&self) -> usize { + self.0.len() } } -#[cfg(not(Py_LIMITED_API))] -mod impl_ { - use super::*; +#[cfg(feature = "gil-refs")] +impl<'py> IntoIterator for &'py PyFrozenSet { + type Item = &'py PyAny; + type IntoIter = PyFrozenSetIterator<'py>; + /// Returns an iterator of values in this set. + fn into_iter(self) -> Self::IntoIter { + PyFrozenSetIterator(BoundFrozenSetIterator::new(self.as_borrowed().to_owned())) + } +} - impl<'a> std::iter::IntoIterator for &'a PyFrozenSet { - type Item = &'a PyAny; - type IntoIter = PyFrozenSetIterator<'a>; +impl<'py> IntoIterator for Bound<'py, PyFrozenSet> { + type Item = Bound<'py, PyAny>; + type IntoIter = BoundFrozenSetIterator<'py>; - fn into_iter(self) -> Self::IntoIter { - PyFrozenSetIterator { set: self, pos: 0 } - } + /// Returns an iterator of values in this set. + fn into_iter(self) -> Self::IntoIter { + BoundFrozenSetIterator::new(self) } +} - /// PyO3 implementation of an iterator for a Python `frozenset` object. - pub struct PyFrozenSetIterator<'py> { - set: &'py PyFrozenSet, - pos: ffi::Py_ssize_t, +impl<'py> IntoIterator for &Bound<'py, PyFrozenSet> { + type Item = Bound<'py, PyAny>; + type IntoIter = BoundFrozenSetIterator<'py>; + + /// Returns an iterator of values in this set. + fn into_iter(self) -> Self::IntoIter { + self.iter() } +} - impl<'py> Iterator for PyFrozenSetIterator<'py> { - type Item = &'py PyAny; +/// PyO3 implementation of an iterator for a Python `frozenset` object. +pub struct BoundFrozenSetIterator<'p> { + it: Bound<'p, PyIterator>, + // Remaining elements in the frozenset + remaining: usize, +} - #[inline] - fn next(&mut self) -> Option { - unsafe { - let mut key: *mut ffi::PyObject = std::ptr::null_mut(); - let mut hash: ffi::Py_hash_t = 0; - if ffi::_PySet_NextEntry(self.set.as_ptr(), &mut self.pos, &mut key, &mut hash) != 0 - { - // _PySet_NextEntry returns borrowed object; for safety must make owned (see #890) - Some(self.set.py().from_owned_ptr(ffi::_Py_NewRef(key))) - } else { - None - } - } +impl<'py> BoundFrozenSetIterator<'py> { + pub(super) fn new(set: Bound<'py, PyFrozenSet>) -> Self { + Self { + it: PyIterator::from_bound_object(&set).unwrap(), + remaining: set.len(), } + } +} - #[inline] - fn size_hint(&self) -> (usize, Option) { - let len = self.len(); - (len, Some(len)) - } +impl<'py> Iterator for BoundFrozenSetIterator<'py> { + type Item = Bound<'py, super::PyAny>; + + /// Advances the iterator and returns the next value. + fn next(&mut self) -> Option { + self.remaining = self.remaining.saturating_sub(1); + self.it.next().map(Result::unwrap) } - impl<'py> ExactSizeIterator for PyFrozenSetIterator<'py> { - fn len(&self) -> usize { - self.set.len().saturating_sub(self.pos as usize) - } + fn size_hint(&self) -> (usize, Option) { + (self.remaining, Some(self.remaining)) } } -pub use impl_::*; +impl<'py> ExactSizeIterator for BoundFrozenSetIterator<'py> { + fn len(&self) -> usize { + self.remaining + } +} #[inline] pub(crate) fn new_from_iter( py: Python<'_>, elements: impl IntoIterator, -) -> PyResult> { - fn inner( - py: Python<'_>, +) -> PyResult> { + fn inner<'py>( + py: Python<'py>, elements: &mut dyn Iterator, - ) -> PyResult> { - let set: Py = unsafe { + ) -> PyResult> { + let set = unsafe { // We create the `Py` pointer because its Drop cleans up the set if user code panics. - Py::from_owned_ptr_or_err(py, ffi::PyFrozenSet_New(std::ptr::null_mut()))? + ffi::PyFrozenSet_New(std::ptr::null_mut()) + .assume_owned_or_err(py)? + .downcast_into_unchecked() }; let ptr = set.as_ptr(); @@ -230,26 +331,27 @@ mod tests { #[test] fn test_frozenset_new_and_len() { Python::with_gil(|py| { - let set = PyFrozenSet::new(py, &[1]).unwrap(); + let set = PyFrozenSet::new_bound(py, &[1]).unwrap(); assert_eq!(1, set.len()); let v = vec![1]; - assert!(PyFrozenSet::new(py, &[v]).is_err()); + assert!(PyFrozenSet::new_bound(py, &[v]).is_err()); }); } #[test] fn test_frozenset_empty() { Python::with_gil(|py| { - let set = PyFrozenSet::empty(py).unwrap(); + let set = PyFrozenSet::empty_bound(py).unwrap(); assert_eq!(0, set.len()); + assert!(set.is_empty()); }); } #[test] fn test_frozenset_contains() { Python::with_gil(|py| { - let set = PyFrozenSet::new(py, &[1]).unwrap(); + let set = PyFrozenSet::new_bound(py, &[1]).unwrap(); assert!(set.contains(1).unwrap()); }); } @@ -257,20 +359,40 @@ mod tests { #[test] fn test_frozenset_iter() { Python::with_gil(|py| { - let set = PyFrozenSet::new(py, &[1]).unwrap(); + let set = PyFrozenSet::new_bound(py, &[1]).unwrap(); - // iter method for el in set { assert_eq!(1i32, el.extract::().unwrap()); } + }); + } - // intoiterator iteration - for el in set { + #[test] + fn test_frozenset_iter_bound() { + Python::with_gil(|py| { + let set = PyFrozenSet::new_bound(py, &[1]).unwrap(); + + for el in &set { assert_eq!(1i32, el.extract::().unwrap()); } }); } + #[test] + fn test_frozenset_iter_size_hint() { + Python::with_gil(|py| { + let set = PyFrozenSet::new_bound(py, &[1]).unwrap(); + let mut iter = set.iter(); + + // Exact size + assert_eq!(iter.len(), 1); + assert_eq!(iter.size_hint(), (1, Some(1))); + iter.next(); + assert_eq!(iter.len(), 0); + assert_eq!(iter.size_hint(), (0, Some(0))); + }); + } + #[test] fn test_frozenset_builder() { use super::PyFrozenSetBuilder; @@ -284,7 +406,7 @@ mod tests { builder.add(2).unwrap(); // finalize it - let set = builder.finalize(); + let set = builder.finalize_bound(); assert!(set.contains(1).unwrap()); assert!(set.contains(2).unwrap()); diff --git a/src/types/function.rs b/src/types/function.rs index b4492b58c65..a127b4e0574 100644 --- a/src/types/function.rs +++ b/src/types/function.rs @@ -1,11 +1,17 @@ +#[cfg(feature = "gil-refs")] use crate::derive_utils::PyFunctionArguments; -use crate::methods::PyMethodDefDestructor; -use crate::prelude::*; +use crate::ffi_ptr_ext::FfiPtrExt; +use crate::py_result_ext::PyResultExt; +use crate::types::capsule::PyCapsuleMethods; +use crate::types::module::PyModuleMethods; +#[cfg(feature = "gil-refs")] +use crate::PyNativeType; use crate::{ ffi, - impl_::pymethods::{self, PyMethodDef}, - types::{PyCapsule, PyDict, PyString, PyTuple}, + impl_::pymethods::{self, PyMethodDef, PyMethodDefDestructor}, + types::{PyCapsule, PyDict, PyModule, PyString, PyTuple}, }; +use crate::{Bound, IntoPy, Py, PyAny, PyResult, Python}; use std::cell::UnsafeCell; use std::ffi::CStr; @@ -16,34 +22,94 @@ pub struct PyCFunction(PyAny); pyobject_native_type_core!(PyCFunction, pyobject_native_static_type_object!(ffi::PyCFunction_Type), #checkfunction=ffi::PyCFunction_Check); impl PyCFunction { - /// Create a new built-in function with keywords (*args and/or **kwargs). + /// Deprecated form of [`PyCFunction::new_with_keywords_bound`] + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`PyCFunction::new_with_keywords` will be replaced by `PyCFunction::new_with_keywords_bound` in a future PyO3 version" + )] pub fn new_with_keywords<'a>( fun: ffi::PyCFunctionWithKeywords, name: &'static str, doc: &'static str, py_or_module: PyFunctionArguments<'a>, ) -> PyResult<&'a Self> { + let (py, module) = py_or_module.into_py_and_maybe_module(); Self::internal_new( - &PyMethodDef::cfunction_with_keywords( - name, - pymethods::PyCFunctionWithKeywords(fun), - doc, - ), - py_or_module, + py, + &PyMethodDef::cfunction_with_keywords(name, fun, doc), + module.map(PyNativeType::as_borrowed).as_deref(), ) + .map(Bound::into_gil_ref) } - /// Create a new built-in function which takes no arguments. + /// Create a new built-in function with keywords (*args and/or **kwargs). + pub fn new_with_keywords_bound<'py>( + py: Python<'py>, + fun: ffi::PyCFunctionWithKeywords, + name: &'static str, + doc: &'static str, + module: Option<&Bound<'py, PyModule>>, + ) -> PyResult> { + Self::internal_new( + py, + &PyMethodDef::cfunction_with_keywords(name, fun, doc), + module, + ) + } + + /// Deprecated form of [`PyCFunction::new`] + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`PyCFunction::new` will be replaced by `PyCFunction::new_bound` in a future PyO3 version" + )] pub fn new<'a>( fun: ffi::PyCFunction, name: &'static str, doc: &'static str, py_or_module: PyFunctionArguments<'a>, ) -> PyResult<&'a Self> { + let (py, module) = py_or_module.into_py_and_maybe_module(); Self::internal_new( - &PyMethodDef::noargs(name, pymethods::PyCFunction(fun), doc), - py_or_module, + py, + &PyMethodDef::noargs(name, fun, doc), + module.map(PyNativeType::as_borrowed).as_deref(), ) + .map(Bound::into_gil_ref) + } + + /// Create a new built-in function which takes no arguments. + pub fn new_bound<'py>( + py: Python<'py>, + fun: ffi::PyCFunction, + name: &'static str, + doc: &'static str, + module: Option<&Bound<'py, PyModule>>, + ) -> PyResult> { + Self::internal_new(py, &PyMethodDef::noargs(name, fun, doc), module) + } + + /// Deprecated form of [`PyCFunction::new_closure`] + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`PyCFunction::new_closure` will be replaced by `PyCFunction::new_closure_bound` in a future PyO3 version" + )] + pub fn new_closure<'a, F, R>( + py: Python<'a>, + name: Option<&'static str>, + doc: Option<&'static str>, + closure: F, + ) -> PyResult<&'a PyCFunction> + where + F: Fn(&PyTuple, Option<&PyDict>) -> R + Send + 'static, + R: crate::callback::IntoPyCallbackOutput<*mut ffi::PyObject>, + { + Self::new_closure_bound(py, name, doc, move |args, kwargs| { + closure(args.as_gil_ref(), kwargs.map(Bound::as_gil_ref)) + }) + .map(Bound::into_gil_ref) } /// Create a new function from a closure. @@ -55,32 +121,32 @@ impl PyCFunction { /// # use pyo3::{py_run, types::{PyCFunction, PyDict, PyTuple}}; /// /// Python::with_gil(|py| { - /// let add_one = |args: &PyTuple, _kwargs: Option<&PyDict>| -> PyResult<_> { + /// let add_one = |args: &Bound<'_, PyTuple>, _kwargs: Option<&Bound<'_, PyDict>>| -> PyResult<_> { /// let i = args.extract::<(i64,)>()?.0; /// Ok(i+1) /// }; - /// let add_one = PyCFunction::new_closure(py, None, None, add_one).unwrap(); + /// let add_one = PyCFunction::new_closure_bound(py, None, None, add_one).unwrap(); /// py_run!(py, add_one, "assert add_one(42) == 43"); /// }); /// ``` - pub fn new_closure<'a, F, R>( - py: Python<'a>, + pub fn new_closure_bound<'py, F, R>( + py: Python<'py>, name: Option<&'static str>, doc: Option<&'static str>, closure: F, - ) -> PyResult<&'a PyCFunction> + ) -> PyResult> where - F: Fn(&PyTuple, Option<&PyDict>) -> R + Send + 'static, + F: Fn(&Bound<'_, PyTuple>, Option<&Bound<'_, PyDict>>) -> R + Send + 'static, R: crate::callback::IntoPyCallbackOutput<*mut ffi::PyObject>, { let method_def = pymethods::PyMethodDef::cfunction_with_keywords( name.unwrap_or("pyo3-closure\0"), - pymethods::PyCFunctionWithKeywords(run_closure::), + run_closure::, doc.unwrap_or("\0"), ); let (def, def_destructor) = method_def.as_method_def()?; - let capsule = PyCapsule::new( + let capsule = PyCapsule::new_bound( py, ClosureDestructor:: { closure, @@ -94,20 +160,18 @@ impl PyCFunction { let data = unsafe { capsule.reference::>() }; unsafe { - py.from_owned_ptr_or_err::(ffi::PyCFunction_NewEx( - data.def.get(), - capsule.as_ptr(), - std::ptr::null_mut(), - )) + ffi::PyCFunction_NewEx(data.def.get(), capsule.as_ptr(), std::ptr::null_mut()) + .assume_owned_or_err(py) + .downcast_into_unchecked() } } #[doc(hidden)] pub fn internal_new<'py>( + py: Python<'py>, method_def: &PyMethodDef, - py_or_module: PyFunctionArguments<'py>, - ) -> PyResult<&'py Self> { - let (py, module) = py_or_module.into_py_and_maybe_module(); + module: Option<&Bound<'py, PyModule>>, + ) -> PyResult> { let (mod_ptr, module_name): (_, Option>) = if let Some(m) = module { let mod_ptr = m.as_ptr(); (mod_ptr, Some(m.name()?.into_py(py))) @@ -125,11 +189,9 @@ impl PyCFunction { .map_or(std::ptr::null_mut(), Py::as_ptr); unsafe { - py.from_owned_ptr_or_err::(ffi::PyCFunction_NewEx( - def, - mod_ptr, - module_name_ptr, - )) + ffi::PyCFunction_NewEx(def, mod_ptr, module_name_ptr) + .assume_owned_or_err(py) + .downcast_into_unchecked() } } } @@ -145,9 +207,11 @@ unsafe extern "C" fn run_closure( kwargs: *mut ffi::PyObject, ) -> *mut ffi::PyObject where - F: Fn(&PyTuple, Option<&PyDict>) -> R + Send + 'static, + F: Fn(&Bound<'_, PyTuple>, Option<&Bound<'_, PyDict>>) -> R + Send + 'static, R: crate::callback::IntoPyCallbackOutput<*mut ffi::PyObject>, { + use crate::types::any::PyAnyMethods; + crate::impl_::trampoline::cfunction_with_keywords( capsule_ptr, args, @@ -156,9 +220,12 @@ where let boxed_fn: &ClosureDestructor = &*(ffi::PyCapsule_GetPointer(capsule_ptr, closure_capsule_name().as_ptr()) as *mut ClosureDestructor); - let args = py.from_borrowed_ptr::(args); - let kwargs = py.from_borrowed_ptr_or_opt::(kwargs); - crate::callback::convert(py, (boxed_fn.closure)(args, kwargs)) + let args = Bound::ref_from_ptr(py, &args).downcast_unchecked::(); + let kwargs = Bound::ref_from_ptr_or_opt(py, &kwargs) + .as_ref() + .map(|b| b.downcast_unchecked::()); + let result = (boxed_fn.closure)(args, kwargs); + crate::callback::convert(py, result) }, ) } diff --git a/src/types/iterator.rs b/src/types/iterator.rs index bf392398eb9..1835f484adf 100644 --- a/src/types/iterator.rs +++ b/src/types/iterator.rs @@ -1,8 +1,9 @@ use crate::ffi_ptr_ext::FfiPtrExt; +use crate::instance::Borrowed; use crate::py_result_ext::PyResultExt; -use crate::{ - ffi, AsPyPointer, Bound, PyAny, PyDowncastError, PyErr, PyNativeType, PyResult, PyTypeCheck, -}; +use crate::{ffi, Bound, PyAny, PyErr, PyResult, PyTypeCheck}; +#[cfg(feature = "gil-refs")] +use crate::{AsPyPointer, PyDowncastError, PyNativeType}; /// A Python iterator object. /// @@ -13,10 +14,10 @@ use crate::{ /// /// # fn main() -> PyResult<()> { /// Python::with_gil(|py| -> PyResult<()> { -/// let list = py.eval("iter([1, 2, 3, 4])", None, None)?; +/// let list = py.eval_bound("iter([1, 2, 3, 4])", None, None)?; /// let numbers: PyResult> = list /// .iter()? -/// .map(|i| i.and_then(PyAny::extract::)) +/// .map(|i| i.and_then(|i|i.extract::())) /// .collect(); /// let sum: usize = numbers?.iter().sum(); /// assert_eq!(sum, 10); @@ -30,14 +31,21 @@ pyobject_native_type_named!(PyIterator); pyobject_native_type_extract!(PyIterator); impl PyIterator { - /// Constructs a `PyIterator` from a Python iterable object. - /// - /// Equivalent to Python's built-in `iter` function. + /// Deprecated form of `PyIterator::from_bound_object`. + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`PyIterator::from_object` will be replaced by `PyIterator::from_bound_object` in a future PyO3 version" + )] pub fn from_object(obj: &PyAny) -> PyResult<&PyIterator> { - Self::from_object2(Bound::borrowed_from_gil_ref(&obj)).map(Bound::into_gil_ref) + Self::from_bound_object(&obj.as_borrowed()).map(Bound::into_gil_ref) } - pub(crate) fn from_object2<'py>(obj: &Bound<'py, PyAny>) -> PyResult> { + /// Builds an iterator for an iterable Python object; the equivalent of calling `iter(obj)` in Python. + /// + /// Usually it is more convenient to write [`obj.iter()`][crate::types::any::PyAnyMethods::iter], + /// which is a more concise way of calling this function. + pub fn from_bound_object<'py>(obj: &Bound<'py, PyAny>) -> PyResult> { unsafe { ffi::PyObject_GetIter(obj.as_ptr()) .assume_owned_or_err(obj.py()) @@ -46,6 +54,7 @@ impl PyIterator { } } +#[cfg(feature = "gil-refs")] impl<'p> Iterator for &'p PyIterator { type Item = PyResult<&'p PyAny>; @@ -56,29 +65,69 @@ impl<'p> Iterator for &'p PyIterator { /// Further `next()` calls after an exception occurs are likely /// to repeatedly result in the same exception. fn next(&mut self) -> Option { - let py = self.0.py(); + self.as_borrowed() + .next() + .map(|result| result.map(Bound::into_gil_ref)) + } - match unsafe { py.from_owned_ptr_or_opt(ffi::PyIter_Next(self.0.as_ptr())) } { - Some(obj) => Some(Ok(obj)), - None => PyErr::take(py).map(Err), - } + #[cfg(not(Py_LIMITED_API))] + fn size_hint(&self) -> (usize, Option) { + self.as_borrowed().size_hint() + } +} + +impl<'py> Iterator for Bound<'py, PyIterator> { + type Item = PyResult>; + + /// Retrieves the next item from an iterator. + /// + /// Returns `None` when the iterator is exhausted. + /// If an exception occurs, returns `Some(Err(..))`. + /// Further `next()` calls after an exception occurs are likely + /// to repeatedly result in the same exception. + #[inline] + fn next(&mut self) -> Option { + Borrowed::from(&*self).next() } #[cfg(not(Py_LIMITED_API))] fn size_hint(&self) -> (usize, Option) { - let hint = unsafe { ffi::PyObject_LengthHint(self.0.as_ptr(), 0) }; + let hint = unsafe { ffi::PyObject_LengthHint(self.as_ptr(), 0) }; (hint.max(0) as usize, None) } } +impl<'py> Borrowed<'_, 'py, PyIterator> { + // TODO: this method is on Borrowed so that &'py PyIterator can use this; once that + // implementation is deleted this method should be moved to the `Bound<'py, PyIterator> impl + fn next(self) -> Option>> { + let py = self.py(); + + match unsafe { ffi::PyIter_Next(self.as_ptr()).assume_owned_or_opt(py) } { + Some(obj) => Some(Ok(obj)), + None => PyErr::take(py).map(Err), + } + } +} + +impl<'py> IntoIterator for &Bound<'py, PyIterator> { + type Item = PyResult>; + type IntoIter = Bound<'py, PyIterator>; + + fn into_iter(self) -> Self::IntoIter { + self.clone() + } +} + impl PyTypeCheck for PyIterator { const NAME: &'static str = "Iterator"; - fn type_check(object: &PyAny) -> bool { + fn type_check(object: &Bound<'_, PyAny>) -> bool { unsafe { ffi::PyIter_Check(object.as_ptr()) != 0 } } } +#[cfg(feature = "gil-refs")] #[allow(deprecated)] impl<'v> crate::PyTryFrom<'v> for PyIterator { fn try_from>(value: V) -> Result<&'v PyIterator, PyDowncastError<'v>> { @@ -107,15 +156,14 @@ impl<'v> crate::PyTryFrom<'v> for PyIterator { mod tests { use super::PyIterator; use crate::exceptions::PyTypeError; - use crate::gil::GILPool; - use crate::types::{PyDict, PyList}; - use crate::{Py, PyAny, Python, ToPyObject}; + use crate::types::{PyAnyMethods, PyDict, PyList, PyListMethods}; + use crate::{Python, ToPyObject}; #[test] fn vec_iter() { Python::with_gil(|py| { let obj = vec![10, 20].to_object(py); - let inst = obj.as_ref(py); + let inst = obj.bind(py); let mut it = inst.iter().unwrap(); assert_eq!( 10_i32, @@ -138,7 +186,7 @@ mod tests { }); Python::with_gil(|py| { - let inst = obj.as_ref(py); + let inst = obj.bind(py); let mut it = inst.iter().unwrap(); assert_eq!( @@ -147,7 +195,7 @@ mod tests { ); }); - Python::with_gil(|py| { + Python::with_gil(move |py| { assert_eq!(count, obj.get_refcnt(py)); }); } @@ -156,26 +204,24 @@ mod tests { fn iter_item_refcnt() { Python::with_gil(|py| { let count; - let obj = py.eval("object()", None, None).unwrap(); + let obj = py.eval_bound("object()", None, None).unwrap(); let list = { - let _pool = unsafe { GILPool::new() }; - let list = PyList::empty(py); + let list = PyList::empty_bound(py); list.append(10).unwrap(); - list.append(obj).unwrap(); + list.append(&obj).unwrap(); count = obj.get_refcnt(); list.to_object(py) }; { - let _pool = unsafe { GILPool::new() }; - let inst = list.as_ref(py); + let inst = list.bind(py); let mut it = inst.iter().unwrap(); assert_eq!( 10_i32, it.next().unwrap().unwrap().extract::<'_, i32>().unwrap() ); - assert!(it.next().unwrap().unwrap().is(obj)); + assert!(it.next().unwrap().unwrap().is(&obj)); assert!(it.next().is_none()); } assert_eq!(count, obj.get_refcnt()); @@ -194,10 +240,11 @@ def fibonacci(target): "#; Python::with_gil(|py| { - let context = PyDict::new(py); - py.run(fibonacci_generator, None, Some(context)).unwrap(); + let context = PyDict::new_bound(py); + py.run_bound(fibonacci_generator, None, Some(&context)) + .unwrap(); - let generator = py.eval("fibonacci(5)", None, Some(context)).unwrap(); + let generator = py.eval_bound("fibonacci(5)", None, Some(&context)).unwrap(); for (actual, expected) in generator.iter().unwrap().zip(&[1, 1, 2, 3, 5]) { let actual = actual.unwrap().extract::().unwrap(); assert_eq!(actual, *expected) @@ -205,22 +252,57 @@ def fibonacci(target): }); } + #[test] + fn fibonacci_generator_bound() { + use crate::types::any::PyAnyMethods; + use crate::Bound; + + let fibonacci_generator = r#" +def fibonacci(target): + a = 1 + b = 1 + for _ in range(target): + yield a + a, b = b, a + b +"#; + + Python::with_gil(|py| { + let context = PyDict::new_bound(py); + py.run_bound(fibonacci_generator, None, Some(&context)) + .unwrap(); + + let generator: Bound<'_, PyIterator> = py + .eval_bound("fibonacci(5)", None, Some(&context)) + .unwrap() + .downcast_into() + .unwrap(); + let mut items = vec![]; + for actual in &generator { + let actual = actual.unwrap().extract::().unwrap(); + items.push(actual); + } + assert_eq!(items, [1, 1, 2, 3, 5]); + }); + } + #[test] fn int_not_iterable() { Python::with_gil(|py| { let x = 5.to_object(py); - let err = PyIterator::from_object(x.as_ref(py)).unwrap_err(); + let err = PyIterator::from_bound_object(x.bind(py)).unwrap_err(); assert!(err.is_instance_of::(py)); }); } #[test] - + #[cfg(feature = "gil-refs")] + #[allow(deprecated)] fn iterator_try_from() { Python::with_gil(|py| { - let obj: Py = vec![10, 20].to_object(py).as_ref(py).iter().unwrap().into(); - let iter: &PyIterator = obj.downcast(py).unwrap(); + let obj: crate::Py = + vec![10, 20].to_object(py).as_ref(py).iter().unwrap().into(); + let iter = ::try_from(obj.as_ref(py)).unwrap(); assert!(obj.is(iter)); }); } @@ -237,14 +319,14 @@ def fibonacci(target): #[crate::pymethods(crate = "crate")] impl Downcaster { - fn downcast_iterator(&mut self, obj: &PyAny) { + fn downcast_iterator(&mut self, obj: &crate::Bound<'_, crate::PyAny>) { self.failed = Some(obj.downcast::().unwrap_err().into()); } } // Regression test for 2913 Python::with_gil(|py| { - let downcaster = Py::new(py, Downcaster { failed: None }).unwrap(); + let downcaster = crate::Py::new(py, Downcaster { failed: None }).unwrap(); crate::py_run!( py, downcaster, @@ -276,13 +358,13 @@ def fibonacci(target): #[cfg(feature = "macros")] fn python_class_iterator() { #[crate::pyfunction(crate = "crate")] - fn assert_iterator(obj: &PyAny) { + fn assert_iterator(obj: &crate::Bound<'_, crate::PyAny>) { assert!(obj.downcast::().is_ok()) } // Regression test for 2913 Python::with_gil(|py| { - let assert_iterator = crate::wrap_pyfunction!(assert_iterator, py).unwrap(); + let assert_iterator = crate::wrap_pyfunction_bound!(assert_iterator, py).unwrap(); crate::py_run!( py, assert_iterator, @@ -301,7 +383,7 @@ def fibonacci(target): #[cfg(not(Py_LIMITED_API))] fn length_hint_becomes_size_hint_lower_bound() { Python::with_gil(|py| { - let list = py.eval("[1, 2, 3]", None, None).unwrap(); + let list = py.eval_bound("[1, 2, 3]", None, None).unwrap(); let iter = list.iter().unwrap(); let hint = iter.size_hint(); assert_eq!(hint, (3, None)); diff --git a/src/types/list.rs b/src/types/list.rs index 0e7fc952b00..0d911e03199 100644 --- a/src/types/list.rs +++ b/src/types/list.rs @@ -1,4 +1,3 @@ -use std::convert::TryInto; use std::iter::FusedIterator; use crate::err::{self, PyResult}; @@ -7,6 +6,8 @@ use crate::ffi_ptr_ext::FfiPtrExt; use crate::instance::Borrowed; use crate::internal_tricks::get_ssize_index; use crate::types::{PySequence, PyTuple}; +#[cfg(feature = "gil-refs")] +use crate::PyNativeType; use crate::{Bound, PyAny, PyObject, Python, ToPyObject}; use crate::types::any::PyAnyMethods; @@ -59,7 +60,7 @@ impl PyList { /// Constructs a new list with the given elements. /// /// If you want to create a [`PyList`] with elements of different or unknown types, or from an - /// iterable that doesn't implement [`ExactSizeIterator`], use [`PyList::append`]. + /// iterable that doesn't implement [`ExactSizeIterator`], use [`PyListMethods::append`]. /// /// # Examples /// @@ -70,7 +71,7 @@ impl PyList { /// # fn main() { /// Python::with_gil(|py| { /// let elements: Vec = vec![0, 1, 2, 3, 4, 5]; - /// let list: &PyList = PyList::new(py, elements); + /// let list = PyList::new_bound(py, elements); /// assert_eq!(format!("{:?}", list), "[0, 1, 2, 3, 4, 5]"); /// }); /// # } @@ -82,28 +83,63 @@ impl PyList { /// All standard library structures implement this trait correctly, if they do, so calling this /// function with (for example) [`Vec`]`` or `&[T]` will always succeed. #[track_caller] - pub fn new(py: Python<'_>, elements: impl IntoIterator) -> &PyList + pub fn new_bound( + py: Python<'_>, + elements: impl IntoIterator, + ) -> Bound<'_, PyList> where T: ToPyObject, U: ExactSizeIterator, { let mut iter = elements.into_iter().map(|e| e.to_object(py)); - new_from_iter(py, &mut iter).into_gil_ref() + new_from_iter(py, &mut iter) } /// Constructs a new empty list. + pub fn empty_bound(py: Python<'_>) -> Bound<'_, PyList> { + unsafe { + ffi::PyList_New(0) + .assume_owned(py) + .downcast_into_unchecked() + } + } +} + +#[cfg(feature = "gil-refs")] +impl PyList { + /// Deprecated form of [`PyList::new_bound`]. + #[inline] + #[track_caller] + #[deprecated( + since = "0.21.0", + note = "`PyList::new` will be replaced by `PyList::new_bound` in a future PyO3 version" + )] + pub fn new(py: Python<'_>, elements: impl IntoIterator) -> &PyList + where + T: ToPyObject, + U: ExactSizeIterator, + { + Self::new_bound(py, elements).into_gil_ref() + } + + /// Deprecated form of [`PyList::empty_bound`]. + #[inline] + #[deprecated( + since = "0.21.0", + note = "`PyList::empty` will be replaced by `PyList::empty_bound` in a future PyO3 version" + )] pub fn empty(py: Python<'_>) -> &PyList { - unsafe { py.from_owned_ptr(ffi::PyList_New(0)) } + Self::empty_bound(py).into_gil_ref() } /// Returns the length of the list. pub fn len(&self) -> usize { - Bound::borrowed_from_gil_ref(&self).len() + self.as_borrowed().len() } /// Checks if the list is empty. pub fn is_empty(&self) -> bool { - Bound::borrowed_from_gil_ref(&self).is_empty() + self.as_borrowed().is_empty() } /// Returns `self` cast as a `PySequence`. @@ -116,15 +152,13 @@ impl PyList { /// ``` /// use pyo3::{prelude::*, types::PyList}; /// Python::with_gil(|py| { - /// let list = PyList::new(py, [2, 3, 5, 7]); + /// let list = PyList::new_bound(py, [2, 3, 5, 7]); /// let obj = list.get_item(0); /// assert_eq!(obj.unwrap().extract::().unwrap(), 2); /// }); /// ``` pub fn get_item(&self, index: usize) -> PyResult<&PyAny> { - Bound::borrowed_from_gil_ref(&self) - .get_item(index) - .map(Bound::into_gil_ref) + self.as_borrowed().get_item(index).map(Bound::into_gil_ref) } /// Gets the list item at the specified index. Undefined behavior on bad index. Use with caution. @@ -134,9 +168,7 @@ impl PyList { /// Caller must verify that the index is within the bounds of the list. #[cfg(not(Py_LIMITED_API))] pub unsafe fn get_item_unchecked(&self, index: usize) -> &PyAny { - Bound::borrowed_from_gil_ref(&self) - .get_item_unchecked(index) - .into_gil_ref() + self.as_borrowed().get_item_unchecked(index).into_gil_ref() } /// Takes the slice `self[low:high]` and returns it as a new list. @@ -144,9 +176,7 @@ impl PyList { /// Indices must be nonnegative, and out-of-range indices are clipped to /// `self.len()`. pub fn get_slice(&self, low: usize, high: usize) -> &PyList { - Bound::borrowed_from_gil_ref(&self) - .get_slice(low, high) - .into_gil_ref() + self.as_borrowed().get_slice(low, high).into_gil_ref() } /// Sets the item at the specified index. @@ -156,7 +186,7 @@ impl PyList { where I: ToPyObject, { - Bound::borrowed_from_gil_ref(&self).set_item(index, item) + self.as_borrowed().set_item(index, item) } /// Deletes the `index`th element of self. @@ -164,7 +194,7 @@ impl PyList { /// This is equivalent to the Python statement `del self[i]`. #[inline] pub fn del_item(&self, index: usize) -> PyResult<()> { - Bound::borrowed_from_gil_ref(&self).del_item(index) + self.as_borrowed().del_item(index) } /// Assigns the sequence `seq` to the slice of `self` from `low` to `high`. @@ -172,7 +202,7 @@ impl PyList { /// This is equivalent to the Python statement `self[low:high] = v`. #[inline] pub fn set_slice(&self, low: usize, high: usize, seq: &PyAny) -> PyResult<()> { - Bound::borrowed_from_gil_ref(&self).set_slice(low, high, Bound::borrowed_from_gil_ref(&seq)) + self.as_borrowed().set_slice(low, high, &seq.as_borrowed()) } /// Deletes the slice from `low` to `high` from `self`. @@ -180,7 +210,7 @@ impl PyList { /// This is equivalent to the Python statement `del self[low:high]`. #[inline] pub fn del_slice(&self, low: usize, high: usize) -> PyResult<()> { - Bound::borrowed_from_gil_ref(&self).del_slice(low, high) + self.as_borrowed().del_slice(low, high) } /// Appends an item to the list. @@ -188,7 +218,7 @@ impl PyList { where I: ToPyObject, { - Bound::borrowed_from_gil_ref(&self).append(item) + self.as_borrowed().append(item) } /// Inserts an item at the specified index. @@ -198,7 +228,7 @@ impl PyList { where I: ToPyObject, { - Bound::borrowed_from_gil_ref(&self).insert(index, item) + self.as_borrowed().insert(index, item) } /// Determines if self contains `value`. @@ -209,7 +239,7 @@ impl PyList { where V: ToPyObject, { - Bound::borrowed_from_gil_ref(&self).contains(value) + self.as_borrowed().contains(value) } /// Returns the first index `i` for which `self[i] == value`. @@ -220,34 +250,33 @@ impl PyList { where V: ToPyObject, { - Bound::borrowed_from_gil_ref(&self).index(value) + self.as_borrowed().index(value) } /// Returns an iterator over this list's items. pub fn iter(&self) -> PyListIterator<'_> { - PyListIterator(Bound::borrowed_from_gil_ref(&self).iter()) + PyListIterator(self.as_borrowed().iter()) } /// Sorts the list in-place. Equivalent to the Python expression `l.sort()`. pub fn sort(&self) -> PyResult<()> { - Bound::borrowed_from_gil_ref(&self).sort() + self.as_borrowed().sort() } /// Reverses the list in-place. Equivalent to the Python expression `l.reverse()`. pub fn reverse(&self) -> PyResult<()> { - Bound::borrowed_from_gil_ref(&self).reverse() + self.as_borrowed().reverse() } /// Return a new tuple containing the contents of the list; equivalent to the Python expression `tuple(list)`. /// /// This method is equivalent to `self.as_sequence().to_tuple()` and faster than `PyTuple::new(py, this_list)`. pub fn to_tuple(&self) -> &PyTuple { - Bound::borrowed_from_gil_ref(&self) - .to_tuple() - .into_gil_ref() + self.as_borrowed().to_tuple().into_gil_ref() } } +#[cfg(feature = "gil-refs")] index_impls!(PyList, "list", PyList::len, PyList::get_slice); /// Implementation of functionality for [`PyList`]. @@ -256,7 +285,7 @@ index_impls!(PyList, "list", PyList::len, PyList::get_slice); /// syntax these methods are separated into a trait, because stable Rust does not yet support /// `arbitrary_self_types`. #[doc(alias = "PyList")] -pub trait PyListMethods<'py> { +pub trait PyListMethods<'py>: crate::sealed::Sealed { /// Returns the length of the list. fn len(&self) -> usize; @@ -266,12 +295,15 @@ pub trait PyListMethods<'py> { /// Returns `self` cast as a `PySequence`. fn as_sequence(&self) -> &Bound<'py, PySequence>; + /// Returns `self` cast as a `PySequence`. + fn into_sequence(self) -> Bound<'py, PySequence>; + /// Gets the list item at the specified index. /// # Example /// ``` /// use pyo3::{prelude::*, types::PyList}; /// Python::with_gil(|py| { - /// let list = PyList::new(py, [2, 3, 5, 7]); + /// let list = PyList::new_bound(py, [2, 3, 5, 7]); /// let obj = list.get_item(0); /// assert_eq!(obj.unwrap().extract::().unwrap(), 2); /// }); @@ -379,12 +411,17 @@ impl<'py> PyListMethods<'py> for Bound<'py, PyList> { unsafe { self.downcast_unchecked() } } + /// Returns `self` cast as a `PySequence`. + fn into_sequence(self) -> Bound<'py, PySequence> { + unsafe { self.into_any().downcast_into_unchecked() } + } + /// Gets the list item at the specified index. /// # Example /// ``` /// use pyo3::{prelude::*, types::PyList}; /// Python::with_gil(|py| { - /// let list = PyList::new(py, [2, 3, 5, 7]); + /// let list = PyList::new_bound(py, [2, 3, 5, 7]); /// let obj = list.get_item(0); /// assert_eq!(obj.unwrap().extract::().unwrap(), 2); /// }); @@ -553,8 +590,10 @@ impl<'py> PyListMethods<'py> for Bound<'py, PyList> { } /// Used by `PyList::iter()`. +#[cfg(feature = "gil-refs")] pub struct PyListIterator<'a>(BoundListIterator<'a>); +#[cfg(feature = "gil-refs")] impl<'a> Iterator for PyListIterator<'a> { type Item = &'a PyAny; @@ -569,6 +608,7 @@ impl<'a> Iterator for PyListIterator<'a> { } } +#[cfg(feature = "gil-refs")] impl<'a> DoubleEndedIterator for PyListIterator<'a> { #[inline] fn next_back(&mut self) -> Option { @@ -576,14 +616,17 @@ impl<'a> DoubleEndedIterator for PyListIterator<'a> { } } +#[cfg(feature = "gil-refs")] impl<'a> ExactSizeIterator for PyListIterator<'a> { fn len(&self) -> usize { self.0.len() } } +#[cfg(feature = "gil-refs")] impl FusedIterator for PyListIterator<'_> {} +#[cfg(feature = "gil-refs")] impl<'a> IntoIterator for &'a PyList { type Item = &'a PyAny; type IntoIter = PyListIterator<'a>; @@ -665,26 +708,29 @@ impl ExactSizeIterator for BoundListIterator<'_> { impl FusedIterator for BoundListIterator<'_> {} -impl<'a, 'py> IntoIterator for &'a Bound<'py, PyList> { +impl<'py> IntoIterator for Bound<'py, PyList> { type Item = Bound<'py, PyAny>; type IntoIter = BoundListIterator<'py>; fn into_iter(self) -> Self::IntoIter { - self.iter() + BoundListIterator::new(self) } } -impl<'py> IntoIterator for Bound<'py, PyList> { +impl<'py> IntoIterator for &Bound<'py, PyList> { type Item = Bound<'py, PyAny>; type IntoIter = BoundListIterator<'py>; fn into_iter(self) -> Self::IntoIter { - BoundListIterator::new(self) + self.iter() } } #[cfg(test)] mod tests { + use crate::types::any::PyAnyMethods; + use crate::types::list::PyListMethods; + use crate::types::sequence::PySequenceMethods; use crate::types::{PyList, PyTuple}; use crate::Python; use crate::{IntoPy, PyObject, ToPyObject}; @@ -692,18 +738,18 @@ mod tests { #[test] fn test_new() { Python::with_gil(|py| { - let list = PyList::new(py, [2, 3, 5, 7]); - assert_eq!(2, list[0].extract::().unwrap()); - assert_eq!(3, list[1].extract::().unwrap()); - assert_eq!(5, list[2].extract::().unwrap()); - assert_eq!(7, list[3].extract::().unwrap()); + let list = PyList::new_bound(py, [2, 3, 5, 7]); + assert_eq!(2, list.get_item(0).unwrap().extract::().unwrap()); + assert_eq!(3, list.get_item(1).unwrap().extract::().unwrap()); + assert_eq!(5, list.get_item(2).unwrap().extract::().unwrap()); + assert_eq!(7, list.get_item(3).unwrap().extract::().unwrap()); }); } #[test] fn test_len() { Python::with_gil(|py| { - let list = PyList::new(py, [1, 2, 3, 4]); + let list = PyList::new_bound(py, [1, 2, 3, 4]); assert_eq!(4, list.len()); }); } @@ -711,7 +757,7 @@ mod tests { #[test] fn test_get_item() { Python::with_gil(|py| { - let list = PyList::new(py, [2, 3, 5, 7]); + let list = PyList::new_bound(py, [2, 3, 5, 7]); assert_eq!(2, list.get_item(0).unwrap().extract::().unwrap()); assert_eq!(3, list.get_item(1).unwrap().extract::().unwrap()); assert_eq!(5, list.get_item(2).unwrap().extract::().unwrap()); @@ -722,7 +768,7 @@ mod tests { #[test] fn test_get_slice() { Python::with_gil(|py| { - let list = PyList::new(py, [2, 3, 5, 7]); + let list = PyList::new_bound(py, [2, 3, 5, 7]); let slice = list.get_slice(1, 3); assert_eq!(2, slice.len()); let slice = list.get_slice(1, 7); @@ -733,12 +779,12 @@ mod tests { #[test] fn test_set_item() { Python::with_gil(|py| { - let list = PyList::new(py, [2, 3, 5, 7]); + let list = PyList::new_bound(py, [2, 3, 5, 7]); let val = 42i32.to_object(py); let val2 = 42i32.to_object(py); - assert_eq!(2, list[0].extract::().unwrap()); + assert_eq!(2, list.get_item(0).unwrap().extract::().unwrap()); list.set_item(0, val).unwrap(); - assert_eq!(42, list[0].extract::().unwrap()); + assert_eq!(42, list.get_item(0).unwrap().extract::().unwrap()); assert!(list.set_item(10, val2).is_err()); }); } @@ -746,15 +792,14 @@ mod tests { #[test] fn test_set_item_refcnt() { Python::with_gil(|py| { - let obj = py.eval("object()", None, None).unwrap(); + let obj = py.eval_bound("object()", None, None).unwrap(); let cnt; { - let _pool = unsafe { crate::GILPool::new() }; let v = vec![2]; let ob = v.to_object(py); - let list: &PyList = ob.downcast(py).unwrap(); + let list = ob.downcast_bound::(py).unwrap(); cnt = obj.get_refcnt(); - list.set_item(0, obj).unwrap(); + list.set_item(0, &obj).unwrap(); } assert_eq!(cnt, obj.get_refcnt()); @@ -764,17 +809,17 @@ mod tests { #[test] fn test_insert() { Python::with_gil(|py| { - let list = PyList::new(py, [2, 3, 5, 7]); + let list = PyList::new_bound(py, [2, 3, 5, 7]); let val = 42i32.to_object(py); let val2 = 43i32.to_object(py); assert_eq!(4, list.len()); - assert_eq!(2, list[0].extract::().unwrap()); + assert_eq!(2, list.get_item(0).unwrap().extract::().unwrap()); list.insert(0, val).unwrap(); list.insert(1000, val2).unwrap(); assert_eq!(6, list.len()); - assert_eq!(42, list[0].extract::().unwrap()); - assert_eq!(2, list[1].extract::().unwrap()); - assert_eq!(43, list[5].extract::().unwrap()); + assert_eq!(42, list.get_item(0).unwrap().extract::().unwrap()); + assert_eq!(2, list.get_item(1).unwrap().extract::().unwrap()); + assert_eq!(43, list.get_item(5).unwrap().extract::().unwrap()); }); } @@ -782,12 +827,11 @@ mod tests { fn test_insert_refcnt() { Python::with_gil(|py| { let cnt; - let obj = py.eval("object()", None, None).unwrap(); + let obj = py.eval_bound("object()", None, None).unwrap(); { - let _pool = unsafe { crate::GILPool::new() }; - let list = PyList::empty(py); + let list = PyList::empty_bound(py); cnt = obj.get_refcnt(); - list.insert(0, obj).unwrap(); + list.insert(0, &obj).unwrap(); } assert_eq!(cnt, obj.get_refcnt()); @@ -797,10 +841,10 @@ mod tests { #[test] fn test_append() { Python::with_gil(|py| { - let list = PyList::new(py, [2]); + let list = PyList::new_bound(py, [2]); list.append(3).unwrap(); - assert_eq!(2, list[0].extract::().unwrap()); - assert_eq!(3, list[1].extract::().unwrap()); + assert_eq!(2, list.get_item(0).unwrap().extract::().unwrap()); + assert_eq!(3, list.get_item(1).unwrap().extract::().unwrap()); }); } @@ -808,12 +852,11 @@ mod tests { fn test_append_refcnt() { Python::with_gil(|py| { let cnt; - let obj = py.eval("object()", None, None).unwrap(); + let obj = py.eval_bound("object()", None, None).unwrap(); { - let _pool = unsafe { crate::GILPool::new() }; - let list = PyList::empty(py); + let list = PyList::empty_bound(py); cnt = obj.get_refcnt(); - list.append(obj).unwrap(); + list.append(&obj).unwrap(); } assert_eq!(cnt, obj.get_refcnt()); }); @@ -823,7 +866,7 @@ mod tests { fn test_iter() { Python::with_gil(|py| { let v = vec![2, 3, 5, 7]; - let list = PyList::new(py, &v); + let list = PyList::new_bound(py, &v); let mut idx = 0; for el in list { assert_eq!(v[idx], el.extract::().unwrap()); @@ -838,7 +881,7 @@ mod tests { Python::with_gil(|py| { let v = vec![2, 3, 5, 7]; let ob = v.to_object(py); - let list: &PyList = ob.downcast(py).unwrap(); + let list = ob.downcast_bound::(py).unwrap(); let mut iter = list.iter(); assert_eq!(iter.size_hint(), (v.len(), Some(v.len()))); @@ -857,7 +900,7 @@ mod tests { Python::with_gil(|py| { let v = vec![2, 3, 5, 7]; let ob = v.to_object(py); - let list: &PyList = ob.downcast(py).unwrap(); + let list = ob.downcast_bound::(py).unwrap(); let mut iter = list.iter().rev(); @@ -883,18 +926,61 @@ mod tests { #[test] fn test_into_iter() { Python::with_gil(|py| { - let list = PyList::new(py, [1, 2, 3, 4]); + let list = PyList::new_bound(py, [1, 2, 3, 4]); for (i, item) in list.iter().enumerate() { assert_eq!((i + 1) as i32, item.extract::().unwrap()); } }); } + #[test] + fn test_into_iter_bound() { + use crate::types::any::PyAnyMethods; + + Python::with_gil(|py| { + let list = PyList::new_bound(py, [1, 2, 3, 4]); + let mut items = vec![]; + for item in &list { + items.push(item.extract::().unwrap()); + } + assert_eq!(items, vec![1, 2, 3, 4]); + }); + } + + #[test] + fn test_as_sequence() { + Python::with_gil(|py| { + let list = PyList::new_bound(py, [1, 2, 3, 4]); + + assert_eq!(list.as_sequence().len().unwrap(), 4); + assert_eq!( + list.as_sequence() + .get_item(1) + .unwrap() + .extract::() + .unwrap(), + 2 + ); + }); + } + + #[test] + fn test_into_sequence() { + Python::with_gil(|py| { + let list = PyList::new_bound(py, [1, 2, 3, 4]); + + let sequence = list.into_sequence(); + + assert_eq!(sequence.len().unwrap(), 4); + assert_eq!(sequence.get_item(1).unwrap().extract::().unwrap(), 2); + }); + } + #[test] fn test_extract() { Python::with_gil(|py| { let v = vec![2, 3, 5, 7]; - let list = PyList::new(py, &v); + let list = PyList::new_bound(py, &v); let v2 = list.as_ref().extract::>().unwrap(); assert_eq!(v, v2); }); @@ -904,16 +990,16 @@ mod tests { fn test_sort() { Python::with_gil(|py| { let v = vec![7, 3, 2, 5]; - let list = PyList::new(py, &v); - assert_eq!(7, list[0].extract::().unwrap()); - assert_eq!(3, list[1].extract::().unwrap()); - assert_eq!(2, list[2].extract::().unwrap()); - assert_eq!(5, list[3].extract::().unwrap()); + let list = PyList::new_bound(py, &v); + assert_eq!(7, list.get_item(0).unwrap().extract::().unwrap()); + assert_eq!(3, list.get_item(1).unwrap().extract::().unwrap()); + assert_eq!(2, list.get_item(2).unwrap().extract::().unwrap()); + assert_eq!(5, list.get_item(3).unwrap().extract::().unwrap()); list.sort().unwrap(); - assert_eq!(2, list[0].extract::().unwrap()); - assert_eq!(3, list[1].extract::().unwrap()); - assert_eq!(5, list[2].extract::().unwrap()); - assert_eq!(7, list[3].extract::().unwrap()); + assert_eq!(2, list.get_item(0).unwrap().extract::().unwrap()); + assert_eq!(3, list.get_item(1).unwrap().extract::().unwrap()); + assert_eq!(5, list.get_item(2).unwrap().extract::().unwrap()); + assert_eq!(7, list.get_item(3).unwrap().extract::().unwrap()); }); } @@ -921,16 +1007,16 @@ mod tests { fn test_reverse() { Python::with_gil(|py| { let v = vec![2, 3, 5, 7]; - let list = PyList::new(py, &v); - assert_eq!(2, list[0].extract::().unwrap()); - assert_eq!(3, list[1].extract::().unwrap()); - assert_eq!(5, list[2].extract::().unwrap()); - assert_eq!(7, list[3].extract::().unwrap()); + let list = PyList::new_bound(py, &v); + assert_eq!(2, list.get_item(0).unwrap().extract::().unwrap()); + assert_eq!(3, list.get_item(1).unwrap().extract::().unwrap()); + assert_eq!(5, list.get_item(2).unwrap().extract::().unwrap()); + assert_eq!(7, list.get_item(3).unwrap().extract::().unwrap()); list.reverse().unwrap(); - assert_eq!(7, list[0].extract::().unwrap()); - assert_eq!(5, list[1].extract::().unwrap()); - assert_eq!(3, list[2].extract::().unwrap()); - assert_eq!(2, list[3].extract::().unwrap()); + assert_eq!(7, list.get_item(0).unwrap().extract::().unwrap()); + assert_eq!(5, list.get_item(1).unwrap().extract::().unwrap()); + assert_eq!(3, list.get_item(2).unwrap().extract::().unwrap()); + assert_eq!(2, list.get_item(3).unwrap().extract::().unwrap()); }); } @@ -938,16 +1024,16 @@ mod tests { fn test_array_into_py() { Python::with_gil(|py| { let array: PyObject = [1, 2].into_py(py); - let list: &PyList = array.downcast(py).unwrap(); - assert_eq!(1, list[0].extract::().unwrap()); - assert_eq!(2, list[1].extract::().unwrap()); + let list = array.downcast_bound::(py).unwrap(); + assert_eq!(1, list.get_item(0).unwrap().extract::().unwrap()); + assert_eq!(2, list.get_item(1).unwrap().extract::().unwrap()); }); } #[test] fn test_list_get_item_invalid_index() { Python::with_gil(|py| { - let list = PyList::new(py, [2, 3, 5, 7]); + let list = PyList::new_bound(py, [2, 3, 5, 7]); let obj = list.get_item(5); assert!(obj.is_err()); assert_eq!( @@ -960,7 +1046,7 @@ mod tests { #[test] fn test_list_get_item_sanity() { Python::with_gil(|py| { - let list = PyList::new(py, [2, 3, 5, 7]); + let list = PyList::new_bound(py, [2, 3, 5, 7]); let obj = list.get_item(0); assert_eq!(obj.unwrap().extract::().unwrap(), 2); }); @@ -970,13 +1056,15 @@ mod tests { #[test] fn test_list_get_item_unchecked_sanity() { Python::with_gil(|py| { - let list = PyList::new(py, [2, 3, 5, 7]); + let list = PyList::new_bound(py, [2, 3, 5, 7]); let obj = unsafe { list.get_item_unchecked(0) }; assert_eq!(obj.extract::().unwrap(), 2); }); } #[test] + #[cfg(feature = "gil-refs")] + #[allow(deprecated)] fn test_list_index_trait() { Python::with_gil(|py| { let list = PyList::new(py, [2, 3, 5]); @@ -988,6 +1076,8 @@ mod tests { #[test] #[should_panic] + #[cfg(feature = "gil-refs")] + #[allow(deprecated)] fn test_list_index_trait_panic() { Python::with_gil(|py| { let list = PyList::new(py, [2, 3, 5]); @@ -996,6 +1086,8 @@ mod tests { } #[test] + #[cfg(feature = "gil-refs")] + #[allow(deprecated)] fn test_list_index_trait_ranges() { Python::with_gil(|py| { let list = PyList::new(py, [2, 3, 5]); @@ -1012,6 +1104,8 @@ mod tests { #[test] #[should_panic = "range start index 5 out of range for list of length 3"] + #[cfg(feature = "gil-refs")] + #[allow(deprecated)] fn test_list_index_trait_range_panic_start() { Python::with_gil(|py| { let list = PyList::new(py, [2, 3, 5]); @@ -1021,6 +1115,8 @@ mod tests { #[test] #[should_panic = "range end index 10 out of range for list of length 3"] + #[cfg(feature = "gil-refs")] + #[allow(deprecated)] fn test_list_index_trait_range_panic_end() { Python::with_gil(|py| { let list = PyList::new(py, [2, 3, 5]); @@ -1030,6 +1126,8 @@ mod tests { #[test] #[should_panic = "slice index starts at 2 but ends at 1"] + #[cfg(feature = "gil-refs")] + #[allow(deprecated)] fn test_list_index_trait_range_panic_wrong_order() { Python::with_gil(|py| { let list = PyList::new(py, [2, 3, 5]); @@ -1040,6 +1138,8 @@ mod tests { #[test] #[should_panic = "range start index 8 out of range for list of length 3"] + #[cfg(feature = "gil-refs")] + #[allow(deprecated)] fn test_list_index_trait_range_from_panic() { Python::with_gil(|py| { let list = PyList::new(py, [2, 3, 5]); @@ -1050,19 +1150,19 @@ mod tests { #[test] fn test_list_del_item() { Python::with_gil(|py| { - let list = PyList::new(py, [1, 1, 2, 3, 5, 8]); + let list = PyList::new_bound(py, [1, 1, 2, 3, 5, 8]); assert!(list.del_item(10).is_err()); - assert_eq!(1, list[0].extract::().unwrap()); + assert_eq!(1, list.get_item(0).unwrap().extract::().unwrap()); assert!(list.del_item(0).is_ok()); - assert_eq!(1, list[0].extract::().unwrap()); + assert_eq!(1, list.get_item(0).unwrap().extract::().unwrap()); assert!(list.del_item(0).is_ok()); - assert_eq!(2, list[0].extract::().unwrap()); + assert_eq!(2, list.get_item(0).unwrap().extract::().unwrap()); assert!(list.del_item(0).is_ok()); - assert_eq!(3, list[0].extract::().unwrap()); + assert_eq!(3, list.get_item(0).unwrap().extract::().unwrap()); assert!(list.del_item(0).is_ok()); - assert_eq!(5, list[0].extract::().unwrap()); + assert_eq!(5, list.get_item(0).unwrap().extract::().unwrap()); assert!(list.del_item(0).is_ok()); - assert_eq!(8, list[0].extract::().unwrap()); + assert_eq!(8, list.get_item(0).unwrap().extract::().unwrap()); assert!(list.del_item(0).is_ok()); assert_eq!(0, list.len()); assert!(list.del_item(0).is_err()); @@ -1072,11 +1172,11 @@ mod tests { #[test] fn test_list_set_slice() { Python::with_gil(|py| { - let list = PyList::new(py, [1, 1, 2, 3, 5, 8]); - let ins = PyList::new(py, [7, 4]); - list.set_slice(1, 4, ins).unwrap(); + let list = PyList::new_bound(py, [1, 1, 2, 3, 5, 8]); + let ins = PyList::new_bound(py, [7, 4]); + list.set_slice(1, 4, &ins).unwrap(); assert_eq!([1, 7, 4, 5, 8], list.extract::<[i32; 5]>().unwrap()); - list.set_slice(3, 100, PyList::empty(py)).unwrap(); + list.set_slice(3, 100, &PyList::empty_bound(py)).unwrap(); assert_eq!([1, 7, 4], list.extract::<[i32; 3]>().unwrap()); }); } @@ -1084,7 +1184,7 @@ mod tests { #[test] fn test_list_del_slice() { Python::with_gil(|py| { - let list = PyList::new(py, [1, 1, 2, 3, 5, 8]); + let list = PyList::new_bound(py, [1, 1, 2, 3, 5, 8]); list.del_slice(1, 4).unwrap(); assert_eq!([1, 5, 8], list.extract::<[i32; 3]>().unwrap()); list.del_slice(1, 100).unwrap(); @@ -1095,7 +1195,7 @@ mod tests { #[test] fn test_list_contains() { Python::with_gil(|py| { - let list = PyList::new(py, [1, 1, 2, 3, 5, 8]); + let list = PyList::new_bound(py, [1, 1, 2, 3, 5, 8]); assert_eq!(6, list.len()); let bad_needle = 7i32.to_object(py); @@ -1112,7 +1212,7 @@ mod tests { #[test] fn test_list_index() { Python::with_gil(|py| { - let list = PyList::new(py, [1, 1, 2, 3, 5, 8]); + let list = PyList::new_bound(py, [1, 1, 2, 3, 5, 8]); assert_eq!(0, list.index(1i32).unwrap()); assert_eq!(2, list.index(2i32).unwrap()); assert_eq!(3, list.index(3i32).unwrap()); @@ -1149,7 +1249,7 @@ mod tests { fn too_long_iterator() { Python::with_gil(|py| { let iter = FaultyIter(0..usize::MAX, 73); - let _list = PyList::new(py, iter); + let _list = PyList::new_bound(py, iter); }) } @@ -1160,7 +1260,7 @@ mod tests { fn too_short_iterator() { Python::with_gil(|py| { let iter = FaultyIter(0..35, 73); - let _list = PyList::new(py, iter); + let _list = PyList::new_bound(py, iter); }) } @@ -1172,7 +1272,7 @@ mod tests { Python::with_gil(|py| { let iter = FaultyIter(0..0, usize::MAX); - let _list = PyList::new(py, iter); + let _list = PyList::new_bound(py, iter); }) } @@ -1231,7 +1331,7 @@ mod tests { Python::with_gil(|py| { std::panic::catch_unwind(|| { let iter = FaultyIter(0..50, 50); - let _list = PyList::new(py, iter); + let _list = PyList::new_bound(py, iter); }) .unwrap_err(); }); @@ -1246,9 +1346,9 @@ mod tests { #[test] fn test_list_to_tuple() { Python::with_gil(|py| { - let list = PyList::new(py, vec![1, 2, 3]); + let list = PyList::new_bound(py, vec![1, 2, 3]); let tuple = list.to_tuple(); - let tuple_expected = PyTuple::new(py, vec![1, 2, 3]); + let tuple_expected = PyTuple::new_bound(py, vec![1, 2, 3]); assert!(tuple.eq(tuple_expected).unwrap()); }) } diff --git a/src/types/mapping.rs b/src/types/mapping.rs index 4eae0cec7c5..aea2b484c3b 100644 --- a/src/types/mapping.rs +++ b/src/types/mapping.rs @@ -1,4 +1,4 @@ -use crate::err::{PyDowncastError, PyResult}; +use crate::err::PyResult; use crate::ffi_ptr_ext::FfiPtrExt; use crate::instance::Bound; use crate::py_result_ext::PyResultExt; @@ -6,7 +6,9 @@ use crate::sync::GILOnceCell; use crate::type_object::PyTypeInfo; use crate::types::any::PyAnyMethods; use crate::types::{PyAny, PyDict, PySequence, PyType}; -use crate::{ffi, Py, PyNativeType, PyTypeCheck, Python, ToPyObject}; +#[cfg(feature = "gil-refs")] +use crate::{err::PyDowncastError, PyNativeType}; +use crate::{ffi, Py, PyTypeCheck, Python, ToPyObject}; /// Represents a reference to a Python object supporting the mapping protocol. #[repr(transparent)] @@ -14,19 +16,31 @@ pub struct PyMapping(PyAny); pyobject_native_type_named!(PyMapping); pyobject_native_type_extract!(PyMapping); +impl PyMapping { + /// Register a pyclass as a subclass of `collections.abc.Mapping` (from the Python standard + /// library). This is equivalent to `collections.abc.Mapping.register(T)` in Python. + /// This registration is required for a pyclass to be downcastable from `PyAny` to `PyMapping`. + pub fn register(py: Python<'_>) -> PyResult<()> { + let ty = T::type_object_bound(py); + get_mapping_abc(py)?.call_method1("register", (ty,))?; + Ok(()) + } +} + +#[cfg(feature = "gil-refs")] impl PyMapping { /// Returns the number of objects in the mapping. /// /// This is equivalent to the Python expression `len(self)`. #[inline] pub fn len(&self) -> PyResult { - Bound::borrowed_from_gil_ref(&self).len() + self.as_borrowed().len() } /// Returns whether the mapping is empty. #[inline] pub fn is_empty(&self) -> PyResult { - Bound::borrowed_from_gil_ref(&self).is_empty() + self.as_borrowed().is_empty() } /// Determines if the mapping contains the specified key. @@ -36,7 +50,7 @@ impl PyMapping { where K: ToPyObject, { - Bound::borrowed_from_gil_ref(&self).contains(key) + self.as_borrowed().contains(key) } /// Gets the item in self with key `key`. @@ -49,9 +63,7 @@ impl PyMapping { where K: ToPyObject, { - Bound::borrowed_from_gil_ref(&self) - .get_item(key) - .map(Bound::into_gil_ref) + self.as_borrowed().get_item(key).map(Bound::into_gil_ref) } /// Sets the item in self with key `key`. @@ -63,7 +75,7 @@ impl PyMapping { K: ToPyObject, V: ToPyObject, { - Bound::borrowed_from_gil_ref(&self).set_item(key, value) + self.as_borrowed().set_item(key, value) } /// Deletes the item with key `key`. @@ -74,40 +86,25 @@ impl PyMapping { where K: ToPyObject, { - Bound::borrowed_from_gil_ref(&self).del_item(key) + self.as_borrowed().del_item(key) } /// Returns a sequence containing all keys in the mapping. #[inline] pub fn keys(&self) -> PyResult<&PySequence> { - Bound::borrowed_from_gil_ref(&self) - .keys() - .map(Bound::into_gil_ref) + self.as_borrowed().keys().map(Bound::into_gil_ref) } /// Returns a sequence containing all values in the mapping. #[inline] pub fn values(&self) -> PyResult<&PySequence> { - Bound::borrowed_from_gil_ref(&self) - .values() - .map(Bound::into_gil_ref) + self.as_borrowed().values().map(Bound::into_gil_ref) } /// Returns a sequence of tuples of all (key, value) pairs in the mapping. #[inline] pub fn items(&self) -> PyResult<&PySequence> { - Bound::borrowed_from_gil_ref(&self) - .items() - .map(Bound::into_gil_ref) - } - - /// Register a pyclass as a subclass of `collections.abc.Mapping` (from the Python standard - /// library). This is equvalent to `collections.abc.Mapping.register(T)` in Python. - /// This registration is required for a pyclass to be downcastable from `PyAny` to `PyMapping`. - pub fn register(py: Python<'_>) -> PyResult<()> { - let ty = T::type_object(py); - get_mapping_abc(py)?.call_method1("register", (ty,))?; - Ok(()) + self.as_borrowed().items().map(Bound::into_gil_ref) } } @@ -117,7 +114,7 @@ impl PyMapping { /// syntax these methods are separated into a trait, because stable Rust does not yet support /// `arbitrary_self_types`. #[doc(alias = "PyMapping")] -pub trait PyMappingMethods<'py> { +pub trait PyMappingMethods<'py>: crate::sealed::Sealed { /// Returns the number of objects in the mapping. /// /// This is equivalent to the Python expression `len(self)`. @@ -240,7 +237,7 @@ impl<'py> PyMappingMethods<'py> for Bound<'py, PyMapping> { } } -fn get_mapping_abc(py: Python<'_>) -> PyResult<&PyType> { +fn get_mapping_abc(py: Python<'_>) -> PyResult<&Bound<'_, PyType>> { static MAPPING_ABC: GILOnceCell> = GILOnceCell::new(); MAPPING_ABC.get_or_try_init_type_ref(py, "collections.abc", "Mapping") @@ -250,19 +247,20 @@ impl PyTypeCheck for PyMapping { const NAME: &'static str = "Mapping"; #[inline] - fn type_check(object: &PyAny) -> bool { + fn type_check(object: &Bound<'_, PyAny>) -> bool { // Using `is_instance` for `collections.abc.Mapping` is slow, so provide // optimized case dict as a well-known mapping - PyDict::is_type_of(object) + PyDict::is_type_of_bound(object) || get_mapping_abc(object.py()) .and_then(|abc| object.is_instance(abc)) .unwrap_or_else(|err| { - err.write_unraisable(object.py(), Some(object)); + err.write_unraisable_bound(object.py(), Some(&object.as_borrowed())); false }) } } +#[cfg(feature = "gil-refs")] #[allow(deprecated)] impl<'v> crate::PyTryFrom<'v> for PyMapping { /// Downcasting to `PyMapping` requires the concrete class to be a subclass (or registered @@ -271,7 +269,7 @@ impl<'v> crate::PyTryFrom<'v> for PyMapping { fn try_from>(value: V) -> Result<&'v PyMapping, PyDowncastError<'v>> { let value = value.into(); - if PyMapping::type_check(value) { + if PyMapping::type_check(&value.as_borrowed()) { unsafe { return Ok(value.downcast_unchecked()) } } @@ -294,11 +292,7 @@ impl<'v> crate::PyTryFrom<'v> for PyMapping { mod tests { use std::collections::HashMap; - use crate::{ - exceptions::PyKeyError, - types::{PyDict, PyTuple}, - Python, - }; + use crate::{exceptions::PyKeyError, types::PyTuple}; use super::*; @@ -307,13 +301,13 @@ mod tests { Python::with_gil(|py| { let mut v = HashMap::new(); let ob = v.to_object(py); - let mapping: &PyMapping = ob.downcast(py).unwrap(); + let mapping = ob.downcast_bound::(py).unwrap(); assert_eq!(0, mapping.len().unwrap()); assert!(mapping.is_empty().unwrap()); v.insert(7, 32); let ob = v.to_object(py); - let mapping2: &PyMapping = ob.downcast(py).unwrap(); + let mapping2 = ob.downcast_bound::(py).unwrap(); assert_eq!(1, mapping2.len().unwrap()); assert!(!mapping2.is_empty().unwrap()); }); @@ -325,7 +319,7 @@ mod tests { let mut v = HashMap::new(); v.insert("key0", 1234); let ob = v.to_object(py); - let mapping: &PyMapping = ob.downcast(py).unwrap(); + let mapping = ob.downcast_bound::(py).unwrap(); mapping.set_item("key1", "foo").unwrap(); assert!(mapping.contains("key0").unwrap()); @@ -340,7 +334,7 @@ mod tests { let mut v = HashMap::new(); v.insert(7, 32); let ob = v.to_object(py); - let mapping: &PyMapping = ob.downcast(py).unwrap(); + let mapping = ob.downcast_bound::(py).unwrap(); assert_eq!( 32, mapping.get_item(7i32).unwrap().extract::().unwrap() @@ -358,7 +352,7 @@ mod tests { let mut v = HashMap::new(); v.insert(7, 32); let ob = v.to_object(py); - let mapping: &PyMapping = ob.downcast(py).unwrap(); + let mapping = ob.downcast_bound::(py).unwrap(); assert!(mapping.set_item(7i32, 42i32).is_ok()); // change assert!(mapping.set_item(8i32, 123i32).is_ok()); // insert assert_eq!( @@ -378,7 +372,7 @@ mod tests { let mut v = HashMap::new(); v.insert(7, 32); let ob = v.to_object(py); - let mapping: &PyMapping = ob.downcast(py).unwrap(); + let mapping = ob.downcast_bound::(py).unwrap(); assert!(mapping.del_item(7i32).is_ok()); assert_eq!(0, mapping.len().unwrap()); assert!(mapping @@ -396,12 +390,12 @@ mod tests { v.insert(8, 42); v.insert(9, 123); let ob = v.to_object(py); - let mapping: &PyMapping = ob.downcast(py).unwrap(); + let mapping = ob.downcast_bound::(py).unwrap(); // Can't just compare against a vector of tuples since we don't have a guaranteed ordering. let mut key_sum = 0; let mut value_sum = 0; for el in mapping.items().unwrap().iter().unwrap() { - let tuple = el.unwrap().downcast::().unwrap(); + let tuple = el.unwrap().downcast_into::().unwrap(); key_sum += tuple.get_item(0).unwrap().extract::().unwrap(); value_sum += tuple.get_item(1).unwrap().extract::().unwrap(); } @@ -418,7 +412,7 @@ mod tests { v.insert(8, 42); v.insert(9, 123); let ob = v.to_object(py); - let mapping: &PyMapping = ob.downcast(py).unwrap(); + let mapping = ob.downcast_bound::(py).unwrap(); // Can't just compare against a vector of tuples since we don't have a guaranteed ordering. let mut key_sum = 0; for el in mapping.keys().unwrap().iter().unwrap() { @@ -436,7 +430,7 @@ mod tests { v.insert(8, 42); v.insert(9, 123); let ob = v.to_object(py); - let mapping: &PyMapping = ob.downcast(py).unwrap(); + let mapping = ob.downcast_bound::(py).unwrap(); // Can't just compare against a vector of tuples since we don't have a guaranteed ordering. let mut values_sum = 0; for el in mapping.values().unwrap().iter().unwrap() { @@ -447,6 +441,7 @@ mod tests { } #[test] + #[cfg(feature = "gil-refs")] #[allow(deprecated)] fn test_mapping_try_from() { use crate::PyTryFrom; diff --git a/src/types/memoryview.rs b/src/types/memoryview.rs index 0d115540689..320b3f9f70b 100644 --- a/src/types/memoryview.rs +++ b/src/types/memoryview.rs @@ -1,5 +1,9 @@ use crate::err::PyResult; -use crate::{ffi, AsPyPointer, PyAny}; +use crate::ffi_ptr_ext::FfiPtrExt; +use crate::py_result_ext::PyResultExt; +use crate::{ffi, Bound, PyAny}; +#[cfg(feature = "gil-refs")] +use crate::{AsPyPointer, PyNativeType}; /// Represents a Python `memoryview`. #[repr(transparent)] @@ -8,22 +12,44 @@ pub struct PyMemoryView(PyAny); pyobject_native_type_core!(PyMemoryView, pyobject_native_static_type_object!(ffi::PyMemoryView_Type), #checkfunction=ffi::PyMemoryView_Check); impl PyMemoryView { + /// Deprecated form of [`PyMemoryView::from_bound`] + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`PyMemoryView::from` will be replaced by `PyMemoryView::from_bound` in a future PyO3 version" + )] + pub fn from(src: &PyAny) -> PyResult<&PyMemoryView> { + PyMemoryView::from_bound(&src.as_borrowed()).map(Bound::into_gil_ref) + } + /// Creates a new Python `memoryview` object from another Python object that /// implements the buffer protocol. - pub fn from(src: &PyAny) -> PyResult<&PyMemoryView> { + pub fn from_bound<'py>(src: &Bound<'py, PyAny>) -> PyResult> { unsafe { - src.py() - .from_owned_ptr_or_err(ffi::PyMemoryView_FromObject(src.as_ptr())) + ffi::PyMemoryView_FromObject(src.as_ptr()) + .assume_owned_or_err(src.py()) + .downcast_into_unchecked() } } } +#[cfg(feature = "gil-refs")] impl<'py> TryFrom<&'py PyAny> for &'py PyMemoryView { type Error = crate::PyErr; /// Creates a new Python `memoryview` object from another Python object that /// implements the buffer protocol. fn try_from(value: &'py PyAny) -> Result { - PyMemoryView::from(value) + PyMemoryView::from_bound(&value.as_borrowed()).map(Bound::into_gil_ref) + } +} + +impl<'py> TryFrom<&Bound<'py, PyAny>> for Bound<'py, PyMemoryView> { + type Error = crate::PyErr; + + /// Creates a new Python `memoryview` object from another Python object that + /// implements the buffer protocol. + fn try_from(value: &Bound<'py, PyAny>) -> Result { + PyMemoryView::from_bound(value) } } diff --git a/src/types/mod.rs b/src/types/mod.rs index 0ff41f80236..12dabda7463 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -1,49 +1,52 @@ //! Various types defined by the Python interpreter such as `int`, `str` and `tuple`. -pub use self::any::PyAny; -pub use self::boolobject::PyBool; -pub use self::bytearray::PyByteArray; -pub use self::bytes::PyBytes; -pub use self::capsule::PyCapsule; -#[cfg(not(Py_LIMITED_API))] +pub use self::any::{PyAny, PyAnyMethods}; +pub use self::boolobject::{PyBool, PyBoolMethods}; +pub use self::bytearray::{PyByteArray, PyByteArrayMethods}; +pub use self::bytes::{PyBytes, PyBytesMethods}; +pub use self::capsule::{PyCapsule, PyCapsuleMethods}; +#[cfg(all(not(Py_LIMITED_API), not(PyPy), not(GraalPy)))] pub use self::code::PyCode; -pub use self::complex::PyComplex; +pub use self::complex::{PyComplex, PyComplexMethods}; +#[allow(deprecated)] +#[cfg(all(not(Py_LIMITED_API), feature = "gil-refs"))] +pub use self::datetime::timezone_utc; #[cfg(not(Py_LIMITED_API))] pub use self::datetime::{ - timezone_utc, PyDate, PyDateAccess, PyDateTime, PyDelta, PyDeltaAccess, PyTime, PyTimeAccess, - PyTzInfo, PyTzInfoAccess, + timezone_utc_bound, PyDate, PyDateAccess, PyDateTime, PyDelta, PyDeltaAccess, PyTime, + PyTimeAccess, PyTzInfo, PyTzInfoAccess, }; -pub use self::dict::{IntoPyDict, PyDict}; -#[cfg(not(PyPy))] +pub use self::dict::{IntoPyDict, PyDict, PyDictMethods}; +#[cfg(not(any(PyPy, GraalPy)))] pub use self::dict::{PyDictItems, PyDictKeys, PyDictValues}; pub use self::ellipsis::PyEllipsis; -pub use self::float::PyFloat; -#[cfg(all(not(Py_LIMITED_API), not(PyPy)))] +pub use self::float::{PyFloat, PyFloatMethods}; +#[cfg(all(not(Py_LIMITED_API), not(PyPy), not(GraalPy)))] pub use self::frame::PyFrame; -pub use self::frozenset::{PyFrozenSet, PyFrozenSetBuilder}; +pub use self::frozenset::{PyFrozenSet, PyFrozenSetBuilder, PyFrozenSetMethods}; pub use self::function::PyCFunction; -#[cfg(all(not(Py_LIMITED_API), not(PyPy)))] +#[cfg(all(not(Py_LIMITED_API), not(PyPy), not(GraalPy)))] pub use self::function::PyFunction; pub use self::iterator::PyIterator; -pub use self::list::PyList; -pub use self::mapping::PyMapping; +pub use self::list::{PyList, PyListMethods}; +pub use self::mapping::{PyMapping, PyMappingMethods}; pub use self::memoryview::PyMemoryView; -pub use self::module::PyModule; +pub use self::module::{PyModule, PyModuleMethods}; pub use self::none::PyNone; pub use self::notimplemented::PyNotImplemented; pub use self::num::PyLong; pub use self::num::PyLong as PyInt; -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] pub use self::pysuper::PySuper; -pub use self::sequence::PySequence; -pub use self::set::PySet; -pub use self::slice::{PySlice, PySliceIndices}; +pub use self::sequence::{PySequence, PySequenceMethods}; +pub use self::set::{PySet, PySetMethods}; +pub use self::slice::{PySlice, PySliceIndices, PySliceMethods}; #[cfg(not(Py_LIMITED_API))] pub use self::string::PyStringData; -pub use self::string::{PyString, PyString as PyUnicode}; -pub use self::traceback::PyTraceback; -pub use self::tuple::PyTuple; -pub use self::typeobject::PyType; +pub use self::string::{PyString, PyString as PyUnicode, PyStringMethods}; +pub use self::traceback::{PyTraceback, PyTracebackMethods}; +pub use self::tuple::{PyTuple, PyTupleMethods}; +pub use self::typeobject::{PyType, PyTypeMethods}; /// Iteration over Python collections. /// @@ -59,9 +62,9 @@ pub use self::typeobject::PyType; /// /// # pub fn main() -> PyResult<()> { /// Python::with_gil(|py| { -/// let dict: &PyDict = py.eval("{'a':'b', 'c':'d'}", None, None)?.downcast()?; +/// let dict = py.eval_bound("{'a':'b', 'c':'d'}", None, None)?.downcast_into::()?; /// -/// for (key, value) in dict { +/// for (key, value) in &dict { /// println!("key: {}, value: {}", key, value); /// } /// @@ -76,11 +79,42 @@ pub use self::typeobject::PyType; /// the Limited API and PyPy, the underlying structures are opaque and that may not be possible. /// In these cases the iterators are implemented by forwarding to [`PyIterator`]. pub mod iter { - pub use super::dict::{BoundDictIterator, PyDictIterator}; - pub use super::frozenset::PyFrozenSetIterator; - pub use super::list::{BoundListIterator, PyListIterator}; - pub use super::set::PySetIterator; - pub use super::tuple::PyTupleIterator; + pub use super::dict::BoundDictIterator; + pub use super::frozenset::BoundFrozenSetIterator; + pub use super::list::BoundListIterator; + pub use super::set::BoundSetIterator; + pub use super::tuple::{BorrowedTupleIterator, BoundTupleIterator}; + + #[cfg(feature = "gil-refs")] + pub use super::{ + dict::PyDictIterator, frozenset::PyFrozenSetIterator, list::PyListIterator, + set::PySetIterator, tuple::PyTupleIterator, + }; +} + +/// Python objects that have a base type. +/// +/// This marks types that can be upcast into a [`PyAny`] and used in its place. +/// This essentially includes every Python object except [`PyAny`] itself. +/// +/// This is used to provide the [`Deref>`](std::ops::Deref) +/// implementations for [`Bound<'_, T>`](crate::Bound). +/// +/// Users should not need to implement this trait directly. It's implementation +/// is provided by the [`#[pyclass]`](macro@crate::pyclass) attribute. +/// +/// ## Note +/// This is needed because the compiler currently tries to figure out all the +/// types in a deref-chain before starting to look for applicable method calls. +/// So we need to prevent [`Bound<'_, PyAny`](crate::Bound) dereferencing to +/// itself in order to avoid running into the recursion limit. This trait is +/// used to exclude this from our blanket implementation. See [this Rust +/// issue][1] for more details. If the compiler limitation gets resolved, this +/// trait will be removed. +/// +/// [1]: https://github.com/rust-lang/rust/issues/19509 +pub trait DerefToPyAny { + // Empty. } // Implementations core to all native types @@ -88,35 +122,41 @@ pub mod iter { #[macro_export] macro_rules! pyobject_native_type_base( ($name:ty $(;$generics:ident)* ) => { + #[cfg(feature = "gil-refs")] unsafe impl<$($generics,)*> $crate::PyNativeType for $name { type AsRefSource = Self; } + #[cfg(feature = "gil-refs")] impl<$($generics,)*> ::std::fmt::Debug for $name { fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::result::Result<(), ::std::fmt::Error> { - let s = self.repr().or(::std::result::Result::Err(::std::fmt::Error))?; + use $crate::{PyNativeType, types::{PyAnyMethods, PyStringMethods}}; + let s = self.as_borrowed().repr().or(::std::result::Result::Err(::std::fmt::Error))?; f.write_str(&s.to_string_lossy()) } } + #[cfg(feature = "gil-refs")] impl<$($generics,)*> ::std::fmt::Display for $name { fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::result::Result<(), ::std::fmt::Error> { - match self.str() { + use $crate::{PyNativeType, types::{PyAnyMethods, PyStringMethods, PyTypeMethods}}; + match self.as_borrowed().str() { ::std::result::Result::Ok(s) => return f.write_str(&s.to_string_lossy()), - ::std::result::Result::Err(err) => err.write_unraisable(self.py(), ::std::option::Option::Some(self)), + ::std::result::Result::Err(err) => err.write_unraisable_bound(self.py(), ::std::option::Option::Some(&self.as_borrowed())), } - match self.get_type().name() { + match self.as_borrowed().get_type().name() { ::std::result::Result::Ok(name) => ::std::write!(f, "", name), ::std::result::Result::Err(_err) => f.write_str(""), } } } + #[cfg(feature = "gil-refs")] impl<$($generics,)*> $crate::ToPyObject for $name { #[inline] @@ -159,6 +199,9 @@ macro_rules! pyobject_native_type_named ( } } + // FIXME https://github.com/PyO3/pyo3/issues/3903 + #[allow(unknown_lints, non_local_definitions)] + #[cfg(feature = "gil-refs")] impl<$($generics,)*> $crate::IntoPy<$crate::Py<$name>> for &'_ $name { #[inline] fn into_py(self, py: $crate::Python<'_>) -> $crate::Py<$name> { @@ -166,6 +209,9 @@ macro_rules! pyobject_native_type_named ( } } + // FIXME https://github.com/PyO3/pyo3/issues/3903 + #[allow(unknown_lints, non_local_definitions)] + #[cfg(feature = "gil-refs")] impl<$($generics,)*> ::std::convert::From<&'_ $name> for $crate::Py<$name> { #[inline] fn from(other: &$name) -> Self { @@ -174,11 +220,16 @@ macro_rules! pyobject_native_type_named ( } } + // FIXME https://github.com/PyO3/pyo3/issues/3903 + #[allow(unknown_lints, non_local_definitions)] + #[cfg(feature = "gil-refs")] impl<'a, $($generics,)*> ::std::convert::From<&'a $name> for &'a $crate::PyAny { fn from(ob: &'a $name) -> Self { unsafe{&*(ob as *const $name as *const $crate::PyAny)} } } + + impl $crate::types::DerefToPyAny for $name {} }; ); @@ -206,12 +257,17 @@ macro_rules! pyobject_native_type_info( $( #[inline] - fn is_type_of(ptr: &$crate::PyAny) -> bool { + fn is_type_of_bound(obj: &$crate::Bound<'_, $crate::PyAny>) -> bool { #[allow(unused_unsafe)] - unsafe { $checkfunction(ptr.as_ptr()) > 0 } + unsafe { $checkfunction(obj.as_ptr()) > 0 } } )? } + + impl $name { + #[doc(hidden)] + pub const _PYO3_DEF: $crate::impl_::pymodule::AddTypeToModule = $crate::impl_::pymodule::AddTypeToModule::new(); + } }; ); @@ -221,10 +277,13 @@ macro_rules! pyobject_native_type_info( #[macro_export] macro_rules! pyobject_native_type_extract { ($name:ty $(;$generics:ident)*) => { + // FIXME https://github.com/PyO3/pyo3/issues/3903 + #[allow(unknown_lints, non_local_definitions)] + #[cfg(feature = "gil-refs")] impl<'py, $($generics,)*> $crate::FromPyObject<'py> for &'py $name { #[inline] - fn extract(obj: &'py $crate::PyAny) -> $crate::PyResult { - obj.downcast().map_err(::std::convert::Into::into) + fn extract_bound(obj: &$crate::Bound<'py, $crate::PyAny>) -> $crate::PyResult { + ::std::clone::Clone::clone(obj).into_gil_ref().downcast().map_err(::std::convert::Into::into) } } } @@ -251,7 +310,7 @@ macro_rules! pyobject_native_type_sized { unsafe impl $crate::type_object::PyLayout<$name> for $layout {} impl $crate::type_object::PySizedLayout<$name> for $layout {} impl<$($generics,)*> $crate::impl_::pyclass::PyClassBaseType for $name { - type LayoutAsBase = $crate::pycell::PyCellBase<$layout>; + type LayoutAsBase = $crate::impl_::pycell::PyClassObjectBase<$layout>; type BaseNativeType = $name; type Initializer = $crate::pyclass_init::PyNativeTypeInitializer; type PyClassMutability = $crate::pycell::impl_::ImmutableClass; @@ -276,33 +335,33 @@ pub(crate) mod any; pub(crate) mod boolobject; pub(crate) mod bytearray; pub(crate) mod bytes; -mod capsule; -#[cfg(not(Py_LIMITED_API))] +pub(crate) mod capsule; +#[cfg(all(not(Py_LIMITED_API), not(PyPy), not(GraalPy)))] mod code; -mod complex; +pub(crate) mod complex; #[cfg(not(Py_LIMITED_API))] pub(crate) mod datetime; pub(crate) mod dict; mod ellipsis; pub(crate) mod float; -#[cfg(all(not(Py_LIMITED_API), not(PyPy)))] +#[cfg(all(not(Py_LIMITED_API), not(PyPy), not(GraalPy)))] mod frame; -mod frozenset; +pub(crate) mod frozenset; mod function; -mod iterator; +pub(crate) mod iterator; pub(crate) mod list; pub(crate) mod mapping; mod memoryview; -mod module; +pub(crate) mod module; mod none; mod notimplemented; mod num; -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] mod pysuper; pub(crate) mod sequence; pub(crate) mod set; -mod slice; +pub(crate) mod slice; pub(crate) mod string; -mod traceback; -mod tuple; -mod typeobject; +pub(crate) mod traceback; +pub(crate) mod tuple; +pub(crate) mod typeobject; diff --git a/src/types/module.rs b/src/types/module.rs index 701a9131307..f0ae7385f23 100644 --- a/src/types/module.rs +++ b/src/types/module.rs @@ -1,13 +1,18 @@ use crate::callback::IntoPyCallbackOutput; use crate::err::{PyErr, PyResult}; -use crate::exceptions; -use crate::ffi; +use crate::ffi_ptr_ext::FfiPtrExt; +use crate::py_result_ext::PyResultExt; use crate::pyclass::PyClass; -use crate::types::{PyAny, PyCFunction, PyDict, PyList, PyString}; -use crate::{IntoPy, Py, PyObject, Python}; -use std::ffi::{CStr, CString}; +use crate::types::{ + any::PyAnyMethods, list::PyListMethods, PyAny, PyCFunction, PyDict, PyList, PyString, +}; +use crate::{exceptions, ffi, Bound, IntoPy, Py, PyObject, Python}; +use std::ffi::CString; use std::str; +#[cfg(feature = "gil-refs")] +use {super::PyStringMethods, crate::PyNativeType}; + /// Represents a Python [`module`][1] object. /// /// As with all other Python objects, modules are first class citizens. @@ -30,17 +35,21 @@ impl PyModule { /// /// # fn main() -> PyResult<()> { /// Python::with_gil(|py| -> PyResult<()> { - /// let module = PyModule::new(py, "my_module")?; + /// let module = PyModule::new_bound(py, "my_module")?; /// - /// assert_eq!(module.name()?, "my_module"); + /// assert_eq!(module.name()?.to_cow()?, "my_module"); /// Ok(()) /// })?; /// # Ok(())} /// ``` - pub fn new<'p>(py: Python<'p>, name: &str) -> PyResult<&'p PyModule> { + pub fn new_bound<'py>(py: Python<'py>, name: &str) -> PyResult> { // Could use PyModule_NewObject, but it doesn't exist on PyPy. let name = CString::new(name)?; - unsafe { py.from_owned_ptr_or_err(ffi::PyModule_New(name.as_ptr())) } + unsafe { + ffi::PyModule_New(name.as_ptr()) + .assume_owned_or_err(py) + .downcast_into_unchecked() + } } /// Imports the Python module with the specified name. @@ -52,7 +61,7 @@ impl PyModule { /// use pyo3::prelude::*; /// /// Python::with_gil(|py| { - /// let module = PyModule::import(py, "antigravity").expect("No flying for you."); + /// let module = PyModule::import_bound(py, "antigravity").expect("No flying for you."); /// }); /// # } /// ``` @@ -61,12 +70,16 @@ impl PyModule { /// ```python /// import antigravity /// ``` - pub fn import(py: Python<'_>, name: N) -> PyResult<&PyModule> + pub fn import_bound(py: Python<'_>, name: N) -> PyResult> where N: IntoPy>, { let name: Py = name.into_py(py); - unsafe { py.from_owned_ptr_or_err(ffi::PyImport_Import(name.as_ptr())) } + unsafe { + ffi::PyImport_Import(name.as_ptr()) + .assume_owned_or_err(py) + .downcast_into_unchecked() + } } /// Creates and loads a module named `module_name`, @@ -98,7 +111,7 @@ impl PyModule { /// let code = include_str!("../../assets/script.py"); /// /// Python::with_gil(|py| -> PyResult<()> { - /// PyModule::from_code(py, code, "example.py", "example")?; + /// PyModule::from_code_bound(py, code, "example.py", "example")?; /// Ok(()) /// })?; /// # Ok(()) @@ -117,45 +130,76 @@ impl PyModule { /// let code = std::fs::read_to_string("assets/script.py")?; /// /// Python::with_gil(|py| -> PyResult<()> { - /// PyModule::from_code(py, &code, "example.py", "example")?; + /// PyModule::from_code_bound(py, &code, "example.py", "example")?; /// Ok(()) /// })?; /// Ok(()) /// # } /// ``` - pub fn from_code<'p>( - py: Python<'p>, + pub fn from_code_bound<'py>( + py: Python<'py>, code: &str, file_name: &str, module_name: &str, - ) -> PyResult<&'p PyModule> { + ) -> PyResult> { let data = CString::new(code)?; let filename = CString::new(file_name)?; let module = CString::new(module_name)?; unsafe { - let cptr = ffi::Py_CompileString(data.as_ptr(), filename.as_ptr(), ffi::Py_file_input); - if cptr.is_null() { - return Err(PyErr::fetch(py)); - } - - let mptr = ffi::PyImport_ExecCodeModuleEx(module.as_ptr(), cptr, filename.as_ptr()); - ffi::Py_DECREF(cptr); - if mptr.is_null() { - return Err(PyErr::fetch(py)); - } + let code = ffi::Py_CompileString(data.as_ptr(), filename.as_ptr(), ffi::Py_file_input) + .assume_owned_or_err(py)?; - <&PyModule as crate::FromPyObject>::extract(py.from_owned_ptr_or_err(mptr)?) + ffi::PyImport_ExecCodeModuleEx(module.as_ptr(), code.as_ptr(), filename.as_ptr()) + .assume_owned_or_err(py) + .downcast_into() } } +} + +#[cfg(feature = "gil-refs")] +impl PyModule { + /// Deprecated form of [`PyModule::new_bound`]. + #[inline] + #[deprecated( + since = "0.21.0", + note = "`PyModule::new` will be replaced by `PyModule::new_bound` in a future PyO3 version" + )] + pub fn new<'py>(py: Python<'py>, name: &str) -> PyResult<&'py PyModule> { + Self::new_bound(py, name).map(Bound::into_gil_ref) + } + + /// Deprecated form of [`PyModule::import_bound`]. + #[inline] + #[deprecated( + since = "0.21.0", + note = "`PyModule::import` will be replaced by `PyModule::import_bound` in a future PyO3 version" + )] + pub fn import(py: Python<'_>, name: N) -> PyResult<&PyModule> + where + N: IntoPy>, + { + Self::import_bound(py, name).map(Bound::into_gil_ref) + } + + /// Deprecated form of [`PyModule::from_code_bound`]. + #[inline] + #[deprecated( + since = "0.21.0", + note = "`PyModule::from_code` will be replaced by `PyModule::from_code_bound` in a future PyO3 version" + )] + pub fn from_code<'py>( + py: Python<'py>, + code: &str, + file_name: &str, + module_name: &str, + ) -> PyResult<&'py PyModule> { + Self::from_code_bound(py, code, file_name, module_name).map(Bound::into_gil_ref) + } /// Returns the module's `__dict__` attribute, which contains the module's symbol table. pub fn dict(&self) -> &PyDict { - unsafe { - // PyModule_GetDict returns borrowed ptr; must make owned for safety (see #890). - let ptr = ffi::PyModule_GetDict(self.as_ptr()); - self.py().from_owned_ptr(ffi::_Py_NewRef(ptr)) - } + self.as_borrowed().dict().into_gil_ref() } /// Returns the index (the `__all__` attribute) of the module, @@ -163,34 +207,14 @@ impl PyModule { /// /// `__all__` declares the items that will be imported with `from my_module import *`. pub fn index(&self) -> PyResult<&PyList> { - let __all__ = __all__(self.py()); - match self.getattr(__all__) { - Ok(idx) => idx.downcast().map_err(PyErr::from), - Err(err) => { - if err.is_instance_of::(self.py()) { - let l = PyList::empty(self.py()); - self.setattr(__all__, l).map_err(PyErr::from)?; - Ok(l) - } else { - Err(err) - } - } - } + self.as_borrowed().index().map(Bound::into_gil_ref) } /// Returns the name (the `__name__` attribute) of the module. /// /// May fail if the module does not have a `__name__` attribute. pub fn name(&self) -> PyResult<&str> { - let ptr = unsafe { ffi::PyModule_GetName(self.as_ptr()) }; - if ptr.is_null() { - Err(PyErr::fetch(self.py())) - } else { - let name = unsafe { CStr::from_ptr(ptr) } - .to_str() - .expect("PyModule_GetName expected to return utf8"); - Ok(name) - } + self.as_borrowed().name()?.into_gil_ref().to_str() } /// Returns the filename (the `__file__` attribute) of the module. @@ -198,11 +222,7 @@ impl PyModule { /// May fail if the module does not have a `__file__` attribute. #[cfg(not(PyPy))] pub fn filename(&self) -> PyResult<&str> { - unsafe { - self.py() - .from_owned_ptr_or_err::(ffi::PyModule_GetFilenameObject(self.as_ptr()))? - .to_str() - } + self.as_borrowed().filename()?.into_gil_ref().to_str() } /// Adds an attribute to the module. @@ -216,7 +236,7 @@ impl PyModule { /// use pyo3::prelude::*; /// /// #[pymodule] - /// fn my_module(_py: Python<'_>, module: &PyModule) -> PyResult<()> { + /// fn my_module(module: &Bound<'_, PyModule>) -> PyResult<()> { /// module.add("c", 299_792_458)?; /// Ok(()) /// } @@ -239,10 +259,7 @@ impl PyModule { where V: IntoPy, { - self.index()? - .append(name) - .expect("could not append __name__ to __all__"); - self.setattr(name, value.into_py(self.py())) + self.as_borrowed().add(name, value) } /// Adds a new class to the module. @@ -260,7 +277,7 @@ impl PyModule { /// struct Foo { /* fields omitted */ } /// /// #[pymodule] - /// fn my_module(_py: Python<'_>, module: &PyModule) -> PyResult<()> { + /// fn my_module(module: &Bound<'_, PyModule>) -> PyResult<()> { /// module.add_class::()?; /// Ok(()) /// } @@ -287,8 +304,7 @@ impl PyModule { where T: PyClass, { - let py = self.py(); - self.add(T::NAME, T::lazy_type_object().get_or_try_init(py)?) + self.as_borrowed().add_class::() } /// Adds a function or a (sub)module to a module, using the functions name as name. @@ -298,14 +314,7 @@ impl PyModule { where T: IntoPyCallbackOutput, { - self._add_wrapped(wrapper(self.py()).convert(self.py())?) - } - - fn _add_wrapped(&self, object: PyObject) -> PyResult<()> { - let py = self.py(); - let name = object.getattr(py, __name__(py))?; - let name = name.extract(py)?; - self.add(name, object) + self.as_borrowed().add_wrapped(wrapper) } /// Adds a submodule to a module. @@ -323,11 +332,11 @@ impl PyModule { /// use pyo3::prelude::*; /// /// #[pymodule] - /// fn my_module(py: Python<'_>, module: &PyModule) -> PyResult<()> { - /// let submodule = PyModule::new(py, "submodule")?; + /// fn my_module(py: Python<'_>, module: &Bound<'_, PyModule>) -> PyResult<()> { + /// let submodule = PyModule::new_bound(py, "submodule")?; /// submodule.add("super_useful_constant", "important")?; /// - /// module.add_submodule(submodule)?; + /// module.add_submodule(&submodule)?; /// Ok(()) /// } /// ``` @@ -349,8 +358,7 @@ impl PyModule { /// [1]: https://github.com/PyO3/pyo3/issues/759 /// [2]: https://github.com/PyO3/pyo3/issues/1517#issuecomment-808664021 pub fn add_submodule(&self, module: &PyModule) -> PyResult<()> { - let name = module.name()?; - self.add(name, module) + self.as_borrowed().add_submodule(&module.as_borrowed()) } /// Add a function to a module. @@ -366,7 +374,7 @@ impl PyModule { /// println!("Hello world!") /// } /// #[pymodule] - /// fn my_module(_py: Python<'_>, module: &PyModule) -> PyResult<()> { + /// fn my_module(module: &Bound<'_, PyModule>) -> PyResult<()> { /// module.add_function(wrap_pyfunction!(say_hello, module)?) /// } /// ``` @@ -388,28 +396,357 @@ impl PyModule { /// [1]: crate::prelude::pyfunction /// [2]: crate::wrap_pyfunction pub fn add_function<'a>(&'a self, fun: &'a PyCFunction) -> PyResult<()> { - let name = fun.getattr(__name__(self.py()))?.extract()?; - self.add(name, fun) + let name = fun + .as_borrowed() + .getattr(__name__(self.py()))? + .downcast_into::()?; + let name = name.to_cow()?; + self.add(&name, fun) } } -fn __all__(py: Python<'_>) -> &PyString { +/// Implementation of functionality for [`PyModule`]. +/// +/// These methods are defined for the `Bound<'py, PyModule>` smart pointer, so to use method call +/// syntax these methods are separated into a trait, because stable Rust does not yet support +/// `arbitrary_self_types`. +#[doc(alias = "PyModule")] +pub trait PyModuleMethods<'py>: crate::sealed::Sealed { + /// Returns the module's `__dict__` attribute, which contains the module's symbol table. + fn dict(&self) -> Bound<'py, PyDict>; + + /// Returns the index (the `__all__` attribute) of the module, + /// creating one if needed. + /// + /// `__all__` declares the items that will be imported with `from my_module import *`. + fn index(&self) -> PyResult>; + + /// Returns the name (the `__name__` attribute) of the module. + /// + /// May fail if the module does not have a `__name__` attribute. + fn name(&self) -> PyResult>; + + /// Returns the filename (the `__file__` attribute) of the module. + /// + /// May fail if the module does not have a `__file__` attribute. + #[cfg(not(PyPy))] + fn filename(&self) -> PyResult>; + + /// Adds an attribute to the module. + /// + /// For adding classes, functions or modules, prefer to use [`PyModuleMethods::add_class`], + /// [`PyModuleMethods::add_function`] or [`PyModuleMethods::add_submodule`] instead, + /// respectively. + /// + /// # Examples + /// + /// ```rust + /// use pyo3::prelude::*; + /// + /// #[pymodule] + /// fn my_module(module: &Bound<'_, PyModule>) -> PyResult<()> { + /// module.add("c", 299_792_458)?; + /// Ok(()) + /// } + /// ``` + /// + /// Python code can then do the following: + /// + /// ```python + /// from my_module import c + /// + /// print("c is", c) + /// ``` + /// + /// This will result in the following output: + /// + /// ```text + /// c is 299792458 + /// ``` + fn add(&self, name: N, value: V) -> PyResult<()> + where + N: IntoPy>, + V: IntoPy; + + /// Adds a new class to the module. + /// + /// Notice that this method does not take an argument. + /// Instead, this method is *generic*, and requires us to use the + /// "turbofish" syntax to specify the class we want to add. + /// + /// # Examples + /// + /// ```rust + /// use pyo3::prelude::*; + /// + /// #[pyclass] + /// struct Foo { /* fields omitted */ } + /// + /// #[pymodule] + /// fn my_module(module: &Bound<'_, PyModule>) -> PyResult<()> { + /// module.add_class::()?; + /// Ok(()) + /// } + /// ``` + /// + /// Python code can see this class as such: + /// ```python + /// from my_module import Foo + /// + /// print("Foo is", Foo) + /// ``` + /// + /// This will result in the following output: + /// ```text + /// Foo is + /// ``` + /// + /// Note that as we haven't defined a [constructor][1], Python code can't actually + /// make an *instance* of `Foo` (or *get* one for that matter, as we haven't exported + /// anything that can return instances of `Foo`). + /// + /// [1]: https://pyo3.rs/latest/class.html#constructor + fn add_class(&self) -> PyResult<()> + where + T: PyClass; + + /// Adds a function or a (sub)module to a module, using the functions name as name. + /// + /// Prefer to use [`PyModuleMethods::add_function`] and/or [`PyModuleMethods::add_submodule`] + /// instead. + fn add_wrapped(&self, wrapper: &impl Fn(Python<'py>) -> T) -> PyResult<()> + where + T: IntoPyCallbackOutput; + + /// Adds a submodule to a module. + /// + /// This is especially useful for creating module hierarchies. + /// + /// Note that this doesn't define a *package*, so this won't allow Python code + /// to directly import submodules by using + /// `from my_module import submodule`. + /// For more information, see [#759][1] and [#1517][2]. + /// + /// # Examples + /// + /// ```rust + /// use pyo3::prelude::*; + /// + /// #[pymodule] + /// fn my_module(py: Python<'_>, module: &Bound<'_, PyModule>) -> PyResult<()> { + /// let submodule = PyModule::new_bound(py, "submodule")?; + /// submodule.add("super_useful_constant", "important")?; + /// + /// module.add_submodule(&submodule)?; + /// Ok(()) + /// } + /// ``` + /// + /// Python code can then do the following: + /// + /// ```python + /// import my_module + /// + /// print("super_useful_constant is", my_module.submodule.super_useful_constant) + /// ``` + /// + /// This will result in the following output: + /// + /// ```text + /// super_useful_constant is important + /// ``` + /// + /// [1]: https://github.com/PyO3/pyo3/issues/759 + /// [2]: https://github.com/PyO3/pyo3/issues/1517#issuecomment-808664021 + fn add_submodule(&self, module: &Bound<'_, PyModule>) -> PyResult<()>; + + /// Add a function to a module. + /// + /// Note that this also requires the [`wrap_pyfunction!`][2] macro + /// to wrap a function annotated with [`#[pyfunction]`][1]. + /// + /// ```rust + /// use pyo3::prelude::*; + /// + /// #[pyfunction] + /// fn say_hello() { + /// println!("Hello world!") + /// } + /// #[pymodule] + /// fn my_module(module: &Bound<'_, PyModule>) -> PyResult<()> { + /// module.add_function(wrap_pyfunction!(say_hello, module)?) + /// } + /// ``` + /// + /// Python code can then do the following: + /// + /// ```python + /// from my_module import say_hello + /// + /// say_hello() + /// ``` + /// + /// This will result in the following output: + /// + /// ```text + /// Hello world! + /// ``` + /// + /// [1]: crate::prelude::pyfunction + /// [2]: crate::wrap_pyfunction + fn add_function(&self, fun: Bound<'_, PyCFunction>) -> PyResult<()>; +} + +impl<'py> PyModuleMethods<'py> for Bound<'py, PyModule> { + fn dict(&self) -> Bound<'py, PyDict> { + unsafe { + // PyModule_GetDict returns borrowed ptr; must make owned for safety (see #890). + ffi::PyModule_GetDict(self.as_ptr()) + .assume_borrowed(self.py()) + .to_owned() + .downcast_into_unchecked() + } + } + + fn index(&self) -> PyResult> { + let __all__ = __all__(self.py()); + match self.getattr(__all__) { + Ok(idx) => idx.downcast_into().map_err(PyErr::from), + Err(err) => { + if err.is_instance_of::(self.py()) { + let l = PyList::empty_bound(self.py()); + self.setattr(__all__, &l).map_err(PyErr::from)?; + Ok(l) + } else { + Err(err) + } + } + } + } + + fn name(&self) -> PyResult> { + #[cfg(not(PyPy))] + { + unsafe { + ffi::PyModule_GetNameObject(self.as_ptr()) + .assume_owned_or_err(self.py()) + .downcast_into_unchecked() + } + } + + #[cfg(PyPy)] + { + self.dict() + .get_item("__name__") + .map_err(|_| exceptions::PyAttributeError::new_err("__name__"))? + .downcast_into() + .map_err(PyErr::from) + } + } + + #[cfg(not(PyPy))] + fn filename(&self) -> PyResult> { + unsafe { + ffi::PyModule_GetFilenameObject(self.as_ptr()) + .assume_owned_or_err(self.py()) + .downcast_into_unchecked() + } + } + + fn add(&self, name: N, value: V) -> PyResult<()> + where + N: IntoPy>, + V: IntoPy, + { + fn inner( + module: &Bound<'_, PyModule>, + name: Bound<'_, PyString>, + value: Bound<'_, PyAny>, + ) -> PyResult<()> { + module + .index()? + .append(&name) + .expect("could not append __name__ to __all__"); + module.setattr(name, value.into_py(module.py())) + } + + let py = self.py(); + inner( + self, + name.into_py(py).into_bound(py), + value.into_py(py).into_bound(py), + ) + } + + fn add_class(&self) -> PyResult<()> + where + T: PyClass, + { + let py = self.py(); + self.add(T::NAME, T::lazy_type_object().get_or_try_init(py)?) + } + + fn add_wrapped(&self, wrapper: &impl Fn(Python<'py>) -> T) -> PyResult<()> + where + T: IntoPyCallbackOutput, + { + fn inner(module: &Bound<'_, PyModule>, object: Bound<'_, PyAny>) -> PyResult<()> { + let name = object.getattr(__name__(module.py()))?; + module.add(name.downcast_into::()?, object) + } + + let py = self.py(); + inner(self, wrapper(py).convert(py)?.into_bound(py)) + } + + fn add_submodule(&self, module: &Bound<'_, PyModule>) -> PyResult<()> { + let name = module.name()?; + self.add(name, module) + } + + fn add_function(&self, fun: Bound<'_, PyCFunction>) -> PyResult<()> { + let name = fun.getattr(__name__(self.py()))?; + self.add(name.downcast_into::()?, fun) + } +} + +fn __all__(py: Python<'_>) -> &Bound<'_, PyString> { intern!(py, "__all__") } -fn __name__(py: Python<'_>) -> &PyString { +fn __name__(py: Python<'_>) -> &Bound<'_, PyString> { intern!(py, "__name__") } #[cfg(test)] mod tests { - use crate::{types::PyModule, Python}; + use crate::{ + types::{module::PyModuleMethods, string::PyStringMethods, PyModule}, + Python, + }; #[test] fn module_import_and_name() { Python::with_gil(|py| { - let builtins = PyModule::import(py, "builtins").unwrap(); - assert_eq!(builtins.name().unwrap(), "builtins"); + let builtins = PyModule::import_bound(py, "builtins").unwrap(); + assert_eq!( + builtins.name().unwrap().to_cow().unwrap().as_ref(), + "builtins" + ); + }) + } + + #[test] + #[cfg(not(PyPy))] + fn module_filename() { + Python::with_gil(|py| { + let site = PyModule::import_bound(py, "site").unwrap(); + assert!(site + .filename() + .unwrap() + .to_cow() + .unwrap() + .ends_with("site.py")); }) } } diff --git a/src/types/none.rs b/src/types/none.rs index 19ee80ede57..0ab1570b92d 100644 --- a/src/types/none.rs +++ b/src/types/none.rs @@ -1,4 +1,8 @@ -use crate::{ffi, IntoPy, PyAny, PyObject, PyTypeInfo, Python, ToPyObject}; +use crate::ffi_ptr_ext::FfiPtrExt; +use crate::{ + ffi, types::any::PyAnyMethods, Borrowed, Bound, IntoPy, PyAny, PyObject, PyTypeInfo, Python, + ToPyObject, +}; /// Represents the Python `None` object. #[repr(transparent)] @@ -9,9 +13,21 @@ pyobject_native_type_extract!(PyNone); impl PyNone { /// Returns the `None` object. + /// Deprecated form of [`PyNone::get_bound`] + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`PyNone::get` will be replaced by `PyNone::get_bound` in a future PyO3 version" + )] #[inline] pub fn get(py: Python<'_>) -> &PyNone { - unsafe { py.from_borrowed_ptr(ffi::Py_None()) } + Self::get_bound(py).into_gil_ref() + } + + /// Returns the `None` object. + #[inline] + pub fn get_bound(py: Python<'_>) -> Borrowed<'_, '_, PyNone> { + unsafe { ffi::Py_None().assume_borrowed(py).downcast_unchecked() } } } @@ -25,62 +41,67 @@ unsafe impl PyTypeInfo for PyNone { } #[inline] - fn is_type_of(object: &PyAny) -> bool { + fn is_type_of_bound(object: &Bound<'_, PyAny>) -> bool { // NoneType is not usable as a base type - Self::is_exact_type_of(object) + Self::is_exact_type_of_bound(object) } #[inline] - fn is_exact_type_of(object: &PyAny) -> bool { - object.is(Self::get(object.py())) + fn is_exact_type_of_bound(object: &Bound<'_, PyAny>) -> bool { + object.is(&**Self::get_bound(object.py())) } } /// `()` is converted to Python `None`. impl ToPyObject for () { fn to_object(&self, py: Python<'_>) -> PyObject { - PyNone::get(py).into() + PyNone::get_bound(py).into_py(py) } } impl IntoPy for () { #[inline] fn into_py(self, py: Python<'_>) -> PyObject { - PyNone::get(py).into() + PyNone::get_bound(py).into_py(py) } } #[cfg(test)] mod tests { + use crate::types::any::PyAnyMethods; use crate::types::{PyDict, PyNone}; use crate::{IntoPy, PyObject, PyTypeInfo, Python, ToPyObject}; - #[test] fn test_none_is_itself() { Python::with_gil(|py| { - assert!(PyNone::get(py).is_instance_of::()); - assert!(PyNone::get(py).is_exact_instance_of::()); + assert!(PyNone::get_bound(py).is_instance_of::()); + assert!(PyNone::get_bound(py).is_exact_instance_of::()); }) } #[test] fn test_none_type_object_consistent() { Python::with_gil(|py| { - assert!(PyNone::get(py).get_type().is(PyNone::type_object(py))); + assert!(PyNone::get_bound(py) + .get_type() + .is(&PyNone::type_object_bound(py))); }) } #[test] fn test_none_is_none() { Python::with_gil(|py| { - assert!(PyNone::get(py).downcast::().unwrap().is_none()); + assert!(PyNone::get_bound(py) + .downcast::() + .unwrap() + .is_none()); }) } #[test] fn test_unit_to_object_is_none() { Python::with_gil(|py| { - assert!(().to_object(py).downcast::(py).is_ok()); + assert!(().to_object(py).downcast_bound::(py).is_ok()); }) } @@ -88,14 +109,14 @@ mod tests { fn test_unit_into_py_is_none() { Python::with_gil(|py| { let obj: PyObject = ().into_py(py); - assert!(obj.downcast::(py).is_ok()); + assert!(obj.downcast_bound::(py).is_ok()); }) } #[test] fn test_dict_is_not_none() { Python::with_gil(|py| { - assert!(PyDict::new(py).downcast::().is_err()); + assert!(PyDict::new_bound(py).downcast::().is_err()); }) } } diff --git a/src/types/notimplemented.rs b/src/types/notimplemented.rs index 0c61fd79bee..7fad1220b26 100644 --- a/src/types/notimplemented.rs +++ b/src/types/notimplemented.rs @@ -1,4 +1,7 @@ -use crate::{ffi, PyAny, PyTypeInfo, Python}; +use crate::{ + ffi, ffi_ptr_ext::FfiPtrExt, types::any::PyAnyMethods, Borrowed, Bound, PyAny, PyTypeInfo, + Python, +}; /// Represents the Python `NotImplemented` object. #[repr(transparent)] @@ -9,9 +12,24 @@ pyobject_native_type_extract!(PyNotImplemented); impl PyNotImplemented { /// Returns the `NotImplemented` object. + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`PyNotImplemented::get` will be replaced by `PyNotImplemented::get_bound` in a future PyO3 version" + )] #[inline] pub fn get(py: Python<'_>) -> &PyNotImplemented { - unsafe { py.from_borrowed_ptr(ffi::Py_NotImplemented()) } + Self::get_bound(py).into_gil_ref() + } + + /// Returns the `NotImplemented` object. + #[inline] + pub fn get_bound(py: Python<'_>) -> Borrowed<'_, '_, PyNotImplemented> { + unsafe { + ffi::Py_NotImplemented() + .assume_borrowed(py) + .downcast_unchecked() + } } } @@ -24,43 +42,46 @@ unsafe impl PyTypeInfo for PyNotImplemented { } #[inline] - fn is_type_of(object: &PyAny) -> bool { + fn is_type_of_bound(object: &Bound<'_, PyAny>) -> bool { // NotImplementedType is not usable as a base type - Self::is_exact_type_of(object) + Self::is_exact_type_of_bound(object) } #[inline] - fn is_exact_type_of(object: &PyAny) -> bool { - object.is(Self::get(object.py())) + fn is_exact_type_of_bound(object: &Bound<'_, PyAny>) -> bool { + object.is(&**Self::get_bound(object.py())) } } #[cfg(test)] mod tests { + use crate::types::any::PyAnyMethods; use crate::types::{PyDict, PyNotImplemented}; use crate::{PyTypeInfo, Python}; #[test] fn test_notimplemented_is_itself() { Python::with_gil(|py| { - assert!(PyNotImplemented::get(py).is_instance_of::()); - assert!(PyNotImplemented::get(py).is_exact_instance_of::()); + assert!(PyNotImplemented::get_bound(py).is_instance_of::()); + assert!(PyNotImplemented::get_bound(py).is_exact_instance_of::()); }) } #[test] fn test_notimplemented_type_object_consistent() { Python::with_gil(|py| { - assert!(PyNotImplemented::get(py) + assert!(PyNotImplemented::get_bound(py) .get_type() - .is(PyNotImplemented::type_object(py))); + .is(&PyNotImplemented::type_object_bound(py))); }) } #[test] fn test_dict_is_not_notimplemented() { Python::with_gil(|py| { - assert!(PyDict::new(py).downcast::().is_err()); + assert!(PyDict::new_bound(py) + .downcast::() + .is_err()); }) } } diff --git a/src/types/num.rs b/src/types/num.rs index 26748f7d1c7..924d4b2c593 100644 --- a/src/types/num.rs +++ b/src/types/num.rs @@ -4,7 +4,7 @@ use crate::{ffi, PyAny}; /// /// You can usually avoid directly working with this type /// by using [`ToPyObject`](crate::conversion::ToPyObject) -/// and [`extract`](PyAny::extract) +/// and [`extract`](super::PyAnyMethods::extract) /// with the primitive Rust integer types. #[repr(transparent)] pub struct PyLong(PyAny); diff --git a/src/types/pysuper.rs b/src/types/pysuper.rs index 5cf05a36a4d..7c4d781525a 100644 --- a/src/types/pysuper.rs +++ b/src/types/pysuper.rs @@ -16,6 +16,17 @@ pyobject_native_type_core!( ); impl PySuper { + /// Deprecated form of `PySuper::new_bound`. + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`PySuper::new` will be replaced by `PySuper::new_bound` in a future PyO3 version" + )] + pub fn new<'py>(ty: &'py PyType, obj: &'py PyAny) -> PyResult<&'py PySuper> { + use crate::PyNativeType; + Self::new_bound(&ty.as_borrowed(), &obj.as_borrowed()).map(Bound::into_gil_ref) + } + /// Constructs a new super object. More read about super object: [docs](https://docs.python.org/3/library/functions.html#super) /// /// # Examples @@ -50,25 +61,17 @@ impl PySuper { /// (SubClass {}, BaseClass::new()) /// } /// - /// fn method(self_: &PyCell) -> PyResult<&PyAny> { + /// fn method<'py>(self_: &Bound<'py, Self>) -> PyResult> { /// let super_ = self_.py_super()?; /// super_.call_method("method", (), None) /// } /// } /// ``` - pub fn new<'py>(ty: &'py PyType, obj: &'py PyAny) -> PyResult<&'py PySuper> { - Self::new2( - Bound::borrowed_from_gil_ref(&ty), - Bound::borrowed_from_gil_ref(&obj), - ) - .map(Bound::into_gil_ref) - } - - pub(crate) fn new2<'py>( + pub fn new_bound<'py>( ty: &Bound<'py, PyType>, obj: &Bound<'py, PyAny>, ) -> PyResult> { - Bound::borrowed_from_gil_ref(&PySuper::type_object(ty.py())) + PySuper::type_object_bound(ty.py()) .call1((ty, obj)) .map(|any| { // Safety: super() always returns instance of super diff --git a/src/types/sequence.rs b/src/types/sequence.rs index 714288bd203..a5765ebc8b2 100644 --- a/src/types/sequence.rs +++ b/src/types/sequence.rs @@ -1,4 +1,4 @@ -use crate::err::{self, PyDowncastError, PyErr, PyResult}; +use crate::err::{self, DowncastError, PyErr, PyResult}; use crate::exceptions::PyTypeError; use crate::ffi_ptr_ext::FfiPtrExt; #[cfg(feature = "experimental-inspect")] @@ -8,8 +8,10 @@ use crate::internal_tricks::get_ssize_index; use crate::py_result_ext::PyResultExt; use crate::sync::GILOnceCell; use crate::type_object::PyTypeInfo; -use crate::types::{PyAny, PyList, PyString, PyTuple, PyType}; -use crate::{ffi, FromPyObject, Py, PyNativeType, PyTypeCheck, Python, ToPyObject}; +use crate::types::{any::PyAnyMethods, PyAny, PyList, PyString, PyTuple, PyType}; +#[cfg(feature = "gil-refs")] +use crate::{err::PyDowncastError, PyNativeType}; +use crate::{ffi, FromPyObject, Py, PyTypeCheck, Python, ToPyObject}; /// Represents a reference to a Python object supporting the sequence protocol. #[repr(transparent)] @@ -17,19 +19,31 @@ pub struct PySequence(PyAny); pyobject_native_type_named!(PySequence); pyobject_native_type_extract!(PySequence); +impl PySequence { + /// Register a pyclass as a subclass of `collections.abc.Sequence` (from the Python standard + /// library). This is equivalent to `collections.abc.Sequence.register(T)` in Python. + /// This registration is required for a pyclass to be downcastable from `PyAny` to `PySequence`. + pub fn register(py: Python<'_>) -> PyResult<()> { + let ty = T::type_object_bound(py); + get_sequence_abc(py)?.call_method1("register", (ty,))?; + Ok(()) + } +} + +#[cfg(feature = "gil-refs")] impl PySequence { /// Returns the number of objects in sequence. /// /// This is equivalent to the Python expression `len(self)`. #[inline] pub fn len(&self) -> PyResult { - Bound::borrowed_from_gil_ref(&self).len() + self.as_borrowed().len() } /// Returns whether the sequence is empty. #[inline] pub fn is_empty(&self) -> PyResult { - Bound::borrowed_from_gil_ref(&self).is_empty() + self.as_borrowed().is_empty() } /// Returns the concatenation of `self` and `other`. @@ -37,8 +51,8 @@ impl PySequence { /// This is equivalent to the Python expression `self + other`. #[inline] pub fn concat(&self, other: &PySequence) -> PyResult<&PySequence> { - Bound::borrowed_from_gil_ref(&self) - .concat(Bound::borrowed_from_gil_ref(&other)) + self.as_borrowed() + .concat(&other.as_borrowed()) .map(Bound::into_gil_ref) } @@ -47,9 +61,7 @@ impl PySequence { /// This is equivalent to the Python expression `self * count`. #[inline] pub fn repeat(&self, count: usize) -> PyResult<&PySequence> { - Bound::borrowed_from_gil_ref(&self) - .repeat(count) - .map(Bound::into_gil_ref) + self.as_borrowed().repeat(count).map(Bound::into_gil_ref) } /// Concatenates `self` and `other`, in place if possible. @@ -61,8 +73,8 @@ impl PySequence { /// possible, but create and return a new object if not. #[inline] pub fn in_place_concat(&self, other: &PySequence) -> PyResult<&PySequence> { - Bound::borrowed_from_gil_ref(&self) - .in_place_concat(Bound::borrowed_from_gil_ref(&other)) + self.as_borrowed() + .in_place_concat(&other.as_borrowed()) .map(Bound::into_gil_ref) } @@ -75,7 +87,7 @@ impl PySequence { /// possible, but create and return a new object if not. #[inline] pub fn in_place_repeat(&self, count: usize) -> PyResult<&PySequence> { - Bound::borrowed_from_gil_ref(&self) + self.as_borrowed() .in_place_repeat(count) .map(Bound::into_gil_ref) } @@ -85,9 +97,7 @@ impl PySequence { /// This is equivalent to the Python expression `self[index]` without support of negative indices. #[inline] pub fn get_item(&self, index: usize) -> PyResult<&PyAny> { - Bound::borrowed_from_gil_ref(&self) - .get_item(index) - .map(|py2| py2.into_gil_ref()) + self.as_borrowed().get_item(index).map(Bound::into_gil_ref) } /// Returns the slice of sequence object between `begin` and `end`. @@ -95,7 +105,7 @@ impl PySequence { /// This is equivalent to the Python expression `self[begin:end]`. #[inline] pub fn get_slice(&self, begin: usize, end: usize) -> PyResult<&PySequence> { - Bound::borrowed_from_gil_ref(&self) + self.as_borrowed() .get_slice(begin, end) .map(Bound::into_gil_ref) } @@ -108,7 +118,7 @@ impl PySequence { where I: ToPyObject, { - Bound::borrowed_from_gil_ref(&self).set_item(i, item) + self.as_borrowed().set_item(i, item) } /// Deletes the `i`th element of self. @@ -116,7 +126,7 @@ impl PySequence { /// This is equivalent to the Python statement `del self[i]`. #[inline] pub fn del_item(&self, i: usize) -> PyResult<()> { - Bound::borrowed_from_gil_ref(&self).del_item(i) + self.as_borrowed().del_item(i) } /// Assigns the sequence `v` to the slice of `self` from `i1` to `i2`. @@ -124,7 +134,7 @@ impl PySequence { /// This is equivalent to the Python statement `self[i1:i2] = v`. #[inline] pub fn set_slice(&self, i1: usize, i2: usize, v: &PyAny) -> PyResult<()> { - Bound::borrowed_from_gil_ref(&self).set_slice(i1, i2, Bound::borrowed_from_gil_ref(&v)) + self.as_borrowed().set_slice(i1, i2, &v.as_borrowed()) } /// Deletes the slice from `i1` to `i2` from `self`. @@ -132,18 +142,18 @@ impl PySequence { /// This is equivalent to the Python statement `del self[i1:i2]`. #[inline] pub fn del_slice(&self, i1: usize, i2: usize) -> PyResult<()> { - Bound::borrowed_from_gil_ref(&self).del_slice(i1, i2) + self.as_borrowed().del_slice(i1, i2) } /// Returns the number of occurrences of `value` in self, that is, return the /// number of keys for which `self[key] == value`. #[inline] - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] pub fn count(&self, value: V) -> PyResult where V: ToPyObject, { - Bound::borrowed_from_gil_ref(&self).count(value) + self.as_borrowed().count(value) } /// Determines if self contains `value`. @@ -154,7 +164,7 @@ impl PySequence { where V: ToPyObject, { - Bound::borrowed_from_gil_ref(&self).contains(value) + self.as_borrowed().contains(value) } /// Returns the first index `i` for which `self[i] == value`. @@ -165,32 +175,19 @@ impl PySequence { where V: ToPyObject, { - Bound::borrowed_from_gil_ref(&self).index(value) + self.as_borrowed().index(value) } /// Returns a fresh list based on the Sequence. #[inline] pub fn to_list(&self) -> PyResult<&PyList> { - Bound::borrowed_from_gil_ref(&self) - .to_list() - .map(|py2| py2.into_gil_ref()) + self.as_borrowed().to_list().map(Bound::into_gil_ref) } /// Returns a fresh tuple based on the Sequence. #[inline] pub fn to_tuple(&self) -> PyResult<&PyTuple> { - Bound::borrowed_from_gil_ref(&self) - .to_tuple() - .map(|py2| py2.into_gil_ref()) - } - - /// Register a pyclass as a subclass of `collections.abc.Sequence` (from the Python standard - /// library). This is equvalent to `collections.abc.Sequence.register(T)` in Python. - /// This registration is required for a pyclass to be downcastable from `PyAny` to `PySequence`. - pub fn register(py: Python<'_>) -> PyResult<()> { - let ty = T::type_object(py); - get_sequence_abc(py)?.call_method1("register", (ty,))?; - Ok(()) + self.as_borrowed().to_tuple().map(Bound::into_gil_ref) } } @@ -200,7 +197,7 @@ impl PySequence { /// syntax these methods are separated into a trait, because stable Rust does not yet support /// `arbitrary_self_types`. #[doc(alias = "PySequence")] -pub trait PySequenceMethods<'py> { +pub trait PySequenceMethods<'py>: crate::sealed::Sealed { /// Returns the number of objects in sequence. /// /// This is equivalent to the Python expression `len(self)`. @@ -473,23 +470,26 @@ impl<'py> PySequenceMethods<'py> for Bound<'py, PySequence> { } #[inline] +#[cfg(feature = "gil-refs")] fn sequence_len(seq: &PySequence) -> usize { seq.len().expect("failed to get sequence length") } #[inline] +#[cfg(feature = "gil-refs")] fn sequence_slice(seq: &PySequence, start: usize, end: usize) -> &PySequence { seq.get_slice(start, end) .expect("sequence slice operation failed") } +#[cfg(feature = "gil-refs")] index_impls!(PySequence, "sequence", sequence_len, sequence_slice); -impl<'a, T> FromPyObject<'a> for Vec +impl<'py, T> FromPyObject<'py> for Vec where - T: FromPyObject<'a>, + T: FromPyObject<'py>, { - fn extract(obj: &'a PyAny) -> PyResult { + fn extract_bound(obj: &Bound<'py, PyAny>) -> PyResult { if obj.is_instance_of::() { return Err(PyTypeError::new_err("Can't extract `str` to `Vec`")); } @@ -502,17 +502,17 @@ where } } -fn extract_sequence<'s, T>(obj: &'s PyAny) -> PyResult> +fn extract_sequence<'py, T>(obj: &Bound<'py, PyAny>) -> PyResult> where - T: FromPyObject<'s>, + T: FromPyObject<'py>, { // Types that pass `PySequence_Check` usually implement enough of the sequence protocol // to support this function and if not, we will only fail extraction safely. - let seq: &PySequence = unsafe { + let seq = unsafe { if ffi::PySequence_Check(obj.as_ptr()) != 0 { - obj.downcast_unchecked() + obj.downcast_unchecked::() } else { - return Err(PyDowncastError::new(obj, "Sequence").into()); + return Err(DowncastError::new(obj, "Sequence").into()); } }; @@ -523,7 +523,7 @@ where Ok(v) } -fn get_sequence_abc(py: Python<'_>) -> PyResult<&PyType> { +fn get_sequence_abc(py: Python<'_>) -> PyResult<&Bound<'_, PyType>> { static SEQUENCE_ABC: GILOnceCell> = GILOnceCell::new(); SEQUENCE_ABC.get_or_try_init_type_ref(py, "collections.abc", "Sequence") @@ -533,20 +533,21 @@ impl PyTypeCheck for PySequence { const NAME: &'static str = "Sequence"; #[inline] - fn type_check(object: &PyAny) -> bool { + fn type_check(object: &Bound<'_, PyAny>) -> bool { // Using `is_instance` for `collections.abc.Sequence` is slow, so provide // optimized cases for list and tuples as common well-known sequences - PyList::is_type_of(object) - || PyTuple::is_type_of(object) + PyList::is_type_of_bound(object) + || PyTuple::is_type_of_bound(object) || get_sequence_abc(object.py()) .and_then(|abc| object.is_instance(abc)) .unwrap_or_else(|err| { - err.write_unraisable(object.py(), Some(object)); + err.write_unraisable_bound(object.py(), Some(&object.as_borrowed())); false }) } } +#[cfg(feature = "gil-refs")] #[allow(deprecated)] impl<'v> crate::PyTryFrom<'v> for PySequence { /// Downcasting to `PySequence` requires the concrete class to be a subclass (or registered @@ -555,7 +556,7 @@ impl<'v> crate::PyTryFrom<'v> for PySequence { fn try_from>(value: V) -> Result<&'v PySequence, PyDowncastError<'v>> { let value = value.into(); - if PySequence::type_check(value) { + if PySequence::type_check(&value.as_borrowed()) { unsafe { return Ok(value.downcast_unchecked::()) } } @@ -575,13 +576,13 @@ impl<'v> crate::PyTryFrom<'v> for PySequence { #[cfg(test)] mod tests { - use crate::types::{PyList, PySequence, PyTuple}; + use crate::types::{PyAnyMethods, PyList, PySequence, PySequenceMethods, PyTuple}; use crate::{PyObject, Python, ToPyObject}; fn get_object() -> PyObject { // Convenience function for getting a single unique object Python::with_gil(|py| { - let obj = py.eval("object()", None, None).unwrap(); + let obj = py.eval_bound("object()", None, None).unwrap(); obj.to_object(py) }) @@ -591,7 +592,7 @@ mod tests { fn test_numbers_are_not_sequences() { Python::with_gil(|py| { let v = 42i32; - assert!(v.to_object(py).downcast::(py).is_err()); + assert!(v.to_object(py).downcast_bound::(py).is_err()); }); } @@ -599,7 +600,7 @@ mod tests { fn test_strings_are_sequences() { Python::with_gil(|py| { let v = "London Calling"; - assert!(v.to_object(py).downcast::(py).is_ok()); + assert!(v.to_object(py).downcast_bound::(py).is_ok()); }); } @@ -609,7 +610,6 @@ mod tests { let v = "London Calling"; let ob = v.to_object(py); - assert!(ob.extract::>(py).is_err()); assert!(ob.extract::>(py).is_err()); assert!(ob.extract::>(py).is_err()); }); @@ -620,7 +620,7 @@ mod tests { Python::with_gil(|py| { let v: Vec = vec![]; let ob = v.to_object(py); - let seq = ob.downcast::(py).unwrap(); + let seq = ob.downcast_bound::(py).unwrap(); assert_eq!(0, seq.len().unwrap()); let needle = 7i32.to_object(py); @@ -632,11 +632,11 @@ mod tests { fn test_seq_is_empty() { Python::with_gil(|py| { let list = vec![1].to_object(py); - let seq = list.downcast::(py).unwrap(); + let seq = list.downcast_bound::(py).unwrap(); assert!(!seq.is_empty().unwrap()); let vec: Vec = Vec::new(); let empty_list = vec.to_object(py); - let empty_seq = empty_list.downcast::(py).unwrap(); + let empty_seq = empty_list.downcast_bound::(py).unwrap(); assert!(empty_seq.is_empty().unwrap()); }); } @@ -646,7 +646,7 @@ mod tests { Python::with_gil(|py| { let v: Vec = vec![1, 1, 2, 3, 5, 8]; let ob = v.to_object(py); - let seq = ob.downcast::(py).unwrap(); + let seq = ob.downcast_bound::(py).unwrap(); assert_eq!(6, seq.len().unwrap()); let bad_needle = 7i32.to_object(py); @@ -665,7 +665,7 @@ mod tests { Python::with_gil(|py| { let v: Vec = vec![1, 1, 2, 3, 5, 8]; let ob = v.to_object(py); - let seq = ob.downcast::(py).unwrap(); + let seq = ob.downcast_bound::(py).unwrap(); assert_eq!(1, seq.get_item(0).unwrap().extract::().unwrap()); assert_eq!(1, seq.get_item(1).unwrap().extract::().unwrap()); assert_eq!(2, seq.get_item(2).unwrap().extract::().unwrap()); @@ -677,6 +677,8 @@ mod tests { } #[test] + #[cfg(feature = "gil-refs")] + #[allow(deprecated)] fn test_seq_index_trait() { Python::with_gil(|py| { let v: Vec = vec![1, 1, 2]; @@ -690,6 +692,8 @@ mod tests { #[test] #[should_panic = "index 7 out of range for sequence"] + #[cfg(feature = "gil-refs")] + #[allow(deprecated)] fn test_seq_index_trait_panic() { Python::with_gil(|py| { let v: Vec = vec![1, 1, 2]; @@ -700,6 +704,8 @@ mod tests { } #[test] + #[cfg(feature = "gil-refs")] + #[allow(deprecated)] fn test_seq_index_trait_ranges() { Python::with_gil(|py| { let v: Vec = vec![1, 1, 2]; @@ -718,6 +724,8 @@ mod tests { #[test] #[should_panic = "range start index 5 out of range for sequence of length 3"] + #[cfg(feature = "gil-refs")] + #[allow(deprecated)] fn test_seq_index_trait_range_panic_start() { Python::with_gil(|py| { let v: Vec = vec![1, 1, 2]; @@ -729,6 +737,8 @@ mod tests { #[test] #[should_panic = "range end index 10 out of range for sequence of length 3"] + #[cfg(feature = "gil-refs")] + #[allow(deprecated)] fn test_seq_index_trait_range_panic_end() { Python::with_gil(|py| { let v: Vec = vec![1, 1, 2]; @@ -740,6 +750,8 @@ mod tests { #[test] #[should_panic = "slice index starts at 2 but ends at 1"] + #[cfg(feature = "gil-refs")] + #[allow(deprecated)] fn test_seq_index_trait_range_panic_wrong_order() { Python::with_gil(|py| { let v: Vec = vec![1, 1, 2]; @@ -752,6 +764,8 @@ mod tests { #[test] #[should_panic = "range start index 8 out of range for sequence of length 3"] + #[cfg(feature = "gil-refs")] + #[allow(deprecated)] fn test_seq_index_trait_range_from_panic() { Python::with_gil(|py| { let v: Vec = vec![1, 1, 2]; @@ -766,19 +780,19 @@ mod tests { Python::with_gil(|py| { let v: Vec = vec![1, 1, 2, 3, 5, 8]; let ob = v.to_object(py); - let seq = ob.downcast::(py).unwrap(); + let seq = ob.downcast_bound::(py).unwrap(); assert!(seq.del_item(10).is_err()); - assert_eq!(1, seq[0].extract::().unwrap()); + assert_eq!(1, seq.get_item(0).unwrap().extract::().unwrap()); assert!(seq.del_item(0).is_ok()); - assert_eq!(1, seq[0].extract::().unwrap()); + assert_eq!(1, seq.get_item(0).unwrap().extract::().unwrap()); assert!(seq.del_item(0).is_ok()); - assert_eq!(2, seq[0].extract::().unwrap()); + assert_eq!(2, seq.get_item(0).unwrap().extract::().unwrap()); assert!(seq.del_item(0).is_ok()); - assert_eq!(3, seq[0].extract::().unwrap()); + assert_eq!(3, seq.get_item(0).unwrap().extract::().unwrap()); assert!(seq.del_item(0).is_ok()); - assert_eq!(5, seq[0].extract::().unwrap()); + assert_eq!(5, seq.get_item(0).unwrap().extract::().unwrap()); assert!(seq.del_item(0).is_ok()); - assert_eq!(8, seq[0].extract::().unwrap()); + assert_eq!(8, seq.get_item(0).unwrap().extract::().unwrap()); assert!(seq.del_item(0).is_ok()); assert_eq!(0, seq.len().unwrap()); assert!(seq.del_item(0).is_err()); @@ -790,10 +804,10 @@ mod tests { Python::with_gil(|py| { let v: Vec = vec![1, 2]; let ob = v.to_object(py); - let seq = ob.downcast::(py).unwrap(); - assert_eq!(2, seq[1].extract::().unwrap()); + let seq = ob.downcast_bound::(py).unwrap(); + assert_eq!(2, seq.get_item(1).unwrap().extract::().unwrap()); assert!(seq.set_item(1, 10).is_ok()); - assert_eq!(10, seq[1].extract::().unwrap()); + assert_eq!(10, seq.get_item(1).unwrap().extract::().unwrap()); }); } @@ -804,12 +818,12 @@ mod tests { Python::with_gil(|py| { let v: Vec = vec![1, 2]; let ob = v.to_object(py); - let seq = ob.downcast::(py).unwrap(); + let seq = ob.downcast_bound::(py).unwrap(); assert!(seq.set_item(1, &obj).is_ok()); - assert!(seq[1].as_ptr() == obj.as_ptr()); + assert!(seq.get_item(1).unwrap().as_ptr() == obj.as_ptr()); }); - Python::with_gil(|py| { + Python::with_gil(move |py| { assert_eq!(1, obj.get_refcnt(py)); }); } @@ -819,7 +833,7 @@ mod tests { Python::with_gil(|py| { let v: Vec = vec![1, 1, 2, 3, 5, 8]; let ob = v.to_object(py); - let seq = ob.downcast::(py).unwrap(); + let seq = ob.downcast_bound::(py).unwrap(); assert_eq!( [1, 2, 3], seq.get_slice(1, 4).unwrap().extract::<[i32; 3]>().unwrap() @@ -840,11 +854,11 @@ mod tests { let v: Vec = vec![1, 1, 2, 3, 5, 8]; let w: Vec = vec![7, 4]; let ob = v.to_object(py); - let seq = ob.downcast::(py).unwrap(); + let seq = ob.downcast_bound::(py).unwrap(); let ins = w.to_object(py); - seq.set_slice(1, 4, ins.as_ref(py)).unwrap(); + seq.set_slice(1, 4, ins.bind(py)).unwrap(); assert_eq!([1, 7, 4, 5, 8], seq.extract::<[i32; 5]>().unwrap()); - seq.set_slice(3, 100, PyList::empty(py)).unwrap(); + seq.set_slice(3, 100, &PyList::empty_bound(py)).unwrap(); assert_eq!([1, 7, 4], seq.extract::<[i32; 3]>().unwrap()); }); } @@ -854,7 +868,7 @@ mod tests { Python::with_gil(|py| { let v: Vec = vec![1, 1, 2, 3, 5, 8]; let ob = v.to_object(py); - let seq = ob.downcast::(py).unwrap(); + let seq = ob.downcast_bound::(py).unwrap(); seq.del_slice(1, 4).unwrap(); assert_eq!([1, 5, 8], seq.extract::<[i32; 3]>().unwrap()); seq.del_slice(1, 100).unwrap(); @@ -867,7 +881,7 @@ mod tests { Python::with_gil(|py| { let v: Vec = vec![1, 1, 2, 3, 5, 8]; let ob = v.to_object(py); - let seq = ob.downcast::(py).unwrap(); + let seq = ob.downcast_bound::(py).unwrap(); assert_eq!(0, seq.index(1i32).unwrap()); assert_eq!(2, seq.index(2i32).unwrap()); assert_eq!(3, seq.index(3i32).unwrap()); @@ -878,12 +892,12 @@ mod tests { } #[test] - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] fn test_seq_count() { Python::with_gil(|py| { let v: Vec = vec![1, 1, 2, 3, 5, 8]; let ob = v.to_object(py); - let seq = ob.downcast::(py).unwrap(); + let seq = ob.downcast_bound::(py).unwrap(); assert_eq!(2, seq.count(1i32).unwrap()); assert_eq!(1, seq.count(2i32).unwrap()); assert_eq!(1, seq.count(3i32).unwrap()); @@ -898,7 +912,7 @@ mod tests { Python::with_gil(|py| { let v: Vec = vec![1, 1, 2, 3, 5, 8]; let ob = v.to_object(py); - let seq = ob.downcast::(py).unwrap(); + let seq = ob.downcast_bound::(py).unwrap(); let mut idx = 0; for el in seq.iter().unwrap() { assert_eq!(v[idx], el.unwrap().extract::().unwrap()); @@ -913,7 +927,7 @@ mod tests { Python::with_gil(|py| { let v = vec!["It", "was", "the", "worst", "of", "times"]; let ob = v.to_object(py); - let seq = ob.downcast::(py).unwrap(); + let seq = ob.downcast_bound::(py).unwrap(); let bad_needle = "blurst".to_object(py); assert!(!seq.contains(bad_needle).unwrap()); @@ -928,7 +942,7 @@ mod tests { Python::with_gil(|py| { let v: Vec = vec![1, 2, 3]; let ob = v.to_object(py); - let seq = ob.downcast::(py).unwrap(); + let seq = ob.downcast_bound::(py).unwrap(); let concat_seq = seq.concat(seq).unwrap(); assert_eq!(6, concat_seq.len().unwrap()); let concat_v: Vec = vec![1, 2, 3, 1, 2, 3]; @@ -943,7 +957,7 @@ mod tests { Python::with_gil(|py| { let v = "string"; let ob = v.to_object(py); - let seq = ob.downcast::(py).unwrap(); + let seq = ob.downcast_bound::(py).unwrap(); let concat_seq = seq.concat(seq).unwrap(); assert_eq!(12, concat_seq.len().unwrap()); let concat_v = "stringstring".to_owned(); @@ -958,7 +972,7 @@ mod tests { Python::with_gil(|py| { let v = vec!["foo", "bar"]; let ob = v.to_object(py); - let seq = ob.downcast::(py).unwrap(); + let seq = ob.downcast_bound::(py).unwrap(); let repeat_seq = seq.repeat(3).unwrap(); assert_eq!(6, repeat_seq.len().unwrap()); let repeated = ["foo", "bar", "foo", "bar", "foo", "bar"]; @@ -973,14 +987,14 @@ mod tests { Python::with_gil(|py| { let v = vec!["foo", "bar"]; let ob = v.to_object(py); - let seq = ob.downcast::(py).unwrap(); + let seq = ob.downcast_bound::(py).unwrap(); let rep_seq = seq.in_place_repeat(3).unwrap(); assert_eq!(6, seq.len().unwrap()); - assert!(seq.is(rep_seq)); + assert!(seq.is(&rep_seq)); let conc_seq = seq.in_place_concat(seq).unwrap(); assert_eq!(12, seq.len().unwrap()); - assert!(seq.is(conc_seq)); + assert!(seq.is(&conc_seq)); }); } @@ -989,8 +1003,12 @@ mod tests { Python::with_gil(|py| { let v = vec!["foo", "bar"]; let ob = v.to_object(py); - let seq = ob.downcast::(py).unwrap(); - assert!(seq.to_list().unwrap().eq(PyList::new(py, &v)).unwrap()); + let seq = ob.downcast_bound::(py).unwrap(); + assert!(seq + .to_list() + .unwrap() + .eq(PyList::new_bound(py, &v)) + .unwrap()); }); } @@ -999,11 +1017,11 @@ mod tests { Python::with_gil(|py| { let v = "foo"; let ob = v.to_object(py); - let seq: &PySequence = ob.downcast(py).unwrap(); + let seq = ob.downcast_bound::(py).unwrap(); assert!(seq .to_list() .unwrap() - .eq(PyList::new(py, ["f", "o", "o"])) + .eq(PyList::new_bound(py, ["f", "o", "o"])) .unwrap()); }); } @@ -1013,11 +1031,11 @@ mod tests { Python::with_gil(|py| { let v = ("foo", "bar"); let ob = v.to_object(py); - let seq = ob.downcast::(py).unwrap(); + let seq = ob.downcast_bound::(py).unwrap(); assert!(seq .to_tuple() .unwrap() - .eq(PyTuple::new(py, ["foo", "bar"])) + .eq(PyTuple::new_bound(py, ["foo", "bar"])) .unwrap()); }); } @@ -1027,15 +1045,23 @@ mod tests { Python::with_gil(|py| { let v = vec!["foo", "bar"]; let ob = v.to_object(py); - let seq = ob.downcast::(py).unwrap(); - assert!(seq.to_tuple().unwrap().eq(PyTuple::new(py, &v)).unwrap()); + let seq = ob.downcast_bound::(py).unwrap(); + assert!(seq + .to_tuple() + .unwrap() + .eq(PyTuple::new_bound(py, &v)) + .unwrap()); }); } #[test] fn test_extract_tuple_to_vec() { Python::with_gil(|py| { - let v: Vec = py.eval("(1, 2)", None, None).unwrap().extract().unwrap(); + let v: Vec = py + .eval_bound("(1, 2)", None, None) + .unwrap() + .extract() + .unwrap(); assert!(v == [1, 2]); }); } @@ -1044,7 +1070,7 @@ mod tests { fn test_extract_range_to_vec() { Python::with_gil(|py| { let v: Vec = py - .eval("range(1, 5)", None, None) + .eval_bound("range(1, 5)", None, None) .unwrap() .extract() .unwrap(); @@ -1056,7 +1082,7 @@ mod tests { fn test_extract_bytearray_to_vec() { Python::with_gil(|py| { let v: Vec = py - .eval("bytearray(b'abc')", None, None) + .eval_bound("bytearray(b'abc')", None, None) .unwrap() .extract() .unwrap(); @@ -1069,7 +1095,7 @@ mod tests { Python::with_gil(|py| { let v = vec!["foo", "bar"]; let ob = v.to_object(py); - let seq = ob.downcast::(py).unwrap(); + let seq = ob.downcast_bound::(py).unwrap(); let type_ptr = seq.as_ref(); let seq_from = unsafe { type_ptr.downcast_unchecked::() }; assert!(seq_from.to_list().is_ok()); @@ -1077,6 +1103,7 @@ mod tests { } #[test] + #[cfg(feature = "gil-refs")] #[allow(deprecated)] fn test_seq_try_from() { use crate::PyTryFrom; diff --git a/src/types/set.rs b/src/types/set.rs index 68b89d656f3..1bc4c86be51 100644 --- a/src/types/set.rs +++ b/src/types/set.rs @@ -1,8 +1,12 @@ -#[cfg(Py_LIMITED_API)] use crate::types::PyIterator; +#[cfg(feature = "gil-refs")] +use crate::PyNativeType; use crate::{ err::{self, PyErr, PyResult}, - Py, + ffi_ptr_ext::FfiPtrExt, + instance::Bound, + py_result_ext::PyResultExt, + types::any::PyAnyMethods, }; use crate::{ffi, PyAny, PyObject, Python, ToPyObject}; use std::ptr; @@ -11,7 +15,7 @@ use std::ptr; #[repr(transparent)] pub struct PySet(PyAny); -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] pyobject_native_type!( PySet, ffi::PySetObject, @@ -19,7 +23,7 @@ pyobject_native_type!( #checkfunction=ffi::PySet_Check ); -#[cfg(PyPy)] +#[cfg(any(PyPy, GraalPy))] pyobject_native_type_core!( PySet, pyobject_native_static_type_object!(ffi::PySet_Type), @@ -31,24 +35,51 @@ impl PySet { /// /// Returns an error if some element is not hashable. #[inline] + pub fn new_bound<'a, 'p, T: ToPyObject + 'a>( + py: Python<'p>, + elements: impl IntoIterator, + ) -> PyResult> { + new_from_iter(py, elements) + } + + /// Creates a new empty set. + pub fn empty_bound(py: Python<'_>) -> PyResult> { + unsafe { + ffi::PySet_New(ptr::null_mut()) + .assume_owned_or_err(py) + .downcast_into_unchecked() + } + } +} + +#[cfg(feature = "gil-refs")] +impl PySet { + /// Deprecated form of [`PySet::new_bound`]. + #[deprecated( + since = "0.21.0", + note = "`PySet::new` will be replaced by `PySet::new_bound` in a future PyO3 version" + )] + #[inline] pub fn new<'a, 'p, T: ToPyObject + 'a>( py: Python<'p>, elements: impl IntoIterator, ) -> PyResult<&'p PySet> { - new_from_iter(py, elements).map(|set| set.into_ref(py)) + Self::new_bound(py, elements).map(Bound::into_gil_ref) } - /// Creates a new empty set. + /// Deprecated form of [`PySet::empty_bound`]. + #[deprecated( + since = "0.21.2", + note = "`PySet::empty` will be replaced by `PySet::empty_bound` in a future PyO3 version" + )] pub fn empty(py: Python<'_>) -> PyResult<&PySet> { - unsafe { py.from_owned_ptr_or_err(ffi::PySet_New(ptr::null_mut())) } + Self::empty_bound(py).map(Bound::into_gil_ref) } /// Removes all elements from the set. #[inline] pub fn clear(&self) { - unsafe { - ffi::PySet_Clear(self.as_ptr()); - } + self.as_borrowed().clear() } /// Returns the number of items in the set. @@ -56,12 +87,12 @@ impl PySet { /// This is equivalent to the Python expression `len(self)`. #[inline] pub fn len(&self) -> usize { - unsafe { ffi::PySet_Size(self.as_ptr()) as usize } + self.as_borrowed().len() } /// Checks if set is empty. pub fn is_empty(&self) -> bool { - self.len() == 0 + self.as_borrowed().is_empty() } /// Determines if the set contains the specified key. @@ -71,7 +102,110 @@ impl PySet { where K: ToPyObject, { - fn inner(set: &PySet, key: PyObject) -> PyResult { + self.as_borrowed().contains(key) + } + + /// Removes the element from the set if it is present. + /// + /// Returns `true` if the element was present in the set. + pub fn discard(&self, key: K) -> PyResult + where + K: ToPyObject, + { + self.as_borrowed().discard(key) + } + + /// Adds an element to the set. + pub fn add(&self, key: K) -> PyResult<()> + where + K: ToPyObject, + { + self.as_borrowed().add(key) + } + + /// Removes and returns an arbitrary element from the set. + pub fn pop(&self) -> Option { + self.as_borrowed().pop().map(Bound::unbind) + } + + /// Returns an iterator of values in this set. + /// + /// # Panics + /// + /// If PyO3 detects that the set is mutated during iteration, it will panic. + pub fn iter(&self) -> PySetIterator<'_> { + PySetIterator(BoundSetIterator::new(self.as_borrowed().to_owned())) + } +} + +/// Implementation of functionality for [`PySet`]. +/// +/// These methods are defined for the `Bound<'py, PySet>` smart pointer, so to use method call +/// syntax these methods are separated into a trait, because stable Rust does not yet support +/// `arbitrary_self_types`. +#[doc(alias = "PySet")] +pub trait PySetMethods<'py>: crate::sealed::Sealed { + /// Removes all elements from the set. + fn clear(&self); + + /// Returns the number of items in the set. + /// + /// This is equivalent to the Python expression `len(self)`. + fn len(&self) -> usize; + + /// Checks if set is empty. + fn is_empty(&self) -> bool { + self.len() == 0 + } + + /// Determines if the set contains the specified key. + /// + /// This is equivalent to the Python expression `key in self`. + fn contains(&self, key: K) -> PyResult + where + K: ToPyObject; + + /// Removes the element from the set if it is present. + /// + /// Returns `true` if the element was present in the set. + fn discard(&self, key: K) -> PyResult + where + K: ToPyObject; + + /// Adds an element to the set. + fn add(&self, key: K) -> PyResult<()> + where + K: ToPyObject; + + /// Removes and returns an arbitrary element from the set. + fn pop(&self) -> Option>; + + /// Returns an iterator of values in this set. + /// + /// # Panics + /// + /// If PyO3 detects that the set is mutated during iteration, it will panic. + fn iter(&self) -> BoundSetIterator<'py>; +} + +impl<'py> PySetMethods<'py> for Bound<'py, PySet> { + #[inline] + fn clear(&self) { + unsafe { + ffi::PySet_Clear(self.as_ptr()); + } + } + + #[inline] + fn len(&self) -> usize { + unsafe { ffi::PySet_Size(self.as_ptr()) as usize } + } + + fn contains(&self, key: K) -> PyResult + where + K: ToPyObject, + { + fn inner(set: &Bound<'_, PySet>, key: Bound<'_, PyAny>) -> PyResult { match unsafe { ffi::PySet_Contains(set.as_ptr(), key.as_ptr()) } { 1 => Ok(true), 0 => Ok(false), @@ -79,17 +213,15 @@ impl PySet { } } - inner(self, key.to_object(self.py())) + let py = self.py(); + inner(self, key.to_object(py).into_bound(py)) } - /// Removes the element from the set if it is present. - /// - /// Returns `true` if the element was present in the set. - pub fn discard(&self, key: K) -> PyResult + fn discard(&self, key: K) -> PyResult where K: ToPyObject, { - fn inner(set: &PySet, key: PyObject) -> PyResult { + fn inner(set: &Bound<'_, PySet>, key: Bound<'_, PyAny>) -> PyResult { match unsafe { ffi::PySet_Discard(set.as_ptr(), key.as_ptr()) } { 1 => Ok(true), 0 => Ok(false), @@ -97,162 +229,160 @@ impl PySet { } } - inner(self, key.to_object(self.py())) + let py = self.py(); + inner(self, key.to_object(py).into_bound(py)) } - /// Adds an element to the set. - pub fn add(&self, key: K) -> PyResult<()> + fn add(&self, key: K) -> PyResult<()> where K: ToPyObject, { - fn inner(set: &PySet, key: PyObject) -> PyResult<()> { + fn inner(set: &Bound<'_, PySet>, key: Bound<'_, PyAny>) -> PyResult<()> { err::error_on_minusone(set.py(), unsafe { ffi::PySet_Add(set.as_ptr(), key.as_ptr()) }) } - inner(self, key.to_object(self.py())) + let py = self.py(); + inner(self, key.to_object(py).into_bound(py)) } - /// Removes and returns an arbitrary element from the set. - pub fn pop(&self) -> Option { - let element = - unsafe { PyObject::from_owned_ptr_or_err(self.py(), ffi::PySet_Pop(self.as_ptr())) }; + fn pop(&self) -> Option> { + let element = unsafe { ffi::PySet_Pop(self.as_ptr()).assume_owned_or_err(self.py()) }; match element { Ok(e) => Some(e), Err(_) => None, } } - /// Returns an iterator of values in this set. + fn iter(&self) -> BoundSetIterator<'py> { + BoundSetIterator::new(self.clone()) + } +} + +/// PyO3 implementation of an iterator for a Python `set` object. +#[cfg(feature = "gil-refs")] +pub struct PySetIterator<'py>(BoundSetIterator<'py>); + +#[cfg(feature = "gil-refs")] +impl<'py> Iterator for PySetIterator<'py> { + type Item = &'py super::PyAny; + + /// Advances the iterator and returns the next value. /// /// # Panics /// /// If PyO3 detects that the set is mutated during iteration, it will panic. - pub fn iter(&self) -> PySetIterator<'_> { - IntoIterator::into_iter(self) + #[inline] + fn next(&mut self) -> Option { + self.0.next().map(Bound::into_gil_ref) + } + + fn size_hint(&self) -> (usize, Option) { + self.0.size_hint() } } -#[cfg(Py_LIMITED_API)] -mod impl_ { - use super::*; - - impl<'a> std::iter::IntoIterator for &'a PySet { - type Item = &'a PyAny; - type IntoIter = PySetIterator<'a>; - - /// Returns an iterator of values in this set. - /// - /// # Panics - /// - /// If PyO3 detects that the set is mutated during iteration, it will panic. - fn into_iter(self) -> Self::IntoIter { - PySetIterator { - it: PyIterator::from_object(self).unwrap(), - } - } +#[cfg(feature = "gil-refs")] +impl ExactSizeIterator for PySetIterator<'_> { + fn len(&self) -> usize { + self.0.len() + } +} + +#[cfg(feature = "gil-refs")] +impl<'py> IntoIterator for &'py PySet { + type Item = &'py PyAny; + type IntoIter = PySetIterator<'py>; + /// Returns an iterator of values in this set. + /// + /// # Panics + /// + /// If PyO3 detects that the set is mutated during iteration, it will panic. + fn into_iter(self) -> Self::IntoIter { + PySetIterator(BoundSetIterator::new(self.as_borrowed().to_owned())) } +} + +impl<'py> IntoIterator for Bound<'py, PySet> { + type Item = Bound<'py, PyAny>; + type IntoIter = BoundSetIterator<'py>; - /// PyO3 implementation of an iterator for a Python `set` object. - pub struct PySetIterator<'p> { - it: &'p PyIterator, + /// Returns an iterator of values in this set. + /// + /// # Panics + /// + /// If PyO3 detects that the set is mutated during iteration, it will panic. + fn into_iter(self) -> Self::IntoIter { + BoundSetIterator::new(self) } +} - impl<'py> Iterator for PySetIterator<'py> { - type Item = &'py super::PyAny; +impl<'py> IntoIterator for &Bound<'py, PySet> { + type Item = Bound<'py, PyAny>; + type IntoIter = BoundSetIterator<'py>; - /// Advances the iterator and returns the next value. - /// - /// # Panics - /// - /// If PyO3 detects that the set is mutated during iteration, it will panic. - #[inline] - fn next(&mut self) -> Option { - self.it.next().map(Result::unwrap) - } + /// Returns an iterator of values in this set. + /// + /// # Panics + /// + /// If PyO3 detects that the set is mutated during iteration, it will panic. + fn into_iter(self) -> Self::IntoIter { + self.iter() } } -#[cfg(not(Py_LIMITED_API))] -mod impl_ { - use super::*; - - /// PyO3 implementation of an iterator for a Python `set` object. - pub struct PySetIterator<'py> { - set: &'py super::PySet, - pos: ffi::Py_ssize_t, - used: ffi::Py_ssize_t, - } - - impl<'a> std::iter::IntoIterator for &'a PySet { - type Item = &'a PyAny; - type IntoIter = PySetIterator<'a>; - /// Returns an iterator of values in this set. - /// - /// # Panics - /// - /// If PyO3 detects that the set is mutated during iteration, it will panic. - fn into_iter(self) -> Self::IntoIter { - PySetIterator { - set: self, - pos: 0, - used: unsafe { ffi::PySet_Size(self.as_ptr()) }, - } +/// PyO3 implementation of an iterator for a Python `set` object. +pub struct BoundSetIterator<'p> { + it: Bound<'p, PyIterator>, + // Remaining elements in the set. This is fine to store because + // Python will error if the set changes size during iteration. + remaining: usize, +} + +impl<'py> BoundSetIterator<'py> { + pub(super) fn new(set: Bound<'py, PySet>) -> Self { + Self { + it: PyIterator::from_bound_object(&set).unwrap(), + remaining: set.len(), } } +} - impl<'py> Iterator for PySetIterator<'py> { - type Item = &'py super::PyAny; - - /// Advances the iterator and returns the next value. - /// - /// # Panics - /// - /// If PyO3 detects that the set is mutated during iteration, it will panic. - #[inline] - fn next(&mut self) -> Option { - unsafe { - let len = ffi::PySet_Size(self.set.as_ptr()); - assert_eq!(self.used, len, "Set changed size during iteration"); - - let mut key: *mut ffi::PyObject = std::ptr::null_mut(); - let mut hash: ffi::Py_hash_t = 0; - if ffi::_PySet_NextEntry(self.set.as_ptr(), &mut self.pos, &mut key, &mut hash) != 0 - { - // _PySet_NextEntry returns borrowed object; for safety must make owned (see #890) - Some(self.set.py().from_owned_ptr(ffi::_Py_NewRef(key))) - } else { - None - } - } - } +impl<'py> Iterator for BoundSetIterator<'py> { + type Item = Bound<'py, super::PyAny>; - #[inline] - fn size_hint(&self) -> (usize, Option) { - let len = self.len(); - (len, Some(len)) - } + /// Advances the iterator and returns the next value. + fn next(&mut self) -> Option { + self.remaining = self.remaining.saturating_sub(1); + self.it.next().map(Result::unwrap) } - impl<'py> ExactSizeIterator for PySetIterator<'py> { - fn len(&self) -> usize { - self.set.len().saturating_sub(self.pos as usize) - } + fn size_hint(&self) -> (usize, Option) { + (self.remaining, Some(self.remaining)) } } -pub use impl_::*; +impl<'py> ExactSizeIterator for BoundSetIterator<'py> { + fn len(&self) -> usize { + self.remaining + } +} #[inline] pub(crate) fn new_from_iter( py: Python<'_>, elements: impl IntoIterator, -) -> PyResult> { - fn inner(py: Python<'_>, elements: &mut dyn Iterator) -> PyResult> { - let set: Py = unsafe { +) -> PyResult> { + fn inner<'py>( + py: Python<'py>, + elements: &mut dyn Iterator, + ) -> PyResult> { + let set = unsafe { // We create the `Py` pointer because its Drop cleans up the set if user code panics. - Py::from_owned_ptr_or_err(py, ffi::PySet_New(std::ptr::null_mut()))? + ffi::PySet_New(std::ptr::null_mut()) + .assume_owned_or_err(py)? + .downcast_into_unchecked() }; let ptr = set.as_ptr(); @@ -270,25 +400,29 @@ pub(crate) fn new_from_iter( #[cfg(test)] mod tests { use super::PySet; - use crate::{Python, ToPyObject}; + use crate::{ + types::{PyAnyMethods, PySetMethods}, + Python, ToPyObject, + }; use std::collections::HashSet; #[test] fn test_set_new() { Python::with_gil(|py| { - let set = PySet::new(py, &[1]).unwrap(); + let set = PySet::new_bound(py, &[1]).unwrap(); assert_eq!(1, set.len()); let v = vec![1]; - assert!(PySet::new(py, &[v]).is_err()); + assert!(PySet::new_bound(py, &[v]).is_err()); }); } #[test] fn test_set_empty() { Python::with_gil(|py| { - let set = PySet::empty(py).unwrap(); + let set = PySet::empty_bound(py).unwrap(); assert_eq!(0, set.len()); + assert!(set.is_empty()); }); } @@ -297,11 +431,11 @@ mod tests { Python::with_gil(|py| { let mut v = HashSet::new(); let ob = v.to_object(py); - let set: &PySet = ob.downcast(py).unwrap(); + let set = ob.downcast_bound::(py).unwrap(); assert_eq!(0, set.len()); v.insert(7); let ob = v.to_object(py); - let set2: &PySet = ob.downcast(py).unwrap(); + let set2 = ob.downcast_bound::(py).unwrap(); assert_eq!(1, set2.len()); }); } @@ -309,7 +443,7 @@ mod tests { #[test] fn test_set_clear() { Python::with_gil(|py| { - let set = PySet::new(py, &[1]).unwrap(); + let set = PySet::new_bound(py, &[1]).unwrap(); assert_eq!(1, set.len()); set.clear(); assert_eq!(0, set.len()); @@ -319,7 +453,7 @@ mod tests { #[test] fn test_set_contains() { Python::with_gil(|py| { - let set = PySet::new(py, &[1]).unwrap(); + let set = PySet::new_bound(py, &[1]).unwrap(); assert!(set.contains(1).unwrap()); }); } @@ -327,7 +461,7 @@ mod tests { #[test] fn test_set_discard() { Python::with_gil(|py| { - let set = PySet::new(py, &[1]).unwrap(); + let set = PySet::new_bound(py, &[1]).unwrap(); assert!(!set.discard(2).unwrap()); assert_eq!(1, set.len()); @@ -342,7 +476,7 @@ mod tests { #[test] fn test_set_add() { Python::with_gil(|py| { - let set = PySet::new(py, &[1, 2]).unwrap(); + let set = PySet::new_bound(py, &[1, 2]).unwrap(); set.add(1).unwrap(); // Add a dupliated element assert!(set.contains(1).unwrap()); }); @@ -351,13 +485,13 @@ mod tests { #[test] fn test_set_pop() { Python::with_gil(|py| { - let set = PySet::new(py, &[1]).unwrap(); + let set = PySet::new_bound(py, &[1]).unwrap(); let val = set.pop(); assert!(val.is_some()); let val2 = set.pop(); assert!(val2.is_none()); assert!(py - .eval("print('Exception state should not be set.')", None, None) + .eval_bound("print('Exception state should not be set.')", None, None) .is_ok()); }); } @@ -365,16 +499,23 @@ mod tests { #[test] fn test_set_iter() { Python::with_gil(|py| { - let set = PySet::new(py, &[1]).unwrap(); + let set = PySet::new_bound(py, &[1]).unwrap(); - // iter method for el in set { assert_eq!(1i32, el.extract::<'_, i32>().unwrap()); } + }); + } - // intoiterator iteration - for el in set { - assert_eq!(1i32, el.extract::<'_, i32>().unwrap()); + #[test] + fn test_set_iter_bound() { + use crate::types::any::PyAnyMethods; + + Python::with_gil(|py| { + let set = PySet::new_bound(py, &[1]).unwrap(); + + for el in &set { + assert_eq!(1i32, el.extract::().unwrap()); } }); } @@ -383,9 +524,9 @@ mod tests { #[should_panic] fn test_set_iter_mutation() { Python::with_gil(|py| { - let set = PySet::new(py, &[1, 2, 3, 4, 5]).unwrap(); + let set = PySet::new_bound(py, &[1, 2, 3, 4, 5]).unwrap(); - for _ in set { + for _ in &set { let _ = set.add(42); } }); @@ -395,9 +536,9 @@ mod tests { #[should_panic] fn test_set_iter_mutation_same_len() { Python::with_gil(|py| { - let set = PySet::new(py, &[1, 2, 3, 4, 5]).unwrap(); + let set = PySet::new_bound(py, &[1, 2, 3, 4, 5]).unwrap(); - for item in set { + for item in &set { let item: i32 = item.extract().unwrap(); let _ = set.del_item(item); let _ = set.add(item + 10); @@ -408,17 +549,15 @@ mod tests { #[test] fn test_set_iter_size_hint() { Python::with_gil(|py| { - let set = PySet::new(py, &[1]).unwrap(); - + let set = PySet::new_bound(py, &[1]).unwrap(); let mut iter = set.iter(); - if cfg!(Py_LIMITED_API) { - assert_eq!(iter.size_hint(), (0, None)); - } else { - assert_eq!(iter.size_hint(), (1, Some(1))); - iter.next(); - assert_eq!(iter.size_hint(), (0, Some(0))); - } + // Exact size + assert_eq!(iter.len(), 1); + assert_eq!(iter.size_hint(), (1, Some(1))); + iter.next(); + assert_eq!(iter.len(), 0); + assert_eq!(iter.size_hint(), (0, Some(0))); }); } } diff --git a/src/types/slice.rs b/src/types/slice.rs index 53e4f4af992..7daa2c030b6 100644 --- a/src/types/slice.rs +++ b/src/types/slice.rs @@ -1,11 +1,14 @@ use crate::err::{PyErr, PyResult}; -use crate::ffi::{self, Py_ssize_t}; -use crate::{PyAny, PyObject, Python, ToPyObject}; -use std::os::raw::c_long; +use crate::ffi; +use crate::ffi_ptr_ext::FfiPtrExt; +use crate::types::any::PyAnyMethods; +#[cfg(feature = "gil-refs")] +use crate::PyNativeType; +use crate::{Bound, PyAny, PyObject, Python, ToPyObject}; /// Represents a Python `slice`. /// -/// Only `c_long` indices supported at the moment by the `PySlice` object. +/// Only `isize` indices supported at the moment by the `PySlice` object. #[repr(transparent)] pub struct PySlice(PyAny); @@ -16,17 +19,21 @@ pyobject_native_type!( #checkfunction=ffi::PySlice_Check ); -/// Return value from [`PySlice::indices`]. +/// Return value from [`PySliceMethods::indices`]. #[derive(Debug, Eq, PartialEq)] pub struct PySliceIndices { /// Start of the slice + /// + /// It can be -1 when the step is negative, otherwise it's non-negative. pub start: isize, /// End of the slice + /// + /// It can be -1 when the step is negative, otherwise it's non-negative. pub stop: isize, /// Increment to use when iterating the slice from `start` to `stop`. pub step: isize, /// The length of the slice calculated from the original input sequence. - pub slicelength: isize, + pub slicelength: usize, } impl PySliceIndices { @@ -43,31 +50,72 @@ impl PySliceIndices { impl PySlice { /// Constructs a new slice with the given elements. - pub fn new(py: Python<'_>, start: isize, stop: isize, step: isize) -> &PySlice { + pub fn new_bound(py: Python<'_>, start: isize, stop: isize, step: isize) -> Bound<'_, PySlice> { unsafe { - let ptr = ffi::PySlice_New( + ffi::PySlice_New( ffi::PyLong_FromSsize_t(start), ffi::PyLong_FromSsize_t(stop), ffi::PyLong_FromSsize_t(step), - ); - py.from_owned_ptr(ptr) + ) + .assume_owned(py) + .downcast_into_unchecked() } } /// Constructs a new full slice that is equivalent to `::`. - pub fn full(py: Python<'_>) -> &PySlice { + pub fn full_bound(py: Python<'_>) -> Bound<'_, PySlice> { unsafe { - let ptr = ffi::PySlice_New(ffi::Py_None(), ffi::Py_None(), ffi::Py_None()); - py.from_owned_ptr(ptr) + ffi::PySlice_New(ffi::Py_None(), ffi::Py_None(), ffi::Py_None()) + .assume_owned(py) + .downcast_into_unchecked() } } +} + +#[cfg(feature = "gil-refs")] +impl PySlice { + /// Deprecated form of `PySlice::new_bound`. + #[deprecated( + since = "0.21.0", + note = "`PySlice::new` will be replaced by `PySlice::new_bound` in a future PyO3 version" + )] + pub fn new(py: Python<'_>, start: isize, stop: isize, step: isize) -> &PySlice { + Self::new_bound(py, start, stop, step).into_gil_ref() + } + + /// Deprecated form of `PySlice::full_bound`. + #[deprecated( + since = "0.21.0", + note = "`PySlice::full` will be replaced by `PySlice::full_bound` in a future PyO3 version" + )] + pub fn full(py: Python<'_>) -> &PySlice { + PySlice::full_bound(py).into_gil_ref() + } /// Retrieves the start, stop, and step indices from the slice object, /// assuming a sequence of length `length`, and stores the length of the /// slice in its `slicelength` member. #[inline] - pub fn indices(&self, length: c_long) -> PyResult { - // non-negative Py_ssize_t should always fit into Rust usize + pub fn indices(&self, length: isize) -> PyResult { + self.as_borrowed().indices(length) + } +} + +/// Implementation of functionality for [`PySlice`]. +/// +/// These methods are defined for the `Bound<'py, PyTuple>` smart pointer, so to use method call +/// syntax these methods are separated into a trait, because stable Rust does not yet support +/// `arbitrary_self_types`. +#[doc(alias = "PySlice")] +pub trait PySliceMethods<'py>: crate::sealed::Sealed { + /// Retrieves the start, stop, and step indices from the slice object, + /// assuming a sequence of length `length`, and stores the length of the + /// slice in its `slicelength` member. + fn indices(&self, length: isize) -> PyResult; +} + +impl<'py> PySliceMethods<'py> for Bound<'py, PySlice> { + fn indices(&self, length: isize) -> PyResult { unsafe { let mut slicelength: isize = 0; let mut start: isize = 0; @@ -75,7 +123,7 @@ impl PySlice { let mut step: isize = 0; let r = ffi::PySlice_GetIndicesEx( self.as_ptr(), - length as Py_ssize_t, + length, &mut start, &mut stop, &mut step, @@ -86,7 +134,8 @@ impl PySlice { start, stop, step, - slicelength, + // non-negative isize should always fit into usize + slicelength: slicelength as _, }) } else { Err(PyErr::fetch(self.py())) @@ -97,7 +146,7 @@ impl PySlice { impl ToPyObject for PySliceIndices { fn to_object(&self, py: Python<'_>) -> PyObject { - PySlice::new(py, self.start, self.stop, self.step).into() + PySlice::new_bound(py, self.start, self.stop, self.step).into() } } @@ -108,7 +157,7 @@ mod tests { #[test] fn test_py_slice_new() { Python::with_gil(|py| { - let slice = PySlice::new(py, isize::MIN, isize::MAX, 1); + let slice = PySlice::new_bound(py, isize::MIN, isize::MAX, 1); assert_eq!( slice.getattr("start").unwrap().extract::().unwrap(), isize::MIN @@ -127,7 +176,7 @@ mod tests { #[test] fn test_py_slice_full() { Python::with_gil(|py| { - let slice = PySlice::full(py); + let slice = PySlice::full_bound(py); assert!(slice.getattr("start").unwrap().is_none(),); assert!(slice.getattr("stop").unwrap().is_none(),); assert!(slice.getattr("step").unwrap().is_none(),); diff --git a/src/types/string.rs b/src/types/string.rs index 03d41963f1e..4f0025acfe8 100644 --- a/src/types/string.rs +++ b/src/types/string.rs @@ -2,9 +2,12 @@ use crate::exceptions::PyUnicodeDecodeError; use crate::ffi_ptr_ext::FfiPtrExt; use crate::instance::Borrowed; +use crate::py_result_ext::PyResultExt; use crate::types::any::PyAnyMethods; use crate::types::bytes::PyBytesMethods; use crate::types::PyBytes; +#[cfg(feature = "gil-refs")] +use crate::PyNativeType; use crate::{ffi, Bound, IntoPy, Py, PyAny, PyResult, Python}; use std::borrow::Cow; use std::os::raw::c_char; @@ -72,9 +75,7 @@ impl<'a> PyStringData<'a> { match self { Self::Ucs1(data) => match str::from_utf8(data) { Ok(s) => Ok(Cow::Borrowed(s)), - Err(e) => Err(crate::PyErr::from_value(PyUnicodeDecodeError::new_utf8( - py, data, e, - )?)), + Err(e) => Err(PyUnicodeDecodeError::new_utf8_bound(py, data, e)?.into()), }, Self::Ucs2(data) => match String::from_utf16(data) { Ok(s) => Ok(Cow::Owned(s)), @@ -82,24 +83,26 @@ impl<'a> PyStringData<'a> { let mut message = e.to_string().as_bytes().to_vec(); message.push(0); - Err(crate::PyErr::from_value(PyUnicodeDecodeError::new( + Err(PyUnicodeDecodeError::new_bound( py, CStr::from_bytes_with_nul(b"utf-16\0").unwrap(), self.as_bytes(), 0..self.as_bytes().len(), CStr::from_bytes_with_nul(&message).unwrap(), - )?)) + )? + .into()) } }, Self::Ucs4(data) => match data.iter().map(|&c| std::char::from_u32(c)).collect() { Some(s) => Ok(Cow::Owned(s)), - None => Err(crate::PyErr::from_value(PyUnicodeDecodeError::new( + None => Err(PyUnicodeDecodeError::new_bound( py, CStr::from_bytes_with_nul(b"utf-32\0").unwrap(), self.as_bytes(), 0..self.as_bytes().len(), CStr::from_bytes_with_nul(b"error converting utf-32\0").unwrap(), - )?)), + )? + .into()), }, } } @@ -137,21 +140,25 @@ impl PyString { /// Creates a new Python string object. /// /// Panics if out of memory. - pub fn new<'p>(py: Python<'p>, s: &str) -> &'p PyString { + pub fn new_bound<'py>(py: Python<'py>, s: &str) -> Bound<'py, PyString> { let ptr = s.as_ptr() as *const c_char; let len = s.len() as ffi::Py_ssize_t; - unsafe { py.from_owned_ptr(ffi::PyUnicode_FromStringAndSize(ptr, len)) } + unsafe { + ffi::PyUnicode_FromStringAndSize(ptr, len) + .assume_owned(py) + .downcast_into_unchecked() + } } /// Intern the given string /// /// This will return a reference to the same Python string object if called repeatedly with the same string. /// - /// Note that while this is more memory efficient than [`PyString::new`], it unconditionally allocates a - /// temporary Python string object and is thereby slower than [`PyString::new`]. + /// Note that while this is more memory efficient than [`PyString::new_bound`], it unconditionally allocates a + /// temporary Python string object and is thereby slower than [`PyString::new_bound`]. /// /// Panics if out of memory. - pub fn intern<'p>(py: Python<'p>, s: &str) -> &'p PyString { + pub fn intern_bound<'py>(py: Python<'py>, s: &str) -> Bound<'py, PyString> { let ptr = s.as_ptr() as *const c_char; let len = s.len() as ffi::Py_ssize_t; unsafe { @@ -159,23 +166,58 @@ impl PyString { if !ob.is_null() { ffi::PyUnicode_InternInPlace(&mut ob); } - py.from_owned_ptr(ob) + ob.assume_owned(py).downcast_into_unchecked() } } /// Attempts to create a Python string from a Python [bytes-like object]. /// /// [bytes-like object]: (https://docs.python.org/3/glossary.html#term-bytes-like-object). - pub fn from_object<'p>(src: &'p PyAny, encoding: &str, errors: &str) -> PyResult<&'p PyString> { + pub fn from_object_bound<'py>( + src: &Bound<'py, PyAny>, + encoding: &str, + errors: &str, + ) -> PyResult> { unsafe { - src.py() - .from_owned_ptr_or_err(ffi::PyUnicode_FromEncodedObject( - src.as_ptr(), - encoding.as_ptr() as *const c_char, - errors.as_ptr() as *const c_char, - )) + ffi::PyUnicode_FromEncodedObject( + src.as_ptr(), + encoding.as_ptr() as *const c_char, + errors.as_ptr() as *const c_char, + ) + .assume_owned_or_err(src.py()) + .downcast_into_unchecked() } } +} + +#[cfg(feature = "gil-refs")] +impl PyString { + /// Deprecated form of [`PyString::new_bound`]. + #[deprecated( + since = "0.21.0", + note = "`PyString::new` will be replaced by `PyString::new_bound` in a future PyO3 version" + )] + pub fn new<'py>(py: Python<'py>, s: &str) -> &'py Self { + Self::new_bound(py, s).into_gil_ref() + } + + /// Deprecated form of [`PyString::intern_bound`]. + #[deprecated( + since = "0.21.0", + note = "`PyString::intern` will be replaced by `PyString::intern_bound` in a future PyO3 version" + )] + pub fn intern<'py>(py: Python<'py>, s: &str) -> &'py Self { + Self::intern_bound(py, s).into_gil_ref() + } + + /// Deprecated form of [`PyString::from_object_bound`]. + #[deprecated( + since = "0.21.0", + note = "`PyString::from_object` will be replaced by `PyString::from_object_bound` in a future PyO3 version" + )] + pub fn from_object<'py>(src: &'py PyAny, encoding: &str, errors: &str) -> PyResult<&'py Self> { + Self::from_object_bound(&src.as_borrowed(), encoding, errors).map(Bound::into_gil_ref) + } /// Gets the Python string as a Rust UTF-8 string slice. /// @@ -184,15 +226,12 @@ impl PyString { pub fn to_str(&self) -> PyResult<&str> { #[cfg(any(Py_3_10, not(Py_LIMITED_API)))] { - Borrowed::from_gil_ref(self).to_str() + self.as_borrowed().to_str() } #[cfg(not(any(Py_3_10, not(Py_LIMITED_API))))] { - let bytes = unsafe { - self.py() - .from_owned_ptr_or_err::(ffi::PyUnicode_AsUTF8String(self.as_ptr())) - }?; + let bytes = self.as_borrowed().encode_utf8()?.into_gil_ref(); Ok(unsafe { std::str::from_utf8_unchecked(bytes.as_bytes()) }) } } @@ -202,7 +241,7 @@ impl PyString { /// Returns a `UnicodeEncodeError` if the input is not valid unicode /// (containing unpaired surrogates). pub fn to_cow(&self) -> PyResult> { - Borrowed::from_gil_ref(self).to_cow() + self.as_borrowed().to_cow() } /// Converts the `PyString` into a Rust string. @@ -210,7 +249,7 @@ impl PyString { /// Unpaired surrogates invalid UTF-8 sequences are /// replaced with `U+FFFD REPLACEMENT CHARACTER`. pub fn to_string_lossy(&self) -> Cow<'_, str> { - Borrowed::from_gil_ref(self).to_string_lossy() + self.as_borrowed().to_string_lossy() } /// Obtains the raw data backing the Python string. @@ -227,9 +266,9 @@ impl PyString { /// /// By using this API, you accept responsibility for testing that PyStringData behaves as /// expected on the targets where you plan to distribute your software. - #[cfg(not(Py_LIMITED_API))] + #[cfg(not(any(Py_LIMITED_API, GraalPy, PyPy)))] pub unsafe fn data(&self) -> PyResult> { - Borrowed::from_gil_ref(self).data() + self.as_borrowed().data() } } @@ -239,7 +278,7 @@ impl PyString { /// syntax these methods are separated into a trait, because stable Rust does not yet support /// `arbitrary_self_types`. #[doc(alias = "PyString")] -pub trait PyStringMethods<'py> { +pub trait PyStringMethods<'py>: crate::sealed::Sealed { /// Gets the Python string as a Rust UTF-8 string slice. /// /// Returns a `UnicodeEncodeError` if the input is not valid unicode @@ -259,6 +298,9 @@ pub trait PyStringMethods<'py> { /// replaced with `U+FFFD REPLACEMENT CHARACTER`. fn to_string_lossy(&self) -> Cow<'_, str>; + /// Encodes this string as a Python `bytes` object, using UTF-8 encoding. + fn encode_utf8(&self) -> PyResult>; + /// Obtains the raw data backing the Python string. /// /// If the Python string object was created through legacy APIs, its internal storage format @@ -273,34 +315,42 @@ pub trait PyStringMethods<'py> { /// /// By using this API, you accept responsibility for testing that PyStringData behaves as /// expected on the targets where you plan to distribute your software. - #[cfg(not(Py_LIMITED_API))] + #[cfg(not(any(Py_LIMITED_API, GraalPy, PyPy)))] unsafe fn data(&self) -> PyResult>; } impl<'py> PyStringMethods<'py> for Bound<'py, PyString> { #[cfg(any(Py_3_10, not(Py_LIMITED_API)))] fn to_str(&self) -> PyResult<&str> { - Borrowed::from(self).to_str() + self.as_borrowed().to_str() } fn to_cow(&self) -> PyResult> { - Borrowed::from(self).to_cow() + self.as_borrowed().to_cow() } fn to_string_lossy(&self) -> Cow<'_, str> { - Borrowed::from(self).to_string_lossy() + self.as_borrowed().to_string_lossy() + } + + fn encode_utf8(&self) -> PyResult> { + unsafe { + ffi::PyUnicode_AsUTF8String(self.as_ptr()) + .assume_owned_or_err(self.py()) + .downcast_into_unchecked::() + } } - #[cfg(not(Py_LIMITED_API))] + #[cfg(not(any(Py_LIMITED_API, GraalPy, PyPy)))] unsafe fn data(&self) -> PyResult> { - Borrowed::from(self).data() + self.as_borrowed().data() } } impl<'a> Borrowed<'a, '_, PyString> { #[cfg(any(Py_3_10, not(Py_LIMITED_API)))] #[allow(clippy::wrong_self_convention)] - fn to_str(self) -> PyResult<&'a str> { + pub(crate) fn to_str(self) -> PyResult<&'a str> { // PyUnicode_AsUTF8AndSize only available on limited API starting with 3.10. let mut size: ffi::Py_ssize_t = 0; let data: *const u8 = @@ -315,7 +365,7 @@ impl<'a> Borrowed<'a, '_, PyString> { } #[allow(clippy::wrong_self_convention)] - fn to_cow(self) -> PyResult> { + pub(crate) fn to_cow(self) -> PyResult> { // TODO: this method can probably be deprecated once Python 3.9 support is dropped, // because all versions then support the more efficient `to_str`. #[cfg(any(Py_3_10, not(Py_LIMITED_API)))] @@ -325,11 +375,7 @@ impl<'a> Borrowed<'a, '_, PyString> { #[cfg(not(any(Py_3_10, not(Py_LIMITED_API))))] { - let bytes = unsafe { - ffi::PyUnicode_AsUTF8String(self.as_ptr()) - .assume_owned_or_err(self.py())? - .downcast_into_unchecked::() - }; + let bytes = self.encode_utf8()?; Ok(Cow::Owned( unsafe { str::from_utf8_unchecked(bytes.as_bytes()) }.to_owned(), )) @@ -358,7 +404,7 @@ impl<'a> Borrowed<'a, '_, PyString> { Cow::Owned(String::from_utf8_lossy(bytes.as_bytes()).into_owned()) } - #[cfg(not(Py_LIMITED_API))] + #[cfg(not(any(Py_LIMITED_API, GraalPy, PyPy)))] unsafe fn data(self) -> PyResult> { let ptr = self.as_ptr(); @@ -435,13 +481,13 @@ impl Py { impl IntoPy> for Bound<'_, PyString> { fn into_py(self, _py: Python<'_>) -> Py { - self.into() + self.unbind() } } impl IntoPy> for &Bound<'_, PyString> { fn into_py(self, _py: Python<'_>) -> Py { - self.clone().into() + self.clone().unbind() } } @@ -454,48 +500,69 @@ impl IntoPy> for &'_ Py { #[cfg(test)] mod tests { use super::*; - use crate::Python; use crate::{PyObject, ToPyObject}; - #[cfg(not(Py_LIMITED_API))] - use std::borrow::Cow; #[test] - fn test_to_str_utf8() { + fn test_to_cow_utf8() { Python::with_gil(|py| { let s = "ascii 🐈"; - let obj: PyObject = PyString::new(py, s).into(); - let py_string: &PyString = obj.downcast(py).unwrap(); - assert_eq!(s, py_string.to_str().unwrap()); + let py_string = PyString::new_bound(py, s); + assert_eq!(s, py_string.to_cow().unwrap()); + }) + } + + #[test] + fn test_to_cow_surrogate() { + Python::with_gil(|py| { + let py_string = py + .eval_bound(r"'\ud800'", None, None) + .unwrap() + .downcast_into::() + .unwrap(); + assert!(py_string.to_cow().is_err()); }) } #[test] - fn test_to_str_surrogate() { + fn test_to_cow_unicode() { Python::with_gil(|py| { - let obj: PyObject = py.eval(r"'\ud800'", None, None).unwrap().into(); - let py_string: &PyString = obj.downcast(py).unwrap(); - assert!(py_string.to_str().is_err()); + let s = "哈哈🐈"; + let py_string = PyString::new_bound(py, s); + assert_eq!(s, py_string.to_cow().unwrap()); }) } #[test] - fn test_to_str_unicode() { + fn test_encode_utf8_unicode() { Python::with_gil(|py| { let s = "哈哈🐈"; - let obj: PyObject = PyString::new(py, s).into(); - let py_string: &PyString = obj.downcast(py).unwrap(); - assert_eq!(s, py_string.to_str().unwrap()); + let obj = PyString::new_bound(py, s); + assert_eq!(s.as_bytes(), obj.encode_utf8().unwrap().as_bytes()); + }) + } + + #[test] + fn test_encode_utf8_surrogate() { + Python::with_gil(|py| { + let obj: PyObject = py.eval_bound(r"'\ud800'", None, None).unwrap().into(); + assert!(obj + .bind(py) + .downcast::() + .unwrap() + .encode_utf8() + .is_err()); }) } #[test] fn test_to_string_lossy() { Python::with_gil(|py| { - let obj: PyObject = py - .eval(r"'🐈 Hello \ud800World'", None, None) + let py_string = py + .eval_bound(r"'🐈 Hello \ud800World'", None, None) .unwrap() - .into(); - let py_string: &PyString = obj.downcast(py).unwrap(); + .downcast_into::() + .unwrap(); + assert_eq!(py_string.to_string_lossy(), "🐈 Hello ���World"); }) } @@ -504,7 +571,7 @@ mod tests { fn test_debug_string() { Python::with_gil(|py| { let v = "Hello\n".to_object(py); - let s: &PyString = v.downcast(py).unwrap(); + let s = v.downcast_bound::(py).unwrap(); assert_eq!(format!("{:?}", s), "'Hello\\n'"); }) } @@ -513,16 +580,16 @@ mod tests { fn test_display_string() { Python::with_gil(|py| { let v = "Hello\n".to_object(py); - let s: &PyString = v.downcast(py).unwrap(); + let s = v.downcast_bound::(py).unwrap(); assert_eq!(format!("{}", s), "Hello\n"); }) } #[test] - #[cfg(not(Py_LIMITED_API))] + #[cfg(not(any(Py_LIMITED_API, PyPy)))] fn test_string_data_ucs1() { Python::with_gil(|py| { - let s = PyString::new(py, "hello, world"); + let s = PyString::new_bound(py, "hello, world"); let data = unsafe { s.data().unwrap() }; assert_eq!(data, PyStringData::Ucs1(b"hello, world")); @@ -532,7 +599,7 @@ mod tests { } #[test] - #[cfg(not(Py_LIMITED_API))] + #[cfg(not(any(Py_LIMITED_API, PyPy)))] fn test_string_data_ucs1_invalid() { Python::with_gil(|py| { // 0xfe is not allowed in UTF-8. @@ -545,11 +612,13 @@ mod tests { ) }; assert!(!ptr.is_null()); - let s: &PyString = unsafe { py.from_owned_ptr(ptr) }; + let s = unsafe { ptr.assume_owned(py).downcast_into_unchecked::() }; let data = unsafe { s.data().unwrap() }; assert_eq!(data, PyStringData::Ucs1(b"f\xfe")); let err = data.to_string(py).unwrap_err(); - assert!(err.get_type(py).is(py.get_type::())); + assert!(err + .get_type_bound(py) + .is(&py.get_type_bound::())); assert!(err .to_string() .contains("'utf-8' codec can't decode byte 0xfe in position 1")); @@ -558,10 +627,10 @@ mod tests { } #[test] - #[cfg(not(Py_LIMITED_API))] + #[cfg(not(any(Py_LIMITED_API, PyPy)))] fn test_string_data_ucs2() { Python::with_gil(|py| { - let s = py.eval("'foo\\ud800'", None, None).unwrap(); + let s = py.eval_bound("'foo\\ud800'", None, None).unwrap(); let py_string = s.downcast::().unwrap(); let data = unsafe { py_string.data().unwrap() }; @@ -574,7 +643,7 @@ mod tests { } #[test] - #[cfg(all(not(Py_LIMITED_API), target_endian = "little"))] + #[cfg(all(not(any(Py_LIMITED_API, PyPy)), target_endian = "little"))] fn test_string_data_ucs2_invalid() { Python::with_gil(|py| { // U+FF22 (valid) & U+d800 (never valid) @@ -587,11 +656,13 @@ mod tests { ) }; assert!(!ptr.is_null()); - let s: &PyString = unsafe { py.from_owned_ptr(ptr) }; + let s = unsafe { ptr.assume_owned(py).downcast_into_unchecked::() }; let data = unsafe { s.data().unwrap() }; assert_eq!(data, PyStringData::Ucs2(&[0xff22, 0xd800])); let err = data.to_string(py).unwrap_err(); - assert!(err.get_type(py).is(py.get_type::())); + assert!(err + .get_type_bound(py) + .is(&py.get_type_bound::())); assert!(err .to_string() .contains("'utf-16' codec can't decode bytes in position 0-3")); @@ -600,11 +671,11 @@ mod tests { } #[test] - #[cfg(not(Py_LIMITED_API))] + #[cfg(not(any(Py_LIMITED_API, PyPy)))] fn test_string_data_ucs4() { Python::with_gil(|py| { let s = "哈哈🐈"; - let py_string = PyString::new(py, s); + let py_string = PyString::new_bound(py, s); let data = unsafe { py_string.data().unwrap() }; assert_eq!(data, PyStringData::Ucs4(&[21704, 21704, 128008])); @@ -613,7 +684,7 @@ mod tests { } #[test] - #[cfg(all(not(Py_LIMITED_API), target_endian = "little"))] + #[cfg(all(not(any(Py_LIMITED_API, PyPy)), target_endian = "little"))] fn test_string_data_ucs4_invalid() { Python::with_gil(|py| { // U+20000 (valid) & U+d800 (never valid) @@ -626,11 +697,13 @@ mod tests { ) }; assert!(!ptr.is_null()); - let s: &PyString = unsafe { py.from_owned_ptr(ptr) }; + let s = unsafe { ptr.assume_owned(py).downcast_into_unchecked::() }; let data = unsafe { s.data().unwrap() }; assert_eq!(data, PyStringData::Ucs4(&[0x20000, 0xd800])); let err = data.to_string(py).unwrap_err(); - assert!(err.get_type(py).is(py.get_type::())); + assert!(err + .get_type_bound(py) + .is(&py.get_type_bound::())); assert!(err .to_string() .contains("'utf-32' codec can't decode bytes in position 0-7")); @@ -641,16 +714,16 @@ mod tests { #[test] fn test_intern_string() { Python::with_gil(|py| { - let py_string1 = PyString::intern(py, "foo"); - assert_eq!(py_string1.to_str().unwrap(), "foo"); + let py_string1 = PyString::intern_bound(py, "foo"); + assert_eq!(py_string1.to_cow().unwrap(), "foo"); - let py_string2 = PyString::intern(py, "foo"); - assert_eq!(py_string2.to_str().unwrap(), "foo"); + let py_string2 = PyString::intern_bound(py, "foo"); + assert_eq!(py_string2.to_cow().unwrap(), "foo"); assert_eq!(py_string1.as_ptr(), py_string2.as_ptr()); - let py_string3 = PyString::intern(py, "bar"); - assert_eq!(py_string3.to_str().unwrap(), "bar"); + let py_string3 = PyString::intern_bound(py, "bar"); + assert_eq!(py_string3.to_cow().unwrap(), "bar"); assert_ne!(py_string1.as_ptr(), py_string3.as_ptr()); }); @@ -660,7 +733,7 @@ mod tests { fn test_py_to_str_utf8() { Python::with_gil(|py| { let s = "ascii 🐈"; - let py_string: Py = PyString::new(py, s).into_py(py); + let py_string: Py = PyString::new_bound(py, s).into_py(py); #[cfg(any(Py_3_10, not(Py_LIMITED_API)))] assert_eq!(s, py_string.to_str(py).unwrap()); @@ -672,8 +745,11 @@ mod tests { #[test] fn test_py_to_str_surrogate() { Python::with_gil(|py| { - let py_string: Py = - py.eval(r"'\ud800'", None, None).unwrap().extract().unwrap(); + let py_string: Py = py + .eval_bound(r"'\ud800'", None, None) + .unwrap() + .extract() + .unwrap(); #[cfg(any(Py_3_10, not(Py_LIMITED_API)))] assert!(py_string.to_str(py).is_err()); @@ -686,7 +762,7 @@ mod tests { fn test_py_to_string_lossy() { Python::with_gil(|py| { let py_string: Py = py - .eval(r"'🐈 Hello \ud800World'", None, None) + .eval_bound(r"'🐈 Hello \ud800World'", None, None) .unwrap() .extract() .unwrap(); diff --git a/src/types/traceback.rs b/src/types/traceback.rs index b9909435976..dbbdb6a85ea 100644 --- a/src/types/traceback.rs +++ b/src/types/traceback.rs @@ -1,7 +1,8 @@ use crate::err::{error_on_minusone, PyResult}; -use crate::ffi; -use crate::types::PyString; -use crate::PyAny; +use crate::types::{any::PyAnyMethods, string::PyStringMethods, PyString}; +#[cfg(feature = "gil-refs")] +use crate::PyNativeType; +use crate::{ffi, Bound, PyAny}; /// Represents a Python traceback. #[repr(transparent)] @@ -13,6 +14,7 @@ pyobject_native_type_core!( #checkfunction=ffi::PyTraceBack_Check ); +#[cfg(feature = "gil-refs")] impl PyTraceback { /// Formats the traceback as a string. /// @@ -24,14 +26,14 @@ impl PyTraceback { /// The following code formats a Python traceback and exception pair from Rust: /// /// ```rust - /// # use pyo3::{Python, PyResult}; + /// # use pyo3::{Python, PyResult, prelude::PyTracebackMethods}; /// # let result: PyResult<()> = /// Python::with_gil(|py| { /// let err = py - /// .run("raise Exception('banana')", None, None) + /// .run_bound("raise Exception('banana')", None, None) /// .expect_err("raise will create a Python error"); /// - /// let traceback = err.traceback(py).expect("raised exception will have a traceback"); + /// let traceback = err.traceback_bound(py).expect("raised exception will have a traceback"); /// assert_eq!( /// format!("{}{}", traceback.format()?, err), /// "\ @@ -46,9 +48,56 @@ impl PyTraceback { /// # result.expect("example failed"); /// ``` pub fn format(&self) -> PyResult { + self.as_borrowed().format() + } +} + +/// Implementation of functionality for [`PyTraceback`]. +/// +/// These methods are defined for the `Bound<'py, PyTraceback>` smart pointer, so to use method call +/// syntax these methods are separated into a trait, because stable Rust does not yet support +/// `arbitrary_self_types`. +#[doc(alias = "PyTraceback")] +pub trait PyTracebackMethods<'py>: crate::sealed::Sealed { + /// Formats the traceback as a string. + /// + /// This does not include the exception type and value. The exception type and value can be + /// formatted using the `Display` implementation for `PyErr`. + /// + /// # Example + /// + /// The following code formats a Python traceback and exception pair from Rust: + /// + /// ```rust + /// # use pyo3::{Python, PyResult, prelude::PyTracebackMethods}; + /// # let result: PyResult<()> = + /// Python::with_gil(|py| { + /// let err = py + /// .run_bound("raise Exception('banana')", None, None) + /// .expect_err("raise will create a Python error"); + /// + /// let traceback = err.traceback_bound(py).expect("raised exception will have a traceback"); + /// assert_eq!( + /// format!("{}{}", traceback.format()?, err), + /// "\ + /// Traceback (most recent call last): + /// File \"\", line 1, in + /// Exception: banana\ + /// " + /// ); + /// Ok(()) + /// }) + /// # ; + /// # result.expect("example failed"); + /// ``` + fn format(&self) -> PyResult; +} + +impl<'py> PyTracebackMethods<'py> for Bound<'py, PyTraceback> { + fn format(&self) -> PyResult { let py = self.py(); let string_io = py - .import(intern!(py, "io"))? + .import_bound(intern!(py, "io"))? .getattr(intern!(py, "StringIO"))? .call0()?; let result = unsafe { ffi::PyTraceBack_Print(self.as_ptr(), string_io.as_ptr()) }; @@ -57,25 +106,28 @@ impl PyTraceback { .getattr(intern!(py, "getvalue"))? .call0()? .downcast::()? - .to_str()? - .to_owned(); + .to_cow()? + .into_owned(); Ok(formatted) } } #[cfg(test)] mod tests { - use crate::{prelude::*, types::PyDict}; + use crate::{ + types::{any::PyAnyMethods, dict::PyDictMethods, traceback::PyTracebackMethods, PyDict}, + IntoPy, PyErr, Python, + }; #[test] fn format_traceback() { Python::with_gil(|py| { let err = py - .run("raise Exception('banana')", None, None) + .run_bound("raise Exception('banana')", None, None) .expect_err("raising should have given us an error"); assert_eq!( - err.traceback(py).unwrap().format().unwrap(), + err.traceback_bound(py).unwrap().format().unwrap(), "Traceback (most recent call last):\n File \"\", line 1, in \n" ); }) @@ -84,9 +136,9 @@ mod tests { #[test] fn test_err_from_value() { Python::with_gil(|py| { - let locals = PyDict::new(py); + let locals = PyDict::new_bound(py); // Produce an error from python so that it has a traceback - py.run( + py.run_bound( r" try: raise ValueError('raised exception') @@ -94,35 +146,35 @@ except Exception as e: err = e ", None, - Some(locals), + Some(&locals), ) .unwrap(); - let err = PyErr::from_value(locals.get_item("err").unwrap().unwrap()); - let traceback = err.value(py).getattr("__traceback__").unwrap(); - assert!(err.traceback(py).unwrap().is(traceback)); + let err = PyErr::from_value_bound(locals.get_item("err").unwrap().unwrap()); + let traceback = err.value_bound(py).getattr("__traceback__").unwrap(); + assert!(err.traceback_bound(py).unwrap().is(&traceback)); }) } #[test] fn test_err_into_py() { Python::with_gil(|py| { - let locals = PyDict::new(py); + let locals = PyDict::new_bound(py); // Produce an error from python so that it has a traceback - py.run( + py.run_bound( r" def f(): raise ValueError('raised exception') ", None, - Some(locals), + Some(&locals), ) .unwrap(); let f = locals.get_item("f").unwrap().unwrap(); let err = f.call0().unwrap_err(); - let traceback = err.traceback(py).unwrap(); - let err_object = err.clone_ref(py).into_py(py).into_ref(py); + let traceback = err.traceback_bound(py).unwrap(); + let err_object = err.clone_ref(py).into_py(py).into_bound(py); - assert!(err_object.getattr("__traceback__").unwrap().is(traceback)); + assert!(err_object.getattr("__traceback__").unwrap().is(&traceback)); }) } } diff --git a/src/types/tuple.rs b/src/types/tuple.rs index 0165061a7a1..afe129879f9 100644 --- a/src/types/tuple.rs +++ b/src/types/tuple.rs @@ -1,22 +1,25 @@ -use std::convert::TryInto; use std::iter::FusedIterator; use crate::ffi::{self, Py_ssize_t}; +use crate::ffi_ptr_ext::FfiPtrExt; #[cfg(feature = "experimental-inspect")] use crate::inspect::types::TypeInfo; +use crate::instance::Borrowed; use crate::internal_tricks::get_ssize_index; -use crate::types::PyList; -use crate::types::PySequence; +use crate::types::{any::PyAnyMethods, sequence::PySequenceMethods, PyList, PySequence}; +#[cfg(feature = "gil-refs")] +use crate::PyNativeType; use crate::{ - exceptions, FromPyObject, IntoPy, Py, PyAny, PyErr, PyObject, PyResult, Python, ToPyObject, + exceptions, Bound, FromPyObject, IntoPy, Py, PyAny, PyErr, PyObject, PyResult, Python, + ToPyObject, }; #[inline] #[track_caller] -fn new_from_iter( - py: Python<'_>, +fn new_from_iter<'py>( + py: Python<'py>, elements: &mut dyn ExactSizeIterator, -) -> Py { +) -> Bound<'py, PyTuple> { unsafe { // PyTuple_New checks for overflow but has a bad error message, so we check ourselves let len: Py_ssize_t = elements @@ -28,14 +31,14 @@ fn new_from_iter( // - Panics if the ptr is null // - Cleans up the tuple if `convert` or the asserts panic - let tup: Py = Py::from_owned_ptr(py, ptr); + let tup = ptr.assume_owned(py).downcast_into_unchecked(); let mut counter: Py_ssize_t = 0; for obj in elements.take(len as usize) { - #[cfg(not(any(Py_LIMITED_API, PyPy)))] + #[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))] ffi::PyTuple_SET_ITEM(ptr, counter, obj.into_ptr()); - #[cfg(any(Py_LIMITED_API, PyPy))] + #[cfg(any(Py_LIMITED_API, PyPy, GraalPy))] ffi::PyTuple_SetItem(ptr, counter, obj.into_ptr()); counter += 1; } @@ -71,7 +74,7 @@ impl PyTuple { /// # fn main() { /// Python::with_gil(|py| { /// let elements: Vec = vec![0, 1, 2, 3, 4, 5]; - /// let tuple: &PyTuple = PyTuple::new(py, elements); + /// let tuple = PyTuple::new_bound(py, elements); /// assert_eq!(format!("{:?}", tuple), "(0, 1, 2, 3, 4, 5)"); /// }); /// # } @@ -83,39 +86,64 @@ impl PyTuple { /// All standard library structures implement this trait correctly, if they do, so calling this /// function using [`Vec`]`` or `&[T]` will always succeed. #[track_caller] - pub fn new( + pub fn new_bound( py: Python<'_>, elements: impl IntoIterator, - ) -> &PyTuple + ) -> Bound<'_, PyTuple> where T: ToPyObject, U: ExactSizeIterator, { let mut elements = elements.into_iter().map(|e| e.to_object(py)); - let tup = new_from_iter(py, &mut elements); - tup.into_ref(py) + new_from_iter(py, &mut elements) } /// Constructs an empty tuple (on the Python side, a singleton object). + pub fn empty_bound(py: Python<'_>) -> Bound<'_, PyTuple> { + unsafe { + ffi::PyTuple_New(0) + .assume_owned(py) + .downcast_into_unchecked() + } + } +} + +#[cfg(feature = "gil-refs")] +impl PyTuple { + /// Deprecated form of `PyTuple::new_bound`. + #[track_caller] + #[deprecated( + since = "0.21.0", + note = "`PyTuple::new` will be replaced by `PyTuple::new_bound` in a future PyO3 version" + )] + pub fn new( + py: Python<'_>, + elements: impl IntoIterator, + ) -> &PyTuple + where + T: ToPyObject, + U: ExactSizeIterator, + { + Self::new_bound(py, elements).into_gil_ref() + } + + /// Deprecated form of `PyTuple::empty_bound`. + #[deprecated( + since = "0.21.0", + note = "`PyTuple::empty` will be replaced by `PyTuple::empty_bound` in a future PyO3 version" + )] pub fn empty(py: Python<'_>) -> &PyTuple { - unsafe { py.from_owned_ptr(ffi::PyTuple_New(0)) } + Self::empty_bound(py).into_gil_ref() } /// Gets the length of the tuple. pub fn len(&self) -> usize { - unsafe { - #[cfg(not(any(Py_LIMITED_API, PyPy)))] - let size = ffi::PyTuple_GET_SIZE(self.as_ptr()); - #[cfg(any(Py_LIMITED_API, PyPy))] - let size = ffi::PyTuple_Size(self.as_ptr()); - // non-negative Py_ssize_t should always fit into Rust uint - size as usize - } + self.as_borrowed().len() } /// Checks if the tuple is empty. pub fn is_empty(&self) -> bool { - self.len() == 0 + self.as_borrowed().is_empty() } /// Returns `self` cast as a `PySequence`. @@ -128,13 +156,7 @@ impl PyTuple { /// Indices must be nonnegative, and out-of-range indices are clipped to /// `self.len()`. pub fn get_slice(&self, low: usize, high: usize) -> &PyTuple { - unsafe { - self.py().from_owned_ptr(ffi::PyTuple_GetSlice( - self.as_ptr(), - get_ssize_index(low), - get_ssize_index(high), - )) - } + self.as_borrowed().get_slice(low, high).into_gil_ref() } /// Gets the tuple item at the specified index. @@ -145,7 +167,7 @@ impl PyTuple { /// # fn main() -> PyResult<()> { /// Python::with_gil(|py| -> PyResult<()> { /// let ob = (1, 2, 3).to_object(py); - /// let tuple: &PyTuple = ob.downcast(py).unwrap(); + /// let tuple = ob.downcast_bound::(py).unwrap(); /// let obj = tuple.get_item(0); /// assert_eq!(obj.unwrap().extract::().unwrap(), 1); /// Ok(()) @@ -153,10 +175,9 @@ impl PyTuple { /// # } /// ``` pub fn get_item(&self, index: usize) -> PyResult<&PyAny> { - unsafe { - let item = ffi::PyTuple_GetItem(self.as_ptr(), index as Py_ssize_t); - self.py().from_borrowed_ptr_or_err(item) - } + self.as_borrowed() + .get_borrowed_item(index) + .map(Borrowed::into_gil_ref) } /// Gets the tuple item at the specified index. Undefined behavior on bad index. Use with caution. @@ -164,14 +185,15 @@ impl PyTuple { /// # Safety /// /// Caller must verify that the index is within the bounds of the tuple. - #[cfg(not(any(Py_LIMITED_API, PyPy)))] + #[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))] pub unsafe fn get_item_unchecked(&self, index: usize) -> &PyAny { - let item = ffi::PyTuple_GET_ITEM(self.as_ptr(), index as Py_ssize_t); - self.py().from_borrowed_ptr(item) + self.as_borrowed() + .get_borrowed_item_unchecked(index) + .into_gil_ref() } /// Returns `self` as a slice of objects. - #[cfg(not(Py_LIMITED_API))] + #[cfg(not(any(Py_LIMITED_API, GraalPy)))] pub fn as_slice(&self) -> &[&PyAny] { // This is safe because &PyAny has the same memory layout as *mut ffi::PyObject, // and because tuples are immutable. @@ -190,7 +212,7 @@ impl PyTuple { where V: ToPyObject, { - self.as_sequence().contains(value) + self.as_borrowed().contains(value) } /// Returns the first index `i` for which `self[i] == value`. @@ -201,54 +223,303 @@ impl PyTuple { where V: ToPyObject, { - self.as_sequence().index(value) + self.as_borrowed().index(value) } /// Returns an iterator over the tuple items. pub fn iter(&self) -> PyTupleIterator<'_> { - PyTupleIterator { - tuple: self, - index: 0, - length: self.len(), - } + PyTupleIterator(BorrowedTupleIterator::new(self.as_borrowed())) } /// Return a new list containing the contents of this tuple; equivalent to the Python expression `list(tuple)`. /// /// This method is equivalent to `self.as_sequence().to_list()` and faster than `PyList::new(py, self)`. pub fn to_list(&self) -> &PyList { + self.as_borrowed().to_list().into_gil_ref() + } +} + +#[cfg(feature = "gil-refs")] +index_impls!(PyTuple, "tuple", PyTuple::len, PyTuple::get_slice); + +/// Implementation of functionality for [`PyTuple`]. +/// +/// These methods are defined for the `Bound<'py, PyTuple>` smart pointer, so to use method call +/// syntax these methods are separated into a trait, because stable Rust does not yet support +/// `arbitrary_self_types`. +#[doc(alias = "PyTuple")] +pub trait PyTupleMethods<'py>: crate::sealed::Sealed { + /// Gets the length of the tuple. + fn len(&self) -> usize; + + /// Checks if the tuple is empty. + fn is_empty(&self) -> bool; + + /// Returns `self` cast as a `PySequence`. + fn as_sequence(&self) -> &Bound<'py, PySequence>; + + /// Returns `self` cast as a `PySequence`. + fn into_sequence(self) -> Bound<'py, PySequence>; + + /// Takes the slice `self[low:high]` and returns it as a new tuple. + /// + /// Indices must be nonnegative, and out-of-range indices are clipped to + /// `self.len()`. + fn get_slice(&self, low: usize, high: usize) -> Bound<'py, PyTuple>; + + /// Gets the tuple item at the specified index. + /// # Example + /// ``` + /// use pyo3::{prelude::*, types::PyTuple}; + /// + /// # fn main() -> PyResult<()> { + /// Python::with_gil(|py| -> PyResult<()> { + /// let ob = (1, 2, 3).to_object(py); + /// let tuple = ob.downcast_bound::(py).unwrap(); + /// let obj = tuple.get_item(0); + /// assert_eq!(obj.unwrap().extract::().unwrap(), 1); + /// Ok(()) + /// }) + /// # } + /// ``` + fn get_item(&self, index: usize) -> PyResult>; + + /// Like [`get_item`][PyTupleMethods::get_item], but returns a borrowed object, which is a slight performance optimization + /// by avoiding a reference count change. + fn get_borrowed_item<'a>(&'a self, index: usize) -> PyResult>; + + /// Gets the tuple item at the specified index. Undefined behavior on bad index. Use with caution. + /// + /// # Safety + /// + /// Caller must verify that the index is within the bounds of the tuple. + #[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))] + unsafe fn get_item_unchecked(&self, index: usize) -> Bound<'py, PyAny>; + + /// Like [`get_item_unchecked`][PyTupleMethods::get_item_unchecked], but returns a borrowed object, + /// which is a slight performance optimization by avoiding a reference count change. + /// + /// # Safety + /// + /// Caller must verify that the index is within the bounds of the tuple. + #[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))] + unsafe fn get_borrowed_item_unchecked<'a>(&'a self, index: usize) -> Borrowed<'a, 'py, PyAny>; + + /// Returns `self` as a slice of objects. + #[cfg(not(any(Py_LIMITED_API, GraalPy)))] + fn as_slice(&self) -> &[Bound<'py, PyAny>]; + + /// Determines if self contains `value`. + /// + /// This is equivalent to the Python expression `value in self`. + fn contains(&self, value: V) -> PyResult + where + V: ToPyObject; + + /// Returns the first index `i` for which `self[i] == value`. + /// + /// This is equivalent to the Python expression `self.index(value)`. + fn index(&self, value: V) -> PyResult + where + V: ToPyObject; + + /// Returns an iterator over the tuple items. + fn iter(&self) -> BoundTupleIterator<'py>; + + /// Like [`iter`][PyTupleMethods::iter], but produces an iterator which returns borrowed objects, + /// which is a slight performance optimization by avoiding a reference count change. + fn iter_borrowed<'a>(&'a self) -> BorrowedTupleIterator<'a, 'py>; + + /// Return a new list containing the contents of this tuple; equivalent to the Python expression `list(tuple)`. + /// + /// This method is equivalent to `self.as_sequence().to_list()` and faster than `PyList::new(py, self)`. + fn to_list(&self) -> Bound<'py, PyList>; +} + +impl<'py> PyTupleMethods<'py> for Bound<'py, PyTuple> { + fn len(&self) -> usize { + unsafe { + #[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))] + let size = ffi::PyTuple_GET_SIZE(self.as_ptr()); + #[cfg(any(Py_LIMITED_API, PyPy, GraalPy))] + let size = ffi::PyTuple_Size(self.as_ptr()); + // non-negative Py_ssize_t should always fit into Rust uint + size as usize + } + } + + fn is_empty(&self) -> bool { + self.len() == 0 + } + + fn as_sequence(&self) -> &Bound<'py, PySequence> { + unsafe { self.downcast_unchecked() } + } + + fn into_sequence(self) -> Bound<'py, PySequence> { + unsafe { self.into_any().downcast_into_unchecked() } + } + + fn get_slice(&self, low: usize, high: usize) -> Bound<'py, PyTuple> { + unsafe { + ffi::PyTuple_GetSlice(self.as_ptr(), get_ssize_index(low), get_ssize_index(high)) + .assume_owned(self.py()) + .downcast_into_unchecked() + } + } + + fn get_item(&self, index: usize) -> PyResult> { + self.get_borrowed_item(index).map(Borrowed::to_owned) + } + + fn get_borrowed_item<'a>(&'a self, index: usize) -> PyResult> { + self.as_borrowed().get_borrowed_item(index) + } + + #[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))] + unsafe fn get_item_unchecked(&self, index: usize) -> Bound<'py, PyAny> { + self.get_borrowed_item_unchecked(index).to_owned() + } + + #[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))] + unsafe fn get_borrowed_item_unchecked<'a>(&'a self, index: usize) -> Borrowed<'a, 'py, PyAny> { + self.as_borrowed().get_borrowed_item_unchecked(index) + } + + #[cfg(not(any(Py_LIMITED_API, GraalPy)))] + fn as_slice(&self) -> &[Bound<'py, PyAny>] { + // This is safe because Bound<'py, PyAny> has the same memory layout as *mut ffi::PyObject, + // and because tuples are immutable. + unsafe { + let ptr = self.as_ptr() as *mut ffi::PyTupleObject; + let slice = std::slice::from_raw_parts((*ptr).ob_item.as_ptr(), self.len()); + &*(slice as *const [*mut ffi::PyObject] as *const [Bound<'py, PyAny>]) + } + } + + #[inline] + fn contains(&self, value: V) -> PyResult + where + V: ToPyObject, + { + self.as_sequence().contains(value) + } + + #[inline] + fn index(&self, value: V) -> PyResult + where + V: ToPyObject, + { + self.as_sequence().index(value) + } + + fn iter(&self) -> BoundTupleIterator<'py> { + BoundTupleIterator::new(self.clone()) + } + + fn iter_borrowed<'a>(&'a self) -> BorrowedTupleIterator<'a, 'py> { + self.as_borrowed().iter_borrowed() + } + + fn to_list(&self) -> Bound<'py, PyList> { self.as_sequence() .to_list() .expect("failed to convert tuple to list") } } -index_impls!(PyTuple, "tuple", PyTuple::len, PyTuple::get_slice); +impl<'a, 'py> Borrowed<'a, 'py, PyTuple> { + fn get_borrowed_item(self, index: usize) -> PyResult> { + unsafe { + ffi::PyTuple_GetItem(self.as_ptr(), index as Py_ssize_t) + .assume_borrowed_or_err(self.py()) + } + } + + #[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))] + unsafe fn get_borrowed_item_unchecked(self, index: usize) -> Borrowed<'a, 'py, PyAny> { + ffi::PyTuple_GET_ITEM(self.as_ptr(), index as Py_ssize_t).assume_borrowed(self.py()) + } + + pub(crate) fn iter_borrowed(self) -> BorrowedTupleIterator<'a, 'py> { + BorrowedTupleIterator::new(self) + } +} /// Used by `PyTuple::iter()`. -pub struct PyTupleIterator<'a> { - tuple: &'a PyTuple, +#[cfg(feature = "gil-refs")] +pub struct PyTupleIterator<'a>(BorrowedTupleIterator<'a, 'a>); + +#[cfg(feature = "gil-refs")] +impl<'a> Iterator for PyTupleIterator<'a> { + type Item = &'a PyAny; + + #[inline] + fn next(&mut self) -> Option { + self.0.next().map(Borrowed::into_gil_ref) + } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + self.0.size_hint() + } +} + +#[cfg(feature = "gil-refs")] +impl<'a> DoubleEndedIterator for PyTupleIterator<'a> { + #[inline] + fn next_back(&mut self) -> Option { + self.0.next_back().map(Borrowed::into_gil_ref) + } +} + +#[cfg(feature = "gil-refs")] +impl<'a> ExactSizeIterator for PyTupleIterator<'a> { + fn len(&self) -> usize { + self.0.len() + } +} + +#[cfg(feature = "gil-refs")] +impl FusedIterator for PyTupleIterator<'_> {} + +#[cfg(feature = "gil-refs")] +impl<'a> IntoIterator for &'a PyTuple { + type Item = &'a PyAny; + type IntoIter = PyTupleIterator<'a>; + + fn into_iter(self) -> Self::IntoIter { + PyTupleIterator(BorrowedTupleIterator::new(self.as_borrowed())) + } +} + +/// Used by `PyTuple::into_iter()`. +pub struct BoundTupleIterator<'py> { + tuple: Bound<'py, PyTuple>, index: usize, length: usize, } -impl<'a> PyTupleIterator<'a> { - unsafe fn get_item(&self, index: usize) -> &'a PyAny { - #[cfg(any(Py_LIMITED_API, PyPy))] - let item = self.tuple.get_item(index).expect("tuple.get failed"); - #[cfg(not(any(Py_LIMITED_API, PyPy)))] - let item = self.tuple.get_item_unchecked(index); - item +impl<'py> BoundTupleIterator<'py> { + fn new(tuple: Bound<'py, PyTuple>) -> Self { + let length = tuple.len(); + BoundTupleIterator { + tuple, + index: 0, + length, + } } } -impl<'a> Iterator for PyTupleIterator<'a> { - type Item = &'a PyAny; +impl<'py> Iterator for BoundTupleIterator<'py> { + type Item = Bound<'py, PyAny>; #[inline] fn next(&mut self) -> Option { if self.index < self.length { - let item = unsafe { self.get_item(self.index) }; + let item = unsafe { + BorrowedTupleIterator::get_item(self.tuple.as_borrowed(), self.index).to_owned() + }; self.index += 1; Some(item) } else { @@ -263,11 +534,14 @@ impl<'a> Iterator for PyTupleIterator<'a> { } } -impl<'a> DoubleEndedIterator for PyTupleIterator<'a> { +impl<'py> DoubleEndedIterator for BoundTupleIterator<'py> { #[inline] fn next_back(&mut self) -> Option { if self.index < self.length { - let item = unsafe { self.get_item(self.length - 1) }; + let item = unsafe { + BorrowedTupleIterator::get_item(self.tuple.as_borrowed(), self.length - 1) + .to_owned() + }; self.length -= 1; Some(item) } else { @@ -276,25 +550,117 @@ impl<'a> DoubleEndedIterator for PyTupleIterator<'a> { } } -impl<'a> ExactSizeIterator for PyTupleIterator<'a> { +impl<'py> ExactSizeIterator for BoundTupleIterator<'py> { fn len(&self) -> usize { self.length.saturating_sub(self.index) } } -impl FusedIterator for PyTupleIterator<'_> {} +impl FusedIterator for BoundTupleIterator<'_> {} -impl<'a> IntoIterator for &'a PyTuple { - type Item = &'a PyAny; - type IntoIter = PyTupleIterator<'a>; +impl<'py> IntoIterator for Bound<'py, PyTuple> { + type Item = Bound<'py, PyAny>; + type IntoIter = BoundTupleIterator<'py>; + + fn into_iter(self) -> Self::IntoIter { + BoundTupleIterator::new(self) + } +} + +impl<'py> IntoIterator for &Bound<'py, PyTuple> { + type Item = Bound<'py, PyAny>; + type IntoIter = BoundTupleIterator<'py>; fn into_iter(self) -> Self::IntoIter { self.iter() } } +/// Used by `PyTuple::iter_borrowed()`. +pub struct BorrowedTupleIterator<'a, 'py> { + tuple: Borrowed<'a, 'py, PyTuple>, + index: usize, + length: usize, +} + +impl<'a, 'py> BorrowedTupleIterator<'a, 'py> { + fn new(tuple: Borrowed<'a, 'py, PyTuple>) -> Self { + let length = tuple.len(); + BorrowedTupleIterator { + tuple, + index: 0, + length, + } + } + + unsafe fn get_item( + tuple: Borrowed<'a, 'py, PyTuple>, + index: usize, + ) -> Borrowed<'a, 'py, PyAny> { + #[cfg(any(Py_LIMITED_API, PyPy, GraalPy))] + let item = tuple.get_borrowed_item(index).expect("tuple.get failed"); + #[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))] + let item = tuple.get_borrowed_item_unchecked(index); + item + } +} + +impl<'a, 'py> Iterator for BorrowedTupleIterator<'a, 'py> { + type Item = Borrowed<'a, 'py, PyAny>; + + #[inline] + fn next(&mut self) -> Option { + if self.index < self.length { + let item = unsafe { Self::get_item(self.tuple, self.index) }; + self.index += 1; + Some(item) + } else { + None + } + } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + let len = self.len(); + (len, Some(len)) + } +} + +impl<'a, 'py> DoubleEndedIterator for BorrowedTupleIterator<'a, 'py> { + #[inline] + fn next_back(&mut self) -> Option { + if self.index < self.length { + let item = unsafe { Self::get_item(self.tuple, self.length - 1) }; + self.length -= 1; + Some(item) + } else { + None + } + } +} + +impl<'a, 'py> ExactSizeIterator for BorrowedTupleIterator<'a, 'py> { + fn len(&self) -> usize { + self.length.saturating_sub(self.index) + } +} + +impl FusedIterator for BorrowedTupleIterator<'_, '_> {} + +impl IntoPy> for Bound<'_, PyTuple> { + fn into_py(self, _: Python<'_>) -> Py { + self.unbind() + } +} + +impl IntoPy> for &'_ Bound<'_, PyTuple> { + fn into_py(self, _: Python<'_>) -> Py { + self.clone().unbind() + } +} + #[cold] -fn wrong_tuple_length(t: &PyTuple, expected_length: usize) -> PyErr { +fn wrong_tuple_length(t: &Bound<'_, PyTuple>, expected_length: usize) -> PyErr { let msg = format!( "expected tuple of length {}, but got tuple of length {}", expected_length, @@ -331,16 +697,16 @@ fn type_output() -> TypeInfo { } } - impl<'s, $($T: FromPyObject<'s>),+> FromPyObject<'s> for ($($T,)+) { - fn extract(obj: &'s PyAny) -> PyResult + impl<'py, $($T: FromPyObject<'py>),+> FromPyObject<'py> for ($($T,)+) { + fn extract_bound(obj: &Bound<'py, PyAny>) -> PyResult { - let t: &PyTuple = obj.downcast()?; + let t = obj.downcast::()?; if t.len() == $length { - #[cfg(any(Py_LIMITED_API, PyPy))] - return Ok(($(t.get_item($n)?.extract::<$T>()?,)+)); + #[cfg(any(Py_LIMITED_API, PyPy, GraalPy))] + return Ok(($(t.get_borrowed_item($n)?.extract::<$T>()?,)+)); - #[cfg(not(any(Py_LIMITED_API, PyPy)))] - unsafe {return Ok(($(t.get_item_unchecked($n).extract::<$T>()?,)+));} + #[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))] + unsafe {return Ok(($(t.get_borrowed_item_unchecked($n).extract::<$T>()?,)+));} } else { Err(wrong_tuple_length(t, $length)) } @@ -358,9 +724,9 @@ fn array_into_tuple(py: Python<'_>, array: [PyObject; N]) -> Py< let ptr = ffi::PyTuple_New(N.try_into().expect("0 < N <= 12")); let tup = Py::from_owned_ptr(py, ptr); for (index, obj) in array.into_iter().enumerate() { - #[cfg(not(any(Py_LIMITED_API, PyPy)))] + #[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))] ffi::PyTuple_SET_ITEM(ptr, index as ffi::Py_ssize_t, obj.into_ptr()); - #[cfg(any(Py_LIMITED_API, PyPy))] + #[cfg(any(Py_LIMITED_API, PyPy, GraalPy))] ffi::PyTuple_SetItem(ptr, index as ffi::Py_ssize_t, obj.into_ptr()); } tup @@ -473,22 +839,22 @@ tuple_conversion!( #[cfg(test)] mod tests { - use crate::types::{PyAny, PyList, PyTuple}; + use crate::types::{any::PyAnyMethods, tuple::PyTupleMethods, PyList, PyTuple}; use crate::{Python, ToPyObject}; use std::collections::HashSet; #[test] fn test_new() { Python::with_gil(|py| { - let ob = PyTuple::new(py, [1, 2, 3]); + let ob = PyTuple::new_bound(py, [1, 2, 3]); assert_eq!(3, ob.len()); - let ob: &PyAny = ob.into(); + let ob = ob.as_any(); assert_eq!((1, 2, 3), ob.extract().unwrap()); let mut map = HashSet::new(); map.insert(1); map.insert(2); - PyTuple::new(py, map); + PyTuple::new_bound(py, map); }); } @@ -496,17 +862,27 @@ mod tests { fn test_len() { Python::with_gil(|py| { let ob = (1, 2, 3).to_object(py); - let tuple: &PyTuple = ob.downcast(py).unwrap(); + let tuple = ob.downcast_bound::(py).unwrap(); assert_eq!(3, tuple.len()); - let ob: &PyAny = tuple.into(); + assert!(!tuple.is_empty()); + let ob = tuple.as_any(); assert_eq!((1, 2, 3), ob.extract().unwrap()); }); } + #[test] + fn test_empty() { + Python::with_gil(|py| { + let tuple = PyTuple::empty_bound(py); + assert!(tuple.is_empty()); + assert_eq!(0, tuple.len()); + }); + } + #[test] fn test_slice() { Python::with_gil(|py| { - let tup = PyTuple::new(py, [2, 3, 5, 7]); + let tup = PyTuple::new_bound(py, [2, 3, 5, 7]); let slice = tup.get_slice(1, 3); assert_eq!(2, slice.len()); let slice = tup.get_slice(1, 7); @@ -518,7 +894,7 @@ mod tests { fn test_iter() { Python::with_gil(|py| { let ob = (1, 2, 3).to_object(py); - let tuple: &PyTuple = ob.downcast(py).unwrap(); + let tuple = ob.downcast_bound::(py).unwrap(); assert_eq!(3, tuple.len()); let mut iter = tuple.iter(); @@ -542,7 +918,7 @@ mod tests { fn test_iter_rev() { Python::with_gil(|py| { let ob = (1, 2, 3).to_object(py); - let tuple: &PyTuple = ob.downcast(py).unwrap(); + let tuple = ob.downcast_bound::(py).unwrap(); assert_eq!(3, tuple.len()); let mut iter = tuple.iter().rev(); @@ -562,11 +938,57 @@ mod tests { }); } + #[test] + fn test_bound_iter() { + Python::with_gil(|py| { + let tuple = PyTuple::new_bound(py, [1, 2, 3]); + assert_eq!(3, tuple.len()); + let mut iter = tuple.iter(); + + assert_eq!(iter.size_hint(), (3, Some(3))); + + assert_eq!(1, iter.next().unwrap().extract::().unwrap()); + assert_eq!(iter.size_hint(), (2, Some(2))); + + assert_eq!(2, iter.next().unwrap().extract::().unwrap()); + assert_eq!(iter.size_hint(), (1, Some(1))); + + assert_eq!(3, iter.next().unwrap().extract::().unwrap()); + assert_eq!(iter.size_hint(), (0, Some(0))); + + assert!(iter.next().is_none()); + assert!(iter.next().is_none()); + }); + } + + #[test] + fn test_bound_iter_rev() { + Python::with_gil(|py| { + let tuple = PyTuple::new_bound(py, [1, 2, 3]); + assert_eq!(3, tuple.len()); + let mut iter = tuple.iter().rev(); + + assert_eq!(iter.size_hint(), (3, Some(3))); + + assert_eq!(3, iter.next().unwrap().extract::().unwrap()); + assert_eq!(iter.size_hint(), (2, Some(2))); + + assert_eq!(2, iter.next().unwrap().extract::().unwrap()); + assert_eq!(iter.size_hint(), (1, Some(1))); + + assert_eq!(1, iter.next().unwrap().extract::().unwrap()); + assert_eq!(iter.size_hint(), (0, Some(0))); + + assert!(iter.next().is_none()); + assert!(iter.next().is_none()); + }); + } + #[test] fn test_into_iter() { Python::with_gil(|py| { let ob = (1, 2, 3).to_object(py); - let tuple: &PyTuple = ob.downcast(py).unwrap(); + let tuple = ob.downcast_bound::(py).unwrap(); assert_eq!(3, tuple.len()); for (i, item) in tuple.iter().enumerate() { @@ -576,11 +998,28 @@ mod tests { } #[test] - #[cfg(not(Py_LIMITED_API))] + fn test_into_iter_bound() { + use crate::Bound; + + Python::with_gil(|py| { + let ob = (1, 2, 3).to_object(py); + let tuple: &Bound<'_, PyTuple> = ob.downcast_bound(py).unwrap(); + assert_eq!(3, tuple.len()); + + let mut items = vec![]; + for item in tuple { + items.push(item.extract::().unwrap()); + } + assert_eq!(items, vec![1, 2, 3]); + }); + } + + #[test] + #[cfg(not(any(Py_LIMITED_API, GraalPy)))] fn test_as_slice() { Python::with_gil(|py| { let ob = (1, 2, 3).to_object(py); - let tuple: &PyTuple = ob.downcast(py).unwrap(); + let tuple = ob.downcast_bound::(py).unwrap(); let slice = tuple.as_slice(); assert_eq!(3, slice.len()); @@ -658,7 +1097,7 @@ mod tests { fn test_tuple_get_item_invalid_index() { Python::with_gil(|py| { let ob = (1, 2, 3).to_object(py); - let tuple: &PyTuple = ob.downcast(py).unwrap(); + let tuple = ob.downcast_bound::(py).unwrap(); let obj = tuple.get_item(5); assert!(obj.is_err()); assert_eq!( @@ -672,24 +1111,26 @@ mod tests { fn test_tuple_get_item_sanity() { Python::with_gil(|py| { let ob = (1, 2, 3).to_object(py); - let tuple: &PyTuple = ob.downcast(py).unwrap(); + let tuple = ob.downcast_bound::(py).unwrap(); let obj = tuple.get_item(0); assert_eq!(obj.unwrap().extract::().unwrap(), 1); }); } - #[cfg(not(any(Py_LIMITED_API, PyPy)))] + #[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))] #[test] fn test_tuple_get_item_unchecked_sanity() { Python::with_gil(|py| { let ob = (1, 2, 3).to_object(py); - let tuple: &PyTuple = ob.downcast(py).unwrap(); + let tuple = ob.downcast_bound::(py).unwrap(); let obj = unsafe { tuple.get_item_unchecked(0) }; assert_eq!(obj.extract::().unwrap(), 1); }); } #[test] + #[cfg(feature = "gil-refs")] + #[allow(deprecated)] fn test_tuple_index_trait() { Python::with_gil(|py| { let ob = (1, 2, 3).to_object(py); @@ -702,6 +1143,8 @@ mod tests { #[test] #[should_panic] + #[cfg(feature = "gil-refs")] + #[allow(deprecated)] fn test_tuple_index_trait_panic() { Python::with_gil(|py| { let ob = (1, 2, 3).to_object(py); @@ -711,6 +1154,8 @@ mod tests { } #[test] + #[cfg(feature = "gil-refs")] + #[allow(deprecated)] fn test_tuple_index_trait_ranges() { Python::with_gil(|py| { let ob = (1, 2, 3).to_object(py); @@ -731,6 +1176,8 @@ mod tests { #[test] #[should_panic = "range start index 5 out of range for tuple of length 3"] + #[cfg(feature = "gil-refs")] + #[allow(deprecated)] fn test_tuple_index_trait_range_panic_start() { Python::with_gil(|py| { let ob = (1, 2, 3).to_object(py); @@ -741,6 +1188,8 @@ mod tests { #[test] #[should_panic = "range end index 10 out of range for tuple of length 3"] + #[cfg(feature = "gil-refs")] + #[allow(deprecated)] fn test_tuple_index_trait_range_panic_end() { Python::with_gil(|py| { let ob = (1, 2, 3).to_object(py); @@ -751,6 +1200,8 @@ mod tests { #[test] #[should_panic = "slice index starts at 2 but ends at 1"] + #[cfg(feature = "gil-refs")] + #[allow(deprecated)] fn test_tuple_index_trait_range_panic_wrong_order() { Python::with_gil(|py| { let ob = (1, 2, 3).to_object(py); @@ -762,6 +1213,8 @@ mod tests { #[test] #[should_panic = "range start index 8 out of range for tuple of length 3"] + #[cfg(feature = "gil-refs")] + #[allow(deprecated)] fn test_tuple_index_trait_range_from_panic() { Python::with_gil(|py| { let ob = (1, 2, 3).to_object(py); @@ -774,7 +1227,7 @@ mod tests { fn test_tuple_contains() { Python::with_gil(|py| { let ob = (1, 1, 2, 3, 5, 8).to_object(py); - let tuple: &PyTuple = ob.downcast(py).unwrap(); + let tuple = ob.downcast_bound::(py).unwrap(); assert_eq!(6, tuple.len()); let bad_needle = 7i32.to_object(py); @@ -792,7 +1245,7 @@ mod tests { fn test_tuple_index() { Python::with_gil(|py| { let ob = (1, 1, 2, 3, 5, 8).to_object(py); - let tuple: &PyTuple = ob.downcast(py).unwrap(); + let tuple = ob.downcast_bound::(py).unwrap(); assert_eq!(0, tuple.index(1i32).unwrap()); assert_eq!(2, tuple.index(2i32).unwrap()); assert_eq!(3, tuple.index(3i32).unwrap()); @@ -829,7 +1282,7 @@ mod tests { fn too_long_iterator() { Python::with_gil(|py| { let iter = FaultyIter(0..usize::MAX, 73); - let _tuple = PyTuple::new(py, iter); + let _tuple = PyTuple::new_bound(py, iter); }) } @@ -840,7 +1293,7 @@ mod tests { fn too_short_iterator() { Python::with_gil(|py| { let iter = FaultyIter(0..35, 73); - let _tuple = PyTuple::new(py, iter); + let _tuple = PyTuple::new_bound(py, iter); }) } @@ -852,14 +1305,14 @@ mod tests { Python::with_gil(|py| { let iter = FaultyIter(0..0, usize::MAX); - let _tuple = PyTuple::new(py, iter); + let _tuple = PyTuple::new_bound(py, iter); }) } #[cfg(feature = "macros")] #[test] fn bad_clone_mem_leaks() { - use crate::{IntoPy, Py}; + use crate::{IntoPy, Py, PyAny}; use std::sync::atomic::{AtomicUsize, Ordering::SeqCst}; static NEEDS_DESTRUCTING_COUNT: AtomicUsize = AtomicUsize::new(0); @@ -912,7 +1365,7 @@ mod tests { Python::with_gil(|py| { std::panic::catch_unwind(|| { let iter = FaultyIter(0..50, 50); - let _tuple = PyTuple::new(py, iter); + let _tuple = PyTuple::new_bound(py, iter); }) .unwrap_err(); }); @@ -927,7 +1380,7 @@ mod tests { #[cfg(feature = "macros")] #[test] fn bad_clone_mem_leaks_2() { - use crate::{IntoPy, Py}; + use crate::{IntoPy, Py, PyAny}; use std::sync::atomic::{AtomicUsize, Ordering::SeqCst}; static NEEDS_DESTRUCTING_COUNT: AtomicUsize = AtomicUsize::new(0); @@ -978,10 +1431,66 @@ mod tests { #[test] fn test_tuple_to_list() { Python::with_gil(|py| { - let tuple = PyTuple::new(py, vec![1, 2, 3]); + let tuple = PyTuple::new_bound(py, vec![1, 2, 3]); let list = tuple.to_list(); - let list_expected = PyList::new(py, vec![1, 2, 3]); + let list_expected = PyList::new_bound(py, vec![1, 2, 3]); assert!(list.eq(list_expected).unwrap()); }) } + + #[test] + fn test_tuple_as_sequence() { + Python::with_gil(|py| { + let tuple = PyTuple::new_bound(py, vec![1, 2, 3]); + let sequence = tuple.as_sequence(); + assert!(tuple.get_item(0).unwrap().eq(1).unwrap()); + assert!(sequence.get_item(0).unwrap().eq(1).unwrap()); + + assert_eq!(tuple.len(), 3); + assert_eq!(sequence.len().unwrap(), 3); + }) + } + + #[test] + fn test_tuple_into_sequence() { + Python::with_gil(|py| { + let tuple = PyTuple::new_bound(py, vec![1, 2, 3]); + let sequence = tuple.into_sequence(); + assert!(sequence.get_item(0).unwrap().eq(1).unwrap()); + assert_eq!(sequence.len().unwrap(), 3); + }) + } + + #[test] + fn test_bound_tuple_get_item() { + Python::with_gil(|py| { + let tuple = PyTuple::new_bound(py, vec![1, 2, 3, 4]); + + assert_eq!(tuple.len(), 4); + assert_eq!(tuple.get_item(0).unwrap().extract::().unwrap(), 1); + assert_eq!( + tuple + .get_borrowed_item(1) + .unwrap() + .extract::() + .unwrap(), + 2 + ); + #[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))] + { + assert_eq!( + unsafe { tuple.get_item_unchecked(2) } + .extract::() + .unwrap(), + 3 + ); + assert_eq!( + unsafe { tuple.get_borrowed_item_unchecked(3) } + .extract::() + .unwrap(), + 4 + ); + } + }) + } } diff --git a/src/types/typeobject.rs b/src/types/typeobject.rs index a1a053f93a4..9c2d5c5f2c4 100644 --- a/src/types/typeobject.rs +++ b/src/types/typeobject.rs @@ -1,9 +1,13 @@ use crate::err::{self, PyResult}; -use crate::{ffi, PyAny, PyTypeInfo, Python}; +use crate::instance::Borrowed; +use crate::types::any::PyAnyMethods; +use crate::types::PyTuple; +#[cfg(feature = "gil-refs")] +use crate::PyNativeType; +use crate::{ffi, Bound, PyAny, PyTypeInfo, Python}; use std::borrow::Cow; #[cfg(not(any(Py_LIMITED_API, PyPy)))] use std::ffi::CStr; - /// Represents a reference to a Python `type object`. #[repr(transparent)] pub struct PyType(PyAny); @@ -13,36 +17,148 @@ pyobject_native_type_core!(PyType, pyobject_native_static_type_object!(ffi::PyTy impl PyType { /// Creates a new type object. #[inline] + pub fn new_bound(py: Python<'_>) -> Bound<'_, PyType> { + T::type_object_bound(py) + } + + /// Converts the given FFI pointer into `Bound`, to use in safe code. + /// + /// The function creates a new reference from the given pointer, and returns + /// it as a `Bound`. + /// + /// # Safety + /// - The pointer must be a valid non-null reference to a `PyTypeObject` + #[inline] + pub unsafe fn from_borrowed_type_ptr( + py: Python<'_>, + p: *mut ffi::PyTypeObject, + ) -> Bound<'_, PyType> { + Borrowed::from_ptr_unchecked(py, p.cast()) + .downcast_unchecked() + .to_owned() + } +} + +#[cfg(feature = "gil-refs")] +impl PyType { + /// Deprecated form of [`PyType::new_bound`]. + #[inline] + #[deprecated( + since = "0.21.0", + note = "`PyType::new` will be replaced by `PyType::new_bound` in a future PyO3 version" + )] pub fn new(py: Python<'_>) -> &PyType { - T::type_object(py) + T::type_object_bound(py).into_gil_ref() } /// Retrieves the underlying FFI pointer associated with this Python object. #[inline] pub fn as_type_ptr(&self) -> *mut ffi::PyTypeObject { - self.as_ptr() as *mut ffi::PyTypeObject + self.as_borrowed().as_type_ptr() } - /// Retrieves the `PyType` instance for the given FFI pointer. + /// Deprecated form of [`PyType::from_borrowed_type_ptr`]. /// /// # Safety - /// - The pointer must be non-null. - /// - The pointer must be valid for the entire of the lifetime for which the reference is used. + /// + /// - The pointer must a valid non-null reference to a `PyTypeObject`. #[inline] + #[deprecated( + since = "0.21.0", + note = "Use `PyType::from_borrowed_type_ptr` instead" + )] pub unsafe fn from_type_ptr(py: Python<'_>, p: *mut ffi::PyTypeObject) -> &PyType { - py.from_borrowed_ptr(p as *mut ffi::PyObject) + Self::from_borrowed_type_ptr(py, p).into_gil_ref() } /// Gets the [qualified name](https://docs.python.org/3/glossary.html#term-qualified-name) of the `PyType`. pub fn qualname(&self) -> PyResult { + self.as_borrowed().qualname() + } + + /// Gets the full name, which includes the module, of the `PyType`. + pub fn name(&self) -> PyResult> { + self.as_borrowed().name() + } + + /// Checks whether `self` is a subclass of `other`. + /// + /// Equivalent to the Python expression `issubclass(self, other)`. + pub fn is_subclass(&self, other: &PyAny) -> PyResult { + self.as_borrowed().is_subclass(&other.as_borrowed()) + } + + /// Checks whether `self` is a subclass of type `T`. + /// + /// Equivalent to the Python expression `issubclass(self, T)`, if the type + /// `T` is known at compile time. + pub fn is_subclass_of(&self) -> PyResult + where + T: PyTypeInfo, + { + self.as_borrowed().is_subclass_of::() + } +} + +/// Implementation of functionality for [`PyType`]. +/// +/// These methods are defined for the `Bound<'py, PyType>` smart pointer, so to use method call +/// syntax these methods are separated into a trait, because stable Rust does not yet support +/// `arbitrary_self_types`. +#[doc(alias = "PyType")] +pub trait PyTypeMethods<'py>: crate::sealed::Sealed { + /// Retrieves the underlying FFI pointer associated with this Python object. + fn as_type_ptr(&self) -> *mut ffi::PyTypeObject; + + /// Gets the full name, which includes the module, of the `PyType`. + fn name(&self) -> PyResult>; + + /// Gets the [qualified name](https://docs.python.org/3/glossary.html#term-qualified-name) of the `PyType`. + fn qualname(&self) -> PyResult; + + /// Checks whether `self` is a subclass of `other`. + /// + /// Equivalent to the Python expression `issubclass(self, other)`. + fn is_subclass(&self, other: &Bound<'_, PyAny>) -> PyResult; + + /// Checks whether `self` is a subclass of type `T`. + /// + /// Equivalent to the Python expression `issubclass(self, T)`, if the type + /// `T` is known at compile time. + fn is_subclass_of(&self) -> PyResult + where + T: PyTypeInfo; + + /// Return the method resolution order for this type. + /// + /// Equivalent to the Python expression `self.__mro__`. + fn mro(&self) -> Bound<'py, PyTuple>; + + /// Return Python bases + /// + /// Equivalent to the Python expression `self.__bases__`. + fn bases(&self) -> Bound<'py, PyTuple>; +} + +impl<'py> PyTypeMethods<'py> for Bound<'py, PyType> { + /// Retrieves the underlying FFI pointer associated with this Python object. + #[inline] + fn as_type_ptr(&self) -> *mut ffi::PyTypeObject { + self.as_ptr() as *mut ffi::PyTypeObject + } + + /// Gets the name of the `PyType`. + fn name(&self) -> PyResult> { + Borrowed::from(self).name() + } + + fn qualname(&self) -> PyResult { #[cfg(any(Py_LIMITED_API, PyPy, not(Py_3_11)))] let name = self.getattr(intern!(self.py(), "__qualname__"))?.extract(); #[cfg(not(any(Py_LIMITED_API, PyPy, not(Py_3_11))))] let name = { use crate::ffi_ptr_ext::FfiPtrExt; - use crate::types::any::PyAnyMethods; - let obj = unsafe { ffi::PyType_GetQualName(self.as_type_ptr()).assume_owned_or_err(self.py())? }; @@ -53,8 +169,71 @@ impl PyType { name } - /// Gets the full name, which includes the module, of the `PyType`. - pub fn name(&self) -> PyResult> { + /// Checks whether `self` is a subclass of `other`. + /// + /// Equivalent to the Python expression `issubclass(self, other)`. + fn is_subclass(&self, other: &Bound<'_, PyAny>) -> PyResult { + let result = unsafe { ffi::PyObject_IsSubclass(self.as_ptr(), other.as_ptr()) }; + err::error_on_minusone(self.py(), result)?; + Ok(result == 1) + } + + /// Checks whether `self` is a subclass of type `T`. + /// + /// Equivalent to the Python expression `issubclass(self, T)`, if the type + /// `T` is known at compile time. + fn is_subclass_of(&self) -> PyResult + where + T: PyTypeInfo, + { + self.is_subclass(&T::type_object_bound(self.py())) + } + + fn mro(&self) -> Bound<'py, PyTuple> { + #[cfg(any(Py_LIMITED_API, PyPy))] + let mro = self + .getattr(intern!(self.py(), "__mro__")) + .expect("Cannot get `__mro__` from object.") + .extract() + .expect("Unexpected type in `__mro__` attribute."); + + #[cfg(not(any(Py_LIMITED_API, PyPy)))] + let mro = unsafe { + use crate::ffi_ptr_ext::FfiPtrExt; + (*self.as_type_ptr()) + .tp_mro + .assume_borrowed(self.py()) + .to_owned() + .downcast_into_unchecked() + }; + + mro + } + + fn bases(&self) -> Bound<'py, PyTuple> { + #[cfg(any(Py_LIMITED_API, PyPy))] + let bases = self + .getattr(intern!(self.py(), "__bases__")) + .expect("Cannot get `__bases__` from object.") + .extract() + .expect("Unexpected type in `__bases__` attribute."); + + #[cfg(not(any(Py_LIMITED_API, PyPy)))] + let bases = unsafe { + use crate::ffi_ptr_ext::FfiPtrExt; + (*self.as_type_ptr()) + .tp_bases + .assume_borrowed(self.py()) + .to_owned() + .downcast_into_unchecked() + }; + + bases + } +} + +impl<'a> Borrowed<'a, '_, PyType> { + fn name(self) -> PyResult> { #[cfg(not(any(Py_LIMITED_API, PyPy)))] { let ptr = self.as_type_ptr(); @@ -79,53 +258,76 @@ impl PyType { #[cfg(Py_3_11)] let name = { use crate::ffi_ptr_ext::FfiPtrExt; - unsafe { ffi::PyType_GetName(self.as_type_ptr()).assume_owned_or_err(self.py())? } }; Ok(Cow::Owned(format!("{}.{}", module, name))) } } - - /// Checks whether `self` is a subclass of `other`. - /// - /// Equivalent to the Python expression `issubclass(self, other)`. - pub fn is_subclass(&self, other: &PyAny) -> PyResult { - let result = unsafe { ffi::PyObject_IsSubclass(self.as_ptr(), other.as_ptr()) }; - err::error_on_minusone(self.py(), result)?; - Ok(result == 1) - } - - /// Checks whether `self` is a subclass of type `T`. - /// - /// Equivalent to the Python expression `issubclass(self, T)`, if the type - /// `T` is known at compile time. - pub fn is_subclass_of(&self) -> PyResult - where - T: PyTypeInfo, - { - self.is_subclass(T::type_object(self.py())) - } } #[cfg(test)] mod tests { - use crate::types::{PyBool, PyLong}; + use crate::types::{PyAnyMethods, PyBool, PyInt, PyLong, PyTuple, PyTypeMethods}; + use crate::PyAny; use crate::Python; #[test] fn test_type_is_subclass() { Python::with_gil(|py| { - let bool_type = py.get_type::(); - let long_type = py.get_type::(); - assert!(bool_type.is_subclass(long_type).unwrap()); + let bool_type = py.get_type_bound::(); + let long_type = py.get_type_bound::(); + assert!(bool_type.is_subclass(&long_type).unwrap()); }); } #[test] fn test_type_is_subclass_of() { Python::with_gil(|py| { - assert!(py.get_type::().is_subclass_of::().unwrap()); + assert!(py + .get_type_bound::() + .is_subclass_of::() + .unwrap()); + }); + } + + #[test] + fn test_mro() { + Python::with_gil(|py| { + assert!(py + .get_type_bound::() + .mro() + .eq(PyTuple::new_bound( + py, + [ + py.get_type_bound::(), + py.get_type_bound::(), + py.get_type_bound::() + ] + )) + .unwrap()); + }); + } + + #[test] + fn test_bases_bool() { + Python::with_gil(|py| { + assert!(py + .get_type_bound::() + .bases() + .eq(PyTuple::new_bound(py, [py.get_type_bound::()])) + .unwrap()); + }); + } + + #[test] + fn test_bases_object() { + Python::with_gil(|py| { + assert!(py + .get_type_bound::() + .bases() + .eq(PyTuple::empty_bound(py)) + .unwrap()); }); } } diff --git a/tests/test_anyhow.rs b/tests/test_anyhow.rs index f0df4dcc1a4..d6f5c036b74 100644 --- a/tests/test_anyhow.rs +++ b/tests/test_anyhow.rs @@ -1,8 +1,10 @@ #![cfg(feature = "anyhow")] +use pyo3::wrap_pyfunction_bound; + #[test] fn test_anyhow_py_function_ok_result() { - use pyo3::{py_run, pyfunction, wrap_pyfunction, Python}; + use pyo3::{py_run, pyfunction, Python}; #[pyfunction] #[allow(clippy::unnecessary_wraps)] @@ -11,7 +13,7 @@ fn test_anyhow_py_function_ok_result() { } Python::with_gil(|py| { - let func = wrap_pyfunction!(produce_ok_result)(py).unwrap(); + let func = wrap_pyfunction_bound!(produce_ok_result)(py).unwrap(); py_run!( py, @@ -25,7 +27,8 @@ fn test_anyhow_py_function_ok_result() { #[test] fn test_anyhow_py_function_err_result() { - use pyo3::{pyfunction, types::PyDict, wrap_pyfunction, Python}; + use pyo3::prelude::PyDictMethods; + use pyo3::{pyfunction, types::PyDict, Python}; #[pyfunction] fn produce_err_result() -> anyhow::Result { @@ -33,16 +36,16 @@ fn test_anyhow_py_function_err_result() { } Python::with_gil(|py| { - let func = wrap_pyfunction!(produce_err_result)(py).unwrap(); - let locals = PyDict::new(py); + let func = wrap_pyfunction_bound!(produce_err_result)(py).unwrap(); + let locals = PyDict::new_bound(py); locals.set_item("func", func).unwrap(); - py.run( + py.run_bound( r#" func() "#, None, - Some(locals), + Some(&locals), ) .unwrap_err(); }); diff --git a/tests/test_append_to_inittab.rs b/tests/test_append_to_inittab.rs index e0a57da1b5c..da35298b4d9 100644 --- a/tests/test_append_to_inittab.rs +++ b/tests/test_append_to_inittab.rs @@ -1,4 +1,5 @@ #![cfg(all(feature = "macros", not(PyPy)))] + use pyo3::prelude::*; #[pyfunction] @@ -7,26 +8,52 @@ fn foo() -> usize { } #[pymodule] -fn module_with_functions(_py: Python<'_>, m: &PyModule) -> PyResult<()> { - m.add_function(wrap_pyfunction!(foo, m)?).unwrap(); +fn module_fn_with_functions(m: &Bound<'_, PyModule>) -> PyResult<()> { + m.add_function(wrap_pyfunction!(foo, m)?)?; Ok(()) } +#[cfg(feature = "experimental-declarative-modules")] +#[pymodule] +mod module_mod_with_functions { + #[pymodule_export] + use super::foo; +} + #[cfg(not(PyPy))] #[test] fn test_module_append_to_inittab() { use pyo3::append_to_inittab; - append_to_inittab!(module_with_functions); + + append_to_inittab!(module_fn_with_functions); + + #[cfg(feature = "experimental-declarative-modules")] + append_to_inittab!(module_mod_with_functions); + + Python::with_gil(|py| { + py.run_bound( + r#" +import module_fn_with_functions +assert module_fn_with_functions.foo() == 123 +"#, + None, + None, + ) + .map_err(|e| e.display(py)) + .unwrap(); + }); + + #[cfg(feature = "experimental-declarative-modules")] Python::with_gil(|py| { - py.run( + py.run_bound( r#" -import module_with_functions -assert module_with_functions.foo() == 123 +import module_mod_with_functions +assert module_mod_with_functions.foo() == 123 "#, None, None, ) .map_err(|e| e.display(py)) .unwrap(); - }) + }); } diff --git a/tests/test_arithmetics.rs b/tests/test_arithmetics.rs index 86078080176..007f42a79e8 100644 --- a/tests/test_arithmetics.rs +++ b/tests/test_arithmetics.rs @@ -35,6 +35,7 @@ impl UnaryArithmetic { Self::new(self.inner.abs()) } + #[pyo3(signature=(_ndigits=None))] fn __round__(&self, _ndigits: Option) -> Self { Self::new(self.inner.round()) } @@ -43,7 +44,7 @@ impl UnaryArithmetic { #[test] fn unary_arithmetic() { Python::with_gil(|py| { - let c = PyCell::new(py, UnaryArithmetic::new(2.7)).unwrap(); + let c = Py::new(py, UnaryArithmetic::new(2.7)).unwrap(); py_run!(py, c, "assert repr(-c) == 'UA(-2.7)'"); py_run!(py, c, "assert repr(+c) == 'UA(2.7)'"); py_run!(py, c, "assert repr(abs(c)) == 'UA(2.7)'"); @@ -77,7 +78,7 @@ impl Indexable { #[test] fn indexable() { Python::with_gil(|py| { - let i = PyCell::new(py, Indexable(5)).unwrap(); + let i = Py::new(py, Indexable(5)).unwrap(); py_run!(py, i, "assert int(i) == 5"); py_run!(py, i, "assert [0, 1, 2, 3, 4, 5][i] == 5"); py_run!(py, i, "assert float(i) == 5.0"); @@ -137,7 +138,7 @@ impl InPlaceOperations { fn inplace_operations() { Python::with_gil(|py| { let init = |value, code| { - let c = PyCell::new(py, InPlaceOperations { value }).unwrap(); + let c = Py::new(py, InPlaceOperations { value }).unwrap(); py_run!(py, c, code); }; @@ -166,39 +167,43 @@ impl BinaryArithmetic { "BA" } - fn __add__(&self, rhs: &PyAny) -> String { + fn __add__(&self, rhs: &Bound<'_, PyAny>) -> String { format!("BA + {:?}", rhs) } - fn __sub__(&self, rhs: &PyAny) -> String { + fn __sub__(&self, rhs: &Bound<'_, PyAny>) -> String { format!("BA - {:?}", rhs) } - fn __mul__(&self, rhs: &PyAny) -> String { + fn __mul__(&self, rhs: &Bound<'_, PyAny>) -> String { format!("BA * {:?}", rhs) } - fn __lshift__(&self, rhs: &PyAny) -> String { + fn __truediv__(&self, rhs: &Bound<'_, PyAny>) -> String { + format!("BA / {:?}", rhs) + } + + fn __lshift__(&self, rhs: &Bound<'_, PyAny>) -> String { format!("BA << {:?}", rhs) } - fn __rshift__(&self, rhs: &PyAny) -> String { + fn __rshift__(&self, rhs: &Bound<'_, PyAny>) -> String { format!("BA >> {:?}", rhs) } - fn __and__(&self, rhs: &PyAny) -> String { + fn __and__(&self, rhs: &Bound<'_, PyAny>) -> String { format!("BA & {:?}", rhs) } - fn __xor__(&self, rhs: &PyAny) -> String { + fn __xor__(&self, rhs: &Bound<'_, PyAny>) -> String { format!("BA ^ {:?}", rhs) } - fn __or__(&self, rhs: &PyAny) -> String { + fn __or__(&self, rhs: &Bound<'_, PyAny>) -> String { format!("BA | {:?}", rhs) } - fn __pow__(&self, rhs: &PyAny, mod_: Option) -> String { + fn __pow__(&self, rhs: &Bound<'_, PyAny>, mod_: Option) -> String { format!("BA ** {:?} (mod: {:?})", rhs, mod_) } } @@ -206,7 +211,7 @@ impl BinaryArithmetic { #[test] fn binary_arithmetic() { Python::with_gil(|py| { - let c = PyCell::new(py, BinaryArithmetic {}).unwrap(); + let c = Py::new(py, BinaryArithmetic {}).unwrap(); py_run!(py, c, "assert c + c == 'BA + BA'"); py_run!(py, c, "assert c.__add__(c) == 'BA + BA'"); py_run!(py, c, "assert c + 1 == 'BA + 1'"); @@ -233,6 +238,18 @@ fn binary_arithmetic() { py_expect_exception!(py, c, "1 ** c", PyTypeError); py_run!(py, c, "assert pow(c, 1, 100) == 'BA ** 1 (mod: Some(100))'"); + + let c: Bound<'_, PyAny> = c.extract(py).unwrap(); + assert_py_eq!(c.add(&c).unwrap(), "BA + BA"); + assert_py_eq!(c.sub(&c).unwrap(), "BA - BA"); + assert_py_eq!(c.mul(&c).unwrap(), "BA * BA"); + assert_py_eq!(c.div(&c).unwrap(), "BA / BA"); + assert_py_eq!(c.lshift(&c).unwrap(), "BA << BA"); + assert_py_eq!(c.rshift(&c).unwrap(), "BA >> BA"); + assert_py_eq!(c.bitand(&c).unwrap(), "BA & BA"); + assert_py_eq!(c.bitor(&c).unwrap(), "BA | BA"); + assert_py_eq!(c.bitxor(&c).unwrap(), "BA ^ BA"); + assert_py_eq!(c.pow(&c, py.None()).unwrap(), "BA ** BA (mod: None)"); }); } @@ -241,39 +258,39 @@ struct RhsArithmetic {} #[pymethods] impl RhsArithmetic { - fn __radd__(&self, other: &PyAny) -> String { + fn __radd__(&self, other: &Bound<'_, PyAny>) -> String { format!("{:?} + RA", other) } - fn __rsub__(&self, other: &PyAny) -> String { + fn __rsub__(&self, other: &Bound<'_, PyAny>) -> String { format!("{:?} - RA", other) } - fn __rmul__(&self, other: &PyAny) -> String { + fn __rmul__(&self, other: &Bound<'_, PyAny>) -> String { format!("{:?} * RA", other) } - fn __rlshift__(&self, other: &PyAny) -> String { + fn __rlshift__(&self, other: &Bound<'_, PyAny>) -> String { format!("{:?} << RA", other) } - fn __rrshift__(&self, other: &PyAny) -> String { + fn __rrshift__(&self, other: &Bound<'_, PyAny>) -> String { format!("{:?} >> RA", other) } - fn __rand__(&self, other: &PyAny) -> String { + fn __rand__(&self, other: &Bound<'_, PyAny>) -> String { format!("{:?} & RA", other) } - fn __rxor__(&self, other: &PyAny) -> String { + fn __rxor__(&self, other: &Bound<'_, PyAny>) -> String { format!("{:?} ^ RA", other) } - fn __ror__(&self, other: &PyAny) -> String { + fn __ror__(&self, other: &Bound<'_, PyAny>) -> String { format!("{:?} | RA", other) } - fn __rpow__(&self, other: &PyAny, _mod: Option<&PyAny>) -> String { + fn __rpow__(&self, other: &Bound<'_, PyAny>, _mod: Option<&Bound<'_, PyAny>>) -> String { format!("{:?} ** RA", other) } } @@ -281,7 +298,7 @@ impl RhsArithmetic { #[test] fn rhs_arithmetic() { Python::with_gil(|py| { - let c = PyCell::new(py, RhsArithmetic {}).unwrap(); + let c = Py::new(py, RhsArithmetic {}).unwrap(); py_run!(py, c, "assert c.__radd__(1) == '1 + RA'"); py_run!(py, c, "assert 1 + c == '1 + RA'"); py_run!(py, c, "assert c.__rsub__(1) == '1 - RA'"); @@ -318,91 +335,91 @@ impl LhsAndRhs { // "BA" // } - fn __add__(lhs: PyRef<'_, Self>, rhs: &PyAny) -> String { + fn __add__(lhs: PyRef<'_, Self>, rhs: &Bound<'_, PyAny>) -> String { format!("{:?} + {:?}", lhs, rhs) } - fn __sub__(lhs: PyRef<'_, Self>, rhs: &PyAny) -> String { + fn __sub__(lhs: PyRef<'_, Self>, rhs: &Bound<'_, PyAny>) -> String { format!("{:?} - {:?}", lhs, rhs) } - fn __mul__(lhs: PyRef<'_, Self>, rhs: &PyAny) -> String { + fn __mul__(lhs: PyRef<'_, Self>, rhs: &Bound<'_, PyAny>) -> String { format!("{:?} * {:?}", lhs, rhs) } - fn __lshift__(lhs: PyRef<'_, Self>, rhs: &PyAny) -> String { + fn __lshift__(lhs: PyRef<'_, Self>, rhs: &Bound<'_, PyAny>) -> String { format!("{:?} << {:?}", lhs, rhs) } - fn __rshift__(lhs: PyRef<'_, Self>, rhs: &PyAny) -> String { + fn __rshift__(lhs: PyRef<'_, Self>, rhs: &Bound<'_, PyAny>) -> String { format!("{:?} >> {:?}", lhs, rhs) } - fn __and__(lhs: PyRef<'_, Self>, rhs: &PyAny) -> String { + fn __and__(lhs: PyRef<'_, Self>, rhs: &Bound<'_, PyAny>) -> String { format!("{:?} & {:?}", lhs, rhs) } - fn __xor__(lhs: PyRef<'_, Self>, rhs: &PyAny) -> String { + fn __xor__(lhs: PyRef<'_, Self>, rhs: &Bound<'_, PyAny>) -> String { format!("{:?} ^ {:?}", lhs, rhs) } - fn __or__(lhs: PyRef<'_, Self>, rhs: &PyAny) -> String { + fn __or__(lhs: PyRef<'_, Self>, rhs: &Bound<'_, PyAny>) -> String { format!("{:?} | {:?}", lhs, rhs) } - fn __pow__(lhs: PyRef<'_, Self>, rhs: &PyAny, _mod: Option) -> String { + fn __pow__(lhs: PyRef<'_, Self>, rhs: &Bound<'_, PyAny>, _mod: Option) -> String { format!("{:?} ** {:?}", lhs, rhs) } - fn __matmul__(lhs: PyRef<'_, Self>, rhs: &PyAny) -> String { + fn __matmul__(lhs: PyRef<'_, Self>, rhs: &Bound<'_, PyAny>) -> String { format!("{:?} @ {:?}", lhs, rhs) } - fn __radd__(&self, other: &PyAny) -> String { + fn __radd__(&self, other: &Bound<'_, PyAny>) -> String { format!("{:?} + RA", other) } - fn __rsub__(&self, other: &PyAny) -> String { + fn __rsub__(&self, other: &Bound<'_, PyAny>) -> String { format!("{:?} - RA", other) } - fn __rmul__(&self, other: &PyAny) -> String { + fn __rmul__(&self, other: &Bound<'_, PyAny>) -> String { format!("{:?} * RA", other) } - fn __rlshift__(&self, other: &PyAny) -> String { + fn __rlshift__(&self, other: &Bound<'_, PyAny>) -> String { format!("{:?} << RA", other) } - fn __rrshift__(&self, other: &PyAny) -> String { + fn __rrshift__(&self, other: &Bound<'_, PyAny>) -> String { format!("{:?} >> RA", other) } - fn __rand__(&self, other: &PyAny) -> String { + fn __rand__(&self, other: &Bound<'_, PyAny>) -> String { format!("{:?} & RA", other) } - fn __rxor__(&self, other: &PyAny) -> String { + fn __rxor__(&self, other: &Bound<'_, PyAny>) -> String { format!("{:?} ^ RA", other) } - fn __ror__(&self, other: &PyAny) -> String { + fn __ror__(&self, other: &Bound<'_, PyAny>) -> String { format!("{:?} | RA", other) } - fn __rpow__(&self, other: &PyAny, _mod: Option<&PyAny>) -> String { + fn __rpow__(&self, other: &Bound<'_, PyAny>, _mod: Option<&Bound<'_, PyAny>>) -> String { format!("{:?} ** RA", other) } - fn __rmatmul__(&self, other: &PyAny) -> String { + fn __rmatmul__(&self, other: &Bound<'_, PyAny>) -> String { format!("{:?} @ RA", other) } - fn __rtruediv__(&self, other: &PyAny) -> String { + fn __rtruediv__(&self, other: &Bound<'_, PyAny>) -> String { format!("{:?} / RA", other) } - fn __rfloordiv__(&self, other: &PyAny) -> String { + fn __rfloordiv__(&self, other: &Bound<'_, PyAny>) -> String { format!("{:?} // RA", other) } } @@ -410,7 +427,7 @@ impl LhsAndRhs { #[test] fn lhs_fellback_to_rhs() { Python::with_gil(|py| { - let c = PyCell::new(py, LhsAndRhs {}).unwrap(); + let c = Py::new(py, LhsAndRhs {}).unwrap(); // If the light hand value is `LhsAndRhs`, LHS is used. py_run!(py, c, "assert c + 1 == 'LR + 1'"); py_run!(py, c, "assert c - 1 == 'LR - 1'"); @@ -445,7 +462,7 @@ impl RichComparisons { "RC" } - fn __richcmp__(&self, other: &PyAny, op: CompareOp) -> String { + fn __richcmp__(&self, other: &Bound<'_, PyAny>, op: CompareOp) -> String { match op { CompareOp::Lt => format!("{} < {:?}", self.__repr__(), other), CompareOp::Le => format!("{} <= {:?}", self.__repr__(), other), @@ -466,11 +483,11 @@ impl RichComparisons2 { "RC2" } - fn __richcmp__(&self, other: &PyAny, op: CompareOp) -> PyObject { + fn __richcmp__(&self, other: &Bound<'_, PyAny>, op: CompareOp) -> PyObject { match op { CompareOp::Eq => true.into_py(other.py()), CompareOp::Ne => false.into_py(other.py()), - _ => other.py().NotImplemented().into(), + _ => other.py().NotImplemented(), } } } @@ -478,7 +495,7 @@ impl RichComparisons2 { #[test] fn rich_comparisons() { Python::with_gil(|py| { - let c = PyCell::new(py, RichComparisons {}).unwrap(); + let c = Py::new(py, RichComparisons {}).unwrap(); py_run!(py, c, "assert (c < c) == 'RC < RC'"); py_run!(py, c, "assert (c < 1) == 'RC < 1'"); py_run!(py, c, "assert (1 < c) == 'RC > 1'"); @@ -503,7 +520,7 @@ fn rich_comparisons() { #[test] fn rich_comparisons_python_3_type_error() { Python::with_gil(|py| { - let c2 = PyCell::new(py, RichComparisons2 {}).unwrap(); + let c2 = Py::new(py, RichComparisons2 {}).unwrap(); py_expect_exception!(py, c2, "c2 < c2", PyTypeError); py_expect_exception!(py, c2, "c2 < 1", PyTypeError); py_expect_exception!(py, c2, "1 < c2", PyTypeError); @@ -540,7 +557,7 @@ mod return_not_implemented { } fn __richcmp__(&self, other: PyRef<'_, Self>, _op: CompareOp) -> PyObject { - other.py().None().into() + other.py().None() } fn __add__<'p>(slf: PyRef<'p, Self>, _other: PyRef<'p, Self>) -> PyRef<'p, Self> { @@ -604,7 +621,7 @@ mod return_not_implemented { fn _test_binary_dunder(dunder: &str) { Python::with_gil(|py| { - let c2 = PyCell::new(py, RichComparisonToSelf {}).unwrap(); + let c2 = Py::new(py, RichComparisonToSelf {}).unwrap(); py_run!( py, c2, @@ -620,7 +637,7 @@ mod return_not_implemented { _test_binary_dunder(dunder); Python::with_gil(|py| { - let c2 = PyCell::new(py, RichComparisonToSelf {}).unwrap(); + let c2 = Py::new(py, RichComparisonToSelf {}).unwrap(); py_expect_exception!( py, c2, diff --git a/tests/test_buffer.rs b/tests/test_buffer.rs index 12d1756fd92..0b3da881884 100644 --- a/tests/test_buffer.rs +++ b/tests/test_buffer.rs @@ -41,8 +41,6 @@ impl TestBufferErrors { return Err(PyBufferError::new_err("Object is not writable")); } - (*view).obj = ffi::_Py_NewRef(slf.as_ptr()); - let bytes = &slf.buf; (*view).buf = bytes.as_ptr() as *mut c_void; @@ -80,6 +78,8 @@ impl TestBufferErrors { } } + (*view).obj = slf.into_ptr(); + Ok(()) } } @@ -96,11 +96,11 @@ fn test_get_buffer_errors() { ) .unwrap(); - assert!(PyBuffer::::get(instance.as_ref(py)).is_ok()); + assert!(PyBuffer::::get_bound(instance.bind(py)).is_ok()); instance.borrow_mut(py).error = Some(TestGetBufferError::NullShape); assert_eq!( - PyBuffer::::get(instance.as_ref(py)) + PyBuffer::::get_bound(instance.bind(py)) .unwrap_err() .to_string(), "BufferError: shape is null" @@ -108,7 +108,7 @@ fn test_get_buffer_errors() { instance.borrow_mut(py).error = Some(TestGetBufferError::NullStrides); assert_eq!( - PyBuffer::::get(instance.as_ref(py)) + PyBuffer::::get_bound(instance.bind(py)) .unwrap_err() .to_string(), "BufferError: strides is null" @@ -116,7 +116,7 @@ fn test_get_buffer_errors() { instance.borrow_mut(py).error = Some(TestGetBufferError::IncorrectItemSize); assert_eq!( - PyBuffer::::get(instance.as_ref(py)) + PyBuffer::::get_bound(instance.bind(py)) .unwrap_err() .to_string(), "BufferError: buffer contents are not compatible with u32" @@ -124,7 +124,7 @@ fn test_get_buffer_errors() { instance.borrow_mut(py).error = Some(TestGetBufferError::IncorrectFormat); assert_eq!( - PyBuffer::::get(instance.as_ref(py)) + PyBuffer::::get_bound(instance.bind(py)) .unwrap_err() .to_string(), "BufferError: buffer contents are not compatible with u32" @@ -132,7 +132,7 @@ fn test_get_buffer_errors() { instance.borrow_mut(py).error = Some(TestGetBufferError::IncorrectAlignment); assert_eq!( - PyBuffer::::get(instance.as_ref(py)) + PyBuffer::::get_bound(instance.bind(py)) .unwrap_err() .to_string(), "BufferError: buffer contents are insufficiently aligned for u32" diff --git a/tests/test_buffer_protocol.rs b/tests/test_buffer_protocol.rs index 4d5dc69f942..700bcdcd206 100644 --- a/tests/test_buffer_protocol.rs +++ b/tests/test_buffer_protocol.rs @@ -24,11 +24,11 @@ struct TestBufferClass { #[pymethods] impl TestBufferClass { unsafe fn __getbuffer__( - slf: &PyCell, + slf: Bound<'_, Self>, view: *mut ffi::Py_buffer, flags: c_int, ) -> PyResult<()> { - fill_view_from_readonly_data(view, flags, &slf.borrow().vec, slf) + fill_view_from_readonly_data(view, flags, &slf.borrow().vec, slf.into_any()) } unsafe fn __releasebuffer__(&self, view: *mut ffi::Py_buffer) { @@ -57,7 +57,7 @@ fn test_buffer() { }, ) .unwrap(); - let env = [("ob", instance)].into_py_dict(py); + let env = [("ob", instance)].into_py_dict_bound(py); py_assert!(py, *env, "bytes(ob) == b' 23'"); }); @@ -77,7 +77,7 @@ fn test_buffer_referenced() { } .into_py(py); - let buf = PyBuffer::::get(instance.as_ref(py)).unwrap(); + let buf = PyBuffer::::get_bound(instance.bind(py)).unwrap(); assert_eq!(buf.to_vec(py).unwrap(), input); drop(instance); buf @@ -105,12 +105,12 @@ fn test_releasebuffer_unraisable_error() { #[pymethods] impl ReleaseBufferError { unsafe fn __getbuffer__( - slf: &PyCell, + slf: Bound<'_, Self>, view: *mut ffi::Py_buffer, flags: c_int, ) -> PyResult<()> { static BUF_BYTES: &[u8] = b"hello world"; - fill_view_from_readonly_data(view, flags, BUF_BYTES, slf) + fill_view_from_readonly_data(view, flags, BUF_BYTES, slf.into_any()) } unsafe fn __releasebuffer__(&self, _view: *mut ffi::Py_buffer) -> PyResult<()> { @@ -122,7 +122,7 @@ fn test_releasebuffer_unraisable_error() { let capture = UnraisableCapture::install(py); let instance = Py::new(py, ReleaseBufferError {}).unwrap(); - let env = [("ob", instance.clone())].into_py_dict(py); + let env = [("ob", instance.clone_ref(py))].into_py_dict_bound(py); assert!(capture.borrow(py).capture.is_none()); @@ -145,7 +145,7 @@ unsafe fn fill_view_from_readonly_data( view: *mut ffi::Py_buffer, flags: c_int, data: &[u8], - owner: &PyAny, + owner: Bound<'_, PyAny>, ) -> PyResult<()> { if view.is_null() { return Err(PyBufferError::new_err("View is null")); @@ -155,7 +155,7 @@ unsafe fn fill_view_from_readonly_data( return Err(PyBufferError::new_err("Object is not writable")); } - (*view).obj = ffi::_Py_NewRef(owner.as_ptr()); + (*view).obj = owner.into_ptr(); (*view).buf = data.as_ptr() as *mut c_void; (*view).len = data.len() as isize; diff --git a/tests/test_bytes.rs b/tests/test_bytes.rs index 3ad1352748c..5adca3f154a 100644 --- a/tests/test_bytes.rs +++ b/tests/test_bytes.rs @@ -14,20 +14,20 @@ fn bytes_pybytes_conversion(bytes: &[u8]) -> &[u8] { #[test] fn test_pybytes_bytes_conversion() { Python::with_gil(|py| { - let f = wrap_pyfunction!(bytes_pybytes_conversion)(py).unwrap(); + let f = wrap_pyfunction_bound!(bytes_pybytes_conversion)(py).unwrap(); py_assert!(py, f, "f(b'Hello World') == b'Hello World'"); }); } #[pyfunction] -fn bytes_vec_conversion(py: Python<'_>, bytes: Vec) -> &PyBytes { - PyBytes::new(py, bytes.as_slice()) +fn bytes_vec_conversion(py: Python<'_>, bytes: Vec) -> Bound<'_, PyBytes> { + PyBytes::new_bound(py, bytes.as_slice()) } #[test] fn test_pybytes_vec_conversion() { Python::with_gil(|py| { - let f = wrap_pyfunction!(bytes_vec_conversion)(py).unwrap(); + let f = wrap_pyfunction_bound!(bytes_vec_conversion)(py).unwrap(); py_assert!(py, f, "f(b'Hello World') == b'Hello World'"); }); } @@ -35,7 +35,7 @@ fn test_pybytes_vec_conversion() { #[test] fn test_bytearray_vec_conversion() { Python::with_gil(|py| { - let f = wrap_pyfunction!(bytes_vec_conversion)(py).unwrap(); + let f = wrap_pyfunction_bound!(bytes_vec_conversion)(py).unwrap(); py_assert!(py, f, "f(bytearray(b'Hello World')) == b'Hello World'"); }); } @@ -43,9 +43,11 @@ fn test_bytearray_vec_conversion() { #[test] fn test_py_as_bytes() { let pyobj: pyo3::Py = - Python::with_gil(|py| pyo3::types::PyBytes::new(py, b"abc").into_py(py)); + Python::with_gil(|py| pyo3::types::PyBytes::new_bound(py, b"abc").unbind()); let data = Python::with_gil(|py| pyobj.as_bytes(py)); assert_eq!(data, b"abc"); + + Python::with_gil(move |_py| drop(pyobj)); } diff --git a/tests/test_class_attributes.rs b/tests/test_class_attributes.rs index 0ff0dd6d9d8..9e544211c3c 100644 --- a/tests/test_class_attributes.rs +++ b/tests/test_class_attributes.rs @@ -56,7 +56,7 @@ impl Foo { #[test] fn class_attributes() { Python::with_gil(|py| { - let foo_obj = py.get_type::(); + let foo_obj = py.get_type_bound::(); py_assert!(py, foo_obj, "foo_obj.MY_CONST == 'foobar'"); py_assert!(py, foo_obj, "foo_obj.RENAMED_CONST == 'foobar_2'"); py_assert!(py, foo_obj, "foo_obj.a == 5"); @@ -72,7 +72,7 @@ fn class_attributes() { #[ignore] fn class_attributes_are_immutable() { Python::with_gil(|py| { - let foo_obj = py.get_type::(); + let foo_obj = py.get_type_bound::(); py_expect_exception!(py, foo_obj, "foo_obj.a = 6", PyTypeError); }); } @@ -88,8 +88,8 @@ impl Bar { #[test] fn recursive_class_attributes() { Python::with_gil(|py| { - let foo_obj = py.get_type::(); - let bar_obj = py.get_type::(); + let foo_obj = py.get_type_bound::(); + let bar_obj = py.get_type_bound::(); py_assert!(py, foo_obj, "foo_obj.a_foo.x == 1"); py_assert!(py, foo_obj, "foo_obj.bar.x == 2"); py_assert!(py, bar_obj, "bar_obj.a_foo.x == 3"); @@ -101,16 +101,16 @@ fn test_fallible_class_attribute() { use pyo3::{exceptions::PyValueError, types::PyString}; struct CaptureStdErr<'py> { - oldstderr: &'py PyAny, - string_io: &'py PyAny, + oldstderr: Bound<'py, PyAny>, + string_io: Bound<'py, PyAny>, } impl<'py> CaptureStdErr<'py> { fn new(py: Python<'py>) -> PyResult { - let sys = py.import("sys")?; + let sys = py.import_bound("sys")?; let oldstderr = sys.getattr("stderr")?; - let string_io = py.import("io")?.getattr("StringIO")?.call0()?; - sys.setattr("stderr", string_io)?; + let string_io = py.import_bound("io")?.getattr("StringIO")?.call0()?; + sys.setattr("stderr", &string_io)?; Ok(Self { oldstderr, string_io, @@ -124,9 +124,9 @@ fn test_fallible_class_attribute() { .getattr("getvalue")? .call0()? .downcast::()? - .to_str()? - .to_owned(); - let sys = py.import("sys")?; + .to_cow()? + .into_owned(); + let sys = py.import_bound("sys")?; sys.setattr("stderr", self.oldstderr)?; Ok(payload) } @@ -145,7 +145,7 @@ fn test_fallible_class_attribute() { Python::with_gil(|py| { let stderr = CaptureStdErr::new(py).unwrap(); - assert!(std::panic::catch_unwind(|| py.get_type::()).is_err()); + assert!(std::panic::catch_unwind(|| py.get_type_bound::()).is_err()); assert_eq!( stderr.reset().unwrap().trim(), "\ @@ -187,15 +187,15 @@ fn test_renaming_all_struct_fields() { use pyo3::types::PyBool; Python::with_gil(|py| { - let struct_class = py.get_type::(); + let struct_class = py.get_type_bound::(); let struct_obj = struct_class.call0().unwrap(); assert!(struct_obj - .setattr("firstField", PyBool::new(py, false)) + .setattr("firstField", PyBool::new_bound(py, false)) .is_ok()); py_assert!(py, struct_obj, "struct_obj.firstField == False"); py_assert!(py, struct_obj, "struct_obj.secondField == 5"); assert!(struct_obj - .setattr("third_field", PyBool::new(py, true)) + .setattr("third_field", PyBool::new_bound(py, true)) .is_ok()); py_assert!(py, struct_obj, "struct_obj.third_field == True"); }); @@ -220,11 +220,11 @@ macro_rules! test_case { //use pyo3::types::PyInt; Python::with_gil(|py| { - let struct_class = py.get_type::<$struct_name>(); + let struct_class = py.get_type_bound::<$struct_name>(); let struct_obj = struct_class.call0().unwrap(); assert!(struct_obj.setattr($renamed_field_name, 2).is_ok()); let attr = struct_obj.getattr($renamed_field_name).unwrap(); - assert_eq!(2, PyAny::extract::(attr).unwrap()); + assert_eq!(2, attr.extract().unwrap()); }); } }; diff --git a/tests/test_class_basics.rs b/tests/test_class_basics.rs index c349ae248fa..6f19eafbef0 100644 --- a/tests/test_class_basics.rs +++ b/tests/test_class_basics.rs @@ -13,7 +13,7 @@ struct EmptyClass {} #[test] fn empty_class() { Python::with_gil(|py| { - let typeobj = py.get_type::(); + let typeobj = py.get_type_bound::(); // By default, don't allow creating instances from python. assert!(typeobj.call((), None).is_err()); @@ -27,7 +27,7 @@ struct UnitClass; #[test] fn unit_class() { Python::with_gil(|py| { - let typeobj = py.get_type::(); + let typeobj = py.get_type_bound::(); // By default, don't allow creating instances from python. assert!(typeobj.call((), None).is_err()); @@ -58,7 +58,7 @@ struct ClassWithDocs { #[test] fn class_with_docstr() { Python::with_gil(|py| { - let typeobj = py.get_type::(); + let typeobj = py.get_type_bound::(); py_run!( py, typeobj, @@ -104,7 +104,7 @@ impl EmptyClass2 { #[test] fn custom_names() { Python::with_gil(|py| { - let typeobj = py.get_type::(); + let typeobj = py.get_type_bound::(); py_assert!(py, typeobj, "typeobj.__name__ == 'CustomName'"); py_assert!(py, typeobj, "typeobj.custom_fn.__name__ == 'custom_fn'"); py_assert!( @@ -137,7 +137,7 @@ impl RawIdents { #[test] fn test_raw_idents() { Python::with_gil(|py| { - let typeobj = py.get_type::(); + let typeobj = py.get_type_bound::(); py_assert!(py, typeobj, "not hasattr(typeobj, 'r#fn')"); py_assert!(py, typeobj, "hasattr(typeobj, 'fn')"); py_assert!(py, typeobj, "hasattr(typeobj, 'type')"); @@ -154,7 +154,7 @@ struct EmptyClassInModule {} #[ignore] fn empty_class_in_module() { Python::with_gil(|py| { - let module = PyModule::new(py, "test_module.nested").unwrap(); + let module = PyModule::new_bound(py, "test_module.nested").unwrap(); module.add_class::().unwrap(); let ty = module.getattr("EmptyClassInModule").unwrap(); @@ -172,6 +172,7 @@ fn empty_class_in_module() { }); } +#[cfg(feature = "py-clone")] #[pyclass] struct ClassWithObjectField { // It used to be that PyObject was not supported with (get, set) @@ -180,6 +181,7 @@ struct ClassWithObjectField { value: PyObject, } +#[cfg(feature = "py-clone")] #[pymethods] impl ClassWithObjectField { #[new] @@ -188,10 +190,11 @@ impl ClassWithObjectField { } } +#[cfg(feature = "py-clone")] #[test] fn class_with_object_field() { Python::with_gil(|py| { - let ty = py.get_type::(); + let ty = py.get_type_bound::(); py_assert!(py, ty, "ty(5).value == 5"); py_assert!(py, ty, "ty(None).value == None"); }); @@ -229,26 +232,25 @@ impl UnsendableChild { } fn test_unsendable() -> PyResult<()> { - let obj = Python::with_gil(|py| -> PyResult<_> { - let obj: Py = PyType::new::(py).call1((5,))?.extract()?; + let (keep_obj_here, obj) = Python::with_gil(|py| -> PyResult<_> { + let obj: Py = PyType::new_bound::(py).call1((5,))?.extract()?; // Accessing the value inside this thread should not panic let caught_panic = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| -> PyResult<_> { - assert_eq!(obj.as_ref(py).getattr("value")?.extract::()?, 5); + assert_eq!(obj.getattr(py, "value")?.extract::(py)?, 5); Ok(()) })) .is_err(); assert!(!caught_panic); - Ok(obj) - })?; - let keep_obj_here = obj.clone(); + Ok((obj.clone_ref(py), obj)) + })?; let caught_panic = std::thread::spawn(move || { // This access must panic - Python::with_gil(|py| { + Python::with_gil(move |py| { obj.borrow(py); }); }) @@ -284,12 +286,16 @@ fn panic_unsendable_child() { test_unsendable::().unwrap(); } -fn get_length(obj: &PyAny) -> PyResult { +fn get_length(obj: &Bound<'_, PyAny>) -> PyResult { let length = obj.len()?; Ok(length) } +fn is_even(obj: &Bound<'_, PyAny>) -> PyResult { + obj.extract::().map(|i| i % 2 == 0) +} + #[pyclass] struct ClassWithFromPyWithMethods {} @@ -299,7 +305,10 @@ impl ClassWithFromPyWithMethods { argument } #[classmethod] - fn classmethod(_cls: &PyType, #[pyo3(from_py_with = "PyAny::len")] argument: usize) -> usize { + fn classmethod( + _cls: &Bound<'_, PyType>, + #[pyo3(from_py_with = "Bound::<'_, PyAny>::len")] argument: usize, + ) -> usize { argument } @@ -307,6 +316,10 @@ impl ClassWithFromPyWithMethods { fn staticmethod(#[pyo3(from_py_with = "get_length")] argument: usize) -> usize { argument } + + fn __contains__(&self, #[pyo3(from_py_with = "is_even")] obj: bool) -> bool { + obj + } } #[test] @@ -323,6 +336,9 @@ fn test_pymethods_from_py_with() { assert instance.instance_method(arg) == 2 assert instance.classmethod(arg) == 2 assert instance.staticmethod(arg) == 2 + + assert 42 in instance + assert 73 not in instance "# ); }) @@ -334,7 +350,7 @@ struct TupleClass(#[pyo3(get, set, name = "value")] i32); #[test] fn test_tuple_struct_class() { Python::with_gil(|py| { - let typeobj = py.get_type::(); + let typeobj = py.get_type_bound::(); assert!(typeobj.call((), None).is_err()); py_assert!(py, typeobj, "typeobj.__name__ == 'TupleClass'"); @@ -364,7 +380,7 @@ struct DunderDictSupport { #[cfg_attr(all(Py_LIMITED_API, not(Py_3_9)), ignore)] fn dunder_dict_support() { Python::with_gil(|py| { - let inst = PyCell::new( + let inst = Py::new( py, DunderDictSupport { _pad: *b"DEADBEEFDEADBEEFDEADBEEFDEADBEEF", @@ -387,7 +403,7 @@ fn dunder_dict_support() { #[cfg_attr(all(Py_LIMITED_API, not(Py_3_10)), ignore)] fn access_dunder_dict() { Python::with_gil(|py| { - let inst = PyCell::new( + let inst = Py::new( py, DunderDictSupport { _pad: *b"DEADBEEFDEADBEEFDEADBEEFDEADBEEF", @@ -415,7 +431,7 @@ struct InheritDict { #[cfg_attr(all(Py_LIMITED_API, not(Py_3_9)), ignore)] fn inherited_dict() { Python::with_gil(|py| { - let inst = PyCell::new( + let inst = Py::new( py, ( InheritDict { _value: 0 }, @@ -446,7 +462,7 @@ struct WeakRefDunderDictSupport { #[cfg_attr(all(Py_LIMITED_API, not(Py_3_9)), ignore)] fn weakref_dunder_dict_support() { Python::with_gil(|py| { - let inst = PyCell::new( + let inst = Py::new( py, WeakRefDunderDictSupport { _pad: *b"DEADBEEFDEADBEEFDEADBEEFDEADBEEF", @@ -470,7 +486,7 @@ struct WeakRefSupport { #[cfg_attr(all(Py_LIMITED_API, not(Py_3_9)), ignore)] fn weakref_support() { Python::with_gil(|py| { - let inst = PyCell::new( + let inst = Py::new( py, WeakRefSupport { _pad: *b"DEADBEEFDEADBEEFDEADBEEFDEADBEEF", @@ -495,7 +511,7 @@ struct InheritWeakRef { #[cfg_attr(all(Py_LIMITED_API, not(Py_3_9)), ignore)] fn inherited_weakref() { Python::with_gil(|py| { - let inst = PyCell::new( + let inst = Py::new( py, ( InheritWeakRef { _value: 0 }, @@ -527,7 +543,7 @@ fn access_frozen_class_without_gil() { value: AtomicUsize::new(0), }; - let cell = PyCell::new(py, counter).unwrap(); + let cell = Bound::new(py, counter).unwrap(); cell.get().value.fetch_add(1, Ordering::Relaxed); @@ -535,6 +551,8 @@ fn access_frozen_class_without_gil() { }); assert_eq!(py_counter.get().value.load(Ordering::Relaxed), 1); + + Python::with_gil(move |_py| drop(py_counter)); } #[test] diff --git a/tests/test_class_conversion.rs b/tests/test_class_conversion.rs index 81a3d7bfbfd..a46132b9586 100644 --- a/tests/test_class_conversion.rs +++ b/tests/test_class_conversion.rs @@ -1,7 +1,6 @@ #![cfg(feature = "macros")] use pyo3::prelude::*; -use pyo3::ToPyObject; #[macro_use] #[path = "../src/tests/common.rs"] @@ -55,16 +54,18 @@ impl SubClass { } } +#[cfg(feature = "py-clone")] #[pyclass] struct PolymorphicContainer { #[pyo3(get, set)] inner: Py, } +#[cfg(feature = "py-clone")] #[test] fn test_polymorphic_container_stores_base_class() { Python::with_gil(|py| { - let p = PyCell::new( + let p = Py::new( py, PolymorphicContainer { inner: Py::new(py, BaseClass::default()).unwrap(), @@ -77,10 +78,11 @@ fn test_polymorphic_container_stores_base_class() { }); } +#[cfg(feature = "py-clone")] #[test] fn test_polymorphic_container_stores_sub_class() { Python::with_gil(|py| { - let p = PyCell::new( + let p = Py::new( py, PolymorphicContainer { inner: Py::new(py, BaseClass::default()).unwrap(), @@ -89,10 +91,10 @@ fn test_polymorphic_container_stores_sub_class() { .unwrap() .to_object(py); - p.as_ref(py) + p.bind(py) .setattr( "inner", - PyCell::new( + Py::new( py, PyClassInitializer::from(BaseClass::default()).add_subclass(SubClass {}), ) @@ -104,10 +106,11 @@ fn test_polymorphic_container_stores_sub_class() { }); } +#[cfg(feature = "py-clone")] #[test] fn test_polymorphic_container_does_not_accept_other_types() { Python::with_gil(|py| { - let p = PyCell::new( + let p = Py::new( py, PolymorphicContainer { inner: Py::new(py, BaseClass::default()).unwrap(), @@ -116,10 +119,10 @@ fn test_polymorphic_container_does_not_accept_other_types() { .unwrap() .to_object(py); - let setattr = |value: PyObject| p.as_ref(py).setattr("inner", value); + let setattr = |value: PyObject| p.bind(py).setattr("inner", value); assert!(setattr(1i32.into_py(py)).is_err()); - assert!(setattr(py.None().into()).is_err()); + assert!(setattr(py.None()).is_err()); assert!(setattr((1i32, 2i32).into_py(py)).is_err()); }); } @@ -127,7 +130,7 @@ fn test_polymorphic_container_does_not_accept_other_types() { #[test] fn test_pyref_as_base() { Python::with_gil(|py| { - let cell = PyCell::new(py, (SubClass {}, BaseClass { value: 120 })).unwrap(); + let cell = Bound::new(py, (SubClass {}, BaseClass { value: 120 })).unwrap(); // First try PyRefMut let sub: PyRefMut<'_, SubClass> = cell.borrow_mut(); @@ -147,12 +150,12 @@ fn test_pyref_as_base() { #[test] fn test_pycell_deref() { Python::with_gil(|py| { - let cell = PyCell::new(py, (SubClass {}, BaseClass { value: 120 })).unwrap(); + let obj = Bound::new(py, (SubClass {}, BaseClass { value: 120 })).unwrap(); // Should be able to deref as PyAny assert_eq!( - cell.call_method0("foo") - .and_then(PyAny::extract::<&str>) + obj.call_method0("foo") + .and_then(|e| e.extract::()) .unwrap(), "SubClass" ); diff --git a/tests/test_class_new.rs b/tests/test_class_new.rs index 8cb426861db..161c60e9489 100644 --- a/tests/test_class_new.rs +++ b/tests/test_class_new.rs @@ -19,17 +19,17 @@ impl EmptyClassWithNew { #[test] fn empty_class_with_new() { Python::with_gil(|py| { - let typeobj = py.get_type::(); + let typeobj = py.get_type_bound::(); assert!(typeobj .call((), None) .unwrap() - .downcast::>() + .downcast::() .is_ok()); // Calling with arbitrary args or kwargs is not ok assert!(typeobj.call(("some", "args"), None).is_err()); assert!(typeobj - .call((), Some([("some", "kwarg")].into_py_dict(py))) + .call((), Some(&[("some", "kwarg")].into_py_dict_bound(py))) .is_err()); }); } @@ -48,11 +48,11 @@ impl UnitClassWithNew { #[test] fn unit_class_with_new() { Python::with_gil(|py| { - let typeobj = py.get_type::(); + let typeobj = py.get_type_bound::(); assert!(typeobj .call((), None) .unwrap() - .downcast::>() + .downcast::() .is_ok()); }); } @@ -71,9 +71,9 @@ impl TupleClassWithNew { #[test] fn tuple_class_with_new() { Python::with_gil(|py| { - let typeobj = py.get_type::(); + let typeobj = py.get_type_bound::(); let wrp = typeobj.call((42,), None).unwrap(); - let obj = wrp.downcast::>().unwrap(); + let obj = wrp.downcast::().unwrap(); let obj_ref = obj.borrow(); assert_eq!(obj_ref.0, 42); }); @@ -96,9 +96,9 @@ impl NewWithOneArg { #[test] fn new_with_one_arg() { Python::with_gil(|py| { - let typeobj = py.get_type::(); + let typeobj = py.get_type_bound::(); let wrp = typeobj.call((42,), None).unwrap(); - let obj = wrp.downcast::>().unwrap(); + let obj = wrp.downcast::().unwrap(); let obj_ref = obj.borrow(); assert_eq!(obj_ref.data, 42); }); @@ -124,12 +124,12 @@ impl NewWithTwoArgs { #[test] fn new_with_two_args() { Python::with_gil(|py| { - let typeobj = py.get_type::(); + let typeobj = py.get_type_bound::(); let wrp = typeobj .call((10, 20), None) .map_err(|e| e.display(py)) .unwrap(); - let obj = wrp.downcast::>().unwrap(); + let obj = wrp.downcast::().unwrap(); let obj_ref = obj.borrow(); assert_eq!(obj_ref.data1, 10); assert_eq!(obj_ref.data2, 20); @@ -155,7 +155,7 @@ impl SuperClass { #[test] fn subclass_new() { Python::with_gil(|py| { - let super_cls = py.get_type::(); + let super_cls = py.get_type_bound::(); let source = pyo3::indoc::indoc!( r#" class Class(SuperClass): @@ -169,9 +169,9 @@ c = Class() assert c.from_rust is False "# ); - let globals = PyModule::import(py, "__main__").unwrap().dict(); + let globals = PyModule::import_bound(py, "__main__").unwrap().dict(); globals.set_item("SuperClass", super_cls).unwrap(); - py.run(source, Some(globals), None) + py.run_bound(source, Some(&globals), None) .map_err(|e| e.display(py)) .unwrap(); }); @@ -200,7 +200,7 @@ impl NewWithCustomError { #[test] fn new_with_custom_error() { Python::with_gil(|py| { - let typeobj = py.get_type::(); + let typeobj = py.get_type_bound::(); let err = typeobj.call0().unwrap_err(); assert_eq!(err.to_string(), "ValueError: custom error"); }); @@ -219,12 +219,8 @@ impl NewExisting { static PRE_BUILT: GILOnceCell<[pyo3::Py; 2]> = GILOnceCell::new(); let existing = PRE_BUILT.get_or_init(py, || { [ - pyo3::PyCell::new(py, NewExisting { num: 0 }) - .unwrap() - .into(), - pyo3::PyCell::new(py, NewExisting { num: 1 }) - .unwrap() - .into(), + pyo3::Py::new(py, NewExisting { num: 0 }).unwrap(), + pyo3::Py::new(py, NewExisting { num: 1 }).unwrap(), ] }); @@ -232,16 +228,14 @@ impl NewExisting { return existing[val].clone_ref(py); } - pyo3::PyCell::new(py, NewExisting { num: val }) - .unwrap() - .into() + pyo3::Py::new(py, NewExisting { num: val }).unwrap() } } #[test] fn test_new_existing() { Python::with_gil(|py| { - let typeobj = py.get_type::(); + let typeobj = py.get_type_bound::(); let obj1 = typeobj.call1((0,)).unwrap(); let obj2 = typeobj.call1((0,)).unwrap(); @@ -257,10 +251,10 @@ fn test_new_existing() { assert!(obj5.getattr("num").unwrap().extract::().unwrap() == 2); assert!(obj6.getattr("num").unwrap().extract::().unwrap() == 2); - assert!(obj1.is(obj2)); - assert!(obj3.is(obj4)); - assert!(!obj1.is(obj3)); - assert!(!obj1.is(obj5)); - assert!(!obj5.is(obj6)); + assert!(obj1.is(&obj2)); + assert!(obj3.is(&obj4)); + assert!(!obj1.is(&obj3)); + assert!(!obj1.is(&obj5)); + assert!(!obj5.is(&obj6)); }); } diff --git a/tests/test_compile_error.rs b/tests/test_compile_error.rs index dfefd18b64f..bc787f353a9 100644 --- a/tests/test_compile_error.rs +++ b/tests/test_compile_error.rs @@ -13,29 +13,57 @@ fn test_compile_errors() { t.compile_fail("tests/ui/invalid_pyfunction_signatures.rs"); #[cfg(any(not(Py_LIMITED_API), Py_3_11))] t.compile_fail("tests/ui/invalid_pymethods_buffer.rs"); + // The output is not stable across abi3 / not abi3 and features + #[cfg(all(not(Py_LIMITED_API), feature = "full", not(feature = "gil-refs")))] t.compile_fail("tests/ui/invalid_pymethods_duplicates.rs"); + t.compile_fail("tests/ui/invalid_pymethod_enum.rs"); t.compile_fail("tests/ui/invalid_pymethod_names.rs"); t.compile_fail("tests/ui/invalid_pymodule_args.rs"); t.compile_fail("tests/ui/reject_generics.rs"); + #[cfg(feature = "gil-refs")] t.compile_fail("tests/ui/deprecations.rs"); t.compile_fail("tests/ui/invalid_closure.rs"); t.compile_fail("tests/ui/pyclass_send.rs"); t.compile_fail("tests/ui/invalid_argument_attributes.rs"); t.compile_fail("tests/ui/invalid_frompy_derive.rs"); t.compile_fail("tests/ui/static_ref.rs"); + #[cfg(not(feature = "gil-refs"))] t.compile_fail("tests/ui/wrong_aspyref_lifetimes.rs"); t.compile_fail("tests/ui/invalid_pyfunctions.rs"); t.compile_fail("tests/ui/invalid_pymethods.rs"); - #[cfg(Py_LIMITED_API)] + // output changes with async feature + #[cfg(all(Py_LIMITED_API, feature = "experimental-async"))] t.compile_fail("tests/ui/abi3_nativetype_inheritance.rs"); + #[cfg(not(feature = "gil-refs"))] t.compile_fail("tests/ui/invalid_intern_arg.rs"); t.compile_fail("tests/ui/invalid_frozen_pyclass_borrow.rs"); t.compile_fail("tests/ui/invalid_pymethod_receiver.rs"); t.compile_fail("tests/ui/missing_intopy.rs"); // adding extra error conversion impls changes the output - #[cfg(not(any(windows, feature = "eyre", feature = "anyhow")))] + #[cfg(not(any( + windows, + feature = "eyre", + feature = "anyhow", + feature = "gil-refs", + Py_LIMITED_API + )))] t.compile_fail("tests/ui/invalid_result_conversion.rs"); t.compile_fail("tests/ui/not_send.rs"); t.compile_fail("tests/ui/get_set_all.rs"); t.compile_fail("tests/ui/traverse.rs"); + #[cfg(feature = "experimental-declarative-modules")] + t.compile_fail("tests/ui/invalid_pymodule_in_root.rs"); + #[cfg(feature = "experimental-declarative-modules")] + t.compile_fail("tests/ui/invalid_pymodule_glob.rs"); + #[cfg(feature = "experimental-declarative-modules")] + t.compile_fail("tests/ui/invalid_pymodule_trait.rs"); + #[cfg(feature = "experimental-declarative-modules")] + t.compile_fail("tests/ui/invalid_pymodule_two_pymodule_init.rs"); + #[cfg(feature = "experimental-async")] + #[cfg(any(not(Py_LIMITED_API), Py_3_10))] // to avoid PyFunctionArgument for &str + t.compile_fail("tests/ui/invalid_cancel_handle.rs"); + t.pass("tests/ui/pymodule_missing_docs.rs"); + #[cfg(all(Py_LIMITED_API, not(feature = "experimental-async")))] + // output changes with async feature + t.compile_fail("tests/ui/abi3_inheritance.rs"); } diff --git a/tests/test_coroutine.rs b/tests/test_coroutine.rs index 206c35da93c..75b524edf78 100644 --- a/tests/test_coroutine.rs +++ b/tests/test_coroutine.rs @@ -1,14 +1,18 @@ -#![cfg(feature = "macros")] +#![cfg(feature = "experimental-async")] #![cfg(not(target_arch = "wasm32"))] -use std::{ops::Deref, task::Poll, thread, time::Duration}; +use std::{task::Poll, thread, time::Duration}; use futures::{channel::oneshot, future::poll_fn, FutureExt}; +#[cfg(not(target_has_atomic = "64"))] +use portable_atomic::{AtomicBool, Ordering}; use pyo3::{ coroutine::CancelHandle, prelude::*, py_run, types::{IntoPyDict, PyType}, }; +#[cfg(target_has_atomic = "64")] +use std::sync::atomic::{AtomicBool, Ordering}; #[path = "../src/tests/common.rs"] mod common; @@ -29,7 +33,7 @@ fn noop_coroutine() { 42 } Python::with_gil(|gil| { - let noop = wrap_pyfunction!(noop, gil).unwrap(); + let noop = wrap_pyfunction_bound!(noop, gil).unwrap(); let test = "import asyncio; assert asyncio.run(noop()) == 42"; py_run!(gil, noop, &handle_windows(test)); }) @@ -65,10 +69,16 @@ fn test_coroutine_qualname() { assert coro.__name__ == name and coro.__qualname__ == qualname "#; let locals = [ - ("my_fn", wrap_pyfunction!(my_fn, gil).unwrap().deref()), - ("MyClass", gil.get_type::()), + ( + "my_fn", + wrap_pyfunction_bound!(my_fn, gil) + .unwrap() + .as_borrowed() + .as_any(), + ), + ("MyClass", gil.get_type_bound::().as_any()), ] - .into_py_dict(gil); + .into_py_dict_bound(gil); py_run!(gil, *locals, &handle_windows(test)); }) } @@ -89,7 +99,7 @@ fn sleep_0_like_coroutine() { .await } Python::with_gil(|gil| { - let sleep_0 = wrap_pyfunction!(sleep_0, gil).unwrap(); + let sleep_0 = wrap_pyfunction_bound!(sleep_0, gil).unwrap(); let test = "import asyncio; assert asyncio.run(sleep_0()) == 42"; py_run!(gil, sleep_0, &handle_windows(test)); }) @@ -108,7 +118,7 @@ async fn sleep(seconds: f64) -> usize { #[test] fn sleep_coroutine() { Python::with_gil(|gil| { - let sleep = wrap_pyfunction!(sleep, gil).unwrap(); + let sleep = wrap_pyfunction_bound!(sleep, gil).unwrap(); let test = r#"import asyncio; assert asyncio.run(sleep(0.1)) == 42"#; py_run!(gil, sleep, &handle_windows(test)); }) @@ -117,7 +127,7 @@ fn sleep_coroutine() { #[test] fn cancelled_coroutine() { Python::with_gil(|gil| { - let sleep = wrap_pyfunction!(sleep, gil).unwrap(); + let sleep = wrap_pyfunction_bound!(sleep, gil).unwrap(); let test = r#" import asyncio async def main(): @@ -127,17 +137,17 @@ fn cancelled_coroutine() { await task asyncio.run(main()) "#; - let globals = gil.import("__main__").unwrap().dict(); + let globals = gil.import_bound("__main__").unwrap().dict(); globals.set_item("sleep", sleep).unwrap(); let err = gil - .run( + .run_bound( &pyo3::unindent::unindent(&handle_windows(test)), - Some(globals), + Some(&globals), None, ) .unwrap_err(); assert_eq!( - err.value(gil).get_type().qualname().unwrap(), + err.value_bound(gil).get_type().qualname().unwrap(), "CancelledError" ); }) @@ -156,7 +166,7 @@ fn coroutine_cancel_handle() { } } Python::with_gil(|gil| { - let cancellable_sleep = wrap_pyfunction!(cancellable_sleep, gil).unwrap(); + let cancellable_sleep = wrap_pyfunction_bound!(cancellable_sleep, gil).unwrap(); let test = r#" import asyncio; async def main(): @@ -166,13 +176,13 @@ fn coroutine_cancel_handle() { return await task assert asyncio.run(main()) == 0 "#; - let globals = gil.import("__main__").unwrap().dict(); + let globals = gil.import_bound("__main__").unwrap().dict(); globals .set_item("cancellable_sleep", cancellable_sleep) .unwrap(); - gil.run( + gil.run_bound( &pyo3::unindent::unindent(&handle_windows(test)), - Some(globals), + Some(&globals), None, ) .unwrap(); @@ -188,7 +198,7 @@ fn coroutine_is_cancelled() { } } Python::with_gil(|gil| { - let sleep_loop = wrap_pyfunction!(sleep_loop, gil).unwrap(); + let sleep_loop = wrap_pyfunction_bound!(sleep_loop, gil).unwrap(); let test = r#" import asyncio; async def main(): @@ -198,11 +208,11 @@ fn coroutine_is_cancelled() { await task asyncio.run(main()) "#; - let globals = gil.import("__main__").unwrap().dict(); + let globals = gil.import_bound("__main__").unwrap().dict(); globals.set_item("sleep_loop", sleep_loop).unwrap(); - gil.run( + gil.run_bound( &pyo3::unindent::unindent(&handle_windows(test)), - Some(globals), + Some(&globals), None, ) .unwrap(); @@ -216,7 +226,7 @@ fn coroutine_panic() { panic!("test panic"); } Python::with_gil(|gil| { - let panic = wrap_pyfunction!(panic, gil).unwrap(); + let panic = wrap_pyfunction_bound!(panic, gil).unwrap(); let test = r#" import asyncio coro = panic() @@ -256,10 +266,19 @@ fn test_async_method_receiver() { self.0 } } + + static IS_DROPPED: AtomicBool = AtomicBool::new(false); + + impl Drop for Counter { + fn drop(&mut self) { + IS_DROPPED.store(true, Ordering::SeqCst); + } + } + Python::with_gil(|gil| { let test = r#" import asyncio - + obj = Counter() coro1 = obj.get() coro2 = obj.get() @@ -286,7 +305,42 @@ fn test_async_method_receiver() { assert False assert asyncio.run(coro3) == 1 "#; - let locals = [("Counter", gil.get_type::())].into_py_dict(gil); + let locals = [("Counter", gil.get_type_bound::())].into_py_dict_bound(gil); py_run!(gil, *locals, test); - }) + }); + + assert!(IS_DROPPED.load(Ordering::SeqCst)); +} + +#[test] +fn test_async_method_receiver_with_other_args() { + #[pyclass] + struct Value(i32); + #[pymethods] + impl Value { + #[new] + fn new() -> Self { + Self(0) + } + async fn get_value_plus_with(&self, v1: i32, v2: i32) -> i32 { + self.0 + v1 + v2 + } + async fn set_value(&mut self, new_value: i32) -> i32 { + self.0 = new_value; + self.0 + } + } + + Python::with_gil(|gil| { + let test = r#" + import asyncio + + v = Value() + assert asyncio.run(v.get_value_plus_with(3, 0)) == 3 + assert asyncio.run(v.set_value(10)) == 10 + assert asyncio.run(v.get_value_plus_with(1, 1)) == 12 + "#; + let locals = [("Value", gil.get_type_bound::())].into_py_dict_bound(gil); + py_run!(gil, *locals, test); + }); } diff --git a/tests/test_datetime.rs b/tests/test_datetime.rs index 32163abe1e0..8a9d190ff7b 100644 --- a/tests/test_datetime.rs +++ b/tests/test_datetime.rs @@ -1,34 +1,34 @@ #![cfg(not(Py_LIMITED_API))] use pyo3::prelude::*; -use pyo3::types::{timezone_utc, IntoPyDict, PyDate, PyDateTime, PyTime}; +use pyo3::types::{timezone_utc_bound, IntoPyDict, PyDate, PyDateTime, PyTime}; use pyo3_ffi::PyDateTime_IMPORT; -fn _get_subclasses<'p>( - py: Python<'p>, +fn _get_subclasses<'py>( + py: Python<'py>, py_type: &str, args: &str, -) -> PyResult<(&'p PyAny, &'p PyAny, &'p PyAny)> { +) -> PyResult<(Bound<'py, PyAny>, Bound<'py, PyAny>, Bound<'py, PyAny>)> { // Import the class from Python and create some subclasses - let datetime = py.import("datetime")?; + let datetime = py.import_bound("datetime")?; - let locals = [(py_type, datetime.getattr(py_type)?)].into_py_dict(py); + let locals = [(py_type, datetime.getattr(py_type)?)].into_py_dict_bound(py); let make_subclass_py = format!("class Subklass({}):\n pass", py_type); let make_sub_subclass_py = "class SubSubklass(Subklass):\n pass"; - py.run(&make_subclass_py, None, Some(locals))?; - py.run(make_sub_subclass_py, None, Some(locals))?; + py.run_bound(&make_subclass_py, None, Some(&locals))?; + py.run_bound(make_sub_subclass_py, None, Some(&locals))?; // Construct an instance of the base class - let obj = py.eval(&format!("{}({})", py_type, args), None, Some(locals))?; + let obj = py.eval_bound(&format!("{}({})", py_type, args), None, Some(&locals))?; // Construct an instance of the subclass - let sub_obj = py.eval(&format!("Subklass({})", args), None, Some(locals))?; + let sub_obj = py.eval_bound(&format!("Subklass({})", args), None, Some(&locals))?; // Construct an instance of the sub-subclass - let sub_sub_obj = py.eval(&format!("SubSubklass({})", args), None, Some(locals))?; + let sub_sub_obj = py.eval_bound(&format!("SubSubklass({})", args), None, Some(&locals))?; Ok((obj, sub_obj, sub_sub_obj)) } @@ -118,14 +118,14 @@ fn test_datetime_utc() { use pyo3::types::PyDateTime; Python::with_gil(|py| { - let utc = timezone_utc(py); + let utc = timezone_utc_bound(py); - let dt = PyDateTime::new(py, 2018, 1, 1, 0, 0, 0, 0, Some(utc)).unwrap(); + let dt = PyDateTime::new_bound(py, 2018, 1, 1, 0, 0, 0, 0, Some(&utc)).unwrap(); - let locals = [("dt", dt)].into_py_dict(py); + let locals = [("dt", dt)].into_py_dict_bound(py); let offset: f32 = py - .eval("dt.utcoffset().total_seconds()", None, Some(locals)) + .eval_bound("dt.utcoffset().total_seconds()", None, Some(&locals)) .unwrap() .extract() .unwrap(); @@ -155,7 +155,7 @@ fn test_pydate_out_of_bounds() { Python::with_gil(|py| { for val in INVALID_DATES { let (year, month, day) = val; - let dt = PyDate::new(py, *year, *month, *day); + let dt = PyDate::new_bound(py, *year, *month, *day); dt.unwrap_err(); } }); @@ -168,7 +168,7 @@ fn test_pytime_out_of_bounds() { Python::with_gil(|py| { for val in INVALID_TIMES { let (hour, minute, second, microsecond) = val; - let dt = PyTime::new(py, *hour, *minute, *second, *microsecond, None); + let dt = PyTime::new_bound(py, *hour, *minute, *second, *microsecond, None); dt.unwrap_err(); } }); @@ -192,7 +192,7 @@ fn test_pydatetime_out_of_bounds() { let (date, time) = val; let (year, month, day) = date; let (hour, minute, second, microsecond) = time; - let dt = PyDateTime::new( + let dt = PyDateTime::new_bound( py, *year, *month, diff --git a/tests/test_datetime_import.rs b/tests/test_datetime_import.rs new file mode 100644 index 00000000000..619df891944 --- /dev/null +++ b/tests/test_datetime_import.rs @@ -0,0 +1,26 @@ +#![cfg(not(Py_LIMITED_API))] + +use pyo3::{prelude::*, types::PyDate}; + +#[test] +#[should_panic(expected = "module 'datetime' has no attribute 'datetime_CAPI'")] +fn test_bad_datetime_module_panic() { + // Create an empty temporary directory + // with an empty "datetime" module which we'll put on the sys.path + let tmpdir = std::env::temp_dir(); + let tmpdir = tmpdir.join("pyo3_test_date_check"); + let _ = std::fs::remove_dir_all(&tmpdir); + std::fs::create_dir(&tmpdir).unwrap(); + std::fs::File::create(tmpdir.join("datetime.py")).unwrap(); + + Python::with_gil(|py: Python<'_>| { + let sys = py.import_bound("sys").unwrap(); + sys.getattr("path") + .unwrap() + .call_method1("insert", (0, tmpdir)) + .unwrap(); + + // This should panic because the "datetime" module is empty + PyDate::new_bound(py, 2018, 1, 1).unwrap(); + }); +} diff --git a/tests/test_declarative_module.rs b/tests/test_declarative_module.rs new file mode 100644 index 00000000000..2e46f4a64d1 --- /dev/null +++ b/tests/test_declarative_module.rs @@ -0,0 +1,189 @@ +#![cfg(feature = "experimental-declarative-modules")] + +use pyo3::create_exception; +use pyo3::exceptions::PyException; +use pyo3::prelude::*; +#[cfg(not(Py_LIMITED_API))] +use pyo3::types::PyBool; + +#[path = "../src/tests/common.rs"] +mod common; + +mod some_module { + use pyo3::create_exception; + use pyo3::exceptions::PyException; + use pyo3::prelude::*; + + #[pyclass] + pub struct SomePyClass; + + create_exception!(some_module, SomeException, PyException); +} + +#[pyclass] +struct ValueClass { + value: usize, +} + +#[pymethods] +impl ValueClass { + #[new] + fn new(value: usize) -> Self { + Self { value } + } +} + +#[pyclass(module = "module")] +struct LocatedClass {} + +#[pyfunction] +fn double(x: usize) -> usize { + x * 2 +} + +create_exception!( + declarative_module, + MyError, + PyException, + "Some description." +); + +/// A module written using declarative syntax. +#[pymodule] +mod declarative_module { + #[pymodule_export] + use super::declarative_submodule; + #[pymodule_export] + // This is not a real constraint but to test cfg attribute support + #[cfg(not(Py_LIMITED_API))] + use super::LocatedClass; + use super::*; + #[pymodule_export] + use super::{declarative_module2, double, MyError, ValueClass as Value}; + + // test for #4036 + #[pymodule_export] + use super::some_module::SomePyClass; + + // test for #4036 + #[pymodule_export] + use super::some_module::SomeException; + + #[pymodule] + mod inner { + use super::*; + + #[pyfunction] + fn triple(x: usize) -> usize { + x * 3 + } + + #[pyclass] + struct Struct; + + #[pymethods] + impl Struct { + #[new] + fn new() -> Self { + Self + } + } + + #[pyclass] + enum Enum { + A, + B, + } + } + + #[pymodule_init] + fn init(m: &Bound<'_, PyModule>) -> PyResult<()> { + m.add("double2", m.getattr("double")?) + } +} + +#[pyfunction] +fn double_value(v: &ValueClass) -> usize { + v.value * 2 +} + +#[pymodule] +mod declarative_submodule { + #[pymodule_export] + use super::{double, double_value}; +} + +#[pymodule] +#[pyo3(name = "declarative_module_renamed")] +mod declarative_module2 { + #[pymodule_export] + use super::double; +} + +#[test] +fn test_declarative_module() { + Python::with_gil(|py| { + let m = pyo3::wrap_pymodule!(declarative_module)(py).into_bound(py); + py_assert!( + py, + m, + "m.__doc__ == 'A module written using declarative syntax.'" + ); + + py_assert!(py, m, "m.double(2) == 4"); + py_assert!(py, m, "m.inner.triple(3) == 9"); + py_assert!(py, m, "m.declarative_submodule.double(4) == 8"); + py_assert!( + py, + m, + "m.declarative_submodule.double_value(m.ValueClass(1)) == 2" + ); + py_assert!(py, m, "str(m.MyError('foo')) == 'foo'"); + py_assert!(py, m, "m.declarative_module_renamed.double(2) == 4"); + #[cfg(Py_LIMITED_API)] + py_assert!(py, m, "not hasattr(m, 'LocatedClass')"); + #[cfg(not(Py_LIMITED_API))] + py_assert!(py, m, "hasattr(m, 'LocatedClass')"); + py_assert!(py, m, "isinstance(m.inner.Struct(), m.inner.Struct)"); + py_assert!(py, m, "isinstance(m.inner.Enum.A, m.inner.Enum)"); + }) +} + +#[cfg(not(Py_LIMITED_API))] +#[pyclass(extends = PyBool)] +struct ExtendsBool; + +#[cfg(not(Py_LIMITED_API))] +#[pymodule] +mod class_initialization_module { + #[pymodule_export] + use super::ExtendsBool; +} + +#[test] +#[cfg(not(Py_LIMITED_API))] +fn test_class_initialization_fails() { + Python::with_gil(|py| { + let err = class_initialization_module::_PYO3_DEF + .make_module(py) + .unwrap_err(); + assert_eq!( + err.to_string(), + "RuntimeError: An error occurred while initializing class ExtendsBool" + ); + }) +} + +#[pymodule] +mod r#type { + #[pymodule_export] + use super::double; +} + +#[test] +fn test_raw_ident_module() { + Python::with_gil(|py| { + let m = pyo3::wrap_pymodule!(r#type)(py).into_bound(py); + py_assert!(py, m, "m.double(2) == 4"); + }) +} diff --git a/tests/test_dict_iter.rs b/tests/test_dict_iter.rs index dc32eb61fd7..e502a4ca2b6 100644 --- a/tests/test_dict_iter.rs +++ b/tests/test_dict_iter.rs @@ -6,7 +6,7 @@ use pyo3::types::IntoPyDict; fn iter_dict_nosegv() { Python::with_gil(|py| { const LEN: usize = 10_000_000; - let dict = (0..LEN as u64).map(|i| (i, i * 2)).into_py_dict(py); + let dict = (0..LEN as u64).map(|i| (i, i * 2)).into_py_dict_bound(py); let mut sum = 0; for (k, _v) in dict { let i: u64 = k.extract().unwrap(); diff --git a/tests/test_enum.rs b/tests/test_enum.rs index 33e94c241c7..63492b8d3cd 100644 --- a/tests/test_enum.rs +++ b/tests/test_enum.rs @@ -1,7 +1,7 @@ #![cfg(feature = "macros")] use pyo3::prelude::*; -use pyo3::{py_run, wrap_pyfunction}; +use pyo3::py_run; #[path = "../src/tests/common.rs"] mod common; @@ -16,7 +16,7 @@ pub enum MyEnum { #[test] fn test_enum_class_attr() { Python::with_gil(|py| { - let my_enum = py.get_type::(); + let my_enum = py.get_type_bound::(); let var = Py::new(py, MyEnum::Variant).unwrap(); py_assert!(py, my_enum var, "my_enum.Variant == var"); }) @@ -30,8 +30,8 @@ fn return_enum() -> MyEnum { #[test] fn test_return_enum() { Python::with_gil(|py| { - let f = wrap_pyfunction!(return_enum)(py).unwrap(); - let mynum = py.get_type::(); + let f = wrap_pyfunction_bound!(return_enum)(py).unwrap(); + let mynum = py.get_type_bound::(); py_run!(py, f mynum, "assert f() == mynum.Variant") }); @@ -45,8 +45,8 @@ fn enum_arg(e: MyEnum) { #[test] fn test_enum_arg() { Python::with_gil(|py| { - let f = wrap_pyfunction!(enum_arg)(py).unwrap(); - let mynum = py.get_type::(); + let f = wrap_pyfunction_bound!(enum_arg)(py).unwrap(); + let mynum = py.get_type_bound::(); py_run!(py, f mynum, "f(mynum.OtherVariant)") }) @@ -83,7 +83,7 @@ enum CustomDiscriminant { fn test_custom_discriminant() { Python::with_gil(|py| { #[allow(non_snake_case)] - let CustomDiscriminant = py.get_type::(); + let CustomDiscriminant = py.get_type_bound::(); let one = Py::new(py, CustomDiscriminant::One).unwrap(); let two = Py::new(py, CustomDiscriminant::Two).unwrap(); py_run!(py, CustomDiscriminant one two, r#" @@ -204,7 +204,7 @@ enum RenameAllVariantsEnum { #[test] fn test_renaming_all_enum_variants() { Python::with_gil(|py| { - let enum_obj = py.get_type::(); + let enum_obj = py.get_type_bound::(); py_assert!(py, enum_obj, "enum_obj.VARIANT_ONE == enum_obj.VARIANT_ONE"); py_assert!(py, enum_obj, "enum_obj.VARIANT_TWO == enum_obj.VARIANT_TWO"); py_assert!( diff --git a/tests/test_exceptions.rs b/tests/test_exceptions.rs index 700a5c54149..e85355fd40e 100644 --- a/tests/test_exceptions.rs +++ b/tests/test_exceptions.rs @@ -1,7 +1,7 @@ #![cfg(feature = "macros")] use pyo3::prelude::*; -use pyo3::{exceptions, py_run, PyErr, PyResult}; +use pyo3::{exceptions, py_run}; use std::error::Error; use std::fmt; #[cfg(not(target_os = "windows"))] @@ -22,7 +22,7 @@ fn fail_to_open_file() -> PyResult<()> { #[cfg(not(target_os = "windows"))] fn test_filenotfounderror() { Python::with_gil(|py| { - let fail_to_open_file = wrap_pyfunction!(fail_to_open_file)(py).unwrap(); + let fail_to_open_file = wrap_pyfunction_bound!(fail_to_open_file)(py).unwrap(); py_run!( py, @@ -68,7 +68,7 @@ fn call_fail_with_custom_error() -> PyResult<()> { fn test_custom_error() { Python::with_gil(|py| { let call_fail_with_custom_error = - wrap_pyfunction!(call_fail_with_custom_error)(py).unwrap(); + wrap_pyfunction_bound!(call_fail_with_custom_error)(py).unwrap(); py_run!( py, @@ -102,7 +102,7 @@ fn test_exception_nosegfault() { #[cfg(Py_3_8)] fn test_write_unraisable() { use common::UnraisableCapture; - use pyo3::{exceptions::PyRuntimeError, ffi}; + use pyo3::{exceptions::PyRuntimeError, ffi, types::PyNotImplemented}; Python::with_gil(|py| { let capture = UnraisableCapture::install(py); @@ -110,14 +110,14 @@ fn test_write_unraisable() { assert!(capture.borrow(py).capture.is_none()); let err = PyRuntimeError::new_err("foo"); - err.write_unraisable(py, None); + err.write_unraisable_bound(py, None); let (err, object) = capture.borrow_mut(py).capture.take().unwrap(); assert_eq!(err.to_string(), "RuntimeError: foo"); assert!(object.is_none(py)); let err = PyRuntimeError::new_err("bar"); - err.write_unraisable(py, Some(py.NotImplemented())); + err.write_unraisable_bound(py, Some(&PyNotImplemented::get_bound(py))); let (err, object) = capture.borrow_mut(py).capture.take().unwrap(); assert_eq!(err.to_string(), "RuntimeError: bar"); diff --git a/tests/test_field_cfg.rs b/tests/test_field_cfg.rs index bd671641e5b..c5fc4958dbf 100644 --- a/tests/test_field_cfg.rs +++ b/tests/test_field_cfg.rs @@ -22,8 +22,8 @@ fn test_cfg() { Python::with_gil(|py| { let cfg = CfgClass { b: 3 }; let py_cfg = Py::new(py, cfg).unwrap(); - assert!(py_cfg.as_ref(py).getattr("a").is_err()); - let b: u32 = py_cfg.as_ref(py).getattr("b").unwrap().extract().unwrap(); + assert!(py_cfg.bind(py).getattr("a").is_err()); + let b: u32 = py_cfg.bind(py).getattr("b").unwrap().extract().unwrap(); assert_eq!(b, 3); }); } diff --git a/tests/test_frompyobject.rs b/tests/test_frompyobject.rs index 47e5ec53e92..5c57a954023 100644 --- a/tests/test_frompyobject.rs +++ b/tests/test_frompyobject.rs @@ -22,13 +22,13 @@ fn extract_traceback(py: Python<'_>, mut error: PyErr) -> String { } #[derive(Debug, FromPyObject)] -pub struct A<'a> { +pub struct A<'py> { #[pyo3(attribute)] s: String, #[pyo3(item)] - t: &'a PyString, + t: Bound<'py, PyString>, #[pyo3(attribute("foo"))] - p: &'a PyAny, + p: Bound<'py, PyAny>, } #[pyclass] @@ -58,8 +58,9 @@ fn test_named_fields_struct() { foo: None, }; let py_c = Py::new(py, pya).unwrap(); - let a: A<'_> = - FromPyObject::extract(py_c.as_ref(py)).expect("Failed to extract A from PyA"); + let a = py_c + .extract::>(py) + .expect("Failed to extract A from PyA"); assert_eq!(a.s, "foo"); assert_eq!(a.t.to_string_lossy(), "bar"); assert!(a.p.is_none()); @@ -76,10 +77,12 @@ pub struct B { fn test_transparent_named_field_struct() { Python::with_gil(|py| { let test: PyObject = "test".into_py(py); - let b: B = FromPyObject::extract(test.as_ref(py)).expect("Failed to extract B from String"); + let b = test + .extract::(py) + .expect("Failed to extract B from String"); assert_eq!(b.test, "test"); let test: PyObject = 1.into_py(py); - let b = B::extract(test.as_ref(py)); + let b = test.extract::(py); assert!(b.is_err()); }); } @@ -94,12 +97,14 @@ pub struct D { fn test_generic_transparent_named_field_struct() { Python::with_gil(|py| { let test: PyObject = "test".into_py(py); - let d: D = - D::extract(test.as_ref(py)).expect("Failed to extract D from String"); + let d = test + .extract::>(py) + .expect("Failed to extract D from String"); assert_eq!(d.test, "test"); let test = 1usize.into_py(py); - let d: D = - D::extract(test.as_ref(py)).expect("Failed to extract D from String"); + let d = test + .extract::>(py) + .expect("Failed to extract D from String"); assert_eq!(d.test, 1); }); } @@ -128,11 +133,12 @@ fn test_generic_named_fields_struct() { } .into_py(py); - let e: E = - E::extract(pye.as_ref(py)).expect("Failed to extract E from PyE"); + let e = pye + .extract::>(py) + .expect("Failed to extract E from PyE"); assert_eq!(e.test, "test"); assert_eq!(e.test2, 2); - let e = E::::extract(pye.as_ref(py)); + let e = pye.extract::>(py); assert!(e.is_err()); }); } @@ -151,7 +157,7 @@ fn test_named_field_with_ext_fn() { test2: 0, } .into_py(py); - let c = C::extract(pyc.as_ref(py)).expect("Failed to extract C from PyE"); + let c = pyc.extract::(py).expect("Failed to extract C from PyE"); assert_eq!(c.test, "foo"); }); } @@ -162,11 +168,13 @@ pub struct Tuple(String, usize); #[test] fn test_tuple_struct() { Python::with_gil(|py| { - let tup = PyTuple::new(py, &[1.into_py(py), "test".into_py(py)]); - let tup = Tuple::extract(tup.as_ref()); + let tup = PyTuple::new_bound(py, &[1.into_py(py), "test".into_py(py)]); + let tup = tup.extract::(); assert!(tup.is_err()); - let tup = PyTuple::new(py, &["test".into_py(py), 1.into_py(py)]); - let tup = Tuple::extract(tup.as_ref()).expect("Failed to extract Tuple from PyTuple"); + let tup = PyTuple::new_bound(py, &["test".into_py(py), 1.into_py(py)]); + let tup = tup + .extract::() + .expect("Failed to extract Tuple from PyTuple"); assert_eq!(tup.0, "test"); assert_eq!(tup.1, 1); }); @@ -179,10 +187,11 @@ pub struct TransparentTuple(String); fn test_transparent_tuple_struct() { Python::with_gil(|py| { let tup: PyObject = 1.into_py(py); - let tup = TransparentTuple::extract(tup.as_ref(py)); + let tup = tup.extract::(py); assert!(tup.is_err()); let test: PyObject = "test".into_py(py); - let tup = TransparentTuple::extract(test.as_ref(py)) + let tup = test + .extract::(py) .expect("Failed to extract TransparentTuple from PyTuple"); assert_eq!(tup.0, "test"); }); @@ -215,7 +224,7 @@ fn test_struct_nested_type_errors() { } .into_py(py); - let test: PyResult> = FromPyObject::extract(pybaz.as_ref(py)); + let test = pybaz.extract::>(py); assert!(test.is_err()); assert_eq!( extract_traceback(py,test.unwrap_err()), @@ -237,7 +246,7 @@ fn test_struct_nested_type_errors_with_generics() { } .into_py(py); - let test: PyResult> = FromPyObject::extract(pybaz.as_ref(py)); + let test = pybaz.extract::>(py); assert!(test.is_err()); assert_eq!( extract_traceback(py, test.unwrap_err()), @@ -251,7 +260,7 @@ fn test_struct_nested_type_errors_with_generics() { fn test_transparent_struct_error_message() { Python::with_gil(|py| { let tup: PyObject = 1.into_py(py); - let tup = B::extract(tup.as_ref(py)); + let tup = tup.extract::(py); assert!(tup.is_err()); assert_eq!( extract_traceback(py,tup.unwrap_err()), @@ -265,7 +274,7 @@ fn test_transparent_struct_error_message() { fn test_tuple_struct_error_message() { Python::with_gil(|py| { let tup: PyObject = (1, "test").into_py(py); - let tup = Tuple::extract(tup.as_ref(py)); + let tup = tup.extract::(py); assert!(tup.is_err()); assert_eq!( extract_traceback(py, tup.unwrap_err()), @@ -279,7 +288,7 @@ fn test_tuple_struct_error_message() { fn test_transparent_tuple_error_message() { Python::with_gil(|py| { let tup: PyObject = 1.into_py(py); - let tup = TransparentTuple::extract(tup.as_ref(py)); + let tup = tup.extract::(py); assert!(tup.is_err()); assert_eq!( extract_traceback(py, tup.unwrap_err()), @@ -290,10 +299,10 @@ fn test_transparent_tuple_error_message() { } #[derive(Debug, FromPyObject)] -pub enum Foo<'a> { +pub enum Foo<'py> { TupleVar(usize, String), StructVar { - test: &'a PyString, + test: Bound<'py, PyString>, }, #[pyo3(transparent)] TransparentTuple(usize), @@ -324,8 +333,10 @@ pub struct PyBool { #[test] fn test_enum() { Python::with_gil(|py| { - let tup = PyTuple::new(py, &[1.into_py(py), "test".into_py(py)]); - let f = Foo::extract(tup.as_ref()).expect("Failed to extract Foo from tuple"); + let tup = PyTuple::new_bound(py, &[1.into_py(py), "test".into_py(py)]); + let f = tup + .extract::>() + .expect("Failed to extract Foo from tuple"); match f { Foo::TupleVar(test, test2) => { assert_eq!(test, 1); @@ -339,43 +350,55 @@ fn test_enum() { test2: 0, } .into_py(py); - let f = Foo::extract(pye.as_ref(py)).expect("Failed to extract Foo from PyE"); + let f = pye + .extract::>(py) + .expect("Failed to extract Foo from PyE"); match f { Foo::StructVar { test } => assert_eq!(test.to_string_lossy(), "foo"), _ => panic!("Expected extracting Foo::StructVar, got {:?}", f), } let int: PyObject = 1.into_py(py); - let f = Foo::extract(int.as_ref(py)).expect("Failed to extract Foo from int"); + let f = int + .extract::>(py) + .expect("Failed to extract Foo from int"); match f { Foo::TransparentTuple(test) => assert_eq!(test, 1), _ => panic!("Expected extracting Foo::TransparentTuple, got {:?}", f), } let none = py.None(); - let f = Foo::extract(none).expect("Failed to extract Foo from int"); + let f = none + .extract::>(py) + .expect("Failed to extract Foo from int"); match f { Foo::TransparentStructVar { a } => assert!(a.is_none()), _ => panic!("Expected extracting Foo::TransparentStructVar, got {:?}", f), } let pybool = PyBool { bla: true }.into_py(py); - let f = Foo::extract(pybool.as_ref(py)).expect("Failed to extract Foo from PyBool"); + let f = pybool + .extract::>(py) + .expect("Failed to extract Foo from PyBool"); match f { Foo::StructVarGetAttrArg { a } => assert!(a), _ => panic!("Expected extracting Foo::StructVarGetAttrArg, got {:?}", f), } - let dict = PyDict::new(py); + let dict = PyDict::new_bound(py); dict.set_item("a", "test").expect("Failed to set item"); - let f = Foo::extract(dict.as_ref()).expect("Failed to extract Foo from dict"); + let f = dict + .extract::>() + .expect("Failed to extract Foo from dict"); match f { Foo::StructWithGetItem { a } => assert_eq!(a, "test"), _ => panic!("Expected extracting Foo::StructWithGetItem, got {:?}", f), } - let dict = PyDict::new(py); + let dict = PyDict::new_bound(py); dict.set_item("foo", "test").expect("Failed to set item"); - let f = Foo::extract(dict.as_ref()).expect("Failed to extract Foo from dict"); + let f = dict + .extract::>() + .expect("Failed to extract Foo from dict"); match f { Foo::StructWithGetItemArg { a } => assert_eq!(a, "test"), _ => panic!("Expected extracting Foo::StructWithGetItemArg, got {:?}", f), @@ -386,8 +409,8 @@ fn test_enum() { #[test] fn test_enum_error() { Python::with_gil(|py| { - let dict = PyDict::new(py); - let err = Foo::extract(dict.as_ref()).unwrap_err(); + let dict = PyDict::new_bound(py); + let err = dict.extract::>().unwrap_err(); assert_eq!( err.to_string(), "\ @@ -401,8 +424,8 @@ TypeError: failed to extract enum Foo ('TupleVar | StructVar | TransparentTuple - variant StructWithGetItemArg (StructWithGetItemArg): KeyError: 'foo'" ); - let tup = PyTuple::empty(py); - let err = Foo::extract(tup.as_ref()).unwrap_err(); + let tup = PyTuple::empty_bound(py); + let err = tup.extract::>().unwrap_err(); assert_eq!( err.to_string(), "\ @@ -419,22 +442,24 @@ TypeError: failed to extract enum Foo ('TupleVar | StructVar | TransparentTuple } #[derive(Debug, FromPyObject)] -enum EnumWithCatchAll<'a> { +enum EnumWithCatchAll<'py> { + #[allow(dead_code)] #[pyo3(transparent)] - Foo(Foo<'a>), + Foo(Foo<'py>), #[pyo3(transparent)] - CatchAll(&'a PyAny), + CatchAll(Bound<'py, PyAny>), } #[test] fn test_enum_catch_all() { Python::with_gil(|py| { - let dict = PyDict::new(py); - let f = EnumWithCatchAll::extract(dict.as_ref()) + let dict = PyDict::new_bound(py); + let f = dict + .extract::>() .expect("Failed to extract EnumWithCatchAll from dict"); match f { EnumWithCatchAll::CatchAll(any) => { - let d = <&PyDict>::extract(any).expect("Expected pydict"); + let d = any.extract::>().expect("Expected pydict"); assert!(d.is_empty()); } _ => panic!( @@ -458,8 +483,8 @@ pub enum Bar { #[test] fn test_err_rename() { Python::with_gil(|py| { - let dict = PyDict::new(py); - let f = Bar::extract(dict.as_ref()); + let dict = PyDict::new_bound(py); + let f = dict.extract::(); assert!(f.is_err()); assert_eq!( f.unwrap_err().to_string(), @@ -477,7 +502,7 @@ pub struct Zap { #[pyo3(item)] name: String, - #[pyo3(from_py_with = "PyAny::len", item("my_object"))] + #[pyo3(from_py_with = "Bound::<'_, PyAny>::len", item("my_object"))] some_object_length: usize, } @@ -485,14 +510,14 @@ pub struct Zap { fn test_from_py_with() { Python::with_gil(|py| { let py_zap = py - .eval( + .eval_bound( r#"{"name": "whatever", "my_object": [1, 2, 3]}"#, None, None, ) .expect("failed to create dict"); - let zap = Zap::extract(py_zap).unwrap(); + let zap = py_zap.extract::().unwrap(); assert_eq!(zap.name, "whatever"); assert_eq!(zap.some_object_length, 3usize); @@ -500,16 +525,19 @@ fn test_from_py_with() { } #[derive(Debug, FromPyObject)] -pub struct ZapTuple(String, #[pyo3(from_py_with = "PyAny::len")] usize); +pub struct ZapTuple( + String, + #[pyo3(from_py_with = "Bound::<'_, PyAny>::len")] usize, +); #[test] fn test_from_py_with_tuple_struct() { Python::with_gil(|py| { let py_zap = py - .eval(r#"("whatever", [1, 2, 3])"#, None, None) + .eval_bound(r#"("whatever", [1, 2, 3])"#, None, None) .expect("failed to create tuple"); - let zap = ZapTuple::extract(py_zap).unwrap(); + let zap = py_zap.extract::().unwrap(); assert_eq!(zap.0, "whatever"); assert_eq!(zap.1, 3usize); @@ -520,10 +548,10 @@ fn test_from_py_with_tuple_struct() { fn test_from_py_with_tuple_struct_error() { Python::with_gil(|py| { let py_zap = py - .eval(r#"("whatever", [1, 2, 3], "third")"#, None, None) + .eval_bound(r#"("whatever", [1, 2, 3], "third")"#, None, None) .expect("failed to create tuple"); - let f = ZapTuple::extract(py_zap); + let f = py_zap.extract::(); assert!(f.is_err()); assert_eq!( @@ -535,18 +563,21 @@ fn test_from_py_with_tuple_struct_error() { #[derive(Debug, FromPyObject, PartialEq, Eq)] pub enum ZapEnum { - Zip(#[pyo3(from_py_with = "PyAny::len")] usize), - Zap(String, #[pyo3(from_py_with = "PyAny::len")] usize), + Zip(#[pyo3(from_py_with = "Bound::<'_, PyAny>::len")] usize), + Zap( + String, + #[pyo3(from_py_with = "Bound::<'_, PyAny>::len")] usize, + ), } #[test] fn test_from_py_with_enum() { Python::with_gil(|py| { let py_zap = py - .eval(r#"("whatever", [1, 2, 3])"#, None, None) + .eval_bound(r#"("whatever", [1, 2, 3])"#, None, None) .expect("failed to create tuple"); - let zap = ZapEnum::extract(py_zap).unwrap(); + let zap = py_zap.extract::().unwrap(); let expected_zap = ZapEnum::Zip(2); assert_eq!(zap, expected_zap); @@ -556,14 +587,16 @@ fn test_from_py_with_enum() { #[derive(Debug, FromPyObject, PartialEq, Eq)] #[pyo3(transparent)] pub struct TransparentFromPyWith { - #[pyo3(from_py_with = "PyAny::len")] + #[pyo3(from_py_with = "Bound::<'_, PyAny>::len")] len: usize, } #[test] fn test_transparent_from_py_with() { Python::with_gil(|py| { - let result = TransparentFromPyWith::extract(PyList::new(py, [1, 2, 3])).unwrap(); + let result = PyList::new_bound(py, [1, 2, 3]) + .extract::() + .unwrap(); let expected = TransparentFromPyWith { len: 3 }; assert_eq!(result, expected); diff --git a/tests/test_gc.rs b/tests/test_gc.rs index 54c3e1a100c..b95abd4adea 100644 --- a/tests/test_gc.rs +++ b/tests/test_gc.rs @@ -3,7 +3,7 @@ use pyo3::class::PyTraverseError; use pyo3::class::PyVisit; use pyo3::prelude::*; -use pyo3::{py_run, PyCell}; +use pyo3::py_run; use std::cell::Cell; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; @@ -89,7 +89,7 @@ impl GcIntegration { fn __clear__(&mut self) { Python::with_gil(|py| { - self.self_ref = py.None().into(); + self.self_ref = py.None(); }); } } @@ -99,10 +99,10 @@ fn gc_integration() { let drop_called = Arc::new(AtomicBool::new(false)); Python::with_gil(|py| { - let inst = PyCell::new( + let inst = Bound::new( py, GcIntegration { - self_ref: py.None().into(), + self_ref: py.None(), dropped: TestDropCall { drop_called: Arc::clone(&drop_called), }, @@ -117,7 +117,7 @@ fn gc_integration() { }); Python::with_gil(|py| { - py.run("import gc; gc.collect()", None, None).unwrap(); + py.run_bound("import gc; gc.collect()", None, None).unwrap(); assert!(drop_called.load(Ordering::Relaxed)); }); } @@ -156,7 +156,7 @@ fn gc_null_traversal() { obj.borrow_mut(py).cycle = Some(obj.clone_ref(py)); // the object doesn't have to be cleaned up, it just needs to be traversed. - py.run("import gc; gc.collect()", None, None).unwrap(); + py.run_bound("import gc; gc.collect()", None, None).unwrap(); }); } @@ -211,11 +211,11 @@ fn inheritance_with_new_methods_with_drop() { let drop_called2 = Arc::new(AtomicBool::new(false)); Python::with_gil(|py| { - let _typebase = py.get_type::(); - let typeobj = py.get_type::(); + let _typebase = py.get_type_bound::(); + let typeobj = py.get_type_bound::(); let inst = typeobj.call((), None).unwrap(); - let obj: &PyCell = inst.downcast().unwrap(); + let obj = inst.downcast::().unwrap(); let mut obj_ref_mut = obj.borrow_mut(); obj_ref_mut.data = Some(Arc::clone(&drop_called1)); let base: &mut BaseClassWithDrop = obj_ref_mut.as_mut(); @@ -255,12 +255,12 @@ fn gc_during_borrow() { Python::with_gil(|py| { unsafe { // get the traverse function - let ty = py.get_type::().as_type_ptr(); - let traverse = get_type_traverse(ty).unwrap(); + let ty = py.get_type_bound::(); + let traverse = get_type_traverse(ty.as_type_ptr()).unwrap(); // create an object and check that traversing it works normally // when it's not borrowed - let cell = PyCell::new(py, TraversableClass::new()).unwrap(); + let cell = Bound::new(py, TraversableClass::new()).unwrap(); let obj = cell.to_object(py); assert!(!cell.borrow().traversed.load(Ordering::Relaxed)); traverse(obj.as_ptr(), novisit, std::ptr::null_mut()); @@ -268,7 +268,7 @@ fn gc_during_borrow() { // create an object and check that it is not traversed if the GC // is invoked while it is already borrowed mutably - let cell2 = PyCell::new(py, TraversableClass::new()).unwrap(); + let cell2 = Bound::new(py, TraversableClass::new()).unwrap(); let obj2 = cell2.to_object(py); let guard = cell2.borrow_mut(); assert!(!guard.traversed.load(Ordering::Relaxed)); @@ -286,9 +286,7 @@ struct PartialTraverse { impl PartialTraverse { fn new(py: Python<'_>) -> Self { - Self { - member: py.None().into(), - } + Self { member: py.None() } } } @@ -305,8 +303,8 @@ impl PartialTraverse { fn traverse_partial() { Python::with_gil(|py| unsafe { // get the traverse function - let ty = py.get_type::().as_type_ptr(); - let traverse = get_type_traverse(ty).unwrap(); + let ty = py.get_type_bound::(); + let traverse = get_type_traverse(ty.as_type_ptr()).unwrap(); // confirm that traversing errors let obj = Py::new(py, PartialTraverse::new(py)).unwrap(); @@ -324,9 +322,7 @@ struct PanickyTraverse { impl PanickyTraverse { fn new(py: Python<'_>) -> Self { - Self { - member: py.None().into(), - } + Self { member: py.None() } } } @@ -342,8 +338,8 @@ impl PanickyTraverse { fn traverse_panic() { Python::with_gil(|py| unsafe { // get the traverse function - let ty = py.get_type::().as_type_ptr(); - let traverse = get_type_traverse(ty).unwrap(); + let ty = py.get_type_bound::(); + let traverse = get_type_traverse(ty.as_type_ptr()).unwrap(); // confirm that traversing errors let obj = Py::new(py, PanickyTraverse::new(py)).unwrap(); @@ -365,8 +361,8 @@ impl TriesGILInTraverse { fn tries_gil_in_traverse() { Python::with_gil(|py| unsafe { // get the traverse function - let ty = py.get_type::().as_type_ptr(); - let traverse = get_type_traverse(ty).unwrap(); + let ty = py.get_type_bound::(); + let traverse = get_type_traverse(ty.as_type_ptr()).unwrap(); // confirm that traversing panicks let obj = Py::new(py, TriesGILInTraverse {}).unwrap(); @@ -402,6 +398,7 @@ impl HijackedTraverse { } } +#[allow(dead_code)] trait Traversable { fn __traverse__(&self, visit: PyVisit<'_>) -> Result<(), PyTraverseError>; } @@ -417,10 +414,10 @@ impl<'a> Traversable for PyRef<'a, HijackedTraverse> { fn traverse_cannot_be_hijacked() { Python::with_gil(|py| unsafe { // get the traverse function - let ty = py.get_type::().as_type_ptr(); - let traverse = get_type_traverse(ty).unwrap(); + let ty = py.get_type_bound::(); + let traverse = get_type_traverse(ty.as_type_ptr()).unwrap(); - let cell = PyCell::new(py, HijackedTraverse::new()).unwrap(); + let cell = Bound::new(py, HijackedTraverse::new()).unwrap(); let obj = cell.to_object(py); assert_eq!(cell.borrow().traversed_and_hijacked(), (false, false)); traverse(obj.as_ptr(), novisit, std::ptr::null_mut()); @@ -448,6 +445,7 @@ impl DropDuringTraversal { } } +#[cfg(not(pyo3_disable_reference_pool))] #[test] fn drop_during_traversal_with_gil() { let drop_called = Arc::new(AtomicBool::new(false)); @@ -473,12 +471,13 @@ fn drop_during_traversal_with_gil() { // (but not too many) collections to get `inst` actually dropped. for _ in 0..10 { Python::with_gil(|py| { - py.run("import gc; gc.collect()", None, None).unwrap(); + py.run_bound("import gc; gc.collect()", None, None).unwrap(); }); } assert!(drop_called.load(Ordering::Relaxed)); } +#[cfg(not(pyo3_disable_reference_pool))] #[test] fn drop_during_traversal_without_gil() { let drop_called = Arc::new(AtomicBool::new(false)); @@ -506,7 +505,7 @@ fn drop_during_traversal_without_gil() { // (but not too many) collections to get `inst` actually dropped. for _ in 0..10 { Python::with_gil(|py| { - py.run("import gc; gc.collect()", None, None).unwrap(); + py.run_bound("import gc; gc.collect()", None, None).unwrap(); }); } assert!(drop_called.load(Ordering::Relaxed)); @@ -531,9 +530,14 @@ impl UnsendableTraversal { #[test] #[cfg(not(target_arch = "wasm32"))] // We are building wasm Python with pthreads disabled fn unsendable_are_not_traversed_on_foreign_thread() { + #[derive(Clone, Copy)] + struct SendablePtr(*mut pyo3::ffi::PyObject); + + unsafe impl Send for SendablePtr {} + Python::with_gil(|py| unsafe { - let ty = py.get_type::().as_type_ptr(); - let traverse = get_type_traverse(ty).unwrap(); + let ty = py.get_type_bound::(); + let traverse = get_type_traverse(ty.as_type_ptr()).unwrap(); let obj = Py::new( py, @@ -582,8 +586,3 @@ extern "C" fn visit_error( ) -> std::os::raw::c_int { -1 } - -#[derive(Clone, Copy)] -struct SendablePtr(*mut pyo3::ffi::PyObject); - -unsafe impl Send for SendablePtr {} diff --git a/tests/test_getter_setter.rs b/tests/test_getter_setter.rs index f16ce61166a..b0b15d78cd0 100644 --- a/tests/test_getter_setter.rs +++ b/tests/test_getter_setter.rs @@ -41,12 +41,27 @@ impl ClassWithProperties { self.num = value; } + #[setter] + fn set_from_len(&mut self, #[pyo3(from_py_with = "extract_len")] value: i32) { + self.num = value; + } + + #[setter] + fn set_from_any(&mut self, value: &Bound<'_, PyAny>) -> PyResult<()> { + self.num = value.extract()?; + Ok(()) + } + #[getter] - fn get_data_list<'py>(&self, py: Python<'py>) -> &'py PyList { - PyList::new(py, [self.num]) + fn get_data_list<'py>(&self, py: Python<'py>) -> Bound<'py, PyList> { + PyList::new_bound(py, [self.num]) } } +fn extract_len(any: &Bound<'_, PyAny>) -> PyResult { + any.len().map(|len| len as i32) +} + #[test] fn class_with_properties() { Python::with_gil(|py| { @@ -64,7 +79,13 @@ fn class_with_properties() { py_run!(py, inst, "assert inst.get_num() == inst.unwrapped == 42"); py_run!(py, inst, "assert inst.data_list == [42]"); - let d = [("C", py.get_type::())].into_py_dict(py); + py_run!(py, inst, "inst.from_len = [0, 0, 0]"); + py_run!(py, inst, "assert inst.get_num() == 3"); + + py_run!(py, inst, "inst.from_any = 15"); + py_run!(py, inst, "assert inst.get_num() == 15"); + + let d = [("C", py.get_type_bound::())].into_py_dict_bound(py); py_assert!(py, *d, "C.DATA.__doc__ == 'a getter for data'"); }); } diff --git a/tests/test_inheritance.rs b/tests/test_inheritance.rs index d1cfe628ef6..fc9a7699c1b 100644 --- a/tests/test_inheritance.rs +++ b/tests/test_inheritance.rs @@ -20,12 +20,12 @@ struct SubclassAble {} #[test] fn subclass() { Python::with_gil(|py| { - let d = [("SubclassAble", py.get_type::())].into_py_dict(py); + let d = [("SubclassAble", py.get_type_bound::())].into_py_dict_bound(py); - py.run( + py.run_bound( "class A(SubclassAble): pass\nassert issubclass(A, SubclassAble)", None, - Some(d), + Some(&d), ) .map_err(|e| e.display(py)) .unwrap(); @@ -41,7 +41,7 @@ impl BaseClass { fn base_method(&self, x: usize) -> usize { x * self.val1 } - fn base_set(&mut self, fn_: &pyo3::PyAny) -> PyResult<()> { + fn base_set(&mut self, fn_: &Bound<'_, PyAny>) -> PyResult<()> { let value: usize = fn_.call0()?.extract()?; self.val1 = value; Ok(()) @@ -72,7 +72,7 @@ impl SubClass { #[test] fn inheritance_with_new_methods() { Python::with_gil(|py| { - let typeobj = py.get_type::(); + let typeobj = py.get_type_bound::(); let inst = typeobj.call((), None).unwrap(); py_run!(py, inst, "assert inst.val1 == 10; assert inst.val2 == 5"); }); @@ -81,7 +81,7 @@ fn inheritance_with_new_methods() { #[test] fn call_base_and_sub_methods() { Python::with_gil(|py| { - let obj = PyCell::new(py, SubClass::new()).unwrap(); + let obj = Py::new(py, SubClass::new()).unwrap(); py_run!( py, obj, @@ -96,10 +96,14 @@ fn call_base_and_sub_methods() { #[test] fn mutation_fails() { Python::with_gil(|py| { - let obj = PyCell::new(py, SubClass::new()).unwrap(); - let global = Some([("obj", obj)].into_py_dict(py)); + let obj = Py::new(py, SubClass::new()).unwrap(); + let global = [("obj", obj)].into_py_dict_bound(py); let e = py - .run("obj.base_set(lambda: obj.sub_set_and_ret(1))", global, None) + .run_bound( + "obj.base_set(lambda: obj.sub_set_and_ret(1))", + Some(&global), + None, + ) .unwrap_err(); assert_eq!(&e.to_string(), "RuntimeError: Already borrowed"); }); @@ -108,16 +112,16 @@ fn mutation_fails() { #[test] fn is_subclass_and_is_instance() { Python::with_gil(|py| { - let sub_ty = py.get_type::(); - let base_ty = py.get_type::(); + let sub_ty = py.get_type_bound::(); + let base_ty = py.get_type_bound::(); assert!(sub_ty.is_subclass_of::().unwrap()); - assert!(sub_ty.is_subclass(base_ty).unwrap()); + assert!(sub_ty.is_subclass(&base_ty).unwrap()); - let obj = PyCell::new(py, SubClass::new()).unwrap(); + let obj = Bound::new(py, SubClass::new()).unwrap().into_any(); assert!(obj.is_instance_of::()); assert!(obj.is_instance_of::()); - assert!(obj.is_instance(sub_ty).unwrap()); - assert!(obj.is_instance(base_ty).unwrap()); + assert!(obj.is_instance(&sub_ty).unwrap()); + assert!(obj.is_instance(&base_ty).unwrap()); }); } @@ -151,7 +155,7 @@ impl SubClass2 { #[test] fn handle_result_in_new() { Python::with_gil(|py| { - let subclass = py.get_type::(); + let subclass = py.get_type_bound::(); py_run!( py, subclass, @@ -173,7 +177,7 @@ except Exception as e: mod inheriting_native_type { use super::*; use pyo3::exceptions::PyException; - use pyo3::types::{IntoPyDict, PyDict}; + use pyo3::types::PyDict; #[cfg(not(PyPy))] #[test] @@ -198,7 +202,7 @@ mod inheriting_native_type { } Python::with_gil(|py| { - let set_sub = pyo3::PyCell::new(py, SetWithName::new()).unwrap(); + let set_sub = pyo3::Py::new(py, SetWithName::new()).unwrap(); py_run!( py, set_sub, @@ -225,7 +229,7 @@ mod inheriting_native_type { #[test] fn inherit_dict() { Python::with_gil(|py| { - let dict_sub = pyo3::PyCell::new(py, DictWithName::new()).unwrap(); + let dict_sub = pyo3::Py::new(py, DictWithName::new()).unwrap(); py_run!( py, dict_sub, @@ -240,10 +244,10 @@ mod inheriting_native_type { let dict_sub = pyo3::Py::new(py, DictWithName::new()).unwrap(); assert_eq!(dict_sub.get_refcnt(py), 1); - let item = py.eval("object()", None, None).unwrap(); + let item = &py.eval_bound("object()", None, None).unwrap(); assert_eq!(item.get_refcnt(), 1); - dict_sub.as_ref(py).set_item("foo", item).unwrap(); + dict_sub.bind(py).set_item("foo", item).unwrap(); assert_eq!(item.get_refcnt(), 2); drop(dict_sub); @@ -260,7 +264,7 @@ mod inheriting_native_type { #[pymethods] impl CustomException { #[new] - fn new(_exc_arg: &PyAny) -> Self { + fn new(_exc_arg: &Bound<'_, PyAny>) -> Self { CustomException { context: "Hello :)", } @@ -270,15 +274,15 @@ mod inheriting_native_type { #[test] fn custom_exception() { Python::with_gil(|py| { - let cls = py.get_type::(); - let dict = [("cls", cls)].into_py_dict(py); - let res = py.run( + let cls = py.get_type_bound::(); + let dict = [("cls", &cls)].into_py_dict_bound(py); + let res = py.run_bound( "e = cls('hello'); assert str(e) == 'hello'; assert e.context == 'Hello :)'; raise e", None, - Some(dict) + Some(&dict) ); let err = res.unwrap_err(); - assert!(err.matches(py, cls), "{}", err); + assert!(err.matches(py, &cls), "{}", err); // catching the exception in Python also works: py_run!( @@ -311,7 +315,7 @@ fn test_subclass_ref_counts() { // regression test for issue #1363 Python::with_gil(|py| { #[allow(non_snake_case)] - let SimpleClass = py.get_type::(); + let SimpleClass = py.get_type_bound::(); py_run!( py, SimpleClass, @@ -350,7 +354,7 @@ fn module_add_class_inherit_bool_fails() { struct ExtendsBool; Python::with_gil(|py| { - let m = PyModule::new(py, "test_module").unwrap(); + let m = PyModule::new_bound(py, "test_module").unwrap(); let err = m.add_class::().unwrap_err(); assert_eq!( diff --git a/tests/test_macro_docs.rs b/tests/test_macro_docs.rs index 964e762886d..6289626d32e 100644 --- a/tests/test_macro_docs.rs +++ b/tests/test_macro_docs.rs @@ -23,7 +23,7 @@ impl MacroDocs { #[test] fn meth_doc() { Python::with_gil(|py| { - let d = [("C", py.get_type::())].into_py_dict(py); + let d = [("C", py.get_type_bound::())].into_py_dict_bound(py); py_assert!( py, *d, diff --git a/tests/test_macros.rs b/tests/test_macros.rs index c01ba853685..6a50e5b36e4 100644 --- a/tests/test_macros.rs +++ b/tests/test_macros.rs @@ -73,17 +73,17 @@ property_rename_via_macro!(my_new_property_name); #[test] fn test_macro_rules_interactions() { Python::with_gil(|py| { - let my_base = py.get_type::(); + let my_base = py.get_type_bound::(); py_assert!(py, my_base, "my_base.__name__ == 'MyClass'"); - let my_func = wrap_pyfunction!(my_function_in_macro, py).unwrap(); + let my_func = wrap_pyfunction_bound!(my_function_in_macro, py).unwrap(); py_assert!( py, my_func, "my_func.__text_signature__ == '(a, b=None, *, c=42)'" ); - let renamed_prop = py.get_type::(); + let renamed_prop = py.get_type_bound::(); py_assert!( py, renamed_prop, diff --git a/tests/test_mapping.rs b/tests/test_mapping.rs index c029ce27c50..784ab8845cd 100644 --- a/tests/test_mapping.rs +++ b/tests/test_mapping.rs @@ -21,11 +21,12 @@ struct Mapping { #[pymethods] impl Mapping { #[new] - fn new(elements: Option<&PyList>) -> PyResult { + #[pyo3(signature=(elements=None))] + fn new(elements: Option<&Bound<'_, PyList>>) -> PyResult { if let Some(pylist) = elements { let mut elems = HashMap::with_capacity(pylist.len()); for (i, pyelem) in pylist.into_iter().enumerate() { - let elem = String::extract(pyelem)?; + let elem = pyelem.extract()?; elems.insert(elem, i); } Ok(Self { index: elems }) @@ -59,6 +60,7 @@ impl Mapping { } } + #[pyo3(signature=(key, default=None))] fn get(&self, py: Python<'_>, key: &str, default: Option) -> Option { self.index .get(key) @@ -68,8 +70,8 @@ impl Mapping { } /// Return a dict with `m = Mapping(['1', '2', '3'])`. -fn map_dict(py: Python<'_>) -> &pyo3::types::PyDict { - let d = [("Mapping", py.get_type::())].into_py_dict(py); +fn map_dict(py: Python<'_>) -> Bound<'_, pyo3::types::PyDict> { + let d = [("Mapping", py.get_type_bound::())].into_py_dict_bound(py); py_run!(py, *d, "m = Mapping(['1', '2', '3'])"); d } @@ -123,7 +125,7 @@ fn mapping_is_not_sequence() { PyMapping::register::(py).unwrap(); - assert!(m.as_ref(py).downcast::().is_ok()); - assert!(m.as_ref(py).downcast::().is_err()); + assert!(m.bind(py).downcast::().is_ok()); + assert!(m.bind(py).downcast::().is_err()); }); } diff --git a/tests/test_methods.rs b/tests/test_methods.rs index 6e42ad66960..615e2dba0af 100644 --- a/tests/test_methods.rs +++ b/tests/test_methods.rs @@ -2,8 +2,8 @@ use pyo3::prelude::*; use pyo3::py_run; +use pyo3::types::PySequence; use pyo3::types::{IntoPyDict, PyDict, PyList, PySet, PyString, PyTuple, PyType}; -use pyo3::PyCell; #[path = "../src/tests/common.rs"] mod common; @@ -29,7 +29,7 @@ impl InstanceMethod { #[test] fn instance_method() { Python::with_gil(|py| { - let obj = PyCell::new(py, InstanceMethod { member: 42 }).unwrap(); + let obj = Bound::new(py, InstanceMethod { member: 42 }).unwrap(); let obj_ref = obj.borrow(); assert_eq!(obj_ref.method(), 42); py_assert!(py, obj, "obj.method() == 42"); @@ -53,7 +53,7 @@ impl InstanceMethodWithArgs { #[test] fn instance_method_with_args() { Python::with_gil(|py| { - let obj = PyCell::new(py, InstanceMethodWithArgs { member: 7 }).unwrap(); + let obj = Bound::new(py, InstanceMethodWithArgs { member: 7 }).unwrap(); let obj_ref = obj.borrow(); assert_eq!(obj_ref.method(6), 42); py_assert!(py, obj, "obj.method(3) == 21"); @@ -73,23 +73,21 @@ impl ClassMethod { #[classmethod] /// Test class method. - fn method(cls: &PyType) -> PyResult { + fn method(cls: &Bound<'_, PyType>) -> PyResult { Ok(format!("{}.method()!", cls.qualname()?)) } #[classmethod] fn method_owned(cls: Py) -> PyResult { - Ok(format!( - "{}.method_owned()!", - Python::with_gil(|gil| cls.as_ref(gil).qualname())? - )) + let qualname = Python::with_gil(|gil| cls.bind(gil).qualname())?; + Ok(format!("{}.method_owned()!", qualname)) } } #[test] fn class_method() { Python::with_gil(|py| { - let d = [("C", py.get_type::())].into_py_dict(py); + let d = [("C", py.get_type_bound::())].into_py_dict_bound(py); py_assert!(py, *d, "C.method() == 'ClassMethod.method()!'"); py_assert!(py, *d, "C().method() == 'ClassMethod.method()!'"); py_assert!( @@ -108,7 +106,7 @@ struct ClassMethodWithArgs {} #[pymethods] impl ClassMethodWithArgs { #[classmethod] - fn method(cls: &PyType, input: &PyString) -> PyResult { + fn method(cls: &Bound<'_, PyType>, input: &Bound<'_, PyString>) -> PyResult { Ok(format!("{}.method({})", cls.qualname()?, input)) } } @@ -116,7 +114,7 @@ impl ClassMethodWithArgs { #[test] fn class_method_with_args() { Python::with_gil(|py| { - let d = [("C", py.get_type::())].into_py_dict(py); + let d = [("C", py.get_type_bound::())].into_py_dict_bound(py); py_assert!( py, *d, @@ -147,7 +145,7 @@ fn static_method() { Python::with_gil(|py| { assert_eq!(StaticMethod::method(py), "StaticMethod.method()!"); - let d = [("C", py.get_type::())].into_py_dict(py); + let d = [("C", py.get_type_bound::())].into_py_dict_bound(py); py_assert!(py, *d, "C.method() == 'StaticMethod.method()!'"); py_assert!(py, *d, "C().method() == 'StaticMethod.method()!'"); py_assert!(py, *d, "C.method.__doc__ == 'Test static method.'"); @@ -171,7 +169,7 @@ fn static_method_with_args() { Python::with_gil(|py| { assert_eq!(StaticMethodWithArgs::method(py, 1234), "0x4d2"); - let d = [("C", py.get_type::())].into_py_dict(py); + let d = [("C", py.get_type_bound::())].into_py_dict_bound(py); py_assert!(py, *d, "C.method(1337) == '0x539'"); }); } @@ -189,6 +187,7 @@ impl MethSignature { fn get_optional2(&self, test: Option) -> Option { test } + #[pyo3(signature=(_t1 = None, t2 = None, _t3 = None))] fn get_optional_positional( &self, _t1: Option, @@ -207,8 +206,13 @@ impl MethSignature { test } #[pyo3(signature = (*args, **kwargs))] - fn get_kwargs(&self, py: Python<'_>, args: &PyTuple, kwargs: Option<&PyDict>) -> PyObject { - [args.into(), kwargs.to_object(py)].to_object(py) + fn get_kwargs( + &self, + py: Python<'_>, + args: &Bound<'_, PyTuple>, + kwargs: Option<&Bound<'_, PyDict>>, + ) -> PyObject { + [args.to_object(py), kwargs.to_object(py)].to_object(py) } #[pyo3(signature = (a, *args, **kwargs))] @@ -216,10 +220,10 @@ impl MethSignature { &self, py: Python<'_>, a: i32, - args: &PyTuple, - kwargs: Option<&PyDict>, + args: &Bound<'_, PyTuple>, + kwargs: Option<&Bound<'_, PyDict>>, ) -> PyObject { - [a.to_object(py), args.into(), kwargs.to_object(py)].to_object(py) + [a.to_object(py), args.to_object(py), kwargs.to_object(py)].to_object(py) } #[pyo3(signature = (a, b, /))] @@ -262,7 +266,7 @@ impl MethSignature { &self, py: Python<'_>, a: i32, - kwargs: Option<&PyDict>, + kwargs: Option<&Bound<'_, PyDict>>, ) -> PyObject { [a.to_object(py), kwargs.to_object(py)].to_object(py) } @@ -272,7 +276,7 @@ impl MethSignature { &self, py: Python<'_>, a: i32, - kwargs: Option<&PyDict>, + kwargs: Option<&Bound<'_, PyDict>>, ) -> PyObject { [a.to_object(py), kwargs.to_object(py)].to_object(py) } @@ -293,7 +297,12 @@ impl MethSignature { } #[pyo3(signature = (*args, a))] - fn get_args_and_required_keyword(&self, py: Python<'_>, args: &PyTuple, a: i32) -> PyObject { + fn get_args_and_required_keyword( + &self, + py: Python<'_>, + args: &Bound<'_, PyTuple>, + a: i32, + ) -> PyObject { (args, a).to_object(py) } @@ -308,7 +317,7 @@ impl MethSignature { } #[pyo3(signature = (a, **kwargs))] - fn get_pos_kw(&self, py: Python<'_>, a: i32, kwargs: Option<&PyDict>) -> PyObject { + fn get_pos_kw(&self, py: Python<'_>, a: i32, kwargs: Option<&Bound<'_, PyDict>>) -> PyObject { [a.to_object(py), kwargs.to_object(py)].to_object(py) } @@ -669,7 +678,7 @@ impl MethDocs { #[test] fn meth_doc() { Python::with_gil(|py| { - let d = [("C", py.get_type::())].into_py_dict(py); + let d = [("C", py.get_type_bound::())].into_py_dict_bound(py); py_assert!(py, *d, "C.__doc__ == 'A class with \"documentation\".'"); py_assert!( py, @@ -689,12 +698,13 @@ struct MethodWithLifeTime {} #[pymethods] impl MethodWithLifeTime { - fn set_to_list<'py>(&self, py: Python<'py>, set: &'py PySet) -> PyResult<&'py PyList> { + fn set_to_list<'py>(&self, set: &Bound<'py, PySet>) -> PyResult> { + let py = set.py(); let mut items = vec![]; for _ in 0..set.len() { items.push(set.pop().unwrap()); } - let list = PyList::new(py, items); + let list = PyList::new_bound(py, items); list.sort()?; Ok(list) } @@ -703,7 +713,7 @@ impl MethodWithLifeTime { #[test] fn method_with_lifetime() { Python::with_gil(|py| { - let obj = PyCell::new(py, MethodWithLifeTime {}).unwrap(); + let obj = Py::new(py, MethodWithLifeTime {}).unwrap(); py_run!( py, obj, @@ -736,11 +746,13 @@ impl MethodWithPyClassArg { fn inplace_add_pyref(&self, mut other: PyRefMut<'_, MethodWithPyClassArg>) { other.value += self.value; } + #[pyo3(signature=(other = None))] fn optional_add(&self, other: Option<&MethodWithPyClassArg>) -> MethodWithPyClassArg { MethodWithPyClassArg { value: self.value + other.map(|o| o.value).unwrap_or(10), } } + #[pyo3(signature=(other = None))] fn optional_inplace_add(&self, other: Option<&mut MethodWithPyClassArg>) { if let Some(other) = other { other.value += self.value; @@ -751,9 +763,9 @@ impl MethodWithPyClassArg { #[test] fn method_with_pyclassarg() { Python::with_gil(|py| { - let obj1 = PyCell::new(py, MethodWithPyClassArg { value: 10 }).unwrap(); - let obj2 = PyCell::new(py, MethodWithPyClassArg { value: 10 }).unwrap(); - let d = [("obj1", obj1), ("obj2", obj2)].into_py_dict(py); + let obj1 = Py::new(py, MethodWithPyClassArg { value: 10 }).unwrap(); + let obj2 = Py::new(py, MethodWithPyClassArg { value: 10 }).unwrap(); + let d = [("obj1", obj1), ("obj2", obj2)].into_py_dict_bound(py); py_run!(py, *d, "obj = obj1.add(obj2); assert obj.value == 20"); py_run!(py, *d, "obj = obj1.add_pyref(obj2); assert obj.value == 20"); py_run!(py, *d, "obj = obj1.optional_add(); assert obj.value == 20"); @@ -842,7 +854,8 @@ struct FromSequence { #[pymethods] impl FromSequence { #[new] - fn new(seq: Option<&pyo3::types::PySequence>) -> PyResult { + #[pyo3(signature=(seq = None))] + fn new(seq: Option<&Bound<'_, PySequence>>) -> PyResult { if let Some(seq) = seq { Ok(FromSequence { numbers: seq.as_ref().extract::>()?, @@ -856,11 +869,12 @@ impl FromSequence { #[test] fn test_from_sequence() { Python::with_gil(|py| { - let typeobj = py.get_type::(); + let typeobj = py.get_type_bound::(); py_assert!(py, typeobj, "typeobj(range(0, 4)).numbers == [0, 1, 2, 3]"); }); } +#[cfg(feature = "py-clone")] #[pyclass] struct r#RawIdents { #[pyo3(get, set)] @@ -869,6 +883,7 @@ struct r#RawIdents { r#subsubtype: PyObject, } +#[cfg(feature = "py-clone")] #[pymethods] impl r#RawIdents { #[new] @@ -915,7 +930,7 @@ impl r#RawIdents { } #[classmethod] - pub fn r#class_method(_: &PyType, r#type: PyObject) -> PyObject { + pub fn r#class_method(_: &Bound<'_, PyType>, r#type: PyObject) -> PyObject { r#type } @@ -933,10 +948,11 @@ impl r#RawIdents { } } +#[cfg(feature = "py-clone")] #[test] fn test_raw_idents() { Python::with_gil(|py| { - let raw_idents_type = py.get_type::(); + let raw_idents_type = py.get_type_bound::(); assert_eq!(raw_idents_type.qualname().unwrap(), "RawIdents"); py_run!( py, @@ -1017,48 +1033,53 @@ macro_rules! issue_1506 { issue_1506!( #[pymethods] impl Issue1506 { + #[pyo3(signature = (_arg, _args, _kwargs=None))] fn issue_1506( &self, _py: Python<'_>, - _arg: &PyAny, - _args: &PyTuple, - _kwargs: Option<&PyDict>, + _arg: &Bound<'_, PyAny>, + _args: &Bound<'_, PyTuple>, + _kwargs: Option<&Bound<'_, PyDict>>, ) { } + #[pyo3(signature = (_arg, _args, _kwargs=None))] fn issue_1506_mut( &mut self, _py: Python<'_>, - _arg: &PyAny, - _args: &PyTuple, - _kwargs: Option<&PyDict>, + _arg: &Bound<'_, PyAny>, + _args: &Bound<'_, PyTuple>, + _kwargs: Option<&Bound<'_, PyDict>>, ) { } + #[pyo3(signature = (_arg, _args, _kwargs=None))] fn issue_1506_custom_receiver( _slf: Py, _py: Python<'_>, - _arg: &PyAny, - _args: &PyTuple, - _kwargs: Option<&PyDict>, + _arg: &Bound<'_, PyAny>, + _args: &Bound<'_, PyTuple>, + _kwargs: Option<&Bound<'_, PyDict>>, ) { } + #[pyo3(signature = (_arg, _args, _kwargs=None))] fn issue_1506_custom_receiver_explicit( _slf: Py, _py: Python<'_>, - _arg: &PyAny, - _args: &PyTuple, - _kwargs: Option<&PyDict>, + _arg: &Bound<'_, PyAny>, + _args: &Bound<'_, PyTuple>, + _kwargs: Option<&Bound<'_, PyDict>>, ) { } #[new] + #[pyo3(signature = (_arg, _args, _kwargs=None))] fn issue_1506_new( _py: Python<'_>, - _arg: &PyAny, - _args: &PyTuple, - _kwargs: Option<&PyDict>, + _arg: &Bound<'_, PyAny>, + _args: &Bound<'_, PyTuple>, + _kwargs: Option<&Bound<'_, PyDict>>, ) -> Self { Issue1506 {} } @@ -1072,21 +1093,23 @@ issue_1506!( fn issue_1506_setter(&self, _py: Python<'_>, _value: i32) {} #[staticmethod] + #[pyo3(signature = (_arg, _args, _kwargs=None))] fn issue_1506_static( _py: Python<'_>, - _arg: &PyAny, - _args: &PyTuple, - _kwargs: Option<&PyDict>, + _arg: &Bound<'_, PyAny>, + _args: &Bound<'_, PyTuple>, + _kwargs: Option<&Bound<'_, PyDict>>, ) { } #[classmethod] + #[pyo3(signature = (_arg, _args, _kwargs=None))] fn issue_1506_class( - _cls: &PyType, + _cls: &Bound<'_, PyType>, _py: Python<'_>, - _arg: &PyAny, - _args: &PyTuple, - _kwargs: Option<&PyDict>, + _arg: &Bound<'_, PyAny>, + _args: &Bound<'_, PyTuple>, + _kwargs: Option<&Bound<'_, PyDict>>, ) { } } @@ -1116,7 +1139,7 @@ fn test_option_pyclass_arg() { } Python::with_gil(|py| { - let f = wrap_pyfunction!(option_class_arg, py).unwrap(); + let f = wrap_pyfunction_bound!(option_class_arg, py).unwrap(); assert!(f.call0().unwrap().is_none()); let obj = Py::new(py, SomePyClass {}).unwrap(); assert!(f @@ -1138,7 +1161,7 @@ fn test_issue_2988() { _data: Vec, // The from_py_with here looks a little odd, we just need some way // to encourage the macro to expand the from_py_with default path too - #[pyo3(from_py_with = "PyAny::extract")] _data2: Vec, + #[pyo3(from_py_with = " as PyAnyMethods>::extract")] _data2: Vec, ) { } } diff --git a/tests/test_module.rs b/tests/test_module.rs index 2de23b38324..b2487cfd8b3 100644 --- a/tests/test_module.rs +++ b/tests/test_module.rs @@ -3,6 +3,7 @@ use pyo3::prelude::*; use pyo3::py_run; +use pyo3::types::PyString; use pyo3::types::{IntoPyDict, PyDict, PyTuple}; #[path = "../src/tests/common.rs"] @@ -35,7 +36,7 @@ fn double(x: usize) -> usize { /// This module is implemented in Rust. #[pymodule] -fn module_with_functions(_py: Python<'_>, m: &PyModule) -> PyResult<()> { +fn module_with_functions(m: &Bound<'_, PyModule>) -> PyResult<()> { #[pyfn(m)] #[pyo3(name = "no_parameters")] fn function_with_name() -> usize { @@ -44,7 +45,7 @@ fn module_with_functions(_py: Python<'_>, m: &PyModule) -> PyResult<()> { #[pyfn(m)] #[pyo3(pass_module)] - fn with_module(module: &PyModule) -> PyResult<&str> { + fn with_module<'py>(module: &Bound<'py, PyModule>) -> PyResult> { module.name() } @@ -53,14 +54,14 @@ fn module_with_functions(_py: Python<'_>, m: &PyModule) -> PyResult<()> { v.value * 2 } - m.add_class::().unwrap(); - m.add_class::().unwrap(); - m.add_class::().unwrap(); + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; - m.add("foo", "bar").unwrap(); + m.add("foo", "bar")?; - m.add_function(wrap_pyfunction!(double, m)?).unwrap(); - m.add("also_double", wrap_pyfunction!(double, m)?).unwrap(); + m.add_function(wrap_pyfunction!(double, m)?)?; + m.add("also_double", wrap_pyfunction!(double, m)?)?; Ok(()) } @@ -74,7 +75,7 @@ fn test_module_with_functions() { "module_with_functions", wrap_pymodule!(module_with_functions)(py), )] - .into_py_dict(py); + .into_py_dict_bound(py); py_assert!( py, @@ -115,9 +116,31 @@ fn test_module_with_functions() { }); } +/// This module uses a legacy two-argument module function. +#[pymodule] +fn module_with_explicit_py_arg(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + m.add_function(wrap_pyfunction!(double, m)?)?; + Ok(()) +} + +#[test] +fn test_module_with_explicit_py_arg() { + use pyo3::wrap_pymodule; + + Python::with_gil(|py| { + let d = [( + "module_with_explicit_py_arg", + wrap_pymodule!(module_with_explicit_py_arg)(py), + )] + .into_py_dict_bound(py); + + py_assert!(py, *d, "module_with_explicit_py_arg.double(3) == 6"); + }); +} + #[pymodule] #[pyo3(name = "other_name")] -fn some_name(_: Python<'_>, m: &PyModule) -> PyResult<()> { +fn some_name(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add("other_name", "other_name")?; Ok(()) } @@ -127,16 +150,16 @@ fn test_module_renaming() { use pyo3::wrap_pymodule; Python::with_gil(|py| { - let d = [("different_name", wrap_pymodule!(some_name)(py))].into_py_dict(py); + let d = [("different_name", wrap_pymodule!(some_name)(py))].into_py_dict_bound(py); py_run!(py, *d, "assert different_name.__name__ == 'other_name'"); }); } #[test] -fn test_module_from_code() { +fn test_module_from_code_bound() { Python::with_gil(|py| { - let adder_mod = PyModule::from_code( + let adder_mod = PyModule::from_code_bound( py, "def add(a,b):\n\treturn a+b", "adder_mod.py", @@ -165,7 +188,7 @@ fn r#move() -> usize { } #[pymodule] -fn raw_ident_module(_py: Python<'_>, module: &PyModule) -> PyResult<()> { +fn raw_ident_module(module: &Bound<'_, PyModule>) -> PyResult<()> { module.add_function(wrap_pyfunction!(r#move, module)?) } @@ -189,7 +212,7 @@ fn custom_named_fn() -> usize { #[test] fn test_custom_names() { #[pymodule] - fn custom_names(_py: Python<'_>, m: &PyModule) -> PyResult<()> { + fn custom_names(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(custom_named_fn, m)?)?; Ok(()) } @@ -205,7 +228,7 @@ fn test_custom_names() { #[test] fn test_module_dict() { #[pymodule] - fn module_dict(_py: Python<'_>, m: &PyModule) -> PyResult<()> { + fn module_dict(m: &Bound<'_, PyModule>) -> PyResult<()> { m.dict().set_item("yay", "me")?; Ok(()) } @@ -221,7 +244,7 @@ fn test_module_dict() { fn test_module_dunder_all() { Python::with_gil(|py| { #[pymodule] - fn dunder_all(_py: Python<'_>, m: &PyModule) -> PyResult<()> { + fn dunder_all(m: &Bound<'_, PyModule>) -> PyResult<()> { m.dict().set_item("yay", "me")?; m.add_function(wrap_pyfunction!(custom_named_fn, m)?)?; Ok(()) @@ -238,13 +261,13 @@ fn subfunction() -> String { "Subfunction".to_string() } -fn submodule(module: &PyModule) -> PyResult<()> { +fn submodule(module: &Bound<'_, PyModule>) -> PyResult<()> { module.add_function(wrap_pyfunction!(subfunction, module)?)?; Ok(()) } #[pymodule] -fn submodule_with_init_fn(_py: Python<'_>, module: &PyModule) -> PyResult<()> { +fn submodule_with_init_fn(module: &Bound<'_, PyModule>) -> PyResult<()> { module.add_function(wrap_pyfunction!(subfunction, module)?)?; Ok(()) } @@ -255,14 +278,14 @@ fn superfunction() -> String { } #[pymodule] -fn supermodule(py: Python<'_>, module: &PyModule) -> PyResult<()> { +fn supermodule(module: &Bound<'_, PyModule>) -> PyResult<()> { module.add_function(wrap_pyfunction!(superfunction, module)?)?; - let module_to_add = PyModule::new(py, "submodule")?; - submodule(module_to_add)?; - module.add_submodule(module_to_add)?; - let module_to_add = PyModule::new(py, "submodule_with_init_fn")?; - submodule_with_init_fn(py, module_to_add)?; - module.add_submodule(module_to_add)?; + let module_to_add = PyModule::new_bound(module.py(), "submodule")?; + submodule(&module_to_add)?; + module.add_submodule(&module_to_add)?; + let module_to_add = PyModule::new_bound(module.py(), "submodule_with_init_fn")?; + submodule_with_init_fn(&module_to_add)?; + module.add_submodule(&module_to_add)?; Ok(()) } @@ -294,14 +317,14 @@ fn test_module_nesting() { // Test that argument parsing specification works for pyfunctions #[pyfunction(signature = (a=5, *args))] -fn ext_vararg_fn(py: Python<'_>, a: i32, args: &PyTuple) -> PyObject { - [a.to_object(py), args.into()].to_object(py) +fn ext_vararg_fn(py: Python<'_>, a: i32, args: &Bound<'_, PyTuple>) -> PyObject { + [a.to_object(py), args.into_py(py)].to_object(py) } #[pymodule] -fn vararg_module(_py: Python<'_>, m: &PyModule) -> PyResult<()> { +fn vararg_module(m: &Bound<'_, PyModule>) -> PyResult<()> { #[pyfn(m, signature = (a=5, *args))] - fn int_vararg_fn(py: Python<'_>, a: i32, args: &PyTuple) -> PyObject { + fn int_vararg_fn(py: Python<'_>, a: i32, args: &Bound<'_, PyTuple>) -> PyObject { ext_vararg_fn(py, a, args) } @@ -327,7 +350,7 @@ fn test_module_with_constant() { // Regression test for #1102 #[pymodule] - fn module_with_constant(_py: Python<'_>, m: &PyModule) -> PyResult<()> { + fn module_with_constant(m: &Bound<'_, PyModule>) -> PyResult<()> { const ANON: AnonClass = AnonClass {}; m.add("ANON", ANON)?; @@ -344,70 +367,66 @@ fn test_module_with_constant() { #[pyfunction] #[pyo3(pass_module)] -fn pyfunction_with_module(module: &PyModule) -> PyResult<&str> { +fn pyfunction_with_module<'py>(module: &Bound<'py, PyModule>) -> PyResult> { module.name() } #[pyfunction] #[pyo3(pass_module)] -fn pyfunction_with_module_owned(module: Py) -> PyResult { - Python::with_gil(|gil| module.as_ref(gil).name().map(Into::into)) +fn pyfunction_with_module_owned( + module: Py, + py: Python<'_>, +) -> PyResult> { + module.bind(py).name() } #[pyfunction] #[pyo3(pass_module)] -fn pyfunction_with_module_and_py<'a>( - module: &'a PyModule, - _python: Python<'a>, -) -> PyResult<&'a str> { +fn pyfunction_with_module_and_py<'py>( + module: &Bound<'py, PyModule>, + _python: Python<'py>, +) -> PyResult> { module.name() } #[pyfunction] #[pyo3(pass_module)] -fn pyfunction_with_module_and_arg(module: &PyModule, string: String) -> PyResult<(&str, String)> { +fn pyfunction_with_module_and_arg<'py>( + module: &Bound<'py, PyModule>, + string: String, +) -> PyResult<(Bound<'py, PyString>, String)> { module.name().map(|s| (s, string)) } #[pyfunction(signature = (string="foo"))] #[pyo3(pass_module)] -fn pyfunction_with_module_and_default_arg<'a>( - module: &'a PyModule, +fn pyfunction_with_module_and_default_arg<'py>( + module: &Bound<'py, PyModule>, string: &str, -) -> PyResult<(&'a str, String)> { +) -> PyResult<(Bound<'py, PyString>, String)> { module.name().map(|s| (s, string.into())) } #[pyfunction(signature = (*args, **kwargs))] #[pyo3(pass_module)] -fn pyfunction_with_module_and_args_kwargs<'a>( - module: &'a PyModule, - args: &PyTuple, - kwargs: Option<&PyDict>, -) -> PyResult<(&'a str, usize, Option)> { +fn pyfunction_with_module_and_args_kwargs<'py>( + module: &Bound<'py, PyModule>, + args: &Bound<'py, PyTuple>, + kwargs: Option<&Bound<'py, PyDict>>, +) -> PyResult<(Bound<'py, PyString>, usize, Option)> { module .name() .map(|s| (s, args.len(), kwargs.map(|d| d.len()))) } -#[pyfunction] -#[pyo3(pass_module)] -fn pyfunction_with_pass_module_in_attribute(module: &PyModule) -> PyResult<&str> { - module.name() -} - #[pymodule] -fn module_with_functions_with_module(_py: Python<'_>, m: &PyModule) -> PyResult<()> { +fn module_with_functions_with_module(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(pyfunction_with_module, m)?)?; m.add_function(wrap_pyfunction!(pyfunction_with_module_owned, m)?)?; m.add_function(wrap_pyfunction!(pyfunction_with_module_and_py, m)?)?; m.add_function(wrap_pyfunction!(pyfunction_with_module_and_arg, m)?)?; m.add_function(wrap_pyfunction!(pyfunction_with_module_and_default_arg, m)?)?; m.add_function(wrap_pyfunction!(pyfunction_with_module_and_args_kwargs, m)?)?; - m.add_function(wrap_pyfunction!( - pyfunction_with_pass_module_in_attribute, - m - )?)?; m.add_function(wrap_pyfunction!(pyfunction_with_module, m)?)?; Ok(()) } @@ -443,11 +462,6 @@ fn test_module_functions_with_module() { "m.pyfunction_with_module_and_args_kwargs(1, x=1, y=2) \ == ('module_with_functions_with_module', 1, 2)" ); - py_assert!( - py, - m, - "m.pyfunction_with_pass_module_in_attribute() == 'module_with_functions_with_module'" - ); }); } @@ -456,7 +470,7 @@ fn test_module_doc_hidden() { #[doc(hidden)] #[allow(clippy::unnecessary_wraps)] #[pymodule] - fn my_module(_py: Python<'_>, _m: &PyModule) -> PyResult<()> { + fn my_module(_m: &Bound<'_, PyModule>) -> PyResult<()> { Ok(()) } diff --git a/tests/test_multiple_pymethods.rs b/tests/test_multiple_pymethods.rs index f78a9c60dc1..308220e78b2 100644 --- a/tests/test_multiple_pymethods.rs +++ b/tests/test_multiple_pymethods.rs @@ -35,7 +35,7 @@ impl PyClassWithMultiplePyMethods { #[pymethods] impl PyClassWithMultiplePyMethods { #[classmethod] - fn classmethod(_ty: &PyType) -> &'static str { + fn classmethod(_ty: &Bound<'_, PyType>) -> &'static str { "classmethod" } } @@ -65,7 +65,7 @@ impl PyClassWithMultiplePyMethods { #[test] fn test_class_with_multiple_pymethods() { Python::with_gil(|py| { - let cls = py.get_type::(); + let cls = py.get_type_bound::(); py_assert!(py, cls, "cls()() == 'call'"); py_assert!(py, cls, "cls().method() == 'method'"); py_assert!(py, cls, "cls.classmethod() == 'classmethod'"); diff --git a/tests/test_no_imports.rs b/tests/test_no_imports.rs index 4f04282702e..89d54f4e057 100644 --- a/tests/test_no_imports.rs +++ b/tests/test_no_imports.rs @@ -2,12 +2,16 @@ #![cfg(feature = "macros")] +use pyo3::prelude::PyAnyMethods; + #[pyo3::pyfunction] #[pyo3(name = "identity", signature = (x = None))] fn basic_function(py: pyo3::Python<'_>, x: Option) -> pyo3::PyObject { - x.unwrap_or_else(|| py.None().into()) + x.unwrap_or_else(|| py.None()) } +#[cfg(feature = "gil-refs")] +#[allow(deprecated)] #[pyo3::pymodule] fn basic_module(_py: pyo3::Python<'_>, m: &pyo3::types::PyModule) -> pyo3::PyResult<()> { #[pyfn(m)] @@ -20,6 +24,21 @@ fn basic_module(_py: pyo3::Python<'_>, m: &pyo3::types::PyModule) -> pyo3::PyRes Ok(()) } +#[pyo3::pymodule] +fn basic_module_bound(m: &pyo3::Bound<'_, pyo3::types::PyModule>) -> pyo3::PyResult<()> { + #[pyfn(m)] + fn answer() -> usize { + 42 + } + + pyo3::types::PyModuleMethods::add_function( + m, + pyo3::wrap_pyfunction_bound!(basic_function, m)?, + )?; + + Ok(()) +} + #[pyo3::pyclass] struct BasicClass { #[pyo3(get)] @@ -34,7 +53,7 @@ impl BasicClass { const OKAY: bool = true; #[new] - fn new(arg: &pyo3::PyAny) -> pyo3::PyResult { + fn new(arg: &pyo3::Bound<'_, pyo3::PyAny>) -> pyo3::PyResult { if let Ok(v) = arg.extract::() { Ok(Self { v, @@ -60,7 +79,9 @@ impl BasicClass { /// Some documentation here #[classmethod] - fn classmethod(cls: &pyo3::types::PyType) -> &pyo3::types::PyType { + fn classmethod<'a, 'py>( + cls: &'a pyo3::Bound<'py, pyo3::types::PyType>, + ) -> &'a pyo3::Bound<'py, pyo3::types::PyType> { cls } @@ -88,14 +109,14 @@ impl BasicClass { #[test] fn test_basic() { pyo3::Python::with_gil(|py| { - let module = pyo3::wrap_pymodule!(basic_module)(py); - let cls = py.get_type::(); - let d = pyo3::types::IntoPyDict::into_py_dict( + let module = pyo3::wrap_pymodule!(basic_module_bound)(py); + let cls = py.get_type_bound::(); + let d = pyo3::types::IntoPyDict::into_py_dict_bound( [ - ("mod", module.as_ref(py).as_ref()), - ("cls", cls.as_ref()), - ("a", cls.call1((8,)).unwrap()), - ("b", cls.call1(("foo",)).unwrap()), + ("mod", module.bind(py).as_any()), + ("cls", &cls), + ("a", &cls.call1((8,)).unwrap()), + ("b", &cls.call1(("foo",)).unwrap()), ], py, ); @@ -122,25 +143,30 @@ fn test_basic() { }); } +#[cfg(feature = "py-clone")] #[pyo3::pyclass] struct NewClassMethod { #[pyo3(get)] cls: pyo3::PyObject, } +#[cfg(feature = "py-clone")] #[pyo3::pymethods] impl NewClassMethod { #[new] #[classmethod] - fn new(cls: &pyo3::types::PyType) -> Self { - Self { cls: cls.into() } + fn new(cls: &pyo3::Bound<'_, pyo3::types::PyType>) -> Self { + Self { + cls: cls.clone().into_any().unbind(), + } } } +#[cfg(feature = "py-clone")] #[test] fn test_new_class_method() { pyo3::Python::with_gil(|py| { - let cls = py.get_type::(); + let cls = py.get_type_bound::(); pyo3::py_run!(py, cls, "assert cls().cls is cls"); }); } diff --git a/tests/test_pep_587.rs b/tests/test_pep_587.rs deleted file mode 100644 index 24e1f07d2d8..00000000000 --- a/tests/test_pep_587.rs +++ /dev/null @@ -1,63 +0,0 @@ -#![cfg(all(Py_3_8, not(any(PyPy, Py_LIMITED_API))))] - -use pyo3::ffi; - -#[cfg(Py_3_10)] -use widestring::WideCString; - -#[test] -fn test_default_interpreter() { - macro_rules! ensure { - ($py_call:expr) => {{ - let status = $py_call; - unsafe { - if ffi::PyStatus_Exception(status) != 0 { - ffi::Py_ExitStatusException(status); - } - } - }}; - } - - let mut preconfig = unsafe { std::mem::zeroed() }; - - unsafe { ffi::PyPreConfig_InitPythonConfig(&mut preconfig) }; - preconfig.utf8_mode = 1; - - ensure!(unsafe { ffi::Py_PreInitialize(&preconfig) }); - - let mut config = unsafe { std::mem::zeroed() }; - unsafe { ffi::PyConfig_InitPythonConfig(&mut config) }; - - // Require manually calling _Py_InitializeMain to exercise more ffi code - #[allow(clippy::used_underscore_binding)] - { - config._init_main = 0; - } - - #[cfg(Py_3_10)] - unsafe { - ffi::PyConfig_SetBytesString( - &mut config, - &mut config.program_name, - "some_test\0".as_ptr().cast(), - ); - } - - ensure!(unsafe { ffi::Py_InitializeFromConfig(&config) }); - - // The GIL is held. - assert_eq!(unsafe { ffi::PyGILState_Check() }, 1); - - // Now proceed with the Python main initialization. - ensure!(unsafe { ffi::_Py_InitializeMain() }); - - // The GIL is held after finishing initialization. - assert_eq!(unsafe { ffi::PyGILState_Check() }, 1); - - // Confirm program name set above was picked up correctly - #[cfg(Py_3_10)] - { - let program_name = unsafe { WideCString::from_ptr_str(ffi::Py_GetProgramName().cast()) }; - assert_eq!(program_name.to_string().unwrap(), "some_test"); - } -} diff --git a/tests/test_proto_methods.rs b/tests/test_proto_methods.rs index caaae751354..5f0fa105e1f 100644 --- a/tests/test_proto_methods.rs +++ b/tests/test_proto_methods.rs @@ -2,8 +2,8 @@ use pyo3::exceptions::{PyAttributeError, PyIndexError, PyValueError}; use pyo3::types::{PyDict, PyList, PyMapping, PySequence, PySlice, PyType}; -use pyo3::{prelude::*, py_run, PyCell}; -use std::{isize, iter}; +use pyo3::{prelude::*, py_run}; +use std::iter; #[path = "../src/tests/common.rs"] mod common; @@ -28,7 +28,7 @@ impl ExampleClass { } } - fn __setattr__(&mut self, attr: &str, value: &PyAny) -> PyResult<()> { + fn __setattr__(&mut self, attr: &str, value: &Bound<'_, PyAny>) -> PyResult<()> { if attr == "special_custom_attr" { self.custom_attr = Some(value.extract()?); Ok(()) @@ -64,8 +64,8 @@ impl ExampleClass { } } -fn make_example(py: Python<'_>) -> &PyCell { - Py::new( +fn make_example(py: Python<'_>) -> Bound<'_, ExampleClass> { + Bound::new( py, ExampleClass { value: 5, @@ -73,7 +73,6 @@ fn make_example(py: Python<'_>) -> &PyCell { }, ) .unwrap() - .into_ref(py) } #[test] @@ -132,7 +131,7 @@ fn test_delattr() { fn test_str() { Python::with_gil(|py| { let example_py = make_example(py); - assert_eq!(example_py.str().unwrap().to_str().unwrap(), "5"); + assert_eq!(example_py.str().unwrap().to_cow().unwrap(), "5"); }) } @@ -141,7 +140,7 @@ fn test_repr() { Python::with_gil(|py| { let example_py = make_example(py); assert_eq!( - example_py.repr().unwrap().to_str().unwrap(), + example_py.repr().unwrap().to_cow().unwrap(), "ExampleClass(value=5)" ); }) @@ -191,20 +190,20 @@ pub struct Mapping { #[pymethods] impl Mapping { fn __len__(&self, py: Python<'_>) -> usize { - self.values.as_ref(py).len() + self.values.bind(py).len() } - fn __getitem__<'a>(&'a self, key: &'a PyAny) -> PyResult<&'a PyAny> { - let any: &PyAny = self.values.as_ref(key.py()).as_ref(); + fn __getitem__<'py>(&self, key: &Bound<'py, PyAny>) -> PyResult> { + let any: &Bound<'py, PyAny> = self.values.bind(key.py()); any.get_item(key) } - fn __setitem__(&self, key: &PyAny, value: &PyAny) -> PyResult<()> { - self.values.as_ref(key.py()).set_item(key, value) + fn __setitem__<'py>(&self, key: &Bound<'py, PyAny>, value: &Bound<'py, PyAny>) -> PyResult<()> { + self.values.bind(key.py()).set_item(key, value) } - fn __delitem__(&self, key: &PyAny) -> PyResult<()> { - self.values.as_ref(key.py()).del_item(key) + fn __delitem__(&self, key: &Bound<'_, PyAny>) -> PyResult<()> { + self.values.bind(key.py()).del_item(key) } } @@ -216,12 +215,12 @@ fn mapping() { let inst = Py::new( py, Mapping { - values: PyDict::new(py).into(), + values: PyDict::new_bound(py).into(), }, ) .unwrap(); - let mapping: &PyMapping = inst.as_ref(py).downcast().unwrap(); + let mapping: &Bound<'_, PyMapping> = inst.bind(py).downcast().unwrap(); py_assert!(py, inst, "len(inst) == 0"); @@ -248,9 +247,9 @@ fn mapping() { } #[derive(FromPyObject)] -enum SequenceIndex<'a> { +enum SequenceIndex<'py> { Integer(isize), - Slice(&'a PySlice), + Slice(Bound<'py, PySlice>), } #[pyclass] @@ -264,13 +263,13 @@ impl Sequence { self.values.len() } - fn __getitem__(&self, index: SequenceIndex<'_>) -> PyResult { + fn __getitem__(&self, index: SequenceIndex<'_>, py: Python<'_>) -> PyResult { match index { SequenceIndex::Integer(index) => { let uindex = self.usize_index(index)?; self.values .get(uindex) - .map(Clone::clone) + .map(|o| o.clone_ref(py)) .ok_or_else(|| PyIndexError::new_err(index)) } // Just to prove that slicing can be implemented @@ -323,7 +322,7 @@ fn sequence() { let inst = Py::new(py, Sequence { values: vec![] }).unwrap(); - let sequence: &PySequence = inst.as_ref(py).downcast().unwrap(); + let sequence: &Bound<'_, PySequence> = inst.bind(py).downcast().unwrap(); py_assert!(py, inst, "len(inst) == 0"); @@ -350,16 +349,16 @@ fn sequence() { // indices. assert!(sequence.len().is_err()); // however regular python len() works thanks to mp_len slot - assert_eq!(inst.as_ref(py).len().unwrap(), 0); + assert_eq!(inst.bind(py).len().unwrap(), 0); py_run!(py, inst, "inst.append(0)"); sequence.set_item(0, 5).unwrap(); - assert_eq!(inst.as_ref(py).len().unwrap(), 1); + assert_eq!(inst.bind(py).len().unwrap(), 1); assert_eq!(sequence.get_item(0).unwrap().extract::().unwrap(), 5); sequence.del_item(0).unwrap(); - assert_eq!(inst.as_ref(py).len().unwrap(), 0); + assert_eq!(inst.bind(py).len().unwrap(), 0); }); } @@ -437,7 +436,7 @@ impl SetItem { #[test] fn setitem() { Python::with_gil(|py| { - let c = PyCell::new(py, SetItem { key: 0, val: 0 }).unwrap(); + let c = Bound::new(py, SetItem { key: 0, val: 0 }).unwrap(); py_run!(py, c, "c[1] = 2"); { let c = c.borrow(); @@ -463,7 +462,7 @@ impl DelItem { #[test] fn delitem() { Python::with_gil(|py| { - let c = PyCell::new(py, DelItem { key: 0 }).unwrap(); + let c = Bound::new(py, DelItem { key: 0 }).unwrap(); py_run!(py, c, "del c[1]"); { let c = c.borrow(); @@ -492,7 +491,7 @@ impl SetDelItem { #[test] fn setdelitem() { Python::with_gil(|py| { - let c = PyCell::new(py, SetDelItem { val: None }).unwrap(); + let c = Bound::new(py, SetDelItem { val: None }).unwrap(); py_run!(py, c, "c[1] = 2"); { let c = c.borrow(); @@ -529,7 +528,7 @@ struct GetItem {} #[pymethods] impl GetItem { - fn __getitem__(&self, idx: &PyAny) -> PyResult<&'static str> { + fn __getitem__(&self, idx: &Bound<'_, PyAny>) -> PyResult<&'static str> { if let Ok(slice) = idx.downcast::() { let indices = slice.indices(1000)?; if indices.start == 100 && indices.stop == 200 && indices.step == 1 { @@ -570,7 +569,7 @@ impl ClassWithGetAttr { #[test] fn getattr_doesnt_override_member() { Python::with_gil(|py| { - let inst = PyCell::new(py, ClassWithGetAttr { data: 4 }).unwrap(); + let inst = Py::new(py, ClassWithGetAttr { data: 4 }).unwrap(); py_assert!(py, inst, "inst.data == 4"); py_assert!(py, inst, "inst.a == 8"); }); @@ -592,7 +591,7 @@ impl ClassWithGetAttribute { #[test] fn getattribute_overrides_member() { Python::with_gil(|py| { - let inst = PyCell::new(py, ClassWithGetAttribute { data: 4 }).unwrap(); + let inst = Py::new(py, ClassWithGetAttribute { data: 4 }).unwrap(); py_assert!(py, inst, "inst.data == 8"); py_assert!(py, inst, "inst.y == 8"); }); @@ -625,7 +624,7 @@ impl ClassWithGetAttrAndGetAttribute { #[test] fn getattr_and_getattribute() { Python::with_gil(|py| { - let inst = PyCell::new(py, ClassWithGetAttrAndGetAttribute).unwrap(); + let inst = Py::new(py, ClassWithGetAttrAndGetAttribute).unwrap(); py_assert!(py, inst, "inst.exists == 42"); py_assert!(py, inst, "inst.lucky == 57"); py_expect_exception!(py, inst, "inst.error", PyValueError); @@ -658,10 +657,10 @@ impl OnceFuture { fn __iter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> { slf } - fn __next__<'py>(&'py mut self, py: Python<'py>) -> Option<&'py PyAny> { + fn __next__<'py>(&mut self, py: Python<'py>) -> Option<&Bound<'py, PyAny>> { if !self.polled { self.polled = true; - Some(self.future.as_ref(py)) + Some(self.future.bind(py)) } else { None } @@ -672,7 +671,7 @@ impl OnceFuture { #[cfg(not(target_arch = "wasm32"))] // Won't work without wasm32 event loop (e.g., Pyodide has WebLoop) fn test_await() { Python::with_gil(|py| { - let once = py.get_type::(); + let once = py.get_type_bound::(); let source = r#" import asyncio import sys @@ -687,9 +686,9 @@ if sys.platform == "win32" and sys.version_info >= (3, 8, 0): asyncio.run(main()) "#; - let globals = PyModule::import(py, "__main__").unwrap().dict(); + let globals = PyModule::import_bound(py, "__main__").unwrap().dict(); globals.set_item("Once", once).unwrap(); - py.run(source, Some(globals), None) + py.run_bound(source, Some(&globals), None) .map_err(|e| e.display(py)) .unwrap(); }); @@ -722,7 +721,7 @@ impl AsyncIterator { #[cfg(not(target_arch = "wasm32"))] // Won't work without wasm32 event loop (e.g., Pyodide has WebLoop) fn test_anext_aiter() { Python::with_gil(|py| { - let once = py.get_type::(); + let once = py.get_type_bound::(); let source = r#" import asyncio import sys @@ -741,12 +740,12 @@ if sys.platform == "win32" and sys.version_info >= (3, 8, 0): asyncio.run(main()) "#; - let globals = PyModule::import(py, "__main__").unwrap().dict(); + let globals = PyModule::import_bound(py, "__main__").unwrap().dict(); globals.set_item("Once", once).unwrap(); globals - .set_item("AsyncIterator", py.get_type::()) + .set_item("AsyncIterator", py.get_type_bound::()) .unwrap(); - py.run(source, Some(globals), None) + py.run_bound(source, Some(&globals), None) .map_err(|e| e.display(py)) .unwrap(); }); @@ -768,18 +767,18 @@ impl DescrCounter { /// Each access will increase the count fn __get__<'a>( mut slf: PyRefMut<'a, Self>, - _instance: &PyAny, - _owner: Option<&PyType>, + _instance: &Bound<'_, PyAny>, + _owner: Option<&Bound<'_, PyType>>, ) -> PyRefMut<'a, Self> { slf.count += 1; slf } /// Allow assigning a new counter to the descriptor, copying the count across - fn __set__(&self, _instance: &PyAny, new_value: &mut Self) { + fn __set__(&self, _instance: &Bound<'_, PyAny>, new_value: &mut Self) { new_value.count = self.count; } /// Delete to reset the counter - fn __delete__(&mut self, _instance: &PyAny) { + fn __delete__(&mut self, _instance: &Bound<'_, PyAny>) { self.count = 0; } } @@ -787,7 +786,7 @@ impl DescrCounter { #[test] fn descr_getset() { Python::with_gil(|py| { - let counter = py.get_type::(); + let counter = py.get_type_bound::(); let source = pyo3::indoc::indoc!( r#" class Class: @@ -813,9 +812,9 @@ del c.counter assert c.counter.count == 1 "# ); - let globals = PyModule::import(py, "__main__").unwrap().dict(); + let globals = PyModule::import_bound(py, "__main__").unwrap().dict(); globals.set_item("Counter", counter).unwrap(); - py.run(source, Some(globals), None) + py.run_bound(source, Some(&globals), None) .map_err(|e| e.display(py)) .unwrap(); }); @@ -850,7 +849,7 @@ struct DefaultedContains; #[pymethods] impl DefaultedContains { fn __iter__(&self, py: Python<'_>) -> PyObject { - PyList::new(py, ["a", "b", "c"]) + PyList::new_bound(py, ["a", "b", "c"]) .as_ref() .iter() .unwrap() @@ -864,7 +863,7 @@ struct NoContains; #[pymethods] impl NoContains { fn __iter__(&self, py: Python<'_>) -> PyObject { - PyList::new(py, ["a", "b", "c"]) + PyList::new_bound(py, ["a", "b", "c"]) .as_ref() .iter() .unwrap() diff --git a/tests/test_pyfunction.rs b/tests/test_pyfunction.rs index ea23bfcebb7..aee9dcf8911 100644 --- a/tests/test_pyfunction.rs +++ b/tests/test_pyfunction.rs @@ -23,7 +23,7 @@ fn optional_bool(arg: Option) -> String { fn test_optional_bool() { // Regression test for issue #932 Python::with_gil(|py| { - let f = wrap_pyfunction!(optional_bool)(py).unwrap(); + let f = wrap_pyfunction_bound!(optional_bool)(py).unwrap(); py_assert!(py, f, "f() == 'Some(true)'"); py_assert!(py, f, "f(True) == 'Some(true)'"); @@ -47,7 +47,7 @@ fn buffer_inplace_add(py: Python<'_>, x: PyBuffer, y: PyBuffer) { #[test] fn test_buffer_add() { Python::with_gil(|py| { - let f = wrap_pyfunction!(buffer_inplace_add)(py).unwrap(); + let f = wrap_pyfunction_bound!(buffer_inplace_add)(py).unwrap(); py_expect_exception!( py, @@ -77,20 +77,22 @@ assert a, array.array("i", [2, 4, 6, 8]) #[cfg(not(any(Py_LIMITED_API, PyPy)))] #[pyfunction] -fn function_with_pyfunction_arg(fun: &PyFunction) -> PyResult<&PyAny> { +fn function_with_pyfunction_arg<'py>(fun: &Bound<'py, PyFunction>) -> PyResult> { fun.call((), None) } #[pyfunction] -fn function_with_pycfunction_arg(fun: &PyCFunction) -> PyResult<&PyAny> { +fn function_with_pycfunction_arg<'py>( + fun: &Bound<'py, PyCFunction>, +) -> PyResult> { fun.call((), None) } #[test] fn test_functions_with_function_args() { Python::with_gil(|py| { - let py_cfunc_arg = wrap_pyfunction!(function_with_pycfunction_arg)(py).unwrap(); - let bool_to_string = wrap_pyfunction!(optional_bool)(py).unwrap(); + let py_cfunc_arg = wrap_pyfunction_bound!(function_with_pycfunction_arg)(py).unwrap(); + let bool_to_string = wrap_pyfunction_bound!(optional_bool)(py).unwrap(); pyo3::py_run!( py, @@ -103,7 +105,7 @@ fn test_functions_with_function_args() { #[cfg(not(any(Py_LIMITED_API, PyPy)))] { - let py_func_arg = wrap_pyfunction!(function_with_pyfunction_arg)(py).unwrap(); + let py_func_arg = wrap_pyfunction_bound!(function_with_pyfunction_arg)(py).unwrap(); pyo3::py_run!( py, @@ -118,8 +120,8 @@ fn test_functions_with_function_args() { } #[cfg(not(Py_LIMITED_API))] -fn datetime_to_timestamp(dt: &PyAny) -> PyResult { - let dt: &PyDateTime = dt.extract()?; +fn datetime_to_timestamp(dt: &Bound<'_, PyAny>) -> PyResult { + let dt = dt.downcast::()?; let ts: f64 = dt.call_method0("timestamp")?.extract()?; Ok(ts as i64) @@ -137,7 +139,7 @@ fn function_with_custom_conversion( #[test] fn test_function_with_custom_conversion() { Python::with_gil(|py| { - let custom_conv_func = wrap_pyfunction!(function_with_custom_conversion)(py).unwrap(); + let custom_conv_func = wrap_pyfunction_bound!(function_with_custom_conversion)(py).unwrap(); pyo3::py_run!( py, @@ -156,7 +158,7 @@ fn test_function_with_custom_conversion() { #[test] fn test_function_with_custom_conversion_error() { Python::with_gil(|py| { - let custom_conv_func = wrap_pyfunction!(function_with_custom_conversion)(py).unwrap(); + let custom_conv_func = wrap_pyfunction_bound!(function_with_custom_conversion)(py).unwrap(); py_expect_exception!( py, @@ -170,7 +172,7 @@ fn test_function_with_custom_conversion_error() { #[test] fn test_from_py_with_defaults() { - fn optional_int(x: &PyAny) -> PyResult> { + fn optional_int(x: &Bound<'_, PyAny>) -> PyResult> { if x.is_none() { Ok(None) } else { @@ -180,23 +182,26 @@ fn test_from_py_with_defaults() { // issue 2280 combination of from_py_with and Option did not compile #[pyfunction] + #[pyo3(signature = (int=None))] fn from_py_with_option(#[pyo3(from_py_with = "optional_int")] int: Option) -> i32 { int.unwrap_or(0) } #[pyfunction(signature = (len=0))] - fn from_py_with_default(#[pyo3(from_py_with = "PyAny::len")] len: usize) -> usize { + fn from_py_with_default( + #[pyo3(from_py_with = " as PyAnyMethods>::len")] len: usize, + ) -> usize { len } Python::with_gil(|py| { - let f = wrap_pyfunction!(from_py_with_option)(py).unwrap(); + let f = wrap_pyfunction_bound!(from_py_with_option)(py).unwrap(); assert_eq!(f.call0().unwrap().extract::().unwrap(), 0); assert_eq!(f.call1((123,)).unwrap().extract::().unwrap(), 123); assert_eq!(f.call1((999,)).unwrap().extract::().unwrap(), 999); - let f2 = wrap_pyfunction!(from_py_with_default)(py).unwrap(); + let f2 = wrap_pyfunction_bound!(from_py_with_default)(py).unwrap(); assert_eq!(f2.call0().unwrap().extract::().unwrap(), 0); assert_eq!(f2.call1(("123",)).unwrap().extract::().unwrap(), 3); @@ -212,10 +217,11 @@ struct ValueClass { } #[pyfunction] +#[pyo3(signature=(str_arg, int_arg, tuple_arg, option_arg = None, struct_arg = None))] fn conversion_error( str_arg: &str, int_arg: i64, - tuple_arg: (&str, f64), + tuple_arg: (String, f64), option_arg: Option, struct_arg: Option, ) { @@ -228,7 +234,7 @@ fn conversion_error( #[test] fn test_conversion_error() { Python::with_gil(|py| { - let conversion_error = wrap_pyfunction!(conversion_error)(py).unwrap(); + let conversion_error = wrap_pyfunction_bound!(conversion_error)(py).unwrap(); py_expect_exception!( py, conversion_error, @@ -323,11 +329,12 @@ fn test_pycfunction_new() { ffi::PyLong_FromLong(4200) } - let py_fn = PyCFunction::new( + let py_fn = PyCFunction::new_bound( + py, c_fn, "py_fn", "py_fn for test (this is the docstring)", - py.into(), + None, ) .unwrap(); @@ -380,11 +387,12 @@ fn test_pycfunction_new_with_keywords() { ffi::PyLong_FromLong(foo * bar) } - let py_fn = PyCFunction::new_with_keywords( + let py_fn = PyCFunction::new_with_keywords_bound( + py, c_fn, "py_fn", "py_fn for test (this is the docstring)", - py.into(), + None, ) .unwrap(); @@ -401,7 +409,9 @@ fn test_pycfunction_new_with_keywords() { #[test] fn test_closure() { Python::with_gil(|py| { - let f = |args: &types::PyTuple, _kwargs: Option<&types::PyDict>| -> PyResult<_> { + let f = |args: &Bound<'_, types::PyTuple>, + _kwargs: Option<&Bound<'_, types::PyDict>>| + -> PyResult<_> { Python::with_gil(|py| { let res: Vec<_> = args .iter() @@ -422,7 +432,7 @@ fn test_closure() { }) }; let closure_py = - PyCFunction::new_closure(py, Some("test_fn"), Some("test_fn doc"), f).unwrap(); + PyCFunction::new_closure_bound(py, Some("test_fn"), Some("test_fn doc"), f).unwrap(); py_assert!(py, closure_py, "closure_py(42) == [43]"); py_assert!(py, closure_py, "closure_py.__name__ == 'test_fn'"); @@ -439,13 +449,14 @@ fn test_closure() { fn test_closure_counter() { Python::with_gil(|py| { let counter = std::cell::RefCell::new(0); - let counter_fn = - move |_args: &types::PyTuple, _kwargs: Option<&types::PyDict>| -> PyResult { - let mut counter = counter.borrow_mut(); - *counter += 1; - Ok(*counter) - }; - let counter_py = PyCFunction::new_closure(py, None, None, counter_fn).unwrap(); + let counter_fn = move |_args: &Bound<'_, types::PyTuple>, + _kwargs: Option<&Bound<'_, types::PyDict>>| + -> PyResult { + let mut counter = counter.borrow_mut(); + *counter += 1; + Ok(*counter) + }; + let counter_py = PyCFunction::new_closure_bound(py, None, None, counter_fn).unwrap(); py_assert!(py, counter_py, "counter_py() == 1"); py_assert!(py, counter_py, "counter_py() == 2"); @@ -468,12 +479,12 @@ fn use_pyfunction() { use function_in_module::foo; // check imported name can be wrapped - let f = wrap_pyfunction!(foo, py).unwrap(); + let f = wrap_pyfunction_bound!(foo, py).unwrap(); assert_eq!(f.call1((5,)).unwrap().extract::().unwrap(), 5); assert_eq!(f.call1((42,)).unwrap().extract::().unwrap(), 42); // check path import can be wrapped - let f2 = wrap_pyfunction!(function_in_module::foo, py).unwrap(); + let f2 = wrap_pyfunction_bound!(function_in_module::foo, py).unwrap(); assert_eq!(f2.call1((5,)).unwrap().extract::().unwrap(), 5); assert_eq!(f2.call1((42,)).unwrap().extract::().unwrap(), 42); }) @@ -501,7 +512,7 @@ fn return_value_borrows_from_arguments<'py>( #[test] fn test_return_value_borrows_from_arguments() { Python::with_gil(|py| { - let function = wrap_pyfunction!(return_value_borrows_from_arguments, py).unwrap(); + let function = wrap_pyfunction_bound!(return_value_borrows_from_arguments, py).unwrap(); let key = Py::new(py, Key("key".to_owned())).unwrap(); let value = Py::new(py, Value(42)).unwrap(); @@ -525,7 +536,25 @@ fn test_some_wrap_arguments() { } Python::with_gil(|py| { - let function = wrap_pyfunction!(some_wrap_arguments, py).unwrap(); + let function = wrap_pyfunction_bound!(some_wrap_arguments, py).unwrap(); py_assert!(py, function, "function() == [1, 2, None, None]"); }) } + +#[test] +fn test_reference_to_bound_arguments() { + #[pyfunction] + #[pyo3(signature = (x, y = None))] + fn reference_args<'py>( + x: &Bound<'py, PyAny>, + y: Option<&Bound<'py, PyAny>>, + ) -> PyResult> { + y.map_or_else(|| Ok(x.clone()), |y| y.add(x)) + } + + Python::with_gil(|py| { + let function = wrap_pyfunction_bound!(reference_args, py).unwrap(); + py_assert!(py, function, "function(1) == 1"); + py_assert!(py, function, "function(1, 2) == 3"); + }) +} diff --git a/tests/test_pyself.rs b/tests/test_pyself.rs index ed922d3e2b0..901f52de530 100644 --- a/tests/test_pyself.rs +++ b/tests/test_pyself.rs @@ -3,7 +3,6 @@ //! Test slf: PyRef/PyMutRef(especially, slf.into::) works use pyo3::prelude::*; use pyo3::types::{PyBytes, PyString}; -use pyo3::PyCell; use std::collections::HashMap; #[path = "../src/tests/common.rs"] @@ -19,15 +18,18 @@ struct Reader { #[pymethods] impl Reader { - fn clone_ref(slf: &PyCell) -> &PyCell { + fn clone_ref<'a, 'py>(slf: &'a Bound<'py, Self>) -> &'a Bound<'py, Self> { slf } - fn clone_ref_with_py<'py>(slf: &'py PyCell, _py: Python<'py>) -> &'py PyCell { + fn clone_ref_with_py<'a, 'py>( + slf: &'a Bound<'py, Self>, + _py: Python<'py>, + ) -> &'a Bound<'py, Self> { slf } - fn get_iter(slf: &PyCell, keys: Py) -> Iter { + fn get_iter(slf: &Bound<'_, Self>, keys: Py) -> Iter { Iter { - reader: slf.into(), + reader: slf.clone().unbind(), keys, idx: 0, } @@ -63,17 +65,17 @@ impl Iter { } fn __next__(mut slf: PyRefMut<'_, Self>) -> PyResult> { - let bytes = slf.keys.as_ref(slf.py()).as_bytes(); + let bytes = slf.keys.bind(slf.py()).as_bytes(); match bytes.get(slf.idx) { Some(&b) => { slf.idx += 1; let py = slf.py(); - let reader = slf.reader.as_ref(py); + let reader = slf.reader.bind(py); let reader_ref = reader.try_borrow()?; let res = reader_ref .inner .get(&b) - .map(|s| PyString::new(py, s).into()); + .map(|s| PyString::new_bound(py, s).into()); Ok(res) } None => Ok(None), @@ -112,7 +114,7 @@ fn test_clone_ref() { #[test] fn test_nested_iter_reset() { Python::with_gil(|py| { - let reader = PyCell::new(py, reader()).unwrap(); + let reader = Bound::new(py, reader()).unwrap(); py_assert!( py, reader, diff --git a/tests/test_sequence.rs b/tests/test_sequence.rs index b11e4a6929d..8adba35c86a 100644 --- a/tests/test_sequence.rs +++ b/tests/test_sequence.rs @@ -17,11 +17,12 @@ struct ByteSequence { #[pymethods] impl ByteSequence { #[new] - fn new(elements: Option<&PyList>) -> PyResult { + #[pyo3(signature=(elements = None))] + fn new(elements: Option<&Bound<'_, PyList>>) -> PyResult { if let Some(pylist) = elements { let mut elems = Vec::with_capacity(pylist.len()); for pyelem in pylist { - let elem = u8::extract(pyelem)?; + let elem = pyelem.extract()?; elems.push(elem); } Ok(Self { elements: elems }) @@ -60,8 +61,8 @@ impl ByteSequence { } } - fn __contains__(&self, other: &PyAny) -> bool { - match u8::extract(other) { + fn __contains__(&self, other: &Bound<'_, PyAny>) -> bool { + match other.extract::() { Ok(x) => self.elements.contains(&x), Err(_) => false, } @@ -105,8 +106,8 @@ impl ByteSequence { } /// Return a dict with `s = ByteSequence([1, 2, 3])`. -fn seq_dict(py: Python<'_>) -> &pyo3::types::PyDict { - let d = [("ByteSequence", py.get_type::())].into_py_dict(py); +fn seq_dict(py: Python<'_>) -> Bound<'_, pyo3::types::PyDict> { + let d = [("ByteSequence", py.get_type_bound::())].into_py_dict_bound(py); // Though we can construct `s` in Rust, let's test `__new__` works. py_run!(py, *d, "s = ByteSequence([1, 2, 3])"); d @@ -138,7 +139,7 @@ fn test_setitem() { #[test] fn test_delitem() { Python::with_gil(|py| { - let d = [("ByteSequence", py.get_type::())].into_py_dict(py); + let d = [("ByteSequence", py.get_type_bound::())].into_py_dict_bound(py); py_run!( py, @@ -234,7 +235,7 @@ fn test_repeat() { #[test] fn test_inplace_repeat() { Python::with_gil(|py| { - let d = [("ByteSequence", py.get_type::())].into_py_dict(py); + let d = [("ByteSequence", py.get_type_bound::())].into_py_dict_bound(py); py_run!( py, @@ -247,12 +248,14 @@ fn test_inplace_repeat() { // Check that #[pyo3(get, set)] works correctly for Vec +#[cfg(feature = "py-clone")] #[pyclass] struct GenericList { #[pyo3(get, set)] items: Vec, } +#[cfg(feature = "py-clone")] #[test] fn test_generic_list_get() { Python::with_gil(|py| { @@ -265,10 +268,11 @@ fn test_generic_list_get() { }); } +#[cfg(feature = "py-clone")] #[test] fn test_generic_list_set() { Python::with_gil(|py| { - let list = PyCell::new(py, GenericList { items: vec![] }).unwrap(); + let list = Bound::new(py, GenericList { items: vec![] }).unwrap(); py_run!(py, list, "list.items = [1, 2, 3]"); assert!(list @@ -276,7 +280,7 @@ fn test_generic_list_set() { .items .iter() .zip(&[1u32, 2, 3]) - .all(|(a, b)| a.as_ref(py).eq(&b.into_py(py)).unwrap())); + .all(|(a, b)| a.bind(py).eq(&b.into_py(py)).unwrap())); }); } @@ -304,7 +308,7 @@ impl OptionList { fn test_option_list_get() { // Regression test for #798 Python::with_gil(|py| { - let list = PyCell::new( + let list = Py::new( py, OptionList { items: vec![Some(1), None], @@ -321,31 +325,33 @@ fn test_option_list_get() { #[test] fn sequence_is_not_mapping() { Python::with_gil(|py| { - let list = PyCell::new( + let list = Bound::new( py, OptionList { items: vec![Some(1), None], }, ) - .unwrap(); + .unwrap() + .into_any(); PySequence::register::(py).unwrap(); - assert!(list.as_ref().downcast::().is_err()); - assert!(list.as_ref().downcast::().is_ok()); + assert!(list.downcast::().is_err()); + assert!(list.downcast::().is_ok()); }) } #[test] fn sequence_length() { Python::with_gil(|py| { - let list = PyCell::new( + let list = Bound::new( py, OptionList { items: vec![Some(1), None], }, ) - .unwrap(); + .unwrap() + .into_any(); assert_eq!(list.len().unwrap(), 2); assert_eq!(unsafe { ffi::PySequence_Length(list.as_ptr()) }, 2); diff --git a/tests/test_serde.rs b/tests/test_serde.rs index f1d5bee4bee..9e97946b5f2 100644 --- a/tests/test_serde.rs +++ b/tests/test_serde.rs @@ -11,7 +11,7 @@ mod test_serde { } #[pyclass] - #[derive(Debug, Clone, Serialize, Deserialize)] + #[derive(Debug, Serialize, Deserialize)] struct User { username: String, group: Option>, @@ -27,7 +27,8 @@ mod test_serde { }; let friend2 = User { username: "friend 2".into(), - ..friend1.clone() + group: None, + friends: vec![], }; let user = Python::with_gil(|py| { diff --git a/tests/test_static_slots.rs b/tests/test_static_slots.rs index 402128c50df..3c270a3154c 100644 --- a/tests/test_static_slots.rs +++ b/tests/test_static_slots.rs @@ -37,8 +37,8 @@ impl Count5 { } /// Return a dict with `s = Count5()`. -fn test_dict(py: Python<'_>) -> &pyo3::types::PyDict { - let d = [("Count5", py.get_type::())].into_py_dict(py); +fn test_dict(py: Python<'_>) -> Bound<'_, pyo3::types::PyDict> { + let d = [("Count5", py.get_type_bound::())].into_py_dict_bound(py); // Though we can construct `s` in Rust, let's test `__new__` works. py_run!(py, *d, "s = Count5()"); d diff --git a/tests/test_string.rs b/tests/test_string.rs index 02bf2ecd4df..d90c5a81b83 100644 --- a/tests/test_string.rs +++ b/tests/test_string.rs @@ -11,7 +11,7 @@ fn take_str(_s: &str) {} #[test] fn test_unicode_encode_error() { Python::with_gil(|py| { - let take_str = wrap_pyfunction!(take_str)(py).unwrap(); + let take_str = wrap_pyfunction_bound!(take_str)(py).unwrap(); py_expect_exception!( py, take_str, diff --git a/tests/test_super.rs b/tests/test_super.rs index dca12457011..3647e7d5b23 100644 --- a/tests/test_super.rs +++ b/tests/test_super.rs @@ -29,13 +29,14 @@ impl SubClass { (SubClass {}, BaseClass::new()) } - fn method(self_: &PyCell) -> PyResult<&PyAny> { + fn method<'py>(self_: &Bound<'py, Self>) -> PyResult> { let super_ = self_.py_super()?; super_.call_method("method", (), None) } - fn method_super_new(self_: &PyCell) -> PyResult<&PyAny> { - let super_ = PySuper::new(self_.get_type(), self_)?; + fn method_super_new<'py>(self_: &Bound<'py, Self>) -> PyResult> { + #[cfg_attr(not(feature = "gil-refs"), allow(deprecated))] + let super_ = PySuper::new_bound(&self_.get_type(), self_)?; super_.call_method("method", (), None) } } @@ -43,7 +44,7 @@ impl SubClass { #[test] fn test_call_super_method() { Python::with_gil(|py| { - let cls = py.get_type::(); + let cls = py.get_type_bound::(); pyo3::py_run!( py, cls, diff --git a/tests/test_text_signature.rs b/tests/test_text_signature.rs index 5b0491d9970..3899878bd56 100644 --- a/tests/test_text_signature.rs +++ b/tests/test_text_signature.rs @@ -2,7 +2,7 @@ use pyo3::prelude::*; use pyo3::types::{PyDict, PyTuple}; -use pyo3::{types::PyType, wrap_pymodule, PyCell}; +use pyo3::{types::PyType, wrap_pymodule}; #[path = "../src/tests/common.rs"] mod common; @@ -13,7 +13,7 @@ fn class_without_docs_or_signature() { struct MyClass {} Python::with_gil(|py| { - let typeobj = py.get_type::(); + let typeobj = py.get_type_bound::(); py_assert!(py, typeobj, "typeobj.__doc__ is None"); py_assert!(py, typeobj, "typeobj.__text_signature__ is None"); @@ -28,7 +28,7 @@ fn class_with_docs() { struct MyClass {} Python::with_gil(|py| { - let typeobj = py.get_type::(); + let typeobj = py.get_type_bound::(); py_assert!(py, typeobj, "typeobj.__doc__ == 'docs line1\\ndocs line2'"); py_assert!(py, typeobj, "typeobj.__text_signature__ is None"); @@ -52,7 +52,7 @@ fn class_with_signature_no_doc() { } Python::with_gil(|py| { - let typeobj = py.get_type::(); + let typeobj = py.get_type_bound::(); py_assert!(py, typeobj, "typeobj.__doc__ == ''"); py_assert!( py, @@ -81,7 +81,7 @@ fn class_with_docs_and_signature() { } Python::with_gil(|py| { - let typeobj = py.get_type::(); + let typeobj = py.get_type_bound::(); py_assert!(py, typeobj, "typeobj.__doc__ == 'docs line1\\ndocs line2'"); py_assert!( @@ -101,7 +101,7 @@ fn test_function() { } Python::with_gil(|py| { - let f = wrap_pyfunction!(my_function)(py).unwrap(); + let f = wrap_pyfunction_bound!(my_function)(py).unwrap(); py_assert!(py, f, "f.__text_signature__ == '(a, b=None, *, c=42)'"); }); @@ -115,7 +115,7 @@ fn test_auto_test_signature_function() { } #[pyfunction(pass_module)] - fn my_function_2(module: &PyModule, a: i32, b: i32, c: i32) { + fn my_function_2(module: &Bound<'_, PyModule>, a: i32, b: i32, c: i32) { let _ = (module, a, b, c); } @@ -128,10 +128,10 @@ fn test_auto_test_signature_function() { fn my_function_4( a: i32, b: Option, - args: &PyTuple, + args: &Bound<'_, PyTuple>, c: i32, d: i32, - kwargs: Option<&PyDict>, + kwargs: Option<&Bound<'_, PyDict>>, ) { let _ = (a, b, args, c, d, kwargs); } @@ -142,47 +142,48 @@ fn test_auto_test_signature_function() { } #[pyfunction] + #[pyo3(signature=(a, b=None, c=None))] fn my_function_6(a: i32, b: Option, c: Option) { let _ = (a, b, c); } Python::with_gil(|py| { - let f = wrap_pyfunction!(my_function)(py).unwrap(); + let f = wrap_pyfunction_bound!(my_function)(py).unwrap(); py_assert!( py, f, "f.__text_signature__ == '(a, b, c)', f.__text_signature__" ); - let f = wrap_pyfunction!(my_function_2)(py).unwrap(); + let f = wrap_pyfunction_bound!(my_function_2)(py).unwrap(); py_assert!( py, f, "f.__text_signature__ == '($module, a, b, c)', f.__text_signature__" ); - let f = wrap_pyfunction!(my_function_3)(py).unwrap(); + let f = wrap_pyfunction_bound!(my_function_3)(py).unwrap(); py_assert!( py, f, "f.__text_signature__ == '(a, /, b=None, *, c=5)', f.__text_signature__" ); - let f = wrap_pyfunction!(my_function_4)(py).unwrap(); + let f = wrap_pyfunction_bound!(my_function_4)(py).unwrap(); py_assert!( py, f, "f.__text_signature__ == '(a, /, b=None, *args, c, d=5, **kwargs)', f.__text_signature__" ); - let f = wrap_pyfunction!(my_function_5)(py).unwrap(); + let f = wrap_pyfunction_bound!(my_function_5)(py).unwrap(); py_assert!( py, f, "f.__text_signature__ == '(a=1, /, b=None, c=1.5, d=5, e=\"pyo3\", f=\\'f\\', h=True)', f.__text_signature__" ); - let f = wrap_pyfunction!(my_function_6)(py).unwrap(); + let f = wrap_pyfunction_bound!(my_function_6)(py).unwrap(); py_assert!( py, f, @@ -218,10 +219,10 @@ fn test_auto_test_signature_method() { &self, a: i32, b: Option, - args: &PyTuple, + args: &Bound<'_, PyTuple>, c: i32, d: i32, - kwargs: Option<&PyDict>, + kwargs: Option<&Bound<'_, PyDict>>, ) { let _ = (a, b, args, c, d, kwargs); } @@ -232,13 +233,13 @@ fn test_auto_test_signature_method() { } #[classmethod] - fn classmethod(cls: &PyType, a: i32, b: i32, c: i32) { + fn classmethod(cls: &Bound<'_, PyType>, a: i32, b: i32, c: i32) { let _ = (cls, a, b, c); } } Python::with_gil(|py| { - let cls = py.get_type::(); + let cls = py.get_type_bound::(); #[cfg(any(not(Py_LIMITED_API), Py_3_10))] py_assert!(py, cls, "cls.__text_signature__ == '(a, b, c)'"); py_assert!( @@ -311,19 +312,19 @@ fn test_auto_test_signature_opt_out() { #[classmethod] #[pyo3(text_signature = None)] - fn classmethod(cls: &PyType, a: i32, b: i32, c: i32) { + fn classmethod(cls: &Bound<'_, PyType>, a: i32, b: i32, c: i32) { let _ = (cls, a, b, c); } } Python::with_gil(|py| { - let f = wrap_pyfunction!(my_function)(py).unwrap(); + let f = wrap_pyfunction_bound!(my_function)(py).unwrap(); py_assert!(py, f, "f.__text_signature__ == None"); - let f = wrap_pyfunction!(my_function_2)(py).unwrap(); + let f = wrap_pyfunction_bound!(my_function_2)(py).unwrap(); py_assert!(py, f, "f.__text_signature__ == None"); - let cls = py.get_type::(); + let cls = py.get_type_bound::(); py_assert!(py, cls, "cls.__text_signature__ == None"); py_assert!(py, cls, "cls.method.__text_signature__ == None"); py_assert!(py, cls, "cls.method_2.__text_signature__ == None"); @@ -335,7 +336,7 @@ fn test_auto_test_signature_opt_out() { #[test] fn test_pyfn() { #[pymodule] - fn my_module(_py: Python<'_>, m: &PyModule) -> PyResult<()> { + fn my_module(m: &Bound<'_, PyModule>) -> PyResult<()> { #[pyfn(m, signature = (a, b=None, *, c=42))] #[pyo3(text_signature = "(a, b=None, *, c=42)")] fn my_function(a: i32, b: Option, c: i32) { @@ -367,12 +368,12 @@ fn test_methods() { let _ = a; } #[pyo3(text_signature = "($self, b)")] - fn pyself_method(_this: &PyCell, b: i32) { + fn pyself_method(_this: &Bound<'_, Self>, b: i32) { let _ = b; } #[classmethod] #[pyo3(text_signature = "($cls, c)")] - fn class_method(_cls: &PyType, c: i32) { + fn class_method(_cls: &Bound<'_, PyType>, c: i32) { let _ = c; } #[staticmethod] @@ -383,7 +384,7 @@ fn test_methods() { } Python::with_gil(|py| { - let typeobj = py.get_type::(); + let typeobj = py.get_type_bound::(); py_assert!( py, @@ -424,7 +425,7 @@ fn test_raw_identifiers() { } Python::with_gil(|py| { - let typeobj = py.get_type::(); + let typeobj = py.get_type_bound::(); py_assert!(py, typeobj, "typeobj.__text_signature__ == '()'"); diff --git a/tests/test_variable_arguments.rs b/tests/test_variable_arguments.rs index a1f8bb354cf..3724689d836 100644 --- a/tests/test_variable_arguments.rs +++ b/tests/test_variable_arguments.rs @@ -13,13 +13,13 @@ struct MyClass {} impl MyClass { #[staticmethod] #[pyo3(signature = (*args))] - fn test_args(args: &PyTuple) -> &PyTuple { + fn test_args(args: Bound<'_, PyTuple>) -> Bound<'_, PyTuple> { args } #[staticmethod] #[pyo3(signature = (**kwargs))] - fn test_kwargs(kwargs: Option<&PyDict>) -> Option<&PyDict> { + fn test_kwargs(kwargs: Option>) -> Option> { kwargs } } @@ -27,7 +27,7 @@ impl MyClass { #[test] fn variable_args() { Python::with_gil(|py| { - let my_obj = py.get_type::(); + let my_obj = py.get_type_bound::(); py_assert!(py, my_obj, "my_obj.test_args() == ()"); py_assert!(py, my_obj, "my_obj.test_args(1) == (1,)"); py_assert!(py, my_obj, "my_obj.test_args(1, 2) == (1, 2)"); @@ -37,7 +37,7 @@ fn variable_args() { #[test] fn variable_kwargs() { Python::with_gil(|py| { - let my_obj = py.get_type::(); + let my_obj = py.get_type_bound::(); py_assert!(py, my_obj, "my_obj.test_kwargs() == None"); py_assert!(py, my_obj, "my_obj.test_kwargs(test=1) == {'test': 1}"); py_assert!( diff --git a/tests/test_various.rs b/tests/test_various.rs index 076d2ba2cb5..0e619f49a28 100644 --- a/tests/test_various.rs +++ b/tests/test_various.rs @@ -1,8 +1,8 @@ #![cfg(feature = "macros")] use pyo3::prelude::*; +use pyo3::py_run; use pyo3::types::{PyDict, PyTuple}; -use pyo3::{py_run, PyCell}; use std::fmt; @@ -31,7 +31,7 @@ fn mut_ref_arg() { let inst2 = Py::new(py, MutRefArg { n: 0 }).unwrap(); py_run!(py, inst1 inst2, "inst1.set_other(inst2)"); - let inst2 = inst2.as_ref(py).borrow(); + let inst2 = inst2.bind(py).borrow(); assert_eq!(inst2.n, 100); }); } @@ -56,7 +56,7 @@ fn return_custom_class() { assert_eq!(get_zero().value, 0); // Using from python - let get_zero = wrap_pyfunction!(get_zero)(py).unwrap(); + let get_zero = wrap_pyfunction_bound!(get_zero)(py).unwrap(); py_assert!(py, get_zero, "get_zero().value == 0"); }); } @@ -79,8 +79,8 @@ struct SimplePyClass {} fn intopytuple_pyclass() { Python::with_gil(|py| { let tup = ( - PyCell::new(py, SimplePyClass {}).unwrap(), - PyCell::new(py, SimplePyClass {}).unwrap(), + Py::new(py, SimplePyClass {}).unwrap(), + Py::new(py, SimplePyClass {}).unwrap(), ); py_assert!(py, tup, "type(tup[0]).__name__ == 'SimplePyClass'"); py_assert!(py, tup, "type(tup[0]).__name__ == type(tup[1]).__name__"); @@ -91,7 +91,7 @@ fn intopytuple_pyclass() { #[test] fn pytuple_primitive_iter() { Python::with_gil(|py| { - let tup = PyTuple::new(py, [1u32, 2, 3].iter()); + let tup = PyTuple::new_bound(py, [1u32, 2, 3].iter()); py_assert!(py, tup, "tup == (1, 2, 3)"); }); } @@ -99,11 +99,11 @@ fn pytuple_primitive_iter() { #[test] fn pytuple_pyclass_iter() { Python::with_gil(|py| { - let tup = PyTuple::new( + let tup = PyTuple::new_bound( py, [ - PyCell::new(py, SimplePyClass {}).unwrap(), - PyCell::new(py, SimplePyClass {}).unwrap(), + Py::new(py, SimplePyClass {}).unwrap(), + Py::new(py, SimplePyClass {}).unwrap(), ] .iter(), ); @@ -124,17 +124,17 @@ impl PickleSupport { } pub fn __reduce__<'py>( - slf: &'py PyCell, + slf: &Bound<'py, Self>, py: Python<'py>, - ) -> PyResult<(PyObject, &'py PyTuple, PyObject)> { + ) -> PyResult<(PyObject, Bound<'py, PyTuple>, PyObject)> { let cls = slf.to_object(py).getattr(py, "__class__")?; let dict = slf.to_object(py).getattr(py, "__dict__")?; - Ok((cls, PyTuple::empty(py), dict)) + Ok((cls, PyTuple::empty_bound(py), dict)) } } -fn add_module(py: Python<'_>, module: &PyModule) -> PyResult<()> { - py.import("sys")? +fn add_module(module: Bound<'_, PyModule>) -> PyResult<()> { + PyModule::import_bound(module.py(), "sys")? .dict() .get_item("modules") .unwrap() @@ -147,10 +147,10 @@ fn add_module(py: Python<'_>, module: &PyModule) -> PyResult<()> { #[cfg_attr(all(Py_LIMITED_API, not(Py_3_10)), ignore)] fn test_pickle() { Python::with_gil(|py| { - let module = PyModule::new(py, "test_module").unwrap(); + let module = PyModule::new_bound(py, "test_module").unwrap(); module.add_class::().unwrap(); - add_module(py, module).unwrap(); - let inst = PyCell::new(py, PickleSupport {}).unwrap(); + add_module(module).unwrap(); + let inst = Py::new(py, PickleSupport {}).unwrap(); py_run!( py, inst, @@ -201,6 +201,6 @@ fn result_conversion_function() -> Result<(), MyError> { #[test] fn test_result_conversion() { Python::with_gil(|py| { - wrap_pyfunction!(result_conversion_function)(py).unwrap(); + wrap_pyfunction_bound!(result_conversion_function)(py).unwrap(); }); } diff --git a/tests/test_wrap_pyfunction_deduction.rs b/tests/test_wrap_pyfunction_deduction.rs index 8723ad43fbd..e205003113e 100644 --- a/tests/test_wrap_pyfunction_deduction.rs +++ b/tests/test_wrap_pyfunction_deduction.rs @@ -5,11 +5,25 @@ use pyo3::{prelude::*, types::PyCFunction}; #[pyfunction] fn f() {} +#[cfg(feature = "gil-refs")] pub fn add_wrapped(wrapper: &impl Fn(Python<'_>) -> PyResult<&PyCFunction>) { let _ = wrapper; } #[test] fn wrap_pyfunction_deduction() { + #[allow(deprecated)] + #[cfg(feature = "gil-refs")] add_wrapped(wrap_pyfunction!(f)); + #[cfg(not(feature = "gil-refs"))] + add_wrapped_bound(wrap_pyfunction!(f)); +} + +pub fn add_wrapped_bound(wrapper: &impl Fn(Python<'_>) -> PyResult>) { + let _ = wrapper; +} + +#[test] +fn wrap_pyfunction_deduction_bound() { + add_wrapped_bound(wrap_pyfunction_bound!(f)); } diff --git a/tests/ui/abi3_inheritance.rs b/tests/ui/abi3_inheritance.rs new file mode 100644 index 00000000000..60972e4cf7a --- /dev/null +++ b/tests/ui/abi3_inheritance.rs @@ -0,0 +1,10 @@ +use pyo3::exceptions::PyException; +use pyo3::prelude::*; + +#[pyclass(extends=PyException)] +#[derive(Clone)] +struct MyException { + code: u32, +} + +fn main() {} diff --git a/tests/ui/abi3_inheritance.stderr b/tests/ui/abi3_inheritance.stderr new file mode 100644 index 00000000000..756df2eb75e --- /dev/null +++ b/tests/ui/abi3_inheritance.stderr @@ -0,0 +1,24 @@ +error[E0277]: the trait bound `PyException: PyClassBaseType` is not satisfied + --> tests/ui/abi3_inheritance.rs:4:19 + | +4 | #[pyclass(extends=PyException)] + | ^^^^^^^^^^^ the trait `PyClass` is not implemented for `PyException`, which is required by `PyException: PyClassBaseType` + | + = note: with the `abi3` feature enabled, PyO3 does not support subclassing native types + = help: the trait `PyClassBaseType` is implemented for `PyAny` + = note: required for `PyException` to implement `PyClassBaseType` +note: required by a bound in `PyClassImpl::BaseType` + --> src/impl_/pyclass.rs + | + | type BaseType: PyTypeInfo + PyClassBaseType; + | ^^^^^^^^^^^^^^^ required by this bound in `PyClassImpl::BaseType` + +error[E0277]: the trait bound `PyException: PyClass` is not satisfied + --> tests/ui/abi3_inheritance.rs:4:1 + | +4 | #[pyclass(extends=PyException)] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `PyClass` is not implemented for `PyException`, which is required by `PyException: PyClassBaseType` + | + = help: the trait `PyClass` is implemented for `MyException` + = note: required for `PyException` to implement `PyClassBaseType` + = note: this error originates in the attribute macro `pyclass` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/tests/ui/abi3_nativetype_inheritance.stderr b/tests/ui/abi3_nativetype_inheritance.stderr index 08fef60654d..5cd985ccfe5 100644 --- a/tests/ui/abi3_nativetype_inheritance.stderr +++ b/tests/ui/abi3_nativetype_inheritance.stderr @@ -1,11 +1,26 @@ +error[E0277]: the trait bound `PyDict: PyClassBaseType` is not satisfied + --> tests/ui/abi3_nativetype_inheritance.rs:5:19 + | +5 | #[pyclass(extends=PyDict)] + | ^^^^^^ the trait `PyClass` is not implemented for `PyDict`, which is required by `PyDict: PyClassBaseType` + | + = note: with the `abi3` feature enabled, PyO3 does not support subclassing native types + = help: the trait `PyClassBaseType` is implemented for `PyAny` + = note: required for `PyDict` to implement `PyClassBaseType` +note: required by a bound in `PyClassImpl::BaseType` + --> src/impl_/pyclass.rs + | + | type BaseType: PyTypeInfo + PyClassBaseType; + | ^^^^^^^^^^^^^^^ required by this bound in `PyClassImpl::BaseType` + error[E0277]: the trait bound `PyDict: PyClass` is not satisfied --> tests/ui/abi3_nativetype_inheritance.rs:5:1 | 5 | #[pyclass(extends=PyDict)] - | ^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `PyClass` is not implemented for `PyDict` + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `PyClass` is not implemented for `PyDict`, which is required by `PyDict: PyClassBaseType` | = help: the following other types implement trait `PyClass`: TestClass - Coroutine + pyo3::coroutine::Coroutine = note: required for `PyDict` to implement `PyClassBaseType` = note: this error originates in the attribute macro `pyclass` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/tests/ui/deprecations.rs b/tests/ui/deprecations.rs index 4177dd6da22..fc9e8687cae 100644 --- a/tests/ui/deprecations.rs +++ b/tests/ui/deprecations.rs @@ -1,6 +1,8 @@ #![deny(deprecated)] +#![allow(dead_code)] use pyo3::prelude::*; +use pyo3::types::{PyString, PyType}; #[pyclass] struct MyClass; @@ -11,6 +13,186 @@ impl MyClass { fn new() -> Self { Self } + + #[classmethod] + fn cls_method_gil_ref(_cls: &PyType) {} + + #[classmethod] + fn cls_method_bound(_cls: &Bound<'_, PyType>) {} + + fn method_gil_ref(_slf: &PyCell) {} + + fn method_bound(_slf: &Bound<'_, Self>) {} + + #[staticmethod] + fn static_method_gil_ref(_any: &PyAny) {} + + #[setter] + fn set_foo_gil_ref(&self, #[pyo3(from_py_with = "extract_gil_ref")] _value: i32) {} + + #[setter] + fn set_foo_bound(&self, #[pyo3(from_py_with = "extract_bound")] _value: i32) {} + + #[setter] + fn set_bar_gil_ref(&self, _value: &PyAny) {} + + #[setter] + fn set_bar_bound(&self, _value: &Bound<'_, PyAny>) {} + + #[setter] + fn set_option(&self, _value: Option) {} + + fn __eq__(&self, #[pyo3(from_py_with = "extract_gil_ref")] _other: i32) -> bool { + true + } + + fn __contains__(&self, #[pyo3(from_py_with = "extract_bound")] _value: i32) -> bool { + true + } } fn main() {} + +#[pyfunction] +#[pyo3(pass_module)] +fn pyfunction_with_module<'py>(module: &Bound<'py, PyModule>) -> PyResult> { + module.name() +} + +#[pyfunction] +#[pyo3(pass_module)] +fn pyfunction_with_module_gil_ref(_module: &PyModule) -> PyResult<&str> { + todo!() +} + +#[pyfunction] +fn double(x: usize) -> usize { + x * 2 +} + +#[pymodule] +fn module_gil_ref(_m: &PyModule) -> PyResult<()> { + Ok(()) +} + +#[pymodule] +fn module_gil_ref_with_explicit_py_arg(_py: Python<'_>, _m: &PyModule) -> PyResult<()> { + Ok(()) +} + +#[pymodule] +fn module_bound(m: &Bound<'_, PyModule>) -> PyResult<()> { + m.add_function(wrap_pyfunction!(double, m)?)?; + Ok(()) +} + +#[pymodule] +fn module_bound_with_explicit_py_arg(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + m.add_function(wrap_pyfunction!(double, m)?)?; + Ok(()) +} + +#[pymodule] +fn module_bound_by_value(m: Bound<'_, PyModule>) -> PyResult<()> { + m.add_function(wrap_pyfunction!(double, &m)?)?; + Ok(()) +} + +fn extract_gil_ref(obj: &PyAny) -> PyResult { + obj.extract() +} + +fn extract_bound(obj: &Bound<'_, PyAny>) -> PyResult { + obj.extract() +} + +fn extract_options(obj: &Bound<'_, PyAny>) -> PyResult> { + obj.extract() +} + +#[pyfunction] +fn pyfunction_from_py_with( + #[pyo3(from_py_with = "extract_gil_ref")] _gil_ref: i32, + #[pyo3(from_py_with = "extract_bound")] _bound: i32, +) { +} + +#[pyfunction] +fn pyfunction_gil_ref(_any: &PyAny) {} + +#[pyfunction] +#[pyo3(signature = (_any))] +fn pyfunction_option_gil_ref(_any: Option<&PyAny>) {} + +#[pyfunction] +#[pyo3(signature = (_i, _any=None))] +fn pyfunction_option_1(_i: u32, _any: Option) {} + +#[pyfunction] +fn pyfunction_option_2(_i: u32, _any: Option) {} + +#[pyfunction] +fn pyfunction_option_3(_i: u32, _any: Option, _foo: Option) {} + +#[pyfunction] +fn pyfunction_option_4( + _i: u32, + #[pyo3(from_py_with = "extract_options")] _any: Option, + _foo: Option, +) { +} + +#[derive(Debug, FromPyObject)] +pub struct Zap { + #[pyo3(item)] + name: String, + + #[pyo3(from_py_with = "PyAny::len", item("my_object"))] + some_object_length: usize, + + #[pyo3(from_py_with = "extract_bound")] + some_number: i32, +} + +#[derive(Debug, FromPyObject)] +pub struct ZapTuple( + String, + #[pyo3(from_py_with = "PyAny::len")] usize, + #[pyo3(from_py_with = "extract_bound")] i32, +); + +#[derive(Debug, FromPyObject, PartialEq, Eq)] +pub enum ZapEnum { + Zip(#[pyo3(from_py_with = "extract_gil_ref")] i32), + Zap(String, #[pyo3(from_py_with = "extract_bound")] i32), +} + +#[derive(Debug, FromPyObject, PartialEq, Eq)] +#[pyo3(transparent)] +pub struct TransparentFromPyWithGilRef { + #[pyo3(from_py_with = "extract_gil_ref")] + len: i32, +} + +#[derive(Debug, FromPyObject, PartialEq, Eq)] +#[pyo3(transparent)] +pub struct TransparentFromPyWithBound { + #[pyo3(from_py_with = "extract_bound")] + len: i32, +} + +fn test_wrap_pyfunction(py: Python<'_>, m: &Bound<'_, PyModule>) { + // should lint + let _ = wrap_pyfunction!(double, py); + + // should lint but currently does not + let _ = wrap_pyfunction!(double)(py); + + // should not lint + let _ = wrap_pyfunction!(double, m); + let _ = wrap_pyfunction!(double)(m); + let _ = wrap_pyfunction!(double, m.as_gil_ref()); + let _ = wrap_pyfunction!(double)(m.as_gil_ref()); + let _ = wrap_pyfunction_bound!(double, py); + let _ = wrap_pyfunction_bound!(double)(py); +} diff --git a/tests/ui/deprecations.stderr b/tests/ui/deprecations.stderr index a857b5ee420..d014a06bbcc 100644 --- a/tests/ui/deprecations.stderr +++ b/tests/ui/deprecations.stderr @@ -1,7 +1,7 @@ -error: use of deprecated constant `pyo3::impl_::deprecations::PYMETHODS_NEW_DEPRECATED_FORM`: use `#[new]` instead of `#[__new__]` - --> tests/ui/deprecations.rs:10:7 +error: use of deprecated constant `pyo3::deprecations::PYMETHODS_NEW_DEPRECATED_FORM`: use `#[new]` instead of `#[__new__]` + --> tests/ui/deprecations.rs:12:7 | -10 | #[__new__] +12 | #[__new__] | ^^^^^^^ | note: the lint level is defined here @@ -9,3 +9,137 @@ note: the lint level is defined here | 1 | #![deny(deprecated)] | ^^^^^^^^^^ + +error: use of deprecated constant `MyClass::__pymethod_set_set_option__::SIGNATURE`: This function has implicit defaults for the trailing `Option` arguments. These implicit defaults are being phased out. Add `#[pyo3(signature = (_value=None)]` to this function to silence this warning and keep the current behavior + --> tests/ui/deprecations.rs:43:8 + | +43 | fn set_option(&self, _value: Option) {} + | ^^^^^^^^^^ + +error: use of deprecated constant `__pyfunction_pyfunction_option_2::SIGNATURE`: This function has implicit defaults for the trailing `Option` arguments. These implicit defaults are being phased out. Add `#[pyo3(signature = (_i, _any=None)]` to this function to silence this warning and keep the current behavior + --> tests/ui/deprecations.rs:132:4 + | +132 | fn pyfunction_option_2(_i: u32, _any: Option) {} + | ^^^^^^^^^^^^^^^^^^^ + +error: use of deprecated constant `__pyfunction_pyfunction_option_3::SIGNATURE`: This function has implicit defaults for the trailing `Option` arguments. These implicit defaults are being phased out. Add `#[pyo3(signature = (_i, _any=None, _foo=None)]` to this function to silence this warning and keep the current behavior + --> tests/ui/deprecations.rs:135:4 + | +135 | fn pyfunction_option_3(_i: u32, _any: Option, _foo: Option) {} + | ^^^^^^^^^^^^^^^^^^^ + +error: use of deprecated constant `__pyfunction_pyfunction_option_4::SIGNATURE`: This function has implicit defaults for the trailing `Option` arguments. These implicit defaults are being phased out. Add `#[pyo3(signature = (_i, _any=None, _foo=None)]` to this function to silence this warning and keep the current behavior + --> tests/ui/deprecations.rs:138:4 + | +138 | fn pyfunction_option_4( + | ^^^^^^^^^^^^^^^^^^^ + +error: use of deprecated struct `pyo3::PyCell`: `PyCell` was merged into `Bound`, use that instead; see the migration guide for more info + --> tests/ui/deprecations.rs:23:30 + | +23 | fn method_gil_ref(_slf: &PyCell) {} + | ^^^^^^ + +error: use of deprecated method `pyo3::deprecations::GilRefs::::from_py_with_arg`: use `&Bound<'_, PyAny>` as the argument for this `from_py_with` extractor + --> tests/ui/deprecations.rs:45:44 + | +45 | fn __eq__(&self, #[pyo3(from_py_with = "extract_gil_ref")] _other: i32) -> bool { + | ^^^^^^^^^^^^^^^^^ + +error: use of deprecated method `pyo3::deprecations::GilRefs::::function_arg`: use `&Bound<'_, T>` instead for this function argument + --> tests/ui/deprecations.rs:18:33 + | +18 | fn cls_method_gil_ref(_cls: &PyType) {} + | ^ + +error: use of deprecated method `pyo3::deprecations::GilRefs::::function_arg`: use `&Bound<'_, T>` instead for this function argument + --> tests/ui/deprecations.rs:23:29 + | +23 | fn method_gil_ref(_slf: &PyCell) {} + | ^ + +error: use of deprecated method `pyo3::deprecations::GilRefs::::function_arg`: use `&Bound<'_, T>` instead for this function argument + --> tests/ui/deprecations.rs:28:36 + | +28 | fn static_method_gil_ref(_any: &PyAny) {} + | ^ + +error: use of deprecated method `pyo3::deprecations::GilRefs::::from_py_with_arg`: use `&Bound<'_, PyAny>` as the argument for this `from_py_with` extractor + --> tests/ui/deprecations.rs:31:53 + | +31 | fn set_foo_gil_ref(&self, #[pyo3(from_py_with = "extract_gil_ref")] _value: i32) {} + | ^^^^^^^^^^^^^^^^^ + +error: use of deprecated method `pyo3::deprecations::GilRefs::::function_arg`: use `&Bound<'_, T>` instead for this function argument + --> tests/ui/deprecations.rs:37:39 + | +37 | fn set_bar_gil_ref(&self, _value: &PyAny) {} + | ^ + +error: use of deprecated method `pyo3::deprecations::GilRefs::::function_arg`: use `&Bound<'_, T>` instead for this function argument + --> tests/ui/deprecations.rs:64:44 + | +64 | fn pyfunction_with_module_gil_ref(_module: &PyModule) -> PyResult<&str> { + | ^ + +error: use of deprecated method `pyo3::deprecations::GilRefs::::function_arg`: use `&Bound<'_, T>` instead for this function argument + --> tests/ui/deprecations.rs:74:19 + | +74 | fn module_gil_ref(_m: &PyModule) -> PyResult<()> { + | ^^ + +error: use of deprecated method `pyo3::deprecations::GilRefs::::function_arg`: use `&Bound<'_, T>` instead for this function argument + --> tests/ui/deprecations.rs:79:57 + | +79 | fn module_gil_ref_with_explicit_py_arg(_py: Python<'_>, _m: &PyModule) -> PyResult<()> { + | ^^ + +error: use of deprecated method `pyo3::deprecations::GilRefs::::from_py_with_arg`: use `&Bound<'_, PyAny>` as the argument for this `from_py_with` extractor + --> tests/ui/deprecations.rs:115:27 + | +115 | #[pyo3(from_py_with = "extract_gil_ref")] _gil_ref: i32, + | ^^^^^^^^^^^^^^^^^ + +error: use of deprecated method `pyo3::deprecations::GilRefs::::function_arg`: use `&Bound<'_, T>` instead for this function argument + --> tests/ui/deprecations.rs:121:29 + | +121 | fn pyfunction_gil_ref(_any: &PyAny) {} + | ^ + +error: use of deprecated method `pyo3::deprecations::OptionGilRefs::>::function_arg`: use `Option<&Bound<'_, T>>` instead for this function argument + --> tests/ui/deprecations.rs:125:36 + | +125 | fn pyfunction_option_gil_ref(_any: Option<&PyAny>) {} + | ^^^^^^ + +error: use of deprecated method `pyo3::deprecations::GilRefs::::from_py_with_arg`: use `&Bound<'_, PyAny>` as the argument for this `from_py_with` extractor + --> tests/ui/deprecations.rs:150:27 + | +150 | #[pyo3(from_py_with = "PyAny::len", item("my_object"))] + | ^^^^^^^^^^^^ + +error: use of deprecated method `pyo3::deprecations::GilRefs::::from_py_with_arg`: use `&Bound<'_, PyAny>` as the argument for this `from_py_with` extractor + --> tests/ui/deprecations.rs:160:27 + | +160 | #[pyo3(from_py_with = "PyAny::len")] usize, + | ^^^^^^^^^^^^ + +error: use of deprecated method `pyo3::deprecations::GilRefs::::from_py_with_arg`: use `&Bound<'_, PyAny>` as the argument for this `from_py_with` extractor + --> tests/ui/deprecations.rs:166:31 + | +166 | Zip(#[pyo3(from_py_with = "extract_gil_ref")] i32), + | ^^^^^^^^^^^^^^^^^ + +error: use of deprecated method `pyo3::deprecations::GilRefs::::from_py_with_arg`: use `&Bound<'_, PyAny>` as the argument for this `from_py_with` extractor + --> tests/ui/deprecations.rs:173:27 + | +173 | #[pyo3(from_py_with = "extract_gil_ref")] + | ^^^^^^^^^^^^^^^^^ + +error: use of deprecated method `pyo3::deprecations::GilRefs::>::is_python`: use `wrap_pyfunction_bound!` instead + --> tests/ui/deprecations.rs:186:13 + | +186 | let _ = wrap_pyfunction!(double, py); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: this error originates in the macro `wrap_pyfunction` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/tests/ui/invalid_argument_attributes.rs b/tests/ui/invalid_argument_attributes.rs index ed9d6ce6971..6797642d77b 100644 --- a/tests/ui/invalid_argument_attributes.rs +++ b/tests/ui/invalid_argument_attributes.rs @@ -1,43 +1,18 @@ use pyo3::prelude::*; #[pyfunction] -fn invalid_attribute(#[pyo3(get)] param: String) {} +fn invalid_attribute(#[pyo3(get)] _param: String) {} #[pyfunction] -fn from_py_with_no_value(#[pyo3(from_py_with)] param: String) {} +fn from_py_with_no_value(#[pyo3(from_py_with)] _param: String) {} #[pyfunction] -fn from_py_with_string(#[pyo3("from_py_with")] param: String) {} +fn from_py_with_string(#[pyo3("from_py_with")] _param: String) {} #[pyfunction] -fn from_py_with_value_not_a_string(#[pyo3(from_py_with = func)] param: String) {} +fn from_py_with_value_not_a_string(#[pyo3(from_py_with = func)] _param: String) {} #[pyfunction] -fn from_py_with_repeated(#[pyo3(from_py_with = "func", from_py_with = "func")] param: String) {} - -#[pyfunction] -async fn from_py_with_value_and_cancel_handle( - #[pyo3(from_py_with = "func", cancel_handle)] _param: String, -) { -} - -#[pyfunction] -async fn cancel_handle_repeated(#[pyo3(cancel_handle, cancel_handle)] _param: String) {} - -#[pyfunction] -async fn cancel_handle_repeated2( - #[pyo3(cancel_handle)] _param: String, - #[pyo3(cancel_handle)] _param2: String, -) { -} - -#[pyfunction] -fn cancel_handle_synchronous(#[pyo3(cancel_handle)] _param: String) {} - -#[pyfunction] -async fn cancel_handle_wrong_type(#[pyo3(cancel_handle)] _param: String) {} - -#[pyfunction] -async fn missing_cancel_handle_attribute(_param: pyo3::coroutine::CancelHandle) {} +fn from_py_with_repeated(#[pyo3(from_py_with = "func", from_py_with = "func")] _param: String) {} fn main() {} diff --git a/tests/ui/invalid_argument_attributes.stderr b/tests/ui/invalid_argument_attributes.stderr index 4903fbc5c3a..e6c42f82a87 100644 --- a/tests/ui/invalid_argument_attributes.stderr +++ b/tests/ui/invalid_argument_attributes.stderr @@ -1,107 +1,29 @@ error: expected `cancel_handle` or `from_py_with` --> tests/ui/invalid_argument_attributes.rs:4:29 | -4 | fn invalid_attribute(#[pyo3(get)] param: String) {} +4 | fn invalid_attribute(#[pyo3(get)] _param: String) {} | ^^^ error: expected `=` --> tests/ui/invalid_argument_attributes.rs:7:45 | -7 | fn from_py_with_no_value(#[pyo3(from_py_with)] param: String) {} +7 | fn from_py_with_no_value(#[pyo3(from_py_with)] _param: String) {} | ^ error: expected `cancel_handle` or `from_py_with` --> tests/ui/invalid_argument_attributes.rs:10:31 | -10 | fn from_py_with_string(#[pyo3("from_py_with")] param: String) {} +10 | fn from_py_with_string(#[pyo3("from_py_with")] _param: String) {} | ^^^^^^^^^^^^^^ error: expected string literal --> tests/ui/invalid_argument_attributes.rs:13:58 | -13 | fn from_py_with_value_not_a_string(#[pyo3(from_py_with = func)] param: String) {} +13 | fn from_py_with_value_not_a_string(#[pyo3(from_py_with = func)] _param: String) {} | ^^^^ error: `from_py_with` may only be specified once per argument --> tests/ui/invalid_argument_attributes.rs:16:56 | -16 | fn from_py_with_repeated(#[pyo3(from_py_with = "func", from_py_with = "func")] param: String) {} +16 | fn from_py_with_repeated(#[pyo3(from_py_with = "func", from_py_with = "func")] _param: String) {} | ^^^^^^^^^^^^ - -error: `from_py_with` and `cancel_handle` cannot be specified together - --> tests/ui/invalid_argument_attributes.rs:20:35 - | -20 | #[pyo3(from_py_with = "func", cancel_handle)] _param: String, - | ^^^^^^^^^^^^^ - -error: `cancel_handle` may only be specified once per argument - --> tests/ui/invalid_argument_attributes.rs:25:55 - | -25 | async fn cancel_handle_repeated(#[pyo3(cancel_handle, cancel_handle)] _param: String) {} - | ^^^^^^^^^^^^^ - -error: `cancel_handle` may only be specified once - --> tests/ui/invalid_argument_attributes.rs:30:28 - | -30 | #[pyo3(cancel_handle)] _param2: String, - | ^^^^^^^ - -error: `cancel_handle` attribute can only be used with `async fn` - --> tests/ui/invalid_argument_attributes.rs:35:53 - | -35 | fn cancel_handle_synchronous(#[pyo3(cancel_handle)] _param: String) {} - | ^^^^^^ - -error[E0308]: mismatched types - --> tests/ui/invalid_argument_attributes.rs:37:1 - | -37 | #[pyfunction] - | ^^^^^^^^^^^^^ - | | - | expected `String`, found `CancelHandle` - | arguments to this function are incorrect - | -note: function defined here - --> tests/ui/invalid_argument_attributes.rs:38:10 - | -38 | async fn cancel_handle_wrong_type(#[pyo3(cancel_handle)] _param: String) {} - | ^^^^^^^^^^^^^^^^^^^^^^^^ -------------- - = note: this error originates in the attribute macro `pyfunction` (in Nightly builds, run with -Z macro-backtrace for more info) - -error[E0277]: the trait bound `CancelHandle: PyClass` is not satisfied - --> tests/ui/invalid_argument_attributes.rs:41:50 - | -41 | async fn missing_cancel_handle_attribute(_param: pyo3::coroutine::CancelHandle) {} - | ^^^^ the trait `PyClass` is not implemented for `CancelHandle` - | - = help: the trait `PyClass` is implemented for `Coroutine` - = note: required for `CancelHandle` to implement `FromPyObject<'_>` - = note: required for `CancelHandle` to implement `PyFunctionArgument<'_, '_>` -note: required by a bound in `extract_argument` - --> src/impl_/extract_argument.rs - | - | pub fn extract_argument<'a, 'py, T>( - | ---------------- required by a bound in this function -... - | T: PyFunctionArgument<'a, 'py>, - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `extract_argument` - -error[E0277]: the trait bound `CancelHandle: Clone` is not satisfied - --> tests/ui/invalid_argument_attributes.rs:41:50 - | -41 | async fn missing_cancel_handle_attribute(_param: pyo3::coroutine::CancelHandle) {} - | ^^^^ the trait `Clone` is not implemented for `CancelHandle` - | - = help: the following other types implement trait `PyFunctionArgument<'a, 'py>`: - &'a Coroutine - &'a mut Coroutine - = note: required for `CancelHandle` to implement `FromPyObject<'_>` - = note: required for `CancelHandle` to implement `PyFunctionArgument<'_, '_>` -note: required by a bound in `extract_argument` - --> src/impl_/extract_argument.rs - | - | pub fn extract_argument<'a, 'py, T>( - | ---------------- required by a bound in this function -... - | T: PyFunctionArgument<'a, 'py>, - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `extract_argument` diff --git a/tests/ui/invalid_cancel_handle.rs b/tests/ui/invalid_cancel_handle.rs new file mode 100644 index 00000000000..cff6c5dcbad --- /dev/null +++ b/tests/ui/invalid_cancel_handle.rs @@ -0,0 +1,28 @@ +use pyo3::prelude::*; + +#[pyfunction] +async fn cancel_handle_repeated(#[pyo3(cancel_handle, cancel_handle)] _param: String) {} + +#[pyfunction] +async fn cancel_handle_repeated2( + #[pyo3(cancel_handle)] _param: String, + #[pyo3(cancel_handle)] _param2: String, +) { +} + +#[pyfunction] +fn cancel_handle_synchronous(#[pyo3(cancel_handle)] _param: String) {} + +#[pyfunction] +async fn cancel_handle_wrong_type(#[pyo3(cancel_handle)] _param: String) {} + +#[pyfunction] +async fn missing_cancel_handle_attribute(_param: pyo3::coroutine::CancelHandle) {} + +#[pyfunction] +async fn cancel_handle_and_from_py_with( + #[pyo3(cancel_handle, from_py_with = "cancel_handle")] _param: pyo3::coroutine::CancelHandle, +) { +} + +fn main() {} diff --git a/tests/ui/invalid_cancel_handle.stderr b/tests/ui/invalid_cancel_handle.stderr new file mode 100644 index 00000000000..f6452611679 --- /dev/null +++ b/tests/ui/invalid_cancel_handle.stderr @@ -0,0 +1,85 @@ +error: `cancel_handle` may only be specified once per argument + --> tests/ui/invalid_cancel_handle.rs:4:55 + | +4 | async fn cancel_handle_repeated(#[pyo3(cancel_handle, cancel_handle)] _param: String) {} + | ^^^^^^^^^^^^^ + +error: `cancel_handle` may only be specified once + --> tests/ui/invalid_cancel_handle.rs:9:28 + | +9 | #[pyo3(cancel_handle)] _param2: String, + | ^^^^^^^ + +error: `cancel_handle` attribute can only be used with `async fn` + --> tests/ui/invalid_cancel_handle.rs:14:53 + | +14 | fn cancel_handle_synchronous(#[pyo3(cancel_handle)] _param: String) {} + | ^^^^^^ + +error: `from_py_with` and `cancel_handle` cannot be specified together + --> tests/ui/invalid_cancel_handle.rs:24:12 + | +24 | #[pyo3(cancel_handle, from_py_with = "cancel_handle")] _param: pyo3::coroutine::CancelHandle, + | ^^^^^^^^^^^^^ + +error[E0308]: mismatched types + --> tests/ui/invalid_cancel_handle.rs:16:1 + | +16 | #[pyfunction] + | ^^^^^^^^^^^^^ + | | + | expected `String`, found `CancelHandle` + | arguments to this function are incorrect + | +note: function defined here + --> tests/ui/invalid_cancel_handle.rs:17:10 + | +17 | async fn cancel_handle_wrong_type(#[pyo3(cancel_handle)] _param: String) {} + | ^^^^^^^^^^^^^^^^^^^^^^^^ -------------- + = note: this error originates in the attribute macro `pyfunction` (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0277]: the trait bound `CancelHandle: PyFunctionArgument<'_, '_>` is not satisfied + --> tests/ui/invalid_cancel_handle.rs:20:50 + | +20 | async fn missing_cancel_handle_attribute(_param: pyo3::coroutine::CancelHandle) {} + | ^^^^ the trait `PyClass` is not implemented for `CancelHandle`, which is required by `CancelHandle: PyFunctionArgument<'_, '_>` + | + = help: the following other types implement trait `PyFunctionArgument<'a, 'py>`: + Option<&'a pyo3::Bound<'py, T>> + &'a pyo3::Bound<'py, T> + &'a pyo3::coroutine::Coroutine + &'a mut pyo3::coroutine::Coroutine + = note: required for `CancelHandle` to implement `FromPyObject<'_>` + = note: required for `CancelHandle` to implement `FromPyObjectBound<'_, '_>` + = note: required for `CancelHandle` to implement `PyFunctionArgument<'_, '_>` +note: required by a bound in `extract_argument` + --> src/impl_/extract_argument.rs + | + | pub fn extract_argument<'a, 'py, T>( + | ---------------- required by a bound in this function +... + | T: PyFunctionArgument<'a, 'py>, + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `extract_argument` + +error[E0277]: the trait bound `CancelHandle: PyFunctionArgument<'_, '_>` is not satisfied + --> tests/ui/invalid_cancel_handle.rs:20:50 + | +20 | async fn missing_cancel_handle_attribute(_param: pyo3::coroutine::CancelHandle) {} + | ^^^^ the trait `Clone` is not implemented for `CancelHandle`, which is required by `CancelHandle: PyFunctionArgument<'_, '_>` + | + = help: the following other types implement trait `PyFunctionArgument<'a, 'py>`: + Option<&'a pyo3::Bound<'py, T>> + &'a pyo3::Bound<'py, T> + &'a pyo3::coroutine::Coroutine + &'a mut pyo3::coroutine::Coroutine + = note: required for `CancelHandle` to implement `FromPyObject<'_>` + = note: required for `CancelHandle` to implement `FromPyObjectBound<'_, '_>` + = note: required for `CancelHandle` to implement `PyFunctionArgument<'_, '_>` +note: required by a bound in `extract_argument` + --> src/impl_/extract_argument.rs + | + | pub fn extract_argument<'a, 'py, T>( + | ---------------- required by a bound in this function +... + | T: PyFunctionArgument<'a, 'py>, + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `extract_argument` diff --git a/tests/ui/invalid_closure.rs b/tests/ui/invalid_closure.rs index b724f31b97e..eca988f1e57 100644 --- a/tests/ui/invalid_closure.rs +++ b/tests/ui/invalid_closure.rs @@ -6,11 +6,14 @@ fn main() { let local_data = vec![0, 1, 2, 3, 4]; let ref_: &[u8] = &local_data; - let closure_fn = |_args: &PyTuple, _kwargs: Option<&PyDict>| -> PyResult<()> { - println!("This is five: {:?}", ref_.len()); - Ok(()) - }; - PyCFunction::new_closure(py, None, None, closure_fn).unwrap().into() + let closure_fn = + |_args: &Bound<'_, PyTuple>, _kwargs: Option<&Bound<'_, PyDict>>| -> PyResult<()> { + println!("This is five: {:?}", ref_.len()); + Ok(()) + }; + PyCFunction::new_closure_bound(py, None, None, closure_fn) + .unwrap() + .into() }); Python::with_gil(|py| { diff --git a/tests/ui/invalid_closure.stderr b/tests/ui/invalid_closure.stderr index 90240e5db26..890d7640502 100644 --- a/tests/ui/invalid_closure.stderr +++ b/tests/ui/invalid_closure.stderr @@ -6,7 +6,8 @@ error[E0597]: `local_data` does not live long enough 7 | let ref_: &[u8] = &local_data; | ^^^^^^^^^^^ borrowed value does not live long enough ... -13 | PyCFunction::new_closure(py, None, None, closure_fn).unwrap().into() - | ---------------------------------------------------- argument requires that `local_data` is borrowed for `'static` -14 | }); +14 | PyCFunction::new_closure_bound(py, None, None, closure_fn) + | ---------------------------------------------------------- argument requires that `local_data` is borrowed for `'static` +... +17 | }); | - `local_data` dropped here while still borrowed diff --git a/tests/ui/invalid_frozen_pyclass_borrow.rs b/tests/ui/invalid_frozen_pyclass_borrow.rs index 1f18eab6170..6379a8707c5 100644 --- a/tests/ui/invalid_frozen_pyclass_borrow.rs +++ b/tests/ui/invalid_frozen_pyclass_borrow.rs @@ -12,7 +12,7 @@ impl Foo { } fn borrow_mut_fails(foo: Py, py: Python) { - let borrow = foo.as_ref(py).borrow_mut(); + let borrow = foo.bind(py).borrow_mut(); } #[pyclass(subclass)] @@ -22,14 +22,14 @@ struct MutableBase; struct ImmutableChild; fn borrow_mut_of_child_fails(child: Py, py: Python) { - let borrow = child.as_ref(py).borrow_mut(); + let borrow = child.bind(py).borrow_mut(); } fn py_get_of_mutable_class_fails(class: Py) { class.get(); } -fn pyclass_get_of_mutable_class_fails(class: &PyCell) { +fn pyclass_get_of_mutable_class_fails(class: &Bound<'_, MutableBase>) { class.get(); } diff --git a/tests/ui/invalid_frozen_pyclass_borrow.stderr b/tests/ui/invalid_frozen_pyclass_borrow.stderr index 5e09d512ae7..52a0623f282 100644 --- a/tests/ui/invalid_frozen_pyclass_borrow.stderr +++ b/tests/ui/invalid_frozen_pyclass_borrow.stderr @@ -17,34 +17,47 @@ note: required by a bound in `extract_pyclass_ref_mut` | ^^^^^^^^^^^^^^ required by this bound in `extract_pyclass_ref_mut` error[E0271]: type mismatch resolving `::Frozen == False` - --> tests/ui/invalid_frozen_pyclass_borrow.rs:15:33 + --> tests/ui/invalid_frozen_pyclass_borrow.rs:9:1 + | +9 | #[pymethods] + | ^^^^^^^^^^^^ expected `False`, found `True` + | +note: required by a bound in `PyRefMut` + --> src/pycell.rs + | + | pub struct PyRefMut<'p, T: PyClass> { + | ^^^^^^^^^^^^^^ required by this bound in `PyRefMut` + = note: this error originates in the attribute macro `pymethods` (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0271]: type mismatch resolving `::Frozen == False` + --> tests/ui/invalid_frozen_pyclass_borrow.rs:15:31 | -15 | let borrow = foo.as_ref(py).borrow_mut(); - | ^^^^^^^^^^ expected `False`, found `True` +15 | let borrow = foo.bind(py).borrow_mut(); + | ^^^^^^^^^^ expected `False`, found `True` | -note: required by a bound in `pyo3::PyCell::::borrow_mut` - --> src/pycell.rs +note: required by a bound in `pyo3::Bound::<'py, T>::borrow_mut` + --> src/instance.rs | - | pub fn borrow_mut(&self) -> PyRefMut<'_, T> + | pub fn borrow_mut(&self) -> PyRefMut<'py, T> | ---------- required by a bound in this associated function | where | T: PyClass, - | ^^^^^^^^^^^^^^ required by this bound in `PyCell::::borrow_mut` + | ^^^^^^^^^^^^^^ required by this bound in `Bound::<'py, T>::borrow_mut` error[E0271]: type mismatch resolving `::Frozen == False` - --> tests/ui/invalid_frozen_pyclass_borrow.rs:25:35 + --> tests/ui/invalid_frozen_pyclass_borrow.rs:25:33 | -25 | let borrow = child.as_ref(py).borrow_mut(); - | ^^^^^^^^^^ expected `False`, found `True` +25 | let borrow = child.bind(py).borrow_mut(); + | ^^^^^^^^^^ expected `False`, found `True` | -note: required by a bound in `pyo3::PyCell::::borrow_mut` - --> src/pycell.rs +note: required by a bound in `pyo3::Bound::<'py, T>::borrow_mut` + --> src/instance.rs | - | pub fn borrow_mut(&self) -> PyRefMut<'_, T> + | pub fn borrow_mut(&self) -> PyRefMut<'py, T> | ---------- required by a bound in this associated function | where | T: PyClass, - | ^^^^^^^^^^^^^^ required by this bound in `PyCell::::borrow_mut` + | ^^^^^^^^^^^^^^ required by this bound in `Bound::<'py, T>::borrow_mut` error[E0271]: type mismatch resolving `::Frozen == True` --> tests/ui/invalid_frozen_pyclass_borrow.rs:29:11 @@ -67,11 +80,11 @@ error[E0271]: type mismatch resolving `::Frozen == True` 33 | class.get(); | ^^^ expected `True`, found `False` | -note: required by a bound in `pyo3::PyCell::::get` - --> src/pycell.rs +note: required by a bound in `pyo3::Bound::<'py, T>::get` + --> src/instance.rs | | pub fn get(&self) -> &T | --- required by a bound in this associated function | where | T: PyClass + Sync, - | ^^^^^^^^^^^^^ required by this bound in `PyCell::::get` + | ^^^^^^^^^^^^^ required by this bound in `Bound::<'py, T>::get` diff --git a/tests/ui/invalid_intern_arg.rs b/tests/ui/invalid_intern_arg.rs index fa9e1e59f0c..eb479431b90 100644 --- a/tests/ui/invalid_intern_arg.rs +++ b/tests/ui/invalid_intern_arg.rs @@ -1,6 +1,6 @@ use pyo3::Python; fn main() { - let foo = if true { "foo" } else { "bar" }; - Python::with_gil(|py| py.import(pyo3::intern!(py, foo)).unwrap()); + let _foo = if true { "foo" } else { "bar" }; + Python::with_gil(|py| py.import_bound(pyo3::intern!(py, _foo)).unwrap()); } diff --git a/tests/ui/invalid_intern_arg.stderr b/tests/ui/invalid_intern_arg.stderr index bb84d00e15b..7d1aad1ae28 100644 --- a/tests/ui/invalid_intern_arg.stderr +++ b/tests/ui/invalid_intern_arg.stderr @@ -1,8 +1,17 @@ error[E0435]: attempt to use a non-constant value in a constant - --> tests/ui/invalid_intern_arg.rs:5:55 + --> tests/ui/invalid_intern_arg.rs:5:61 | -5 | Python::with_gil(|py| py.import(pyo3::intern!(py, foo)).unwrap()); - | ------------------^^^- - | | | - | | non-constant value - | help: consider using `let` instead of `static`: `let INTERNED` +5 | Python::with_gil(|py| py.import_bound(pyo3::intern!(py, _foo)).unwrap()); + | ------------------^^^^- + | | | + | | non-constant value + | help: consider using `let` instead of `static`: `let INTERNED` + +error: lifetime may not live long enough + --> tests/ui/invalid_intern_arg.rs:5:27 + | +5 | Python::with_gil(|py| py.import_bound(pyo3::intern!(py, _foo)).unwrap()); + | --- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ returning this value requires that `'1` must outlive `'2` + | | | + | | return type of closure is pyo3::Bound<'2, PyModule> + | has type `Python<'1>` diff --git a/tests/ui/invalid_property_args.rs b/tests/ui/invalid_property_args.rs index 3f335952235..b5eba27eb60 100644 --- a/tests/ui/invalid_property_args.rs +++ b/tests/ui/invalid_property_args.rs @@ -6,7 +6,7 @@ struct ClassWithGetter {} #[pymethods] impl ClassWithGetter { #[getter] - fn getter_with_arg(&self, py: Python<'_>, index: u32) {} + fn getter_with_arg(&self, _py: Python<'_>, _index: u32) {} } #[pyclass] @@ -15,13 +15,13 @@ struct ClassWithSetter {} #[pymethods] impl ClassWithSetter { #[setter] - fn setter_with_no_arg(&mut self, py: Python<'_>) {} + fn setter_with_no_arg(&mut self, _py: Python<'_>) {} } #[pymethods] impl ClassWithSetter { #[setter] - fn setter_with_too_many_args(&mut self, py: Python<'_>, foo: u32, bar: u32) {} + fn setter_with_too_many_args(&mut self, _py: Python<'_>, _foo: u32, _bar: u32) {} } #[pyclass] diff --git a/tests/ui/invalid_property_args.stderr b/tests/ui/invalid_property_args.stderr index a41b6c79b3a..dea2e3fb2b4 100644 --- a/tests/ui/invalid_property_args.stderr +++ b/tests/ui/invalid_property_args.stderr @@ -1,20 +1,20 @@ error: getter function can only have one argument (of type pyo3::Python) - --> tests/ui/invalid_property_args.rs:9:54 + --> tests/ui/invalid_property_args.rs:9:56 | -9 | fn getter_with_arg(&self, py: Python<'_>, index: u32) {} - | ^^^ +9 | fn getter_with_arg(&self, _py: Python<'_>, _index: u32) {} + | ^^^ error: setter function expected to have one argument --> tests/ui/invalid_property_args.rs:18:8 | -18 | fn setter_with_no_arg(&mut self, py: Python<'_>) {} +18 | fn setter_with_no_arg(&mut self, _py: Python<'_>) {} | ^^^^^^^^^^^^^^^^^^ error: setter function can have at most two arguments ([pyo3::Python,] and value) - --> tests/ui/invalid_property_args.rs:24:76 + --> tests/ui/invalid_property_args.rs:24:79 | -24 | fn setter_with_too_many_args(&mut self, py: Python<'_>, foo: u32, bar: u32) {} - | ^^^ +24 | fn setter_with_too_many_args(&mut self, _py: Python<'_>, _foo: u32, _bar: u32) {} + | ^^^ error: `get` and `set` with tuple struct fields require `name` --> tests/ui/invalid_property_args.rs:28:50 diff --git a/tests/ui/invalid_proto_pymethods.rs b/tests/ui/invalid_proto_pymethods.rs index d370c4fddb5..c40790c3168 100644 --- a/tests/ui/invalid_proto_pymethods.rs +++ b/tests/ui/invalid_proto_pymethods.rs @@ -54,11 +54,11 @@ struct EqAndRichcmp; #[pymethods] impl EqAndRichcmp { - fn __eq__(&self, other: &Self) -> bool { + fn __eq__(&self, _other: &Self) -> bool { true } - fn __richcmp__(&self, other: &Self, op: CompareOp) -> bool { + fn __richcmp__(&self, _other: &Self, _op: CompareOp) -> bool { true } } diff --git a/tests/ui/invalid_proto_pymethods.stderr b/tests/ui/invalid_proto_pymethods.stderr index 275a6b93c46..82c99c2ddc3 100644 --- a/tests/ui/invalid_proto_pymethods.stderr +++ b/tests/ui/invalid_proto_pymethods.stderr @@ -22,6 +22,24 @@ error: `text_signature` cannot be used with magic method `__bool__` 46 | #[pyo3(name = "__bool__", text_signature = "")] | ^^^^^^^^^^^^^^ +error[E0034]: multiple applicable items in scope + --> tests/ui/invalid_proto_pymethods.rs:55:1 + | +55 | #[pymethods] + | ^^^^^^^^^^^^ multiple `__pymethod___richcmp____` found + | +note: candidate #1 is defined in an impl for the type `EqAndRichcmp` + --> tests/ui/invalid_proto_pymethods.rs:55:1 + | +55 | #[pymethods] + | ^^^^^^^^^^^^ +note: candidate #2 is defined in an impl for the type `EqAndRichcmp` + --> tests/ui/invalid_proto_pymethods.rs:55:1 + | +55 | #[pymethods] + | ^^^^^^^^^^^^ + = note: this error originates in the macro `::pyo3::impl_::pyclass::generate_pyclass_richcompare_slot` which comes from the expansion of the attribute macro `pymethods` (in Nightly builds, run with -Z macro-backtrace for more info) + error[E0592]: duplicate definitions with name `__pymethod___richcmp____` --> tests/ui/invalid_proto_pymethods.rs:55:1 | @@ -31,4 +49,22 @@ error[E0592]: duplicate definitions with name `__pymethod___richcmp____` | duplicate definitions for `__pymethod___richcmp____` | other definition for `__pymethod___richcmp____` | - = note: this error originates in the macro `_pyo3::impl_::pyclass::generate_pyclass_richcompare_slot` which comes from the expansion of the attribute macro `pymethods` (in Nightly builds, run with -Z macro-backtrace for more info) + = note: this error originates in the macro `::pyo3::impl_::pyclass::generate_pyclass_richcompare_slot` which comes from the expansion of the attribute macro `pymethods` (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0034]: multiple applicable items in scope + --> tests/ui/invalid_proto_pymethods.rs:55:1 + | +55 | #[pymethods] + | ^^^^^^^^^^^^ multiple `__pymethod___richcmp____` found + | +note: candidate #1 is defined in an impl for the type `EqAndRichcmp` + --> tests/ui/invalid_proto_pymethods.rs:55:1 + | +55 | #[pymethods] + | ^^^^^^^^^^^^ +note: candidate #2 is defined in an impl for the type `EqAndRichcmp` + --> tests/ui/invalid_proto_pymethods.rs:55:1 + | +55 | #[pymethods] + | ^^^^^^^^^^^^ + = note: this error originates in the attribute macro `pymethods` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/tests/ui/invalid_pyclass_enum.rs b/tests/ui/invalid_pyclass_enum.rs index 4bc53238a2c..e98010fea32 100644 --- a/tests/ui/invalid_pyclass_enum.rs +++ b/tests/ui/invalid_pyclass_enum.rs @@ -15,4 +15,17 @@ enum NotDrivedClass { #[pyclass] enum NoEmptyEnum {} +#[pyclass] +enum NoUnitVariants { + StructVariant { field: i32 }, + UnitVariant, +} + +#[pyclass] +enum SimpleNoSignature { + #[pyo3(constructor = (a, b))] + A, + B, +} + fn main() {} diff --git a/tests/ui/invalid_pyclass_enum.stderr b/tests/ui/invalid_pyclass_enum.stderr index 8f340a762bb..7e3b6ffa425 100644 --- a/tests/ui/invalid_pyclass_enum.stderr +++ b/tests/ui/invalid_pyclass_enum.stderr @@ -15,3 +15,17 @@ error: #[pyclass] can't be used on enums without any variants | 16 | enum NoEmptyEnum {} | ^^ + +error: Unit variant `UnitVariant` is not yet supported in a complex enum + = help: change to an empty tuple variant instead: `UnitVariant()` + = note: the enum is complex because of non-unit variant `StructVariant` + --> tests/ui/invalid_pyclass_enum.rs:21:5 + | +21 | UnitVariant, + | ^^^^^^^^^^^ + +error: `constructor` can't be used on a simple enum variant + --> tests/ui/invalid_pyclass_enum.rs:26:12 + | +26 | #[pyo3(constructor = (a, b))] + | ^^^^^^^^^^^ diff --git a/tests/ui/invalid_pyfunction_signatures.rs b/tests/ui/invalid_pyfunction_signatures.rs index f5a9bee4e6c..86033aa12ea 100644 --- a/tests/ui/invalid_pyfunction_signatures.rs +++ b/tests/ui/invalid_pyfunction_signatures.rs @@ -35,6 +35,7 @@ fn function_with_args_sep_after_args_sep() {} #[pyo3(signature = (**kwargs, *args))] fn function_with_args_after_kwargs(kwargs: Option<&PyDict>, args: &PyTuple) { let _ = args; + let _ = kwargs; } #[pyfunction] diff --git a/tests/ui/invalid_pyfunction_signatures.stderr b/tests/ui/invalid_pyfunction_signatures.stderr index dbca169d8ea..97d0fd3b4af 100644 --- a/tests/ui/invalid_pyfunction_signatures.stderr +++ b/tests/ui/invalid_pyfunction_signatures.stderr @@ -41,19 +41,19 @@ error: `*args` not allowed after `**kwargs` | ^ error: `**kwargs_b` not allowed after `**kwargs_a` - --> tests/ui/invalid_pyfunction_signatures.rs:41:33 + --> tests/ui/invalid_pyfunction_signatures.rs:42:33 | -41 | #[pyo3(signature = (**kwargs_a, **kwargs_b))] +42 | #[pyo3(signature = (**kwargs_a, **kwargs_b))] | ^ error: arguments of type `Python` must not be part of the signature - --> tests/ui/invalid_pyfunction_signatures.rs:47:27 + --> tests/ui/invalid_pyfunction_signatures.rs:48:27 | -47 | #[pyfunction(signature = (py))] +48 | #[pyfunction(signature = (py))] | ^^ error: cannot find attribute `args` in this scope - --> tests/ui/invalid_pyfunction_signatures.rs:57:7 + --> tests/ui/invalid_pyfunction_signatures.rs:58:7 | -57 | #[args(x)] +58 | #[args(x)] | ^^^^ diff --git a/tests/ui/invalid_pyfunctions.rs b/tests/ui/invalid_pyfunctions.rs index fba7d8b5016..1c0c45d6b95 100644 --- a/tests/ui/invalid_pyfunctions.rs +++ b/tests/ui/invalid_pyfunctions.rs @@ -1,25 +1,41 @@ use pyo3::prelude::*; +use pyo3::types::{PyDict, PyString, PyTuple}; #[pyfunction] -fn generic_function(value: T) {} +fn generic_function(_value: T) {} #[pyfunction] -fn impl_trait_function(impl_trait: impl AsRef) {} +fn impl_trait_function(_impl_trait: impl AsRef) {} #[pyfunction] fn wildcard_argument(_: i32) {} #[pyfunction] -fn destructured_argument((a, b): (i32, i32)) {} +fn destructured_argument((_a, _b): (i32, i32)) {} #[pyfunction] fn function_with_required_after_option(_opt: Option, _x: i32) {} +#[pyfunction] +#[pyo3(signature=(*args))] +fn function_with_optional_args(args: Option>) { + let _ = args; +} + +#[pyfunction] +#[pyo3(signature=(**kwargs))] +fn function_with_required_kwargs(kwargs: Bound<'_, PyDict>) { + let _ = kwargs; +} + #[pyfunction(pass_module)] fn pass_module_but_no_arguments<'py>() {} #[pyfunction(pass_module)] -fn first_argument_not_module<'py>(string: &str, module: &'py PyModule) -> PyResult<&'py str> { +fn first_argument_not_module<'a, 'py>( + _string: &str, + module: &'a Bound<'py, PyModule>, +) -> PyResult> { module.name() } diff --git a/tests/ui/invalid_pyfunctions.stderr b/tests/ui/invalid_pyfunctions.stderr index a3fd845d580..830f17ee877 100644 --- a/tests/ui/invalid_pyfunctions.stderr +++ b/tests/ui/invalid_pyfunctions.stderr @@ -1,45 +1,57 @@ error: Python functions cannot have generic type parameters - --> tests/ui/invalid_pyfunctions.rs:4:21 + --> tests/ui/invalid_pyfunctions.rs:5:21 | -4 | fn generic_function(value: T) {} +5 | fn generic_function(_value: T) {} | ^ error: Python functions cannot have `impl Trait` arguments - --> tests/ui/invalid_pyfunctions.rs:7:36 + --> tests/ui/invalid_pyfunctions.rs:8:37 | -7 | fn impl_trait_function(impl_trait: impl AsRef) {} - | ^^^^ +8 | fn impl_trait_function(_impl_trait: impl AsRef) {} + | ^^^^ error: wildcard argument names are not supported - --> tests/ui/invalid_pyfunctions.rs:10:22 + --> tests/ui/invalid_pyfunctions.rs:11:22 | -10 | fn wildcard_argument(_: i32) {} +11 | fn wildcard_argument(_: i32) {} | ^ error: destructuring in arguments is not supported - --> tests/ui/invalid_pyfunctions.rs:13:26 + --> tests/ui/invalid_pyfunctions.rs:14:26 | -13 | fn destructured_argument((a, b): (i32, i32)) {} - | ^^^^^^ +14 | fn destructured_argument((_a, _b): (i32, i32)) {} + | ^^^^^^^^ error: required arguments after an `Option<_>` argument are ambiguous = help: add a `#[pyo3(signature)]` annotation on this function to unambiguously specify the default values for all optional parameters - --> tests/ui/invalid_pyfunctions.rs:16:63 + --> tests/ui/invalid_pyfunctions.rs:17:63 | -16 | fn function_with_required_after_option(_opt: Option, _x: i32) {} +17 | fn function_with_required_after_option(_opt: Option, _x: i32) {} | ^^^ +error: args cannot be optional + --> tests/ui/invalid_pyfunctions.rs:21:32 + | +21 | fn function_with_optional_args(args: Option>) { + | ^^^^ + +error: kwargs must be Option<_> + --> tests/ui/invalid_pyfunctions.rs:27:34 + | +27 | fn function_with_required_kwargs(kwargs: Bound<'_, PyDict>) { + | ^^^^^^ + error: expected `&PyModule` or `Py` as first argument with `pass_module` - --> tests/ui/invalid_pyfunctions.rs:19:37 + --> tests/ui/invalid_pyfunctions.rs:32:37 | -19 | fn pass_module_but_no_arguments<'py>() {} +32 | fn pass_module_but_no_arguments<'py>() {} | ^^ -error[E0277]: the trait bound `&str: From<&pyo3::prelude::PyModule>` is not satisfied - --> tests/ui/invalid_pyfunctions.rs:22:43 +error[E0277]: the trait bound `&str: From>` is not satisfied + --> tests/ui/invalid_pyfunctions.rs:36:14 | -22 | fn first_argument_not_module<'py>(string: &str, module: &'py PyModule) -> PyResult<&'py str> { - | ^ the trait `From<&pyo3::prelude::PyModule>` is not implemented for `&str` +36 | _string: &str, + | ^ the trait `From>` is not implemented for `&str`, which is required by `BoundRef<'_, '_, pyo3::prelude::PyModule>: Into<_>` | = help: the following other types implement trait `From`: > @@ -48,4 +60,4 @@ error[E0277]: the trait bound `&str: From<&pyo3::prelude::PyModule>` is not sati > > > - = note: required for `&pyo3::prelude::PyModule` to implement `Into<&str>` + = note: required for `BoundRef<'_, '_, pyo3::prelude::PyModule>` to implement `Into<&str>` diff --git a/tests/ui/invalid_pymethod_enum.rs b/tests/ui/invalid_pymethod_enum.rs new file mode 100644 index 00000000000..5c41d19d4e7 --- /dev/null +++ b/tests/ui/invalid_pymethod_enum.rs @@ -0,0 +1,35 @@ +use pyo3::prelude::*; + +#[pyclass] +enum ComplexEnum { + Int { int: i32 }, + Str { string: String }, +} + +#[pymethods] +impl ComplexEnum { + fn mutate_in_place(&mut self) { + *self = match self { + ComplexEnum::Int { int } => ComplexEnum::Str { string: int.to_string() }, + ComplexEnum::Str { string } => ComplexEnum::Int { int: string.len() as i32 }, + } + } +} + +#[pyclass] +enum TupleEnum { + Int(i32), + Str(String), +} + +#[pymethods] +impl TupleEnum { + fn mutate_in_place(&mut self) { + *self = match self { + TupleEnum::Int(int) => TupleEnum::Str(int.to_string()), + TupleEnum::Str(string) => TupleEnum::Int(string.len() as i32), + } + } +} + +fn main() {} diff --git a/tests/ui/invalid_pymethod_enum.stderr b/tests/ui/invalid_pymethod_enum.stderr new file mode 100644 index 00000000000..bc377d2a055 --- /dev/null +++ b/tests/ui/invalid_pymethod_enum.stderr @@ -0,0 +1,49 @@ +error[E0271]: type mismatch resolving `::Frozen == False` + --> tests/ui/invalid_pymethod_enum.rs:11:24 + | +11 | fn mutate_in_place(&mut self) { + | ^ expected `False`, found `True` + | +note: required by a bound in `extract_pyclass_ref_mut` + --> src/impl_/extract_argument.rs + | + | pub fn extract_pyclass_ref_mut<'a, 'py: 'a, T: PyClass>( + | ^^^^^^^^^^^^^^ required by this bound in `extract_pyclass_ref_mut` + +error[E0271]: type mismatch resolving `::Frozen == False` + --> tests/ui/invalid_pymethod_enum.rs:9:1 + | +9 | #[pymethods] + | ^^^^^^^^^^^^ expected `False`, found `True` + | +note: required by a bound in `PyRefMut` + --> src/pycell.rs + | + | pub struct PyRefMut<'p, T: PyClass> { + | ^^^^^^^^^^^^^^ required by this bound in `PyRefMut` + = note: this error originates in the attribute macro `pymethods` (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0271]: type mismatch resolving `::Frozen == False` + --> tests/ui/invalid_pymethod_enum.rs:27:24 + | +27 | fn mutate_in_place(&mut self) { + | ^ expected `False`, found `True` + | +note: required by a bound in `extract_pyclass_ref_mut` + --> src/impl_/extract_argument.rs + | + | pub fn extract_pyclass_ref_mut<'a, 'py: 'a, T: PyClass>( + | ^^^^^^^^^^^^^^ required by this bound in `extract_pyclass_ref_mut` + +error[E0271]: type mismatch resolving `::Frozen == False` + --> tests/ui/invalid_pymethod_enum.rs:25:1 + | +25 | #[pymethods] + | ^^^^^^^^^^^^ expected `False`, found `True` + | +note: required by a bound in `PyRefMut` + --> src/pycell.rs + | + | pub struct PyRefMut<'p, T: PyClass> { + | ^^^^^^^^^^^^^^ required by this bound in `PyRefMut` + = note: this error originates in the attribute macro `pymethods` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/tests/ui/invalid_pymethod_receiver.rs b/tests/ui/invalid_pymethod_receiver.rs index 77832e12857..4949ff96022 100644 --- a/tests/ui/invalid_pymethod_receiver.rs +++ b/tests/ui/invalid_pymethod_receiver.rs @@ -5,7 +5,7 @@ struct MyClass {} #[pymethods] impl MyClass { - fn method_with_invalid_self_type(slf: i32, py: Python<'_>, index: u32) {} + fn method_with_invalid_self_type(_slf: i32, _py: Python<'_>, _index: u32) {} } fn main() {} diff --git a/tests/ui/invalid_pymethod_receiver.stderr b/tests/ui/invalid_pymethod_receiver.stderr index b7a7880dbde..2c8ec045819 100644 --- a/tests/ui/invalid_pymethod_receiver.stderr +++ b/tests/ui/invalid_pymethod_receiver.stderr @@ -1,8 +1,8 @@ -error[E0277]: the trait bound `i32: From<&PyCell>` is not satisfied - --> tests/ui/invalid_pymethod_receiver.rs:8:43 +error[E0277]: the trait bound `i32: TryFrom>` is not satisfied + --> tests/ui/invalid_pymethod_receiver.rs:8:44 | -8 | fn method_with_invalid_self_type(slf: i32, py: Python<'_>, index: u32) {} - | ^^^ the trait `From<&PyCell>` is not implemented for `i32` +8 | fn method_with_invalid_self_type(_slf: i32, _py: Python<'_>, _index: u32) {} + | ^^^ the trait `From>` is not implemented for `i32`, which is required by `i32: TryFrom>` | = help: the following other types implement trait `From`: > @@ -10,6 +10,5 @@ error[E0277]: the trait bound `i32: From<&PyCell>` is not satisfied > > > - > - = note: required for `&PyCell` to implement `Into` - = note: required for `i32` to implement `TryFrom<&PyCell>` + = note: required for `BoundRef<'_, '_, MyClass>` to implement `Into` + = note: required for `i32` to implement `TryFrom>` diff --git a/tests/ui/invalid_pymethods.rs b/tests/ui/invalid_pymethods.rs index 00bddfe2fad..41786cd7895 100644 --- a/tests/ui/invalid_pymethods.rs +++ b/tests/ui/invalid_pymethods.rs @@ -6,7 +6,7 @@ struct MyClass {} #[pymethods] impl MyClass { #[classattr] - fn class_attr_with_args(foo: i32) {} + fn class_attr_with_args(_foo: i32) {} } #[pymethods] @@ -164,23 +164,23 @@ impl MyClass { #[pymethods] impl MyClass { - fn generic_method(value: T) {} + fn generic_method(_value: T) {} } #[pymethods] impl MyClass { - fn impl_trait_method_first_arg(impl_trait: impl AsRef) {} + fn impl_trait_method_first_arg(_impl_trait: impl AsRef) {} } #[pymethods] impl MyClass { - fn impl_trait_method_second_arg(&self, impl_trait: impl AsRef) {} + fn impl_trait_method_second_arg(&self, _impl_trait: impl AsRef) {} } #[pymethods] impl MyClass { #[pyo3(pass_module)] - fn method_cannot_pass_module(&self, m: &PyModule) {} + fn method_cannot_pass_module(&self, _m: &PyModule) {} } #[pymethods] diff --git a/tests/ui/invalid_pymethods.stderr b/tests/ui/invalid_pymethods.stderr index 1a50c4da0c1..9b090e31adc 100644 --- a/tests/ui/invalid_pymethods.stderr +++ b/tests/ui/invalid_pymethods.stderr @@ -1,8 +1,8 @@ error: #[classattr] can only have one argument (of type pyo3::Python) - --> tests/ui/invalid_pymethods.rs:9:34 + --> tests/ui/invalid_pymethods.rs:9:35 | -9 | fn class_attr_with_args(foo: i32) {} - | ^^^ +9 | fn class_attr_with_args(_foo: i32) {} + | ^^^ error: `#[classattr]` does not take any arguments --> tests/ui/invalid_pymethods.rs:14:5 @@ -22,13 +22,13 @@ error: unexpected receiver 26 | fn staticmethod_with_receiver(&self) {} | ^ -error: Expected `&PyType` or `Py` as the first argument to `#[classmethod]` +error: Expected `&Bound` or `Py` as the first argument to `#[classmethod]` --> tests/ui/invalid_pymethods.rs:32:33 | 32 | fn classmethod_with_receiver(&self) {} | ^^^^^^^ -error: Expected `&PyType` or `Py` as the first argument to `#[classmethod]` +error: Expected `&Bound` or `Py` as the first argument to `#[classmethod]` --> tests/ui/invalid_pymethods.rs:38:36 | 38 | fn classmethod_missing_argument() -> Self { @@ -144,20 +144,20 @@ error: `#[classattr]` does not take any arguments error: Python functions cannot have generic type parameters --> tests/ui/invalid_pymethods.rs:167:23 | -167 | fn generic_method(value: T) {} +167 | fn generic_method(_value: T) {} | ^ error: Python functions cannot have `impl Trait` arguments - --> tests/ui/invalid_pymethods.rs:172:48 + --> tests/ui/invalid_pymethods.rs:172:49 | -172 | fn impl_trait_method_first_arg(impl_trait: impl AsRef) {} - | ^^^^ +172 | fn impl_trait_method_first_arg(_impl_trait: impl AsRef) {} + | ^^^^ error: Python functions cannot have `impl Trait` arguments - --> tests/ui/invalid_pymethods.rs:177:56 + --> tests/ui/invalid_pymethods.rs:177:57 | -177 | fn impl_trait_method_second_arg(&self, impl_trait: impl AsRef) {} - | ^^^^ +177 | fn impl_trait_method_second_arg(&self, _impl_trait: impl AsRef) {} + | ^^^^ error: `pass_module` cannot be used on Python methods --> tests/ui/invalid_pymethods.rs:182:12 @@ -179,11 +179,11 @@ error: macros cannot be used as items in `#[pymethods]` impl blocks 197 | macro_invocation!(); | ^^^^^^^^^^^^^^^^ -error[E0277]: the trait bound `i32: From<&PyType>` is not satisfied +error[E0277]: the trait bound `i32: From>` is not satisfied --> tests/ui/invalid_pymethods.rs:46:45 | 46 | fn classmethod_wrong_first_argument(_x: i32) -> Self { - | ^^^ the trait `From<&PyType>` is not implemented for `i32` + | ^^^ the trait `From>` is not implemented for `i32`, which is required by `BoundRef<'_, '_, PyType>: Into<_>` | = help: the following other types implement trait `From`: > @@ -191,5 +191,4 @@ error[E0277]: the trait bound `i32: From<&PyType>` is not satisfied > > > - > - = note: required for `&PyType` to implement `Into` + = note: required for `BoundRef<'_, '_, PyType>` to implement `Into` diff --git a/tests/ui/invalid_pymethods_duplicates.stderr b/tests/ui/invalid_pymethods_duplicates.stderr index 38bb6f8655b..db301336e4f 100644 --- a/tests/ui/invalid_pymethods_duplicates.stderr +++ b/tests/ui/invalid_pymethods_duplicates.stderr @@ -9,6 +9,23 @@ error[E0119]: conflicting implementations of trait `pyo3::impl_::pyclass::PyClas | = note: this error originates in the attribute macro `pymethods` (in Nightly builds, run with -Z macro-backtrace for more info) +error[E0277]: the trait bound `TwoNew: PyTypeInfo` is not satisfied + --> tests/ui/invalid_pymethods_duplicates.rs:9:6 + | +9 | impl TwoNew { + | ^^^^^^ the trait `PyTypeInfo` is not implemented for `TwoNew` + | + = help: the following other types implement trait `PyTypeInfo`: + PyAny + PyBool + PyByteArray + PyBytes + PyCapsule + PyCode + PyComplex + PyDate + and $N others + error[E0592]: duplicate definitions with name `__pymethod___new____` --> tests/ui/invalid_pymethods_duplicates.rs:8:1 | @@ -30,3 +47,71 @@ error[E0592]: duplicate definitions with name `__pymethod_func__` | other definition for `__pymethod_func__` | = note: this error originates in the attribute macro `pymethods` (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0277]: the trait bound `TwoNew: PyClass` is not satisfied + --> tests/ui/invalid_pymethods_duplicates.rs:8:1 + | +8 | #[pymethods] + | ^^^^^^^^^^^^ the trait `PyClass` is not implemented for `TwoNew` + | + = help: the trait `PyClass` is implemented for `pyo3::coroutine::Coroutine` +note: required by a bound in `PyClassInitializer` + --> src/pyclass_init.rs + | + | pub struct PyClassInitializer(PyClassInitializerImpl); + | ^^^^^^^ required by this bound in `PyClassInitializer` + = note: this error originates in the attribute macro `pymethods` (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0599]: no method named `convert` found for struct `TwoNew` in the current scope + --> tests/ui/invalid_pymethods_duplicates.rs:8:1 + | +6 | struct TwoNew {} + | ------------- method `convert` not found for this struct +7 | +8 | #[pymethods] + | ^^^^^^^^^^^^ method not found in `TwoNew` + | + = help: items from traits can only be used if the trait is implemented and in scope + = note: the following trait defines an item `convert`, perhaps you need to implement it: + candidate #1: `IntoPyCallbackOutput` + = note: this error originates in the attribute macro `pymethods` (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0277]: the trait bound `DuplicateMethod: PyClass` is not satisfied + --> tests/ui/invalid_pymethods_duplicates.rs:26:15 + | +26 | fn func_a(&self) {} + | ^ the trait `PyClass` is not implemented for `DuplicateMethod` + | + = help: the trait `PyClass` is implemented for `pyo3::coroutine::Coroutine` +note: required by a bound in `extract_pyclass_ref` + --> src/impl_/extract_argument.rs + | + | pub fn extract_pyclass_ref<'a, 'py: 'a, T: PyClass>( + | ^^^^^^^ required by this bound in `extract_pyclass_ref` + +error[E0277]: the trait bound `DuplicateMethod: PyClass` is not satisfied + --> tests/ui/invalid_pymethods_duplicates.rs:23:1 + | +23 | #[pymethods] + | ^^^^^^^^^^^^ the trait `PyClass` is not implemented for `DuplicateMethod` + | + = help: the trait `PyClass` is implemented for `pyo3::coroutine::Coroutine` +note: required by a bound in `PyRef` + --> src/pycell.rs + | + | pub struct PyRef<'p, T: PyClass> { + | ^^^^^^^ required by this bound in `PyRef` + = note: this error originates in the attribute macro `pymethods` (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0277]: the trait bound `DuplicateMethod: PyClass` is not satisfied + --> tests/ui/invalid_pymethods_duplicates.rs:29:15 + | +29 | fn func_b(&self) {} + | ^ the trait `PyClass` is not implemented for `DuplicateMethod` + | + = help: the trait `PyClass` is implemented for `pyo3::coroutine::Coroutine` +note: required by a bound in `extract_pyclass_ref` + --> src/impl_/extract_argument.rs + | + | pub fn extract_pyclass_ref<'a, 'py: 'a, T: PyClass>( + | ^^^^^^^ required by this bound in `extract_pyclass_ref` diff --git a/tests/ui/invalid_pymodule_args.rs b/tests/ui/invalid_pymodule_args.rs index ebd229eef2e..37e53960fd3 100644 --- a/tests/ui/invalid_pymodule_args.rs +++ b/tests/ui/invalid_pymodule_args.rs @@ -1,7 +1,7 @@ use pyo3::prelude::*; #[pymodule(some_arg)] -fn module(_py: Python<'_>, m: &PyModule) -> PyResult<()> { +fn module(m: &Bound<'_, PyModule>) -> PyResult<()> { Ok(()) } diff --git a/tests/ui/invalid_pymodule_glob.rs b/tests/ui/invalid_pymodule_glob.rs new file mode 100644 index 00000000000..107cdf9382a --- /dev/null +++ b/tests/ui/invalid_pymodule_glob.rs @@ -0,0 +1,14 @@ +use pyo3::prelude::*; + +#[pyfunction] +fn foo() -> usize { + 0 +} + +#[pymodule] +mod module { + #[pymodule_export] + use super::*; +} + +fn main() {} diff --git a/tests/ui/invalid_pymodule_glob.stderr b/tests/ui/invalid_pymodule_glob.stderr new file mode 100644 index 00000000000..237e02037aa --- /dev/null +++ b/tests/ui/invalid_pymodule_glob.stderr @@ -0,0 +1,5 @@ +error: #[pymodule] cannot import glob statements + --> tests/ui/invalid_pymodule_glob.rs:11:16 + | +11 | use super::*; + | ^ diff --git a/tests/ui/invalid_pymodule_in_root.rs b/tests/ui/invalid_pymodule_in_root.rs new file mode 100644 index 00000000000..47af4205f71 --- /dev/null +++ b/tests/ui/invalid_pymodule_in_root.rs @@ -0,0 +1,6 @@ +use pyo3::prelude::*; + +#[pymodule] +mod invalid_pymodule_in_root_module; + +fn main() {} diff --git a/tests/ui/invalid_pymodule_in_root.stderr b/tests/ui/invalid_pymodule_in_root.stderr new file mode 100644 index 00000000000..91783be0e97 --- /dev/null +++ b/tests/ui/invalid_pymodule_in_root.stderr @@ -0,0 +1,13 @@ +error[E0658]: non-inline modules in proc macro input are unstable + --> tests/ui/invalid_pymodule_in_root.rs:4:1 + | +4 | mod invalid_pymodule_in_root_module; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: see issue #54727 for more information + +error: `#[pymodule]` can only be used on inline modules + --> tests/ui/invalid_pymodule_in_root.rs:4:1 + | +4 | mod invalid_pymodule_in_root_module; + | ^^^ diff --git a/tests/ui/invalid_pymodule_trait.rs b/tests/ui/invalid_pymodule_trait.rs new file mode 100644 index 00000000000..6649a3547a0 --- /dev/null +++ b/tests/ui/invalid_pymodule_trait.rs @@ -0,0 +1,9 @@ +use pyo3::prelude::*; + +#[pymodule] +mod module { + #[pymodule_export] + trait Foo {} +} + +fn main() {} diff --git a/tests/ui/invalid_pymodule_trait.stderr b/tests/ui/invalid_pymodule_trait.stderr new file mode 100644 index 00000000000..4b02f14a540 --- /dev/null +++ b/tests/ui/invalid_pymodule_trait.stderr @@ -0,0 +1,5 @@ +error: `#[pymodule_export]` may only be used on `use` statements + --> tests/ui/invalid_pymodule_trait.rs:5:5 + | +5 | #[pymodule_export] + | ^ diff --git a/tests/ui/invalid_pymodule_two_pymodule_init.rs b/tests/ui/invalid_pymodule_two_pymodule_init.rs new file mode 100644 index 00000000000..d676b0fa277 --- /dev/null +++ b/tests/ui/invalid_pymodule_two_pymodule_init.rs @@ -0,0 +1,16 @@ +use pyo3::prelude::*; + +#[pymodule] +mod module { + #[pymodule_init] + fn init(m: &PyModule) -> PyResult<()> { + Ok(()) + } + + #[pymodule_init] + fn init2(m: &PyModule) -> PyResult<()> { + Ok(()) + } +} + +fn main() {} diff --git a/tests/ui/invalid_pymodule_two_pymodule_init.stderr b/tests/ui/invalid_pymodule_two_pymodule_init.stderr new file mode 100644 index 00000000000..c117ebd573f --- /dev/null +++ b/tests/ui/invalid_pymodule_two_pymodule_init.stderr @@ -0,0 +1,5 @@ +error: only one `#[pymodule_init]` may be specified + --> tests/ui/invalid_pymodule_two_pymodule_init.rs:11:5 + | +11 | fn init2(m: &PyModule) -> PyResult<()> { + | ^^ diff --git a/tests/ui/invalid_result_conversion.rs b/tests/ui/invalid_result_conversion.rs index 4cf3e0bd8fc..373d3cacd9d 100644 --- a/tests/ui/invalid_result_conversion.rs +++ b/tests/ui/invalid_result_conversion.rs @@ -20,11 +20,13 @@ impl fmt::Display for MyError { #[pyfunction] fn should_not_work() -> Result<(), MyError> { - Err(MyError { descr: "something went wrong" }) + Err(MyError { + descr: "something went wrong", + }) } fn main() { - Python::with_gil(|py|{ - wrap_pyfunction!(should_not_work)(py); + Python::with_gil(|py| { + wrap_pyfunction_bound!(should_not_work)(py); }); } diff --git a/tests/ui/invalid_result_conversion.stderr b/tests/ui/invalid_result_conversion.stderr index b3e65517e36..8da8f49fac3 100644 --- a/tests/ui/invalid_result_conversion.stderr +++ b/tests/ui/invalid_result_conversion.stderr @@ -2,13 +2,13 @@ error[E0277]: the trait bound `PyErr: From` is not satisfied --> tests/ui/invalid_result_conversion.rs:21:1 | 21 | #[pyfunction] - | ^^^^^^^^^^^^^ the trait `From` is not implemented for `PyErr` + | ^^^^^^^^^^^^^ the trait `From` is not implemented for `PyErr`, which is required by `MyError: Into` | = help: the following other types implement trait `From`: + >> > > > - >> >> >> > diff --git a/tests/ui/missing_intopy.stderr b/tests/ui/missing_intopy.stderr index 26b32b3e742..c0a60143671 100644 --- a/tests/ui/missing_intopy.stderr +++ b/tests/ui/missing_intopy.stderr @@ -1,18 +1,9 @@ -error[E0277]: the trait bound `Blah: IntoPy>` is not satisfied +error[E0277]: the trait bound `Blah: OkWrap` is not satisfied --> tests/ui/missing_intopy.rs:3:1 | 3 | #[pyo3::pyfunction] - | ^^^^^^^^^^^^^^^^^^^ the trait `IntoPy>` is not implemented for `Blah` + | ^^^^^^^^^^^^^^^^^^^ the trait `IntoPy>` is not implemented for `Blah`, which is required by `Blah: OkWrap<_>` | - = help: the following other types implement trait `IntoPy`: - >> - >> - >> - >> - >> - >> - >> - >> - and $N others + = help: the trait `OkWrap` is implemented for `Result` = note: required for `Blah` to implement `OkWrap` = note: this error originates in the attribute macro `pyo3::pyfunction` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/tests/ui/not_send.stderr b/tests/ui/not_send.stderr index 78936ba78a8..044e3603b27 100644 --- a/tests/ui/not_send.stderr +++ b/tests/ui/not_send.stderr @@ -6,24 +6,24 @@ error[E0277]: `*mut pyo3::Python<'static>` cannot be shared between threads safe | | | required by a bound introduced by this call | - = help: within `pyo3::Python<'_>`, the trait `Sync` is not implemented for `*mut pyo3::Python<'static>` -note: required because it appears within the type `PhantomData<*mut Python<'static>>` + = help: within `pyo3::Python<'_>`, the trait `Sync` is not implemented for `*mut pyo3::Python<'static>`, which is required by `{closure@$DIR/tests/ui/not_send.rs:6:33: 6:35}: Send` +note: required because it appears within the type `PhantomData<*mut pyo3::Python<'static>>` --> $RUST/core/src/marker.rs | | pub struct PhantomData; | ^^^^^^^^^^^ -note: required because it appears within the type `NotSend` +note: required because it appears within the type `impl_::not_send::NotSend` --> src/impl_/not_send.rs | | pub(crate) struct NotSend(PhantomData<*mut Python<'static>>); | ^^^^^^^ - = note: required because it appears within the type `(&GILGuard, NotSend)` -note: required because it appears within the type `PhantomData<(&GILGuard, NotSend)>` + = note: required because it appears within the type `(&pyo3::gil::GILGuard, impl_::not_send::NotSend)` +note: required because it appears within the type `PhantomData<(&pyo3::gil::GILGuard, impl_::not_send::NotSend)>` --> $RUST/core/src/marker.rs | | pub struct PhantomData; | ^^^^^^^^^^^ -note: required because it appears within the type `Python<'_>` +note: required because it appears within the type `pyo3::Python<'_>` --> src/marker.rs | | pub struct Python<'py>(PhantomData<(&'py GILGuard, NotSend)>); @@ -43,40 +43,8 @@ note: required by a bound in `RemoteAllowThreads::<'py>::with` | F: Send + FnOnce() -> T, | ^^^^ required by this bound in `RemoteAllowThreads::<'py>::with` -error[E0277]: `UnsafeCell` cannot be shared between threads safely - --> tests/ui/not_send.rs:14:33 +error[E0599]: no function or associated item named `new` found for struct `PyString` in the current scope + --> tests/ui/not_send.rs:12:32 | -14 | py.allow_threads().with(|| { - | ____________________________----_^ - | | | - | | required by a bound introduced by this call -15 | | println!("{:?}", string); -16 | | }); - | |_________^ `UnsafeCell` cannot be shared between threads safely - | - = help: within `&PyString`, the trait `Sync` is not implemented for `UnsafeCell` -note: required because it appears within the type `PyAny` - --> src/types/any.rs - | - | pub struct PyAny(UnsafeCell); - | ^^^^^ -note: required because it appears within the type `PyString` - --> src/types/string.rs - | - | pub struct PyString(PyAny); - | ^^^^^^^^ - = note: required because it appears within the type `&PyString` - = note: required for `&&PyString` to implement `Send` -note: required because it's used within this closure - --> tests/ui/not_send.rs:14:33 - | -14 | py.allow_threads().with(|| { - | ^^ -note: required by a bound in `RemoteAllowThreads::<'py>::with` - --> src/sync.rs - | - | pub fn with(self, f: F) -> T - | ---- required by a bound in this associated function - | where - | F: Send + FnOnce() -> T, - | ^^^^ required by this bound in `RemoteAllowThreads::<'py>::with` +12 | let string = PyString::new(py, "foo"); + | ^^^ function or associated item not found in `PyString` diff --git a/tests/ui/pyclass_send.rs b/tests/ui/pyclass_send.rs index 533302740d7..a587c071f51 100644 --- a/tests/ui/pyclass_send.rs +++ b/tests/ui/pyclass_send.rs @@ -8,15 +8,15 @@ struct NotThreadSafe { fn main() { let obj = Python::with_gil(|py| { - PyCell::new(py, NotThreadSafe { data: Rc::new(5) }) + Bound::new(py, NotThreadSafe { data: Rc::new(5) }) .unwrap() - .to_object(py) + .unbind() }); std::thread::spawn(move || { Python::with_gil(|py| { // Uh oh, moved Rc to a new thread! - let c: &PyCell = obj.as_ref(py).downcast().unwrap(); + let c = obj.bind(py).downcast::().unwrap(); assert_eq!(*c.borrow().data, 5); }) diff --git a/tests/ui/pyclass_send.stderr b/tests/ui/pyclass_send.stderr index f279acc5f9f..0db9106ab42 100644 --- a/tests/ui/pyclass_send.stderr +++ b/tests/ui/pyclass_send.stderr @@ -4,7 +4,28 @@ error[E0277]: `Rc` cannot be sent between threads safely 4 | #[pyclass] | ^^^^^^^^^^ `Rc` cannot be sent between threads safely | - = help: within `NotThreadSafe`, the trait `Send` is not implemented for `Rc` + = help: within `NotThreadSafe`, the trait `Send` is not implemented for `Rc`, which is required by `SendablePyClass: pyo3::impl_::pyclass::PyClassThreadChecker` + = help: the trait `pyo3::impl_::pyclass::PyClassThreadChecker` is implemented for `SendablePyClass` +note: required because it appears within the type `NotThreadSafe` + --> tests/ui/pyclass_send.rs:5:8 + | +5 | struct NotThreadSafe { + | ^^^^^^^^^^^^^ + = note: required for `SendablePyClass` to implement `pyo3::impl_::pyclass::PyClassThreadChecker` +note: required by a bound in `PyClassImpl::ThreadChecker` + --> src/impl_/pyclass.rs + | + | type ThreadChecker: PyClassThreadChecker; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `PyClassImpl::ThreadChecker` + = note: this error originates in the attribute macro `pyclass` (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0277]: `Rc` cannot be sent between threads safely + --> tests/ui/pyclass_send.rs:4:1 + | +4 | #[pyclass] + | ^^^^^^^^^^ `Rc` cannot be sent between threads safely + | + = help: within `NotThreadSafe`, the trait `Send` is not implemented for `Rc`, which is required by `NotThreadSafe: Send` note: required because it appears within the type `NotThreadSafe` --> tests/ui/pyclass_send.rs:5:8 | diff --git a/tests/ui/pymodule_missing_docs.rs b/tests/ui/pymodule_missing_docs.rs new file mode 100644 index 00000000000..1b196fa65e0 --- /dev/null +++ b/tests/ui/pymodule_missing_docs.rs @@ -0,0 +1,12 @@ +#![deny(missing_docs)] +//! Some crate docs + +use pyo3::prelude::*; + +/// Some module documentation +#[pymodule] +pub fn python_module(_m: &Bound<'_, PyModule>) -> PyResult<()> { + Ok(()) +} + +fn main() {} diff --git a/tests/ui/static_ref.rs b/tests/ui/static_ref.rs index a426536d499..e8015db2f64 100644 --- a/tests/ui/static_ref.rs +++ b/tests/ui/static_ref.rs @@ -2,7 +2,12 @@ use pyo3::prelude::*; use pyo3::types::PyList; #[pyfunction] -fn static_ref(list: &'static PyList) -> usize { +fn static_ref(list: &'static Bound<'_, PyList>) -> usize { + list.len() +} + +#[pyfunction] +fn static_py(list: &Bound<'static, PyList>) -> usize { list.len() } diff --git a/tests/ui/static_ref.stderr b/tests/ui/static_ref.stderr index 2dd3342e5ba..6004c4037e5 100644 --- a/tests/ui/static_ref.stderr +++ b/tests/ui/static_ref.stderr @@ -8,3 +8,38 @@ error: lifetime may not live long enough | cast requires that `'py` must outlive `'static` | = note: this error originates in the attribute macro `pyfunction` (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0597]: `output[_]` does not live long enough + --> tests/ui/static_ref.rs:4:1 + | +4 | #[pyfunction] + | ^^^^^^^^^^^^- + | | | + | | `output[_]` dropped here while still borrowed + | borrowed value does not live long enough + | argument requires that `output[_]` is borrowed for `'static` + | + = note: this error originates in the attribute macro `pyfunction` (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0597]: `holder_0` does not live long enough + --> tests/ui/static_ref.rs:5:15 + | +4 | #[pyfunction] + | ------------- + | | | + | | `holder_0` dropped here while still borrowed + | binding `holder_0` declared here + | argument requires that `holder_0` is borrowed for `'static` +5 | fn static_ref(list: &'static Bound<'_, PyList>) -> usize { + | ^^^^^^^ borrowed value does not live long enough + +error: lifetime may not live long enough + --> tests/ui/static_ref.rs:9:1 + | +9 | #[pyfunction] + | ^^^^^^^^^^^^^ + | | + | lifetime `'py` defined here + | cast requires that `'py` must outlive `'static` + | + = note: this error originates in the attribute macro `pyfunction` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/tests/ui/traverse.rs b/tests/ui/traverse.rs index 034224951c9..faa7b5c041d 100644 --- a/tests/ui/traverse.rs +++ b/tests/ui/traverse.rs @@ -1,27 +1,67 @@ use pyo3::prelude::*; -use pyo3::PyVisit; use pyo3::PyTraverseError; +use pyo3::PyVisit; #[pyclass] struct TraverseTriesToTakePyRef {} #[pymethods] impl TraverseTriesToTakePyRef { - fn __traverse__(slf: PyRef, visit: PyVisit) {} + fn __traverse__(_slf: PyRef, _visit: PyVisit) -> Result<(), PyTraverseError> { + Ok(()) + } } #[pyclass] -struct Class; +struct TraverseTriesToTakePyRefMut {} #[pymethods] -impl Class { - fn __traverse__(&self, py: Python<'_>, visit: PyVisit<'_>) -> Result<(), PyTraverseError> { +impl TraverseTriesToTakePyRefMut { + fn __traverse__(_slf: PyRefMut, _visit: PyVisit) -> Result<(), PyTraverseError> { + Ok(()) + } +} + +#[pyclass] +struct TraverseTriesToTakeBound {} + +#[pymethods] +impl TraverseTriesToTakeBound { + fn __traverse__(_slf: Bound<'_, Self>, _visit: PyVisit) -> Result<(), PyTraverseError> { + Ok(()) + } +} + +#[pyclass] +struct TraverseTriesToTakeMutSelf {} + +#[pymethods] +impl TraverseTriesToTakeMutSelf { + fn __traverse__(&mut self, _visit: PyVisit) -> Result<(), PyTraverseError> { Ok(()) } +} - fn __clear__(&mut self) { +#[pyclass] +struct TraverseTriesToTakeSelf {} + +#[pymethods] +impl TraverseTriesToTakeSelf { + fn __traverse__(&self, _visit: PyVisit) -> Result<(), PyTraverseError> { + Ok(()) } } +#[pyclass] +struct Class; + +#[pymethods] +impl Class { + fn __traverse__(&self, _py: Python<'_>, _visit: PyVisit<'_>) -> Result<(), PyTraverseError> { + Ok(()) + } + + fn __clear__(&mut self) {} +} fn main() {} diff --git a/tests/ui/traverse.stderr b/tests/ui/traverse.stderr index e2718c76e56..504a6dfa671 100644 --- a/tests/ui/traverse.stderr +++ b/tests/ui/traverse.stderr @@ -1,23 +1,29 @@ -error: __traverse__ may not take `Python`. Usually, an implementation of `__traverse__` should do nothing but calls to `visit.call`. Most importantly, safe access to the GIL is prohibited inside implementations of `__traverse__`, i.e. `Python::with_gil` will panic. - --> tests/ui/traverse.rs:18:32 +error: __traverse__ may not take a receiver other than `&self`. Usually, an implementation of `__traverse__(&self, visit: PyVisit<'_>) -> Result<(), PyTraverseError>` should do nothing but calls to `visit.call`. Most importantly, safe access to the GIL is prohibited inside implementations of `__traverse__`, i.e. `Python::with_gil` will panic. + --> tests/ui/traverse.rs:10:27 | -18 | fn __traverse__(&self, py: Python<'_>, visit: PyVisit<'_>) -> Result<(), PyTraverseError> { - | ^^^^^^^^^^ +10 | fn __traverse__(_slf: PyRef, _visit: PyVisit) -> Result<(), PyTraverseError> { + | ^^^^^ -error[E0308]: mismatched types - --> tests/ui/traverse.rs:9:6 +error: __traverse__ may not take a receiver other than `&self`. Usually, an implementation of `__traverse__(&self, visit: PyVisit<'_>) -> Result<(), PyTraverseError>` should do nothing but calls to `visit.call`. Most importantly, safe access to the GIL is prohibited inside implementations of `__traverse__`, i.e. `Python::with_gil` will panic. + --> tests/ui/traverse.rs:20:27 | -8 | #[pymethods] - | ------------ arguments to this function are incorrect -9 | impl TraverseTriesToTakePyRef { - | ______^ -10 | | fn __traverse__(slf: PyRef, visit: PyVisit) {} - | |___________________^ expected fn pointer, found fn item +20 | fn __traverse__(_slf: PyRefMut, _visit: PyVisit) -> Result<(), PyTraverseError> { + | ^^^^^^^^ + +error: __traverse__ may not take a receiver other than `&self`. Usually, an implementation of `__traverse__(&self, visit: PyVisit<'_>) -> Result<(), PyTraverseError>` should do nothing but calls to `visit.call`. Most importantly, safe access to the GIL is prohibited inside implementations of `__traverse__`, i.e. `Python::with_gil` will panic. + --> tests/ui/traverse.rs:30:27 + | +30 | fn __traverse__(_slf: Bound<'_, Self>, _visit: PyVisit) -> Result<(), PyTraverseError> { + | ^^^^^ + +error: __traverse__ may not take a receiver other than `&self`. Usually, an implementation of `__traverse__(&self, visit: PyVisit<'_>) -> Result<(), PyTraverseError>` should do nothing but calls to `visit.call`. Most importantly, safe access to the GIL is prohibited inside implementations of `__traverse__`, i.e. `Python::with_gil` will panic. + --> tests/ui/traverse.rs:40:21 | - = note: expected fn pointer `for<'a, 'b> fn(&'a TraverseTriesToTakePyRef, PyVisit<'b>) -> Result<(), PyTraverseError>` - found fn item `for<'a, 'b> fn(pyo3::PyRef<'a, TraverseTriesToTakePyRef>, PyVisit<'b>) {TraverseTriesToTakePyRef::__traverse__}` -note: function defined here - --> src/impl_/pymethods.rs +40 | fn __traverse__(&mut self, _visit: PyVisit) -> Result<(), PyTraverseError> { + | ^ + +error: __traverse__ may not take `Python`. Usually, an implementation of `__traverse__(&self, visit: PyVisit<'_>) -> Result<(), PyTraverseError>` should do nothing but calls to `visit.call`. Most importantly, safe access to the GIL is prohibited inside implementations of `__traverse__`, i.e. `Python::with_gil` will panic. + --> tests/ui/traverse.rs:60:33 | - | pub unsafe fn _call_traverse( - | ^^^^^^^^^^^^^^ +60 | fn __traverse__(&self, _py: Python<'_>, _visit: PyVisit<'_>) -> Result<(), PyTraverseError> { + | ^^^^^^^^^^ diff --git a/tests/ui/wrong_aspyref_lifetimes.rs b/tests/ui/wrong_aspyref_lifetimes.rs index 5cd8a2d3cb6..755b0cf2a2c 100644 --- a/tests/ui/wrong_aspyref_lifetimes.rs +++ b/tests/ui/wrong_aspyref_lifetimes.rs @@ -1,10 +1,10 @@ -use pyo3::{types::PyDict, Py, Python}; +use pyo3::{types::PyDict, Bound, Py, Python}; fn main() { - let dict: Py = Python::with_gil(|py| PyDict::new(py).into()); + let dict: Py = Python::with_gil(|py| PyDict::new_bound(py).unbind()); // Should not be able to get access to Py contents outside of with_gil. - let dict: &PyDict = Python::with_gil(|py| dict.as_ref(py)); + let dict: &Bound<'_, PyDict> = Python::with_gil(|py| dict.bind(py)); let _py: Python = dict.py(); // Obtain a Python<'p> without GIL. } diff --git a/tests/ui/wrong_aspyref_lifetimes.stderr b/tests/ui/wrong_aspyref_lifetimes.stderr index def6f94c02d..f2f43d99f25 100644 --- a/tests/ui/wrong_aspyref_lifetimes.stderr +++ b/tests/ui/wrong_aspyref_lifetimes.stderr @@ -1,8 +1,8 @@ error: lifetime may not live long enough - --> tests/ui/wrong_aspyref_lifetimes.rs:7:47 + --> tests/ui/wrong_aspyref_lifetimes.rs:7:58 | -7 | let dict: &PyDict = Python::with_gil(|py| dict.as_ref(py)); - | --- ^^^^^^^^^^^^^^^ returning this value requires that `'1` must outlive `'2` - | | | - | | return type of closure is &'2 PyDict - | has type `pyo3::Python<'1>` +7 | let dict: &Bound<'_, PyDict> = Python::with_gil(|py| dict.bind(py)); + | --- ^^^^^^^^^^^^^ returning this value requires that `'1` must outlive `'2` + | | | + | | return type of closure is &'2 pyo3::Bound<'_, PyDict> + | has type `Python<'1>`