diff --git a/.cargo/config b/.cargo/config index c6d1fd86..22c67c22 100644 --- a/.cargo/config +++ b/.cargo/config @@ -1,8 +1,21 @@ [alias] xtask = "run --package xtask --" -pyo3_doc = "doc --lib --no-default-features --features=full --no-deps --workspace --open --exclude pyo3-macros --exclude pyo3-macros-backend" -pyo3_doc_scrape = "doc --lib --no-default-features --features=full --no-deps --workspace --open --exclude pyo3-macros --exclude pyo3-macros-backend -Z unstable-options -Z rustdoc-scrape-examples=examples" -pyo3_doc_internal = "doc --lib --no-default-features --features=full --no-deps --workspace --open --document-private-items -Z unstable-options -Z rustdoc-scrape-examples=examples" -[build] -rustdocflags = ["--cfg", "docsrs"] +[target.'cfg(feature = "cargo-clippy")'] +rustflags = [ + "-Dclippy::checked_conversions", + "-Dclippy::dbg_macro", + "-Dclippy::explicit_into_iter_loop", + "-Dclippy::explicit_iter_loop", + "-Dclippy::filter_map_next", + "-Dclippy::flat_map_option", + "-Dclippy::let_unit_value", + "-Dclippy::manual_assert", + "-Dclippy::manual_ok_or", + "-Dclippy::todo", + "-Dclippy::unnecessary_wraps", + "-Dclippy::useless_transmute", + "-Delided_lifetimes_in_paths", + "-Dunused_lifetimes", + "-Drust_2021_prelude_collisions" +] \ No newline at end of file diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 3429c6c3..077f8ec1 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -5,11 +5,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 -Be aware the CI pipeline will check your pull request for the following: - - Rust tests (Just `cargo test` or `make test` if you need to test examples) - - Rust lints (`make clippy`) - - Rust formatting (`cargo fmt`) - - Python formatting (`black . --check`. You can install black with `pip install black`) - - Compatibility with all supported Python versions for all examples. This uses `tox`; you can do run it using `cargo xtask test-py`. - -You can run a similar set of checks as the CI pipeline using `make test`. +PyO3's CI pipeline will check your pull request. To run its tests +locally, you can run ```cargo xtask ci```. See its documentation + [here](https://github.com/PyO3/pyo3/tree/main/xtask#readme). diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml deleted file mode 100644 index d6d673f6..00000000 --- a/.github/workflows/bench.yml +++ /dev/null @@ -1,95 +0,0 @@ -on: - push: - branches: - - main - pull_request: - -name: Benchmark - -jobs: - benchmark: - name: Cargo benchmark - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: stable - override: true - - - uses: actions/cache@v2 - with: - path: | - ~/.cargo/registry - ~/.cargo/git - target - key: cargo-${{ runner.os }}-bench-${{ hashFiles('**/Cargo.toml') }} - continue-on-error: true - - - name: Run benchmarks - run: | - for bench in call dict gil list pyclass pyobject set tuple; do - cargo bench --features hashbrown --bench "bench_$bench" -- --output-format bencher | tee -a output.txt - done - - # Download previous benchmark result from cache (if exists) - - name: Download previous benchmark data - uses: actions/cache@v1 - with: - path: ./cache - key: ${{ runner.os }}-benchmark - - # Run `github-action-benchmark` action - - name: Store benchmark result - uses: rhysd/github-action-benchmark@v1 - with: - name: pyo3-bench - # What benchmark tool the output.txt came from - tool: "cargo" - # Where the output from the benchmark tool is stored - output-file-path: output.txt - # GitHub API token to make a commit comment - github-token: ${{ secrets.GITHUB_TOKEN }} - auto-push: ${{ github.event_name != 'pull_request' }} - - pytest-benchmark: - name: pytest benchmark - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 - - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: stable - override: true - - - uses: actions/cache@v2 - with: - path: | - ~/.cargo/registry - ~/.cargo/git - target - key: cargo-${{ runner.os }}-pytest-bench-${{ hashFiles('**/Cargo.toml') }} - continue-on-error: true - - - name: Download previous benchmark data - uses: actions/cache@v1 - with: - path: ./cache - key: ${{ runner.os }}-pytest-benchmark - - - name: Run benchmarks - run: | - pip install nox - nox -f pytests/noxfile.py -s bench -- --benchmark-json $(pwd)/output.json - - name: Store benchmark result - uses: rhysd/github-action-benchmark@v1 - with: - name: pytest-bench - tool: "pytest" - output-file-path: output.json - github-token: ${{ secrets.GITHUB_TOKEN }} - auto-push: ${{ github.event_name != 'pull_request' }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bf4f018c..4a127175 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,6 +6,10 @@ on: - main pull_request: +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }} + cancel-in-progress: true + env: CARGO_TERM_COLOR: always @@ -15,33 +19,36 @@ jobs: steps: - uses: actions/checkout@v2 - uses: actions/setup-python@v2 - - run: pip install black==21.12b0 + - run: pip install nox - uses: actions-rs/toolchain@v1 with: toolchain: stable profile: minimal components: rustfmt - name: Check python formatting (black) - run: make fmt_py + run: nox -s fmt-py - name: Check rust formatting (rustfmt) - run: make fmt_rust + run: nox -s fmt-rust clippy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + - run: pip install nox - uses: actions-rs/toolchain@v1 with: toolchain: stable profile: minimal components: clippy - - run: make clippy + - run: nox -s clippy check-target: needs: [fmt] runs-on: ubuntu-latest strategy: - fail-fast: false # If one platform fails, allow the rest to keep testing. + # 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: target: [powerpc64le-unknown-linux-gnu, s390x-unknown-linux-gnu, wasm32-wasi] name: check-${{ matrix.target }} @@ -70,13 +77,23 @@ jobs: build: needs: [fmt] # don't wait for clippy as fails rarely and takes longer - name: python${{ matrix.python-version }}-${{ matrix.platform.python-architecture }} ${{ matrix.platform.os }} ${{ matrix.msrv }} + name: python${{ matrix.python-version }}-${{ matrix.platform.python-architecture }} ${{ matrix.platform.os }} rust-${{ matrix.rust }} runs-on: ${{ matrix.platform.os }} strategy: - fail-fast: false # If one platform fails, allow the rest to keep testing. + # 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: rust: [stable] - python-version: ["3.7", "3.8", "3.9", "3.10", "3.11-dev", "pypy-3.7", "pypy-3.8"] + python-version: [ + "3.7", + "3.8", + "3.9", + "3.10", + "3.11-dev", + "pypy-3.7", + "pypy-3.8", + "pypy-3.9" + ] platform: [ { @@ -94,18 +111,7 @@ jobs: python-architecture: "x64", rust-target: "x86_64-pc-windows-msvc", }, - { - os: "windows-latest", - python-architecture: "x86", - rust-target: "i686-pc-windows-msvc", - }, ] - exclude: - # PyPy doesn't release 32-bit Windows builds any more - - python-version: pypy-3.7 - platform: { os: "windows-latest", python-architecture: "x86" } - - python-version: pypy-3.8 - platform: { os: "windows-latest", python-architecture: "x86" } include: # Test minimal supported Rust version - rust: 1.48.0 @@ -118,6 +124,26 @@ jobs: } msrv: "MSRV" + # Test the `nightly` feature + - rust: nightly + python-version: "3.10" + platform: + { + os: "ubuntu-latest", + python-architecture: "x64", + rust-target: "x86_64-unknown-linux-gnu", + } + extra_features: "nightly" + + # Test 32-bit Windows only with the latest Python version + - rust: stable + python-version: "3.10" + platform: + { + os: "windows-latest", + python-architecture: "x86", + rust-target: "i686-pc-windows-msvc", + } steps: - uses: actions/checkout@v2 @@ -149,11 +175,16 @@ jobs: - if: matrix.msrv == 'MSRV' name: Prepare minimal package versions (MSRV only) run: | + set -x cargo update -p indexmap --precise 1.6.2 - cargo update -p hashbrown:0.11.2 --precise 0.9.1 + cargo update -p hashbrown:0.12.0 --precise 0.9.1 + PROJECTS=("." "examples/decorator" "examples/maturin-starter" "examples/setuptools-rust-starter" "examples/word-count") + for PROJ in ${PROJECTS[@]}; do + cargo update --manifest-path "$PROJ/Cargo.toml" -p parking_lot --precise 0.11.0 + done - name: Build docs - run: cargo doc --no-deps --no-default-features --features full + run: cargo doc --no-deps --no-default-features --features "full ${{ matrix.extra_features }}" - name: Build (no features) run: cargo build --lib --tests --no-default-features @@ -176,26 +207,26 @@ jobs: cargo test --no-default-features - name: Build (all additive features) - run: cargo build --lib --tests --no-default-features --features full + run: cargo build --lib --tests --no-default-features --features "full ${{ matrix.extra_features }}" - if: ${{ startsWith(matrix.python-version, 'pypy') }} name: Build PyPy (abi3-py37) - run: cargo build --lib --tests --no-default-features --features "abi3-py37 full" + run: cargo build --lib --tests --no-default-features --features "abi3-py37 full ${{ matrix.extra_features }}" # Run tests (except on PyPy, because no embedding API). - if: ${{ !startsWith(matrix.python-version, 'pypy') }} name: Test - run: cargo test --no-default-features --features full + run: cargo test --no-default-features --features "full ${{ matrix.extra_features }}" # Run tests again, but in abi3 mode - if: ${{ !startsWith(matrix.python-version, 'pypy') }} name: Test (abi3) - run: cargo test --no-default-features --features "abi3 full" + run: cargo test --no-default-features --features "abi3 full ${{ matrix.extra_features }}" # Run tests again, for abi3-py37 (the minimal Python version) - if: ${{ (!startsWith(matrix.python-version, 'pypy')) && (matrix.python-version != '3.7') }} name: Test (abi3-py37) - run: cargo test --no-default-features --features "abi3-py37 full" + run: cargo test --no-default-features --features "abi3-py37 full ${{ matrix.extra_features }}" - name: Test proc-macro code run: cargo test --manifest-path=pyo3-macros-backend/Cargo.toml @@ -221,6 +252,25 @@ jobs: manylinux: auto args: --release -i python3.9 --no-sdist -m examples/maturin-starter/Cargo.toml + - run: sudo rm -rf examples/maturin-starter/target + if: ${{ matrix.platform.os == 'ubuntu-latest' && matrix.python-version == '3.9' }} + - name: Test cross compile to same architecture + if: ${{ matrix.platform.os == 'ubuntu-latest' && matrix.python-version == '3.9' }} + uses: messense/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 --no-sdist -m examples/maturin-starter/Cargo.toml + + - name: Test cross compilation + if: ${{ matrix.platform.os == 'macos-latest' && matrix.python-version == '3.9' }} + uses: messense/maturin-action@v1 + with: + target: aarch64-apple-darwin + args: --release -i python3.9 --no-sdist -m examples/maturin-starter/Cargo.toml + env: CARGO_TERM_VERBOSE: true CARGO_BUILD_TARGET: ${{ matrix.platform.rust-target }} diff --git a/.github/workflows/guide.yml b/.github/workflows/gh-pages.yml similarity index 52% rename from .github/workflows/guide.yml rename to .github/workflows/gh-pages.yml index d7566672..3529810f 100644 --- a/.github/workflows/guide.yml +++ b/.github/workflows/gh-pages.yml @@ -1,4 +1,4 @@ -name: guide +name: gh-pages on: push: @@ -8,11 +8,15 @@ on: release: types: [published] +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }} + cancel-in-progress: true + env: CARGO_TERM_COLOR: always jobs: - build: + guide-build: runs-on: ubuntu-latest outputs: tag_name: ${{ steps.prepare_tag.outputs.tag_name }} @@ -41,7 +45,7 @@ jobs: mkdir target mkdir -p gh-pages-build/internal echo "
⚠️ Internal Docs ⚠️ Not Public API 👉 Official Docs Here
" > target/banner.html - cargo +nightly pyo3_doc_internal + cargo xtask doc --internal cp -r target/doc gh-pages-build/internal env: RUSTDOCFLAGS: "--cfg docsrs --Z unstable-options --document-hidden-items --html-before-content target/banner.html" @@ -55,7 +59,7 @@ jobs: destination_dir: internal full_commit_message: "Upload internal documentation" - - name: Clear the extra artefacts created earlier + - name: Clear the extra artefacts created earlier run: rm -rf target # This builds the book in gh-pages-build. See https://github.com/rust-lang-nursery/mdBook/issues/698 @@ -67,7 +71,7 @@ jobs: # This adds the docs to gh-pages-build/doc - name: Build the doc run: | - cargo +nightly pyo3_doc_scrape + cargo xtask doc cp -r target/doc gh-pages-build/doc echo "" > gh-pages-build/doc/index.html @@ -79,8 +83,9 @@ jobs: publish_dir: ./gh-pages-build/ destination_dir: ${{ steps.prepare_tag.outputs.tag_name }} full_commit_message: "Upload documentation for ${{ steps.prepare_tag.outputs.tag_name }}" - release: - needs: build + + guide-release: + needs: guide-build runs-on: ubuntu-latest if: ${{ github.event_name == 'release' }} steps: @@ -99,3 +104,94 @@ jobs: publish_dir: ./public/ full_commit_message: "Release ${{ needs.build.outputs.tag_name }}" keep_files: true + + cargo-benchmark: + if: ${{ github.ref_name == 'main' }} + name: Cargo benchmark + needs: guide-build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + override: true + + - uses: actions/cache@v2 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: cargo-${{ runner.os }}-bench-${{ hashFiles('**/Cargo.toml') }} + continue-on-error: true + + - name: Run benchmarks + run: | + for bench in call dict gil list pyclass pyobject set tuple; do + cargo bench --features hashbrown --bench "bench_$bench" -- --output-format bencher | tee -a output.txt + done + + # Download previous benchmark result from cache (if exists) + - name: Download previous benchmark data + uses: actions/cache@v1 + with: + path: ./cache + key: ${{ runner.os }}-benchmark + + # Run `github-action-benchmark` action + - name: Store benchmark result + uses: benchmark-action/github-action-benchmark@v1 + with: + name: pyo3-bench + # What benchmark tool the output.txt came from + tool: "cargo" + # Where the output from the benchmark tool is stored + output-file-path: output.txt + # GitHub API token to make a commit comment + github-token: ${{ secrets.GITHUB_TOKEN }} + auto-push: ${{ github.event_name != 'pull_request' }} + + pytest-benchmark: + if: ${{ github.ref_name == 'main' }} + name: Pytest benchmark + needs: cargo-benchmark + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + override: true + + - uses: actions/cache@v2 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: cargo-${{ runner.os }}-pytest-bench-${{ hashFiles('**/Cargo.toml') }} + continue-on-error: true + + - name: Download previous benchmark data + uses: actions/cache@v1 + with: + path: ./cache + key: ${{ runner.os }}-pytest-benchmark + + - name: Run benchmarks + run: | + pip install nox + nox -f pytests/noxfile.py -s bench -- --benchmark-json $(pwd)/output.json + - name: Store benchmark result + uses: benchmark-action/github-action-benchmark@v1 + with: + name: pytest-bench + tool: "pytest" + output-file-path: output.json + github-token: ${{ secrets.GITHUB_TOKEN }} + auto-push: ${{ github.event_name != 'pull_request' }} diff --git a/.gitignore b/.gitignore index 7098c348..9e87d2a7 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,4 @@ guide/book/ extensions/stamps/ pip-wheel-metadata valgrind-python.supp +*.pyd diff --git a/Architecture.md b/Architecture.md index f46f6db5..8f26db4b 100644 --- a/Architecture.md +++ b/Architecture.md @@ -144,20 +144,12 @@ For example, you can see `type({})` shows `dict` and `type(type({}))` shows `typ ## 4. Protocol methods -Python has some built-in special methods called dunder, such as `__iter__`. -They are called [abstract objects layer](https://docs.python.org/3/c-api/abstract.html) in +Python has some built-in special methods called dunder methods, such as `__iter__`. +They are called "slots" in the [abstract objects layer](https://docs.python.org/3/c-api/abstract.html) in Python/C API. -We provide a way to implement those protocols by using `#[pyproto]` and specific traits, such -as `PyIterProtocol`. -[`src/class`] defines these traits. -Each protocol method has a corresponding FFI function. -For example, `PyIterProtocol::__iter__` has -`pub unsafe extern "C" fn iter(slf: *mut PyObject) -> *mut PyObject`. -When `#[pyproto]` finds that `T` implements `PyIterProtocol::__iter__`, it automatically -sets `iter` on the type object of `T`. - -Also, [`src/class/methods.rs`] has utilities for `#[pyfunction]` and [`src/class/impl_.rs`] has -some internal tricks for making `#[pyproto]` flexible. +We provide a way to implement those protocols similarly, by recognizing special +names in `#[pymethods]`, with a few new ones for slots that can not be +implemented in Python, such as GC support. ## 5. Procedural macros to simplify usage for users. diff --git a/CHANGELOG.md b/CHANGELOG.md index d4ee9057..60bba62f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,53 @@ PyO3 versions, please see the [migration guide](https://pyo3.rs/latest/migration The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## Unreleased +## [Unreleased] + +### Added + +- Allow dependent crates to access config values from `pyo3-build-config` via cargo link dep env vars. [#2092](https://github.com/PyO3/pyo3/pull/2092) +- Added methods on `InterpreterConfig` to run Python scripts using the configured executable. [#2092](https://github.com/PyO3/pyo3/pull/2092) +- Added FFI definitions for `PyType_FromModuleAndSpec`, `PyType_GetModule`, `PyType_GetModuleState` and `PyModule_AddType`. [#2250](https://github.com/PyO3/pyo3/pull/2250) + +### Changed + +- Allow `#[pyo3(crate = "...", text_signature = "...")]` options to be used directly in `#[pyclass(crate = "...", text_signature = "...")]`. [#2234](https://github.com/PyO3/pyo3/pull/2234) +- Mark `METH_FASTCALL` calling convention as limited API on Python 3.10. [#2250](https://github.com/PyO3/pyo3/pull/2250) + +### Fixed + +- Fix `abi3-py310` feature: use Python 3.10 ABI when available instead of silently falling back to the 3.9 ABI. [#2242](https://github.com/PyO3/pyo3/pull/2242) +- Considered `PYTHONFRAMEWORK` when cross compiling in order that on macos cross compiling against a [Framework bundle](https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPFrameworks/Concepts/FrameworkAnatomy.html) is considered shared. [#2233](https://github.com/PyO3/pyo3/pull/2233) +- Panic during compilation when `PYO3_CROSS_LIB_DIR` is set for some host/target combinations. [#2232](https://github.com/PyO3/pyo3/pull/2232) +- Correct dependency version for `syn` to require correct minimal patch version 1.0.56. [#2240](https://github.com/PyO3/pyo3/pull/2240) + +### Added + +- Added `as_bytes` on `Py`. [#2235](https://github.com/PyO3/pyo3/pull/2235) + +### Packaging + +- Extend `parking_lot` dependency supported versions to include 0.12. [#2239](https://github.com/PyO3/pyo3/pull/2239) + +## [0.16.2] - 2022-03-15 + +### Packaging + +- Warn when modules are imported on PyPy 3.7 versions older than PyPy 7.3.8, as they are known to have binary compatibility issues. [#2217](https://github.com/PyO3/pyo3/pull/2217) +- Ensure build script of `pyo3-ffi` runs before that of `pyo3` to fix cross compilation. [#2224](https://github.com/PyO3/pyo3/pull/2224) + +## [0.16.1] - 2022-03-05 + +### Packaging + +- Extend `hashbrown` optional dependency supported versions to include 0.12. [#2197](https://github.com/PyO3/pyo3/pull/2197) + +### Fixed + +- Fix incorrect platform detection for Windows in `pyo3-build-config`. [#2198](https://github.com/PyO3/pyo3/pull/2198) +- Fix regression from 0.16 preventing cross compiling to aarch64 macOS. [#2201](https://github.com/PyO3/pyo3/pull/2201) + +## [0.16.0] - 2022-02-27 ### Packaging @@ -16,21 +62,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `pyo3-build-config` no longer enables the `resolve-config` feature by default. [#2008](https://github.com/PyO3/pyo3/pull/2008) - Update `inventory` optional dependency to 0.2. [#2019](https://github.com/PyO3/pyo3/pull/2019) - Drop `paste` dependency. [#2081](https://github.com/PyO3/pyo3/pull/2081) -- The bindings found `pyo3::ffi` are now a re-export of the new `pyo3-ffi` crate. [#2126](https://github.com/PyO3/pyo3/pull/2126) +- The bindings found in `pyo3::ffi` are now a re-export of a separate `pyo3-ffi` crate. [#2126](https://github.com/PyO3/pyo3/pull/2126) +- Support PyPy 3.9. [#2143](https://github.com/PyO3/pyo3/pull/2143) ### Added +- Add `PyCapsule` type exposing the [Capsule API](https://docs.python.org/3/c-api/capsule.html#capsules). [#1980](https://github.com/PyO3/pyo3/pull/1980) +- Add `pyo3_build_config::Sysconfigdata` and supporting APIs. [#1996](https://github.com/PyO3/pyo3/pull/1996) - Add `Py::setattr` method. [#2009](https://github.com/PyO3/pyo3/pull/2009) -- Add `PyCapsule`, exposing the [Capsule API](https://docs.python.org/3/c-api/capsule.html#capsules). [#1980](https://github.com/PyO3/pyo3/pull/1980) -- Expose `pyo3-build-config` APIs for cross-compiling and Python configuration discovery for use in other projects. [#1996](https://github.com/PyO3/pyo3/pull/1996) -- All PyO3 proc-macros except the deprecated `#[pyproto]` now accept a supplemental attribute `#[pyo3(crate = "some::path")]` that specifies where to find the `pyo3` crate, in case it has been renamed or is re-exported and not found at the crate root. [#2022](https://github.com/PyO3/pyo3/pull/2022) +- Add `#[pyo3(crate = "some::path")]` option to all attribute macros (except the deprecated `#[pyproto]`). [#2022](https://github.com/PyO3/pyo3/pull/2022) +- Enable `create_exception!` macro to take an optional docstring. [#2027](https://github.com/PyO3/pyo3/pull/2027) - Enable `#[pyclass]` for fieldless (aka C-like) enums. [#2034](https://github.com/PyO3/pyo3/pull/2034) - Add buffer magic methods `__getbuffer__` and `__releasebuffer__` to `#[pymethods]`. [#2067](https://github.com/PyO3/pyo3/pull/2067) -- Accept paths in `wrap_pyfunction` and `wrap_pymodule`. [#2081](https://github.com/PyO3/pyo3/pull/2081) -- Add check for correct number of arguments on magic methods. [#2083](https://github.com/PyO3/pyo3/pull/2083) -- `wrap_pyfunction!` can now wrap a `#[pyfunction]` which is implemented in a different Rust module or crate. [#2091](https://github.com/PyO3/pyo3/pull/2091) +- Add support for paths in `wrap_pyfunction` and `wrap_pymodule`. [#2081](https://github.com/PyO3/pyo3/pull/2081) +- Enable `wrap_pyfunction!` to wrap a `#[pyfunction]` implemented in a different Rust module or crate. [#2091](https://github.com/PyO3/pyo3/pull/2091) - Add `PyAny::contains` method (`in` operator for `PyAny`). [#2115](https://github.com/PyO3/pyo3/pull/2115) - Add `PyMapping::contains` method (`in` operator for `PyMapping`). [#2133](https://github.com/PyO3/pyo3/pull/2133) +- Add garbage collection magic magic methods `__traverse__` and `__clear__` to `#[pymethods]`. [#2159](https://github.com/PyO3/pyo3/pull/2159) +- Add support for `from_py_with` on struct tuples and enums to override the default from-Python conversion. [#2181](https://github.com/PyO3/pyo3/pull/2181) +- Add `eq`, `ne`, `lt`, `le`, `gt`, `ge` methods to `PyAny` that wrap `rich_compare`. [#2175](https://github.com/PyO3/pyo3/pull/2175) +- Add `Py::is` and `PyAny::is` methods to check for object identity. [#2183](https://github.com/PyO3/pyo3/pull/2183) +- Add support for the `__getattribute__` magic method. [#2187](https://github.com/PyO3/pyo3/pull/2187) ### Changed @@ -41,19 +93,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `ptraceback` -> `traceback` - `from_instance` -> `from_value` - `into_instance` -> `into_value` +- `PyErr::new_type` now takes an optional docstring and now returns `PyResult>` rather than a `ffi::PyTypeObject` pointer. [#2027](https://github.com/PyO3/pyo3/pull/2027) - Deprecate `PyType::is_instance`; it is inconsistent with other `is_instance` methods in PyO3. Instead of `typ.is_instance(obj)`, use `obj.is_instance(typ)`. [#2031](https://github.com/PyO3/pyo3/pull/2031) -- `PyErr::new_type` now takes an optional docstring and now returns `PyResult>` rather than a `ffi::PyTypeObject` pointer. -- The `create_exception!` macro can now take an optional docstring. This docstring, if supplied, is visible to users (with `.__doc__` and `help()`) and - accompanies your error type in your crate's documentation. - `__getitem__`, `__setitem__` and `__delitem__` in `#[pymethods]` now implement both a Python mapping and sequence by default. [#2065](https://github.com/PyO3/pyo3/pull/2065) - Improve performance and error messages for `#[derive(FromPyObject)]` for enums. [#2068](https://github.com/PyO3/pyo3/pull/2068) - Reduce generated LLVM code size (to improve compile times) for: - - internal `handle_panic` helper [#2074](https://github.com/PyO3/pyo3/pull/2074) + - internal `handle_panic` helper [#2074](https://github.com/PyO3/pyo3/pull/2074) [#2158](https://github.com/PyO3/pyo3/pull/2158) - `#[pyfunction]` and `#[pymethods]` argument extraction [#2075](https://github.com/PyO3/pyo3/pull/2075) [#2085](https://github.com/PyO3/pyo3/pull/2085) - - `#[pyclass]` type object creation [#2076](https://github.com/PyO3/pyo3/pull/2076) [#2081](https://github.com/PyO3/pyo3/pull/2081) -- `__ipow__` now supports modulo argument on Python 3.8+. [#2083](https://github.com/PyO3/pyo3/pull/2083) -- `pyo3-macros-backend` is now compiled with PyO3 cfgs to enable different magic method definitions based on version. [#2083](https://github.com/PyO3/pyo3/pull/2083) -- `_PyCFunctionFast` now correctly reflects the signature defined in the [Python docs](https://docs.python.org/3/c-api/structures.html#c._PyCFunctionFast). [#2126](https://github.com/PyO3/pyo3/pull/2126) + - `#[pyclass]` type object creation [#2076](https://github.com/PyO3/pyo3/pull/2076) [#2081](https://github.com/PyO3/pyo3/pull/2081) [#2157](https://github.com/PyO3/pyo3/pull/2157) +- 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` 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 behaviour: [#2126](https://github.com/PyO3/pyo3/pull/2126) - `PyDateTime_TimeZone_UTC` @@ -69,10 +119,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `PyTZInfo_CheckExact` - `PyDateTime_FromTimestamp` - `PyDate_FromTimestamp` +- Deprecate the `gc` option for `pyclass` (e.g. `#[pyclass(gc)]`). Just implement a `__traverse__` `#[pymethod]`. [#2159](https://github.com/PyO3/pyo3/pull/2159) +- The `ml_meth` field of `PyMethodDef` is now represented by the `PyMethodDefPointer` union. [2166](https://github.com/PyO3/pyo3/pull/2166) +- Deprecate the `#[pyproto]` traits. [#2173](https://github.com/PyO3/pyo3/pull/2173) ### Removed - Remove all functionality deprecated in PyO3 0.14. [#2007](https://github.com/PyO3/pyo3/pull/2007) +- Remove `Default` impl for `PyMethodDef`. [#2166](https://github.com/PyO3/pyo3/pull/2166) +- Remove `PartialEq` impl for `Py` and `PyAny` (use the new `is()` instead). [#2183](https://github.com/PyO3/pyo3/pull/2183) ### Fixed @@ -80,10 +135,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fix memory leak in `PyErr::into_value`. [#2026](https://github.com/PyO3/pyo3/pull/2026) - Fix clippy warning `needless-option-as-deref` in code generated by `#[pyfunction]` and `#[pymethods]`. [#2040](https://github.com/PyO3/pyo3/pull/2040) - Fix undefined behavior in `PySlice::indices`. [#2061](https://github.com/PyO3/pyo3/pull/2061) -- Use the Rust function path for `wrap_pymodule!` of a `#[pymodule]` with a `#[pyo3(name = "..")]` attribute, not the Python name. [#2081](https://github.com/PyO3/pyo3/pull/2081) +- Fix the `wrap_pymodule!` macro using the wrong name for a `#[pymodule]` with a `#[pyo3(name = "..")]` attribute. [#2081](https://github.com/PyO3/pyo3/pull/2081) +- Fix magic methods in `#[pymethods]` accepting implementations with the wrong number of arguments. [#2083](https://github.com/PyO3/pyo3/pull/2083) - Fix panic in `#[pyfunction]` generated code when a required argument following an `Option` was not provided. [#2093](https://github.com/PyO3/pyo3/pull/2093) - Fixed undefined behaviour caused by incorrect `ExactSizeIterator` implementations. [#2124](https://github.com/PyO3/pyo3/pull/2124) +- Fix missing FFI definition `PyCMethod_New` on Python 3.9 and up. [#2143](https://github.com/PyO3/pyo3/pull/2143) - Add missing FFI definitions `_PyLong_NumBits` and `_PyLong_AsByteArray` on PyPy. [#2146](https://github.com/PyO3/pyo3/pull/2146) +- Fix memory leak in implementation of `AsPyPointer` for `Option`. [#2160](https://github.com/PyO3/pyo3/pull/2160) +- Fix FFI definition of `_PyLong_NumBits` to return `size_t` instead of `c_int`. [#2161](https://github.com/PyO3/pyo3/pull/2161) +- Fix `TypeError` thrown when argument parsing failed missing the originating causes. [2177](https://github.com/PyO3/pyo3/pull/2178) ## [0.15.1] - 2021-11-19 @@ -1066,7 +1126,10 @@ Yanked - Initial release -[unreleased]: https://github.com/pyo3/pyo3/compare/v0.15.1...HEAD +[Unreleased]: https://github.com/pyo3/pyo3/compare/v0.16.2...HEAD +[0.16.2]: https://github.com/pyo3/pyo3/compare/v0.16.1...v0.16.2 +[0.16.1]: https://github.com/pyo3/pyo3/compare/v0.16.0...v0.16.1 +[0.16.0]: https://github.com/pyo3/pyo3/compare/v0.15.1...v0.16.0 [0.15.1]: https://github.com/pyo3/pyo3/compare/v0.15.0...v0.15.1 [0.15.0]: https://github.com/pyo3/pyo3/compare/v0.14.5...v0.15.0 [0.14.5]: https://github.com/pyo3/pyo3/compare/v0.14.4...v0.14.5 diff --git a/Cargo.toml b/Cargo.toml index fe18ed7f..bf66abf5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3" -version = "0.15.1" +version = "0.16.2" description = "Bindings to Python interpreter" authors = ["PyO3 Project and Contributors "] readme = "README.md" @@ -12,18 +12,17 @@ categories = ["api-bindings", "development-tools::ffi"] license = "Apache-2.0" exclude = ["/.gitignore", ".cargo/config", "/codecov.yml", "/Makefile", "/pyproject.toml", "/noxfile.py"] edition = "2018" -links = "python" [dependencies] cfg-if = "1.0" libc = "0.2.62" -parking_lot = "0.11.0" +parking_lot = ">= 0.11, < 0.13" # ffi bindings to the python interpreter, split into a seperate crate so they can be used independently -pyo3-ffi = { path = "pyo3-ffi", version = "=0.15.1" } +pyo3-ffi = { path = "pyo3-ffi", version = "=0.16.2" } # support crates for macros feature -pyo3-macros = { path = "pyo3-macros", version = "=0.15.1", optional = true } +pyo3-macros = { path = "pyo3-macros", version = "=0.16.2", optional = true } indoc = { version = "1.0.3", optional = true } unindent = { version = "0.1.4", optional = true } @@ -33,7 +32,7 @@ inventory = { version = "0.2.0", optional = true } # crate integrations that can be added using the eponymous features anyhow = { version = "1.0", optional = true } eyre = { version = ">= 0.4, < 0.7", optional = true } -hashbrown = { version = ">= 0.9, < 0.12", optional = true } +hashbrown = { version = ">= 0.9, < 0.13", optional = true } indexmap = { version = ">= 1.6, < 1.8", optional = true } num-bigint = { version = "0.4", optional = true } num-complex = { version = ">= 0.2, < 0.5", optional = true } @@ -46,10 +45,12 @@ trybuild = "1.0.49" rustversion = "1.0" # 1.0.0 requires Rust 1.50 proptest = { version = "0.10.1", default-features = false, features = ["std"] } +send_wrapper = "0.5" +serde = { version = "1.0", features = ["derive"] } serde_json = "1.0.61" [build-dependencies] -pyo3-build-config = { path = "pyo3-build-config", version = "0.15.1", features = ["resolve-config"] } +pyo3-build-config = { path = "pyo3-build-config", version = "0.16.2", features = ["resolve-config"] } [features] default = ["macros", "pyproto"] @@ -69,7 +70,7 @@ pyproto = ["pyo3-macros/pyproto"] 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"] +abi3 = ["pyo3-build-config/abi3", "pyo3-ffi/abi3", "pyo3-macros/abi3"] # With abi3, we can manually set the minimum Python version. abi3-py37 = ["abi3-py38", "pyo3-build-config/abi3-py37", "pyo3-ffi/abi3-py37"] @@ -86,7 +87,18 @@ nightly = [] # Activates all additional features # This is mostly intended for testing purposes - activating *all* of these isn't particularly useful. -full = ["macros", "pyproto", "multiple-pymethods", "num-bigint", "num-complex", "hashbrown", "serde", "indexmap", "eyre", "anyhow"] +full = [ + "macros", + "pyproto", + "multiple-pymethods", + "num-bigint", + "num-complex", + "hashbrown", + "serde", + "indexmap", + "eyre", + "anyhow", +] [[bench]] name = "bench_call" diff --git a/Contributing.md b/Contributing.md index 4fc2c246..a48be238 100644 --- a/Contributing.md +++ b/Contributing.md @@ -48,7 +48,7 @@ There are some specific areas of focus where help is currently needed for the do - 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 -```cargo +nightly pyo3_doc_scrape``` +```cargo xtask doc --open``` #### Doctests @@ -87,6 +87,10 @@ Tests run with all supported Python versions with the latest stable Rust compile 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 +```cargo xtask ci``` +See [it's documentation](https://github.com/PyO3/pyo3/tree/main/xtask#readme)for more commands you can run. + ## Python and Rust version support policy PyO3 aims to keep sufficient compatibility to make packaging Python extensions built with PyO3 feasible on most common package managers. diff --git a/Makefile b/Makefile deleted file mode 100644 index 668807e3..00000000 --- a/Makefile +++ /dev/null @@ -1,41 +0,0 @@ -.PHONY: test test_py publish clippy lint fmt fmt_py fmt_rust - -ALL_ADDITIVE_FEATURES = macros multiple-pymethods num-bigint num-complex hashbrown serde indexmap eyre anyhow -COVERAGE_PACKAGES = --package pyo3 --package pyo3-build-config --package pyo3-macros-backend --package pyo3-macros - -test: lint test_py - cargo test - cargo test --features="abi3" - cargo test --features="$(ALL_ADDITIVE_FEATURES)" - cargo test --features="abi3 $(ALL_ADDITIVE_FEATURES)" - -test_py: - @for example in examples/*/noxfile.py; do echo "-- Running nox for $$example --"; nox -f $$example/noxfile.py || exit 1; echo ""; done - echo "-- Running nox for pytests/noxfile.py --"; - nox -f pytests/noxfile.py || exit 1; - -fmt_py: - black . --check - -fmt_rust: - cargo fmt --all -- --check - -fmt: fmt_rust fmt_py - @true - -clippy: - cargo clippy --features="$(ALL_ADDITIVE_FEATURES)" --all-targets --workspace -- -Dwarnings - cargo clippy --features="abi3 $(ALL_ADDITIVE_FEATURES)" --all-targets --workspace -- -Dwarnings - for example in examples/*/; do cargo clippy --manifest-path $$example/Cargo.toml -- -Dwarnings || exit 1; done - -lint: fmt clippy - @true - -publish: test - cargo publish --manifest-path pyo3-build-config/Cargo.toml - sleep 10 - cargo publish --manifest-path pyo3-macros-backend/Cargo.toml - sleep 10 # wait for crates.io to update - cargo publish --manifest-path pyo3-macros/Cargo.toml - sleep 10 # wait for crates.io to update - cargo publish diff --git a/README.md b/README.md index 2e1b8b3c..52e49cea 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,9 @@ version = "0.1.0" edition = "2018" [lib] +# The name of the native library. This is the name which will be used in Python to import the +# library (i.e. `import string_sum`). If you change this, you must also change the name of the +# `#[pymodule]` in `src/lib.rs`. name = "string_sum" # "cdylib" is necessary to produce a shared library for Python to import from. # @@ -65,7 +68,7 @@ name = "string_sum" crate-type = ["cdylib"] [dependencies] -pyo3 = { version = "0.15.1", features = ["extension-module"] } +pyo3 = { version = "0.16.2", features = ["extension-module"] } ``` **`src/lib.rs`** @@ -83,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(_py: Python<'_>, m: &PyModule) -> PyResult<()> { m.add_function(wrap_pyfunction!(sum_as_string, m)?)?; Ok(()) } @@ -129,7 +132,7 @@ Start a new project with `cargo new` and add `pyo3` to the `Cargo.toml` like th ```toml [dependencies.pyo3] -version = "0.15.1" +version = "0.16.2" features = ["auto-initialize"] ``` @@ -166,7 +169,8 @@ about this topic. - [dict-derive](https://github.com/gperinazzo/dict-derive) _Derive FromPyObject to automatically transform Python dicts into Rust structs_ - [pyo3-log](https://github.com/vorner/pyo3-log) _Bridge from Rust to Python logging_ - [pythonize](https://github.com/davidhewitt/pythonize) _Serde serializer for converting Rust objects to JSON-compatible Python objects_ -- [pyo3-asyncio](https://github.com/awestlake87/pyo3-asyncio) Utilities for working with Python's Asyncio library and async functions +- [pyo3-asyncio](https://github.com/awestlake87/pyo3-asyncio) _Utilities for working with Python's Asyncio library and async functions_ +- [rustimport](https://github.com/mityax/rustimport) _Directly import Rust files or crates from Python, without manual compilation step. Provides pyo3 integration by default and generates pyo3 binding code automatically._ ## Examples diff --git a/benches/bench_call.rs b/benches/bench_call.rs index 90c1a7ff..dd98c159 100644 --- a/benches/bench_call.rs +++ b/benches/bench_call.rs @@ -8,7 +8,7 @@ macro_rules! test_module { }; } -fn bench_call_0(b: &mut Bencher) { +fn bench_call_0(b: &mut Bencher<'_>) { Python::with_gil(|py| { let module = test_module!(py, "def foo(): pass"); @@ -22,7 +22,7 @@ fn bench_call_0(b: &mut Bencher) { }) } -fn bench_call_method_0(b: &mut Bencher) { +fn bench_call_method_0(b: &mut Bencher<'_>) { Python::with_gil(|py| { let module = test_module!( py, diff --git a/benches/bench_dict.rs b/benches/bench_dict.rs index e8d98939..7fea4272 100644 --- a/benches/bench_dict.rs +++ b/benches/bench_dict.rs @@ -4,7 +4,7 @@ use pyo3::prelude::*; use pyo3::types::IntoPyDict; use std::collections::{BTreeMap, HashMap}; -fn iter_dict(b: &mut Bencher) { +fn iter_dict(b: &mut Bencher<'_>) { let gil = Python::acquire_gil(); let py = gil.python(); const LEN: usize = 100_000; @@ -18,14 +18,14 @@ fn iter_dict(b: &mut Bencher) { }); } -fn dict_new(b: &mut Bencher) { +fn dict_new(b: &mut Bencher<'_>) { let gil = Python::acquire_gil(); let py = gil.python(); const LEN: usize = 50_000; b.iter(|| (0..LEN as u64).map(|i| (i, i * 2)).into_py_dict(py)); } -fn dict_get_item(b: &mut Bencher) { +fn dict_get_item(b: &mut Bencher<'_>) { let gil = Python::acquire_gil(); let py = gil.python(); const LEN: usize = 50_000; @@ -38,7 +38,7 @@ fn dict_get_item(b: &mut Bencher) { }); } -fn extract_hashmap(b: &mut Bencher) { +fn extract_hashmap(b: &mut Bencher<'_>) { let gil = Python::acquire_gil(); let py = gil.python(); const LEN: usize = 100_000; @@ -46,7 +46,7 @@ fn extract_hashmap(b: &mut Bencher) { b.iter(|| HashMap::::extract(dict)); } -fn extract_btreemap(b: &mut Bencher) { +fn extract_btreemap(b: &mut Bencher<'_>) { let gil = Python::acquire_gil(); let py = gil.python(); const LEN: usize = 100_000; @@ -55,7 +55,7 @@ fn extract_btreemap(b: &mut Bencher) { } #[cfg(feature = "hashbrown")] -fn extract_hashbrown_map(b: &mut Bencher) { +fn extract_hashbrown_map(b: &mut Bencher<'_>) { let gil = Python::acquire_gil(); let py = gil.python(); const LEN: usize = 100_000; diff --git a/benches/bench_err.rs b/benches/bench_err.rs index a0318d14..e3194512 100644 --- a/benches/bench_err.rs +++ b/benches/bench_err.rs @@ -2,7 +2,7 @@ use criterion::{criterion_group, criterion_main, Bencher, Criterion}; use pyo3::{exceptions::PyValueError, prelude::*}; -fn err_new_restore_and_fetch(b: &mut Bencher) { +fn err_new_restore_and_fetch(b: &mut Bencher<'_>) { Python::with_gil(|py| { b.iter(|| { PyValueError::new_err("some exception message").restore(py); @@ -11,7 +11,7 @@ fn err_new_restore_and_fetch(b: &mut Bencher) { }) } -fn err_new_without_gil(b: &mut Bencher) { +fn err_new_without_gil(b: &mut Bencher<'_>) { b.iter(|| PyValueError::new_err("some exception message")) } diff --git a/benches/bench_frompyobject.rs b/benches/bench_frompyobject.rs index 7f1274a8..8cf8a4da 100644 --- a/benches/bench_frompyobject.rs +++ b/benches/bench_frompyobject.rs @@ -9,7 +9,7 @@ enum ManyTypes { String(String), } -fn enum_from_pyobject(b: &mut Bencher) { +fn enum_from_pyobject(b: &mut Bencher<'_>) { Python::with_gil(|py| { let obj = PyString::new(py, "hello world"); b.iter(|| { diff --git a/benches/bench_gil.rs b/benches/bench_gil.rs index 8dfed873..1dc1e625 100644 --- a/benches/bench_gil.rs +++ b/benches/bench_gil.rs @@ -2,7 +2,7 @@ use criterion::{criterion_group, criterion_main, BatchSize, Bencher, Criterion}; use pyo3::{prelude::*, GILPool}; -fn bench_clean_gilpool_new(b: &mut Bencher) { +fn bench_clean_gilpool_new(b: &mut Bencher<'_>) { Python::with_gil(|_py| { b.iter(|| { let _ = unsafe { GILPool::new() }; @@ -10,14 +10,14 @@ fn bench_clean_gilpool_new(b: &mut Bencher) { }); } -fn bench_clean_acquire_gil(b: &mut Bencher) { +fn bench_clean_acquire_gil(b: &mut Bencher<'_>) { // Acquiring first GIL will also create a "clean" GILPool, so this measures the Python overhead. b.iter(|| { let _ = Python::acquire_gil(); }); } -fn bench_dirty_acquire_gil(b: &mut Bencher) { +fn bench_dirty_acquire_gil(b: &mut Bencher<'_>) { let obj = Python::with_gil(|py| py.None()); b.iter_batched( || { diff --git a/benches/bench_list.rs b/benches/bench_list.rs index 12fd4510..b52f9890 100644 --- a/benches/bench_list.rs +++ b/benches/bench_list.rs @@ -3,7 +3,7 @@ use criterion::{criterion_group, criterion_main, Bencher, Criterion}; use pyo3::prelude::*; use pyo3::types::PyList; -fn iter_list(b: &mut Bencher) { +fn iter_list(b: &mut Bencher<'_>) { let gil = Python::acquire_gil(); let py = gil.python(); const LEN: usize = 100_000; @@ -17,14 +17,14 @@ fn iter_list(b: &mut Bencher) { }); } -fn list_new(b: &mut Bencher) { +fn list_new(b: &mut Bencher<'_>) { let gil = Python::acquire_gil(); let py = gil.python(); const LEN: usize = 50_000; b.iter(|| PyList::new(py, 0..LEN)); } -fn list_get_item(b: &mut Bencher) { +fn list_get_item(b: &mut Bencher<'_>) { let gil = Python::acquire_gil(); let py = gil.python(); const LEN: usize = 50_000; @@ -38,7 +38,7 @@ fn list_get_item(b: &mut Bencher) { } #[cfg(not(Py_LIMITED_API))] -fn list_get_item_unchecked(b: &mut Bencher) { +fn list_get_item_unchecked(b: &mut Bencher<'_>) { let gil = Python::acquire_gil(); let py = gil.python(); const LEN: usize = 50_000; diff --git a/benches/bench_pyclass.rs b/benches/bench_pyclass.rs index 04676b1a..2d6868d4 100644 --- a/benches/bench_pyclass.rs +++ b/benches/bench_pyclass.rs @@ -1,5 +1,5 @@ use criterion::{criterion_group, criterion_main, Criterion}; -use pyo3::{class::PyObjectProtocol, prelude::*, type_object::LazyStaticType}; +use pyo3::{prelude::*, type_object::LazyStaticType}; /// This is a feature-rich class instance used to benchmark various parts of the pyclass lifecycle. #[pyclass] @@ -19,17 +19,14 @@ impl MyClass { self.elements.push(new_element); self.elements.len() } -} -#[pyproto] -impl PyObjectProtocol for MyClass { /// A basic __str__ implementation. fn __str__(&self) -> &'static str { "MyClass" } } -pub fn first_time_init(b: &mut criterion::Bencher) { +pub fn first_time_init(b: &mut criterion::Bencher<'_>) { let gil = Python::acquire_gil(); let py = gil.python(); b.iter(|| { diff --git a/benches/bench_pyobject.rs b/benches/bench_pyobject.rs index bc507f90..fb17c050 100644 --- a/benches/bench_pyobject.rs +++ b/benches/bench_pyobject.rs @@ -2,7 +2,7 @@ use criterion::{criterion_group, criterion_main, Bencher, Criterion}; use pyo3::prelude::*; -fn drop_many_objects(b: &mut Bencher) { +fn drop_many_objects(b: &mut Bencher<'_>) { let gil = Python::acquire_gil(); let py = gil.python(); b.iter(|| { diff --git a/benches/bench_set.rs b/benches/bench_set.rs index 7615bb84..698f6eb3 100644 --- a/benches/bench_set.rs +++ b/benches/bench_set.rs @@ -4,7 +4,7 @@ use pyo3::prelude::*; use pyo3::types::PySet; use std::collections::{BTreeSet, HashSet}; -fn iter_set(b: &mut Bencher) { +fn iter_set(b: &mut Bencher<'_>) { let gil = Python::acquire_gil(); let py = gil.python(); const LEN: usize = 100_000; @@ -18,7 +18,7 @@ fn iter_set(b: &mut Bencher) { }); } -fn extract_hashset(b: &mut Bencher) { +fn extract_hashset(b: &mut Bencher<'_>) { let gil = Python::acquire_gil(); let py = gil.python(); const LEN: usize = 100_000; @@ -26,7 +26,7 @@ fn extract_hashset(b: &mut Bencher) { b.iter(|| HashSet::::extract(set)); } -fn extract_btreeset(b: &mut Bencher) { +fn extract_btreeset(b: &mut Bencher<'_>) { let gil = Python::acquire_gil(); let py = gil.python(); const LEN: usize = 100_000; @@ -35,7 +35,7 @@ fn extract_btreeset(b: &mut Bencher) { } #[cfg(feature = "hashbrown")] -fn extract_hashbrown_set(b: &mut Bencher) { +fn extract_hashbrown_set(b: &mut Bencher<'_>) { let gil = Python::acquire_gil(); let py = gil.python(); const LEN: usize = 100_000; diff --git a/benches/bench_tuple.rs b/benches/bench_tuple.rs index 29724318..4a5eb8b5 100644 --- a/benches/bench_tuple.rs +++ b/benches/bench_tuple.rs @@ -3,7 +3,7 @@ use criterion::{criterion_group, criterion_main, Bencher, Criterion}; use pyo3::prelude::*; use pyo3::types::PyTuple; -fn iter_tuple(b: &mut Bencher) { +fn iter_tuple(b: &mut Bencher<'_>) { let gil = Python::acquire_gil(); let py = gil.python(); const LEN: usize = 100_000; @@ -17,14 +17,14 @@ fn iter_tuple(b: &mut Bencher) { }); } -fn tuple_new(b: &mut Bencher) { +fn tuple_new(b: &mut Bencher<'_>) { let gil = Python::acquire_gil(); let py = gil.python(); const LEN: usize = 50_000; b.iter(|| PyTuple::new(py, 0..LEN)); } -fn tuple_get_item(b: &mut Bencher) { +fn tuple_get_item(b: &mut Bencher<'_>) { let gil = Python::acquire_gil(); let py = gil.python(); const LEN: usize = 50_000; @@ -38,7 +38,7 @@ fn tuple_get_item(b: &mut Bencher) { } #[cfg(not(Py_LIMITED_API))] -fn tuple_get_item_unchecked(b: &mut Bencher) { +fn tuple_get_item_unchecked(b: &mut Bencher<'_>) { let gil = Python::acquire_gil(); let py = gil.python(); const LEN: usize = 50_000; diff --git a/build.rs b/build.rs index cfaf06f6..78cebf9a 100644 --- a/build.rs +++ b/build.rs @@ -1,7 +1,7 @@ -use std::{env, process::Command}; +use std::env; use pyo3_build_config::pyo3_build_script_impl::{cargo_env_var, errors::Result}; -use pyo3_build_config::{bail, InterpreterConfig}; +use pyo3_build_config::{bail, print_feature_cfgs, InterpreterConfig}; fn ensure_auto_initialize_ok(interpreter_config: &InterpreterConfig) -> Result<()> { if cargo_env_var("CARGO_FEATURE_AUTO_INITIALIZE").is_some() { @@ -32,17 +32,6 @@ fn ensure_auto_initialize_ok(interpreter_config: &InterpreterConfig) -> Result<( Ok(()) } -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() -} - /// Prepares the PyO3 crate for compilation. /// /// This loads the config from pyo3-build-config and then makes some additional checks to improve UX @@ -55,18 +44,10 @@ fn configure_pyo3() -> Result<()> { interpreter_config.emit_pyo3_cfgs(); - let rustc_minor_version = rustc_minor_version().unwrap_or(0); ensure_auto_initialize_ok(interpreter_config)?; - // Enable use of const generics on Rust 1.51 and greater - if rustc_minor_version >= 51 { - println!("cargo:rustc-cfg=min_const_generics"); - } - - // Enable use of std::ptr::addr_of! on Rust 1.51 and greater - if rustc_minor_version >= 51 { - println!("cargo:rustc-cfg=addr_of"); - } + // Emit cfgs like `addr_of` and `min_const_generics` + print_feature_cfgs(); Ok(()) } diff --git a/examples/Cargo.toml b/examples/Cargo.toml index 6ad255f6..93c6a6f5 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -5,7 +5,7 @@ publish = false edition = "2018" [dev-dependencies] -pyo3 = { version = "0.15.1", path = "..", features = ["auto-initialize", "extension-module"] } +pyo3 = { version = "0.16.2", path = "..", features = ["auto-initialize", "extension-module"] } [[example]] name = "decorator" diff --git a/examples/decorator/.template/pre-script.rhai b/examples/decorator/.template/pre-script.rhai index d8f836a0..cc4a2467 100644 --- a/examples/decorator/.template/pre-script.rhai +++ b/examples/decorator/.template/pre-script.rhai @@ -1,4 +1,4 @@ -variable::set("PYO3_VERSION", "0.15.1"); +variable::set("PYO3_VERSION", "0.16.2"); file::rename(".template/Cargo.toml", "Cargo.toml"); file::rename(".template/pyproject.toml", "pyproject.toml"); file::rename(".template/tox.ini", "tox.ini"); diff --git a/examples/decorator/src/lib.rs b/examples/decorator/src/lib.rs index 35520a17..078b33f0 100644 --- a/examples/decorator/src/lib.rs +++ b/examples/decorator/src/lib.rs @@ -29,7 +29,7 @@ impl PyCounter { #[args(args = "*", kwargs = "**")] fn __call__( &mut self, - py: Python, + py: Python<'_>, args: &PyTuple, kwargs: Option<&PyDict>, ) -> PyResult> { @@ -48,7 +48,7 @@ impl PyCounter { } #[pymodule] -pub fn decorator(_py: Python, module: &PyModule) -> PyResult<()> { +pub fn decorator(_py: Python<'_>, module: &PyModule) -> PyResult<()> { module.add_class::()?; Ok(()) } diff --git a/examples/maturin-starter/.template/pre-script.rhai b/examples/maturin-starter/.template/pre-script.rhai index d8f836a0..cc4a2467 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.15.1"); +variable::set("PYO3_VERSION", "0.16.2"); file::rename(".template/Cargo.toml", "Cargo.toml"); file::rename(".template/pyproject.toml", "pyproject.toml"); file::rename(".template/tox.ini", "tox.ini"); diff --git a/examples/maturin-starter/src/lib.rs b/examples/maturin-starter/src/lib.rs index 3a79ca51..0b845842 100644 --- a/examples/maturin-starter/src/lib.rs +++ b/examples/maturin-starter/src/lib.rs @@ -21,7 +21,7 @@ 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: &PyModule) -> PyResult<()> { m.add_class::()?; m.add_wrapped(wrap_pymodule!(submodule))?; diff --git a/examples/maturin-starter/src/submodule.rs b/examples/maturin-starter/src/submodule.rs index 8096e337..56540b2e 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(_py: Python<'_>, m: &PyModule) -> PyResult<()> { m.add_class::()?; Ok(()) } diff --git a/examples/setuptools-rust-starter/.template/pre-script.rhai b/examples/setuptools-rust-starter/.template/pre-script.rhai index 2f8d2ee3..9ff84f49 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.15.1"); +variable::set("PYO3_VERSION", "0.16.2"); file::rename(".template/Cargo.toml", "Cargo.toml"); file::rename(".template/setup.cfg", "setup.cfg"); file::rename(".template/tox.ini", "tox.ini"); diff --git a/examples/setuptools-rust-starter/src/lib.rs b/examples/setuptools-rust-starter/src/lib.rs index 6f09f43a..46af25f1 100644 --- a/examples/setuptools-rust-starter/src/lib.rs +++ b/examples/setuptools-rust-starter/src/lib.rs @@ -21,7 +21,7 @@ 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: &PyModule) -> PyResult<()> { m.add_class::()?; m.add_wrapped(wrap_pymodule!(submodule))?; diff --git a/examples/setuptools-rust-starter/src/submodule.rs b/examples/setuptools-rust-starter/src/submodule.rs index 8096e337..56540b2e 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(_py: Python<'_>, m: &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 3f0ab336..35864ec3 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.15.1"); +variable::set("PYO3_VERSION", "0.16.2"); file::rename(".template/Cargo.toml", "Cargo.toml"); file::rename(".template/tox.ini", "tox.ini"); file::delete(".template"); diff --git a/examples/word-count/src/lib.rs b/examples/word-count/src/lib.rs index 96f9f9e2..b7d3a803 100644 --- a/examples/word-count/src/lib.rs +++ b/examples/word-count/src/lib.rs @@ -17,7 +17,7 @@ fn search_sequential(contents: &str, needle: &str) -> usize { } #[pyfunction] -fn search_sequential_allow_threads(py: Python, contents: &str, needle: &str) -> usize { +fn search_sequential_allow_threads(py: Python<'_>, contents: &str, needle: &str) -> usize { py.allow_threads(|| search_sequential(contents, needle)) } diff --git a/guide/src/SUMMARY.md b/guide/src/SUMMARY.md index 0d5e4b1f..c1ed4711 100644 --- a/guide/src/SUMMARY.md +++ b/guide/src/SUMMARY.md @@ -8,6 +8,8 @@ - [Python Functions](function.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) - [Type Conversions](conversions.md) - [Mapping of Rust types to Python types](conversions/tables.md)] diff --git a/guide/src/building_and_distribution/multiple_python_versions.md b/guide/src/building_and_distribution/multiple_python_versions.md index ff442072..e7bdd986 100644 --- a/guide/src/building_and_distribution/multiple_python_versions.md +++ b/guide/src/building_and_distribution/multiple_python_versions.md @@ -53,31 +53,31 @@ After these steps you are ready to annotate your code! The `#[cfg]` flags added by `pyo3-build-cfg` can be combined with all of Rust's logic in the `#[cfg]` attribute to create very precise conditional code generation. The following are some common patterns implemented using these flags: -``` +```rust,ignore #[cfg(Py_3_7)] ``` This `#[cfg]` marks code that will only be present on Python 3.7 and upwards. There are similar options `Py_3_8`, `Py_3_9`, `Py_3_10` and so on for each minor version. -``` +```rust,ignore #[cfg(not(Py_3_7))] ``` This `#[cfg]` marks code that will only be present on Python versions before (but not including) Python 3.7. -``` +```rust,ignore #[cfg(not(Py_LIMITED_API))] ``` This `#[cfg]` marks code that is only available when building for the unlimited Python API (i.e. PyO3's `abi3` feature is not enabled). This might be useful if you want to ship your extension module as an `abi3` wheel and also allow users to compile it from source to make use of optimizations only possible with the unlimited API. -``` +```rust,ignore #[cfg(any(Py_3_9, not(Py_LIMITED_API)))] ``` This `#[cfg]` marks code which is available when running Python 3.9 or newer, or when using the unlimited API with an older Python version. Patterns like this are commonly seen on Python APIs which were added to the limited Python API in a specific minor version. -``` +```rust,ignore #[cfg(PyPy)] ``` diff --git a/guide/src/class.md b/guide/src/class.md index 1156ae1c..b590b4f9 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.) Finally, there may be multiple `#[pyproto]` trait implementations for the struct, which are used to define certain python magic methods such as `__str__`. +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__`. 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,31 @@ This chapter will discuss the functionality and configuration these attributes o - [`#[classmethod]`](#class-methods) - [`#[classattr]`](#class-attributes) - [`#[args]`](#method-arguments) -- [`#[pyproto]`](class/protocols.html) +- [Magic methods and slots](class/protocols.html) ## Defining a new class To define a custom Python class, add the `#[pyclass]` attribute to a Rust struct or a fieldless enum. ```rust # #![allow(dead_code)] -# use pyo3::prelude::*; +use pyo3::prelude::*; + #[pyclass] -struct MyClass { - # #[pyo3(get)] - num: i32, +struct Integer{ + inner: i32 +} + +// A "tuple" struct +#[pyclass] +struct Number(i32); + +// PyO3 supports custom discriminants in enums +#[pyclass] +enum HttpResponse { + Ok = 200, + NotFound = 404, + Teapot = 418, + // ... } #[pyclass] @@ -41,20 +54,67 @@ Because Python objects are freely shared between threads by the Python interpret The above example generates implementations for [`PyTypeInfo`], [`PyTypeObject`], and [`PyClass`] for `MyClass` and `MyEnum`. To see these generated implementations, refer to the [implementation details](#implementation-details) at the end of this chapter. -## Adding the class to a module +## Constructor -Custom Python classes can then be added to a module using `add_class()`. +By default it is not possible to create an instance of a custom class from Python code. +To declare a constructor, you need to define a method and annotate it with the `#[new]` +attribute. Only Python's `__new__` method can be specified, `__init__` is not available. ```rust # use pyo3::prelude::*; # #[pyclass] -# struct MyClass { -# #[allow(dead_code)] -# num: i32, -# } +# struct Number(i32); +# +#[pymethods] +impl Number { + #[new] + fn new(value: i32) -> Self { + Number(value) + } +} +``` + +Alternatively, if your `new` method may fail you can return `PyResult`. + +```rust +# use pyo3::prelude::*; +# use pyo3::exceptions::PyValueError; +# #[pyclass] +# struct Nonzero(i32); +# +#[pymethods] +impl Nonzero { + #[new] + fn py_new(value: i32) -> PyResult { + if value == 0 { + Err(PyValueError::new_err("cannot be zero")) + } else { + Ok(Nonzero(value)) + } + } +} +``` + +As you can see, the Rust method name is not important here; this way you can +still use `new()` for a Rust-level constructor. + +If no method marked with `#[new]` is declared, object instances can only be +created from Rust, but not from Python. + +For arguments, see the `Method arguments` section below. + +## Adding the class to a module + +The next step is to create the module initializer and add our class to it + +```rust +# use pyo3::prelude::*; +# #[pyclass] +# struct Number(i32); +# #[pymodule] -fn mymodule(_py: Python, m: &PyModule) -> PyResult<()> { - m.add_class::()?; +fn my_module(_py: Python<'_>, m: &PyModule) -> PyResult<()> { + m.add_class::()?; Ok(()) } ``` @@ -135,68 +195,16 @@ Python::with_gil(|py|{ ## Customizing the class -The `#[pyclass]` macro accepts the following parameters: +{{#include ../../pyo3-macros/docs/pyclass_parameters.md}} -* `name="XXX"` - Set the class name shown in Python code. By default, the struct name is used as the class name. -* `freelist=XXX` - The `freelist` parameter adds support of free allocation list to custom class. -The performance improvement applies to types that are often created and deleted in a row, -so that they can benefit from a freelist. `XXX` is a number of items for the free list. -* `gc` - Classes with the `gc` parameter participate in Python garbage collection. -If a custom class contains references to other Python objects that can be collected, the [`PyGCProtocol`]({{#PYO3_DOCS_URL}}/pyo3/class/gc/trait.PyGCProtocol.html) trait has to be implemented. -* `weakref` - Adds support for Python weak references. -* `extends=BaseType` - Use a custom base class. The base `BaseType` must implement `PyTypeInfo`. `enum` pyclasses can't use a custom base class. -* `subclass` - Allows Python classes to inherit from this class. `enum` pyclasses can't be inherited from. -* `dict` - Adds `__dict__` support, so that the instances of this type have a dictionary containing arbitrary instance variables. -* `unsendable` - Making it safe to expose `!Send` structs to Python, where all object can be accessed - by multiple threads. A class marked with `unsendable` panics when accessed by another thread. -* `module="XXX"` - Set the name of the module the class will be shown as defined in. If not given, the class - will be a virtual member of the `builtins` module. +[params-1]: {{#PYO3_DOCS_URL}}/pyo3/prelude/struct.PyAny.html +[params-2]: https://en.wikipedia.org/wiki/Free_list +[params-3]: https://doc.rust-lang.org/stable/std/marker/trait.Send.html +[params-4]: https://doc.rust-lang.org/stable/std/rc/struct.Rc.html +[params-5]: https://doc.rust-lang.org/stable/std/sync/struct.Rc.html +[params-6]: https://docs.python.org/3/library/weakref.html -## Constructor - -By default it is not possible to create an instance of a custom class from Python code. -To declare a constructor, you need to define a method and annotate it with the `#[new]` -attribute. Only Python's `__new__` method can be specified, `__init__` is not available. - -```rust -# use pyo3::prelude::*; -#[pyclass] -struct MyClass { - # #[allow(dead_code)] - num: i32, -} - -#[pymethods] -impl MyClass { - #[new] - fn new(num: i32) -> Self { - MyClass { num } - } -} -``` - -Alternatively, if your `new` method may fail you can return `PyResult`. -```rust -# use pyo3::prelude::*; -#[pyclass] -struct MyClass { - # #[allow(dead_code)] - num: i32, -} - -#[pymethods] -impl MyClass { - #[new] - fn new(num: i32) -> PyResult { - Ok(MyClass { num }) - } -} -``` - -If no method marked with `#[new]` is declared, object instances can only be -created from Rust, but not from Python. - -For arguments, see the `Method arguments` section below. +These parameters are covered in various sections of this guide. ### Return type @@ -259,7 +267,7 @@ impl SubClass { (SubClass { val2: 15 }, BaseClass::new()) } - fn method2(self_: PyRef) -> PyResult { + fn method2(self_: PyRef<'_, Self>) -> PyResult { let super_ = self_.as_ref(); // Get &BaseClass super_.method().map(|x| x * self_.val2) } @@ -278,9 +286,9 @@ impl SubSubClass { .add_subclass(SubSubClass{val3: 20}) } - fn method3(self_: PyRef) -> PyResult { + fn method3(self_: PyRef<'_, Self>) -> PyResult { let v = self_.val3; - let super_ = self_.into_super(); // Get PyRef + let super_ = self_.into_super(); // Get PyRef<'_, SubClass> SubClass::method2(super_).map(|x| x * v) } } @@ -315,7 +323,7 @@ impl DictWithCounter { fn new() -> Self { Self::default() } - fn set(mut self_: PyRefMut, key: String, value: &PyAny) -> PyResult<()> { + 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())? }; @@ -510,7 +518,7 @@ gets injected by the method wrapper, e.g. # } #[pymethods] impl MyClass { - fn method2(&self, py: Python) -> PyResult { + fn method2(&self, py: Python<'_>) -> PyResult { Ok(10) } } @@ -702,7 +710,7 @@ num=-1 ## Making class method signatures available to Python -The [`#[pyo3(text_signature = "...")]`](./function.md#text_signature) option for `#[pyfunction]` also works for classes and methods: +The [`text_signature = "..."`](./function.md#text_signature) option for `#[pyfunction]` also works for classes and methods: ```rust # #![allow(dead_code)] @@ -710,8 +718,7 @@ use pyo3::prelude::*; use pyo3::types::PyType; // it works even if the item is not documented: -#[pyclass] -#[pyo3(text_signature = "(c, d, /)")] +#[pyclass(text_signature = "(c, d, /)")] struct MyClass {} #[pymethods] @@ -920,9 +927,9 @@ enum BadSubclass{ ## Implementation details -The `#[pyclass]` macros rely on a lot of conditional code generation: each `#[pyclass]` can optionally have a `#[pymethods]` block as well as several different possible `#[pyproto]` trait implementations. +The `#[pyclass]` macros rely on a lot of conditional code generation: each `#[pyclass]` can optionally have a `#[pymethods]` block. -To support this flexibility the `#[pyclass]` macro expands to a blob of boilerplate code which sets up the structure for ["dtolnay specialization"](https://github.com/dtolnay/case-studies/blob/master/autoref-specialization/README.md). This implementation pattern enables the Rust compiler to use `#[pymethods]` and `#[pyproto]` implementations when they are present, and fall back to default (empty) definitions when they are not. +To support this flexibility the `#[pyclass]` macro expands to a blob of boilerplate code which sets up the structure for ["dtolnay specialization"](https://github.com/dtolnay/case-studies/blob/master/autoref-specialization/README.md). This implementation pattern enables the Rust compiler to use `#[pymethods]` implementations when they are present, and fall back to default (empty) definitions when they are not. This simple technique works for the case when there is zero or one implementations. To support multiple `#[pymethods]` for a `#[pyclass]` (in the [`multiple-pymethods`] feature), a registry mechanism provided by the [`inventory`](https://github.com/dtolnay/inventory) crate is used instead. This collects `impl`s at library load time, but isn't supported on all platforms. See [inventory: how it works](https://github.com/dtolnay/inventory#how-it-works) for more details. @@ -941,8 +948,8 @@ unsafe impl ::pyo3::type_object::PyTypeInfo for MyClass { const NAME: &'static str = "MyClass"; const MODULE: ::std::option::Option<&'static str> = ::std::option::Option::None; #[inline] - fn type_object_raw(py: ::pyo3::Python<'_>) -> *mut ::pyo3::ffi::PyTypeObject { - use ::pyo3::type_object::LazyStaticType; + fn type_object_raw(py: pyo3::Python<'_>) -> *mut pyo3::ffi::PyTypeObject { + use pyo3::type_object::LazyStaticType; static TYPE_OBJECT: LazyStaticType = LazyStaticType::new(); TYPE_OBJECT.get_or_init::(py) } @@ -964,15 +971,14 @@ impl<'a> ::pyo3::derive_utils::ExtractExt<'a> for &'a MyClass { type Target = ::pyo3::PyRef<'a, MyClass>; } -impl ::pyo3::IntoPy<::pyo3::PyObject> for MyClass { - fn into_py(self, py: ::pyo3::Python) -> ::pyo3::PyObject { - ::pyo3::IntoPy::into_py(::pyo3::Py::new(py, self).unwrap(), py) +impl pyo3::IntoPy for MyClass { + fn into_py(self, py: pyo3::Python<'_>) -> pyo3::PyObject { + pyo3::IntoPy::into_py(pyo3::Py::new(py, self).unwrap(), py) } } impl pyo3::impl_::pyclass::PyClassImpl for MyClass { const DOC: &'static str = "Class for demonstration\u{0}"; - const IS_GC: bool = false; const IS_BASETYPE: bool = false; const IS_SUBCLASS: bool = false; type Layout = PyCell; @@ -987,21 +993,6 @@ impl pyo3::impl_::pyclass::PyClassImpl for MyClass { visitor(&INTRINSIC_ITEMS); visitor(collector.py_methods()); } - fn get_new() -> Option { - use pyo3::impl_::pyclass::*; - let collector = PyClassImplCollector::::new(); - collector.new_impl() - } - fn get_alloc() -> Option { - use pyo3::impl_::pyclass::*; - let collector = PyClassImplCollector::::new(); - collector.alloc_impl() - } - fn get_free() -> Option { - use pyo3::impl_::pyclass::*; - let collector = PyClassImplCollector::::new(); - collector.free_impl() - } } impl ::pyo3::impl_::pyclass::PyClassDescriptors diff --git a/guide/src/class/call.md b/guide/src/class/call.md index c9fe1217..9a470373 100644 --- a/guide/src/class/call.md +++ b/guide/src/class/call.md @@ -14,7 +14,7 @@ is linked at the end. An example crate containing this pyclass can be found [here](https://github.com/PyO3/pyo3/tree/main/examples/decorator) -```rust +```rust,ignore {{#include ../../../examples/decorator/src/lib.rs}} ``` diff --git a/guide/src/class/numeric.md b/guide/src/class/numeric.md new file mode 100644 index 00000000..a93d2239 --- /dev/null +++ b/guide/src/class/numeric.md @@ -0,0 +1,449 @@ +# Emulating numeric types + +At this point we have a `Number` class that we can't actually do any math on! + +Before proceeding, we should think about how we want to handle overflows. There are three obvious solutions: +- We can have infinite precision just like Python's `int`. However that would be quite boring - we'd + be reinventing the wheel. +- We can raise exceptions whenever `Number` overflows, but that makes the API painful to use. +- We can wrap around the boundary of `i32`. This is the approach we'll take here. To do that we'll just forward to `i32`'s + `wrapping_*` methods. + +### Fixing our constructor + +Let's address the first overflow, in `Number`'s constructor: + +```python +from my_module import Number + +n = Number(1 << 1337) +``` + +```text +Traceback (most recent call last): + File "example.py", line 3, in + n = Number(1 << 1337) +OverflowError: Python int too large to convert to C long +``` + +Instead of relying on the default [`FromPyObject`] extraction to parse arguments, we can specify our +own extraction function, using the `#[pyo3(from_py_with = "...")]` attribute. Unfortunately PyO3 +doesn't provide a way to wrap Python integers out of the box, but we can do a Python call to mask it +and cast it to an `i32`. + +```rust +# #![allow(dead_code)] +use pyo3::prelude::*; + +fn wrap(obj: &PyAny) -> Result { + let val = obj.call_method1("__and__", (0xFFFFFFFF_u32,))?; + let val: u32 = val.extract()?; + // 👇 This intentionally overflows! + Ok(val as i32) +} +``` +We also add documentation, via `///` comments and the `#[pyo3(text_signature = "...")]` attribute, both of which are visible to Python users. + +```rust +# #![allow(dead_code)] +use pyo3::prelude::*; + +fn wrap(obj: &PyAny) -> Result { + let val = obj.call_method1("__and__", (0xFFFFFFFF_u32,))?; + let val: u32 = val.extract()?; + Ok(val as i32) +} + +/// Did you ever hear the tragedy of Darth Signed The Overfloweth? I thought not. +/// It's not a story C would tell you. It's a Rust legend. +#[pyclass(module = "my_module")] +#[pyo3(text_signature = "(int)")] +struct Number(i32); + +#[pymethods] +impl Number { + #[new] + fn new(#[pyo3(from_py_with = "wrap")] value: i32) -> Self { + Self(value) + } +} +``` + + +With that out of the way, let's implement some operators: +```rust +use std::convert::TryInto; +use pyo3::exceptions::{PyZeroDivisionError, PyValueError}; + +# use pyo3::prelude::*; +# +# #[pyclass] +# struct Number(i32); +# +#[pymethods] +impl Number { + fn __add__(&self, other: &Self) -> Self { + Self(self.0.wrapping_add(other.0)) + } + + fn __sub__(&self, other: &Self) -> Self { + Self(self.0.wrapping_sub(other.0)) + } + + fn __mul__(&self, other: &Self) -> Self { + Self(self.0.wrapping_mul(other.0)) + } + + fn __truediv__(&self, other: &Self) -> PyResult { + match self.0.checked_div(other.0) { + Some(i) => Ok(Self(i)), + None => Err(PyZeroDivisionError::new_err("division by zero")), + } + } + + fn __floordiv__(&self, other: &Self) -> PyResult { + match self.0.checked_div(other.0) { + Some(i) => Ok(Self(i)), + None => Err(PyZeroDivisionError::new_err("division by zero")), + } + } + + fn __rshift__(&self, other: &Self) -> PyResult { + match other.0.try_into() { + Ok(rhs) => Ok(Self(self.0.wrapping_shr(rhs))), + Err(_) => Err(PyValueError::new_err("negative shift count")), + } + } + + fn __lshift__(&self, other: &Self) -> PyResult { + match other.0.try_into() { + Ok(rhs) => Ok(Self(self.0.wrapping_shl(rhs))), + Err(_) => Err(PyValueError::new_err("negative shift count")), + } + } +} +``` + +### Unary arithmethic operations + +```rust +# use pyo3::prelude::*; +# +# #[pyclass] +# struct Number(i32); +# +#[pymethods] +impl Number { + fn __pos__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> { + slf + } + + fn __neg__(&self) -> Self { + Self(-self.0) + } + + fn __abs__(&self) -> Self { + Self(self.0.abs()) + } + + fn __invert__(&self) -> Self { + Self(!self.0) + } +} +``` + +### Support for the `complex()`, `int()` and `float()` built-in functions. + +```rust +# use pyo3::prelude::*; +# +# #[pyclass] +# struct Number(i32); +# +use pyo3::types::PyComplex; + +#[pymethods] +impl Number { + fn __int__(&self) -> i32 { + self.0 + } + + fn __float__(&self) -> f64 { + self.0 as f64 + } + + fn __complex__<'py>(&self, py: Python<'py>) -> &'py PyComplex { + PyComplex::from_doubles(py, self.0 as f64, 0.0) + } +} +``` + +We do not implement the in-place operations like `__iadd__` because we do not wish to mutate `Number`. +Similarly we're not interested in supporting operations with different types, so we do not implement + the reflected operations like `__radd__` either. + +Now Python can use our `Number` class: + +```python +from my_module import Number + +def hash_djb2(s: str): + ''' + A version of Daniel J. Bernstein's djb2 string hashing algorithm + Like many hashing algorithms, it relies on integer wrapping. + ''' + + n = Number(0) + five = Number(5) + + for x in s: + n = Number(ord(x)) + ((n << five) - n) + return n + +assert hash_djb2('l50_50') == Number(-1152549421) +``` + +### Final code + +```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 { + let val = obj.call_method1("__and__", (0xFFFFFFFF_u32,))?; + let val: u32 = val.extract()?; + Ok(val as i32) +} +/// Did you ever hear the tragedy of Darth Signed The Overfloweth? I thought not. +/// It's not a story C would tell you. It's a Rust legend. +#[pyclass(module = "my_module")] +#[pyo3(text_signature = "(int)")] +struct Number(i32); + +#[pymethods] +impl Number { + #[new] + fn new(#[pyo3(from_py_with = "wrap")] value: i32) -> Self { + Self(value) + } + + fn __repr__(&self) -> String { + format!("Number({})", self.0) + } + + fn __str__(&self) -> String { + self.0.to_string() + } + + fn __hash__(&self) -> u64 { + let mut hasher = DefaultHasher::new(); + self.0.hash(&mut hasher); + hasher.finish() + } + + fn __richcmp__(&self, other: &Self, op: CompareOp) -> PyResult { + match op { + CompareOp::Lt => Ok(self.0 < other.0), + CompareOp::Le => Ok(self.0 <= other.0), + CompareOp::Eq => Ok(self.0 == other.0), + CompareOp::Ne => Ok(self.0 != other.0), + CompareOp::Gt => Ok(self.0 > other.0), + CompareOp::Ge => Ok(self.0 >= other.0), + } + } + + fn __bool__(&self) -> bool { + self.0 != 0 + } + + fn __add__(&self, other: &Self) -> Self { + Self(self.0.wrapping_add(other.0)) + } + + fn __sub__(&self, other: &Self) -> Self { + Self(self.0.wrapping_sub(other.0)) + } + + fn __mul__(&self, other: &Self) -> Self { + Self(self.0.wrapping_mul(other.0)) + } + + fn __truediv__(&self, other: &Self) -> PyResult { + match self.0.checked_div(other.0) { + Some(i) => Ok(Self(i)), + None => Err(PyZeroDivisionError::new_err("division by zero")), + } + } + + fn __floordiv__(&self, other: &Self) -> PyResult { + match self.0.checked_div(other.0) { + Some(i) => Ok(Self(i)), + None => Err(PyZeroDivisionError::new_err("division by zero")), + } + } + + fn __rshift__(&self, other: &Self) -> PyResult { + match other.0.try_into() { + Ok(rhs) => Ok(Self(self.0.wrapping_shr(rhs))), + Err(_) => Err(PyValueError::new_err("negative shift count")), + } + } + + fn __lshift__(&self, other: &Self) -> PyResult { + match other.0.try_into() { + Ok(rhs) => Ok(Self(self.0.wrapping_shl(rhs))), + Err(_) => Err(PyValueError::new_err("negative shift count")), + } + } + + fn __xor__(&self, other: &Self) -> Self { + Self(self.0 ^ other.0) + } + + fn __or__(&self, other: &Self) -> Self { + Self(self.0 | other.0) + } + + fn __and__(&self, other: &Self) -> Self { + Self(self.0 & other.0) + } + + fn __int__(&self) -> i32 { + self.0 + } + + fn __float__(&self) -> f64 { + self.0 as f64 + } + + fn __complex__<'py>(&self, py: Python<'py>) -> &'py PyComplex { + PyComplex::from_doubles(py, self.0 as f64, 0.0) + } +} + +#[pymodule] +fn my_module(_py: Python<'_>, m: &PyModule) -> PyResult<()> { + m.add_class::()?; + Ok(()) +} +# const SCRIPT: &'static str = r#" +# def hash_djb2(s: str): +# n = Number(0) +# five = Number(5) +# +# for x in s: +# n = Number(ord(x)) + ((n << five) - n) +# return n +# +# assert hash_djb2('l50_50') == Number(-1152549421) +# assert hash_djb2('logo') == Number(3327403) +# assert hash_djb2('horizon') == Number(1097468315) +# +# +# assert Number(2) + Number(2) == Number(4) +# assert Number(2) + Number(2) != Number(5) +# +# assert Number(13) - Number(7) == Number(6) +# assert Number(13) - Number(-7) == Number(20) +# +# assert Number(13) / Number(7) == Number(1) +# assert Number(13) // Number(7) == Number(1) +# +# assert Number(13) * Number(7) == Number(13*7) +# +# assert Number(13) > Number(7) +# assert Number(13) < Number(20) +# assert Number(13) == Number(13) +# assert Number(13) >= Number(7) +# assert Number(13) <= Number(20) +# assert Number(13) == Number(13) +# +# +# assert (True if Number(1) else False) +# assert (False if Number(0) else True) +# +# +# assert int(Number(13)) == 13 +# assert float(Number(13)) == 13 +# assert Number.__doc__ == "Did you ever hear the tragedy of Darth Signed The Overfloweth? I thought not.\nIt's not a story C would tell you. It's a Rust legend." +# assert Number(12345234523452) == Number(1498514748) +# try: +# import inspect +# assert inspect.signature(Number).__str__() == '(int)' +# except ValueError: +# # Not supported with `abi3` before Python 3.10 +# pass +# assert Number(1337).__str__() == '1337' +# assert Number(1337).__repr__() == 'Number(1337)' +"#; + +# +# use pyo3::type_object::PyTypeObject; +# +# fn main() -> PyResult<()> { +# Python::with_gil(|py| -> PyResult<()> { +# let globals = PyModule::import(py, "__main__")?.dict(); +# globals.set_item("Number", Number::type_object(py))?; +# +# py.run(SCRIPT, Some(globals), None)?; +# Ok(()) +# }) +# } + +``` + +## Appendix: Writing some unsafe code + +At the beginning of this chapter we said that PyO3 doesn't provide a way to wrap Python integers out +of the box but that's a half truth. There's not a PyO3 API for it, but there's a Python C API +function that does: + +```c +unsigned long PyLong_AsUnsignedLongMask(PyObject *obj) +``` + +We can call this function from Rust by using [`pyo3::ffi::PyLong_AsUnsignedLongMask`]. This is an *unsafe* +function, which means we have to use an unsafe block to call it and take responsibility for upholding +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). +- 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 +# #![allow(dead_code)] +use std::os::raw::c_ulong; +use pyo3::prelude::*; +use pyo3::ffi; +use pyo3::conversion::AsPyPointer; + +fn wrap(obj: &PyAny) -> Result { + let py: Python<'_> = obj.py(); + + unsafe { + let ptr = obj.as_ptr(); + + let ret: c_ulong = ffi::PyLong_AsUnsignedLongMask(ptr); + if ret == c_ulong::MAX { + if let Some(err) = PyErr::take(py) { + return Err(err); + } + } + + Ok(ret as i32) + } +} +``` + +[`PyErr::take`]: https://docs.rs/pyo3/latest/pyo3/prelude/struct.PyErr.html#method.take +[`Python`]: https://docs.rs/pyo3/latest/pyo3/struct.Python.html +[`FromPyObject`]: https://docs.rs/pyo3/latest/pyo3/conversion/trait.FromPyObject.html +[`pyo3::ffi::PyLong_AsUnsignedLongMask`]: https://docs.rs/pyo3/latest/pyo3/ffi/fn.PyLong_AsUnsignedLongMask.html \ No newline at end of file diff --git a/guide/src/class/object.md b/guide/src/class/object.md new file mode 100644 index 00000000..ade91da8 --- /dev/null +++ b/guide/src/class/object.md @@ -0,0 +1,232 @@ +# Basic object customization + +Recall the `Number` class from the previous chapter: + +```rust +use pyo3::prelude::*; + +#[pyclass] +struct Number(i32); + +#[pymethods] +impl Number { + #[new] + fn new(value: i32) -> Self { + Self(value) + } +} + +#[pymodule] +fn my_module(_py: Python<'_>, m: &PyModule) -> PyResult<()> { + m.add_class::()?; + Ok(()) +} +``` + +At this point Python code can import the module, access the class and create class instances - but +nothing else. + +```python +from my_module import Number + +n = Number(5) +print(n) +``` + +```text + +``` + +### String representations + +It can't even print an user-readable representation of itself! We can fix that by defining the + `__repr__` and `__str__` methods inside a `#[pymethods]` block. We do this by accessing the value + contained inside `Number`. + + ```rust +# use pyo3::prelude::*; +# +# #[pyclass] +# struct Number(i32); +# +#[pymethods] +impl Number { + // For `__repr__` we want to return a string that Python code could use to recreate + // the `Number`, like `Number(5)` for example. + fn __repr__(&self) -> String { + // We use the `format!` macro to create a string. Its first argument is a + // format string, followed by any number of parameters which replace the + // `{}`'s in the format string. + // + // 👇 Tuple field access in Rust uses a dot + format!("Number({})", self.0) + } + + // `__str__` is generally used to create an "informal" representation, so we + // just forward to `i32`'s `ToString` trait implementation to print a bare number. + fn __str__(&self) -> String { + self.0.to_string() + } +} +``` + +### Hashing + + +Let's also implement hashing. We'll just hash the `i32`. For that we need a [`Hasher`]. The one +provided by `std` is [`DefaultHasher`], which uses the [SipHash] algorithm. + +```rust +use std::collections::hash_map::DefaultHasher; + +// Required to call the `.hash` and `.finish` methods, which are defined on traits. +use std::hash::{Hash, Hasher}; + +# use pyo3::prelude::*; +# +# #[pyclass] +# struct Number(i32); +# +#[pymethods] +impl Number { + fn __hash__(&self) -> u64 { + let mut hasher = DefaultHasher::new(); + self.0.hash(&mut hasher); + hasher.finish() + } +} +``` + +> **Note**: When implementing `__hash__` and comparisons, it is important that the following property holds: +> +> ```text +> k1 == k2 -> hash(k1) == hash(k2) +> ``` +> +> In other words, if two keys are equal, their hashes must also be equal. In addition you must take +> care that your classes' hash doesn't change during its lifetime. In this tutorial we do that by not +> letting Python code change our `Number` class. In other words, it is immutable. +> +> By default, all `#[pyclass]` types have a default hash implementation from Python. +> Types which should not be hashable can override this by setting `__hash__` to None. +> This is the same mechanism as for a pure-Python class. This is done like so: +> +> ```rust +> # use pyo3::prelude::*; +> #[pyclass] +> struct NotHashable { } +> +> #[pymethods] +> impl NotHashable { +> #[classattr] +> const __hash__: Option> = None; +>} +> ``` + +### Comparisons + +Unlike in Python, PyO3 does not provide the magic comparison methods you might expect like `__eq__`, + `__lt__` and so on. Instead you have to implement all six operations at once with `__richcmp__`. +This method will be called with a value of `CompareOp` depending on the operation. + +```rust +use pyo3::class::basic::CompareOp; + +# use pyo3::prelude::*; +# +# #[pyclass] +# struct Number(i32); +# +#[pymethods] +impl Number { + fn __richcmp__(&self, other: &Self, op: CompareOp) -> PyResult { + match op { + CompareOp::Lt => Ok(self.0 < other.0), + CompareOp::Le => Ok(self.0 <= other.0), + CompareOp::Eq => Ok(self.0 == other.0), + CompareOp::Ne => Ok(self.0 != other.0), + CompareOp::Gt => Ok(self.0 > other.0), + CompareOp::Ge => Ok(self.0 >= other.0), + } + } +} +``` + +### Truthyness + +We'll consider `Number` to be `True` if it is nonzero: + +```rust +# use pyo3::prelude::*; +# +# #[pyclass] +# struct Number(i32); +# +#[pymethods] +impl Number { + fn __bool__(&self) -> bool { + self.0 != 0 + } +} +``` + +### Final code + +```rust +use std::collections::hash_map::DefaultHasher; +use std::hash::{Hash, Hasher}; + +use pyo3::prelude::*; +use pyo3::class::basic::CompareOp; + +#[pyclass] +struct Number(i32); + +#[pymethods] +impl Number { + #[new] + fn new(value: i32) -> Self { + Self(value) + } + + fn __repr__(&self) -> String { + format!("Number({})", self.0) + } + + fn __str__(&self) -> String { + self.0.to_string() + } + + fn __hash__(&self) -> u64 { + let mut hasher = DefaultHasher::new(); + self.0.hash(&mut hasher); + hasher.finish() + } + + fn __richcmp__(&self, other: &Self, op: CompareOp) -> PyResult { + match op { + CompareOp::Lt => Ok(self.0 < other.0), + CompareOp::Le => Ok(self.0 <= other.0), + CompareOp::Eq => Ok(self.0 == other.0), + CompareOp::Ne => Ok(self.0 != other.0), + CompareOp::Gt => Ok(self.0 > other.0), + CompareOp::Ge => Ok(self.0 >= other.0), + } + } + + fn __bool__(&self) -> bool { + self.0 != 0 + } +} + +#[pymodule] +fn my_module(_py: Python<'_>, m: &PyModule) -> PyResult<()> { + m.add_class::()?; + Ok(()) +} +``` + +[`Hash`]: https://doc.rust-lang.org/std/hash/trait.Hash.html +[`Hasher`]: https://doc.rust-lang.org/std/hash/trait.Hasher.html +[`DefaultHasher`]: https://doc.rust-lang.org/std/collections/hash_map/struct.DefaultHasher.html +[SipHash]: https://en.wikipedia.org/wiki/SipHash \ No newline at end of file diff --git a/guide/src/class/protocols.md b/guide/src/class/protocols.md index d0d570de..320307d0 100644 --- a/guide/src/class/protocols.md +++ b/guide/src/class/protocols.md @@ -1,19 +1,15 @@ -# Class customizations +# Magic methods and slots 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. 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. There are two ways in which this can be done: - - [Experimental for PyO3 0.15, may change slightly in PyO3 0.16] In `#[pymethods]`, if the name of the method is a recognised magic method, PyO3 will place it in the type object automatically. - - [Stable, but expected to be deprecated in PyO3 0.16] In special traits combined with the `#[pyproto]` attribute. + - [New in PyO3 0.15, recommended in PyO3 0.16] In `#[pymethods]`, if the name of the method is a recognised magic method, PyO3 will place it in the type object automatically. + - [Deprecated in PyO3 0.16] In special traits combined with the `#[pyproto]` attribute. (There are also many magic methods which don't have a special slot, such as `__dir__`. These methods can be implemented as normal in `#[pymethods]`.) -This chapter gives a brief overview of the available methods. An in depth example is given in the following sub-chapters. - -### Magic methods in `#[pymethods]` - -In PyO3 0.15, 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 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. 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 @@ -26,8 +22,8 @@ When PyO3 handles a magic method, a couple of changes apply compared to other `# The following sections list of all magic methods PyO3 currently handles. 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` and - `self_: PyRefMut`, as described [here](../class.md#inheritance). + `&self`, `&mut self` or a `PyCell` 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`. - `object` means that any type is allowed that can be extracted from a Python @@ -41,14 +37,16 @@ given signatures should be interpreted as follows: string object. This is indicated by `object (Python type)`. -#### Basic object customization +### Basic object customization - `__str__() -> object (str)` - `__repr__() -> object (str)` + - `__hash__() -> isize` + + Objects that compare equal must have the same hash value.
Disabling Python's default hash - By default, all `#[pyclass]` types have a default hash implementation from Python. Types which should not be hashable can override this by setting `__hash__` to `None`. This is the same mechanism as for a pure-Python class. This is done like so: ```rust @@ -66,33 +64,152 @@ given signatures should be interpreted as follows:
- `__richcmp__(, object, pyo3::basic::CompareOp) -> object` + + Overloads Python comparison operations (`==`, `!=`, `<`, `<=`, `>`, and `>=`). + The `CompareOp` argument indicates the comparison operation being performed. +
+ Return type + The return type will normally be `PyResult`, but any Python object can be returned. + If the `object` is not of the type specified in the signature, the generated code will + automatically `return NotImplemented`. +
+ - `__getattr__(, object) -> object` - - `__setattr__(, object, object) -> ()` + - `__getattribute__(, object) -> object` +
+ Differences between `__getattr__` and `__getattribute__` + As in Python, `__getattr__` is only called if the attribute is not found + by normal attribute lookup. `__getattribute__`, on the other hand, is + called for *every* attribute access. If it wants to access existing + attributes on `self`, it needs to be very careful not to introduce + infinite recursion, and use `baseclass.__getattribute__()`. +
+ + - `__setattr__(, value: object) -> ()` - `__delattr__(, object) -> ()` + + Overrides attribute access. + - `__bool__() -> bool` + Determines the "truthyness" of an object. + - `__call__(, ...) -> object` - here, any argument list can be defined as for normal `pymethods` -#### Iterable objects +### Iterable objects + +Iterators can be defined using these methods: - `__iter__() -> object` - `__next__() -> Option or IterNextOutput` ([see details](#returning-a-value-from-iteration)) -#### Awaitable objects +Returning `None` from `__next__` indicates that that there are no further items. + +Example: + +```rust +use pyo3::prelude::*; + +#[pyclass] +struct MyIterator { + iter: Box + Send>, +} + +#[pymethods] +impl MyIterator { + fn __iter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> { + slf + } + fn __next__(mut slf: PyRefMut<'_, Self>) -> Option { + slf.iter.next() + } +} +``` + +In many cases you'll have a distinction between the type being iterated over +(i.e. the *iterable*) and the iterator it provides. In this case, the iterable +only needs to implement `__iter__()` while the iterator must implement both +`__iter__()` and `__next__()`. For example: + +```rust +# use pyo3::prelude::*; + +#[pyclass] +struct Iter { + inner: std::vec::IntoIter, +} + +#[pymethods] +impl Iter { + fn __iter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> { + slf + } + + fn __next__(mut slf: PyRefMut<'_, Self>) -> Option { + slf.inner.next() + } +} + +#[pyclass] +struct Container { + iter: Vec, +} + +#[pymethods] +impl Container { + fn __iter__(slf: PyRef<'_, Self>) -> PyResult> { + let iter = Iter { + inner: slf.iter.clone().into_iter(), + }; + Py::new(slf.py(), iter) + } +} + +# Python::with_gil(|py| { +# let container = Container { iter: vec![1, 2, 3, 4] }; +# let inst = pyo3::PyCell::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]"); +# }); +``` + +For more details on Python's iteration protocols, check out [the "Iterator Types" section of the library +documentation](https://docs.python.org/library/stdtypes.html#iterator-types). + +#### Returning a value from iteration + +This guide has so far shown how to use `Option` to implement yielding values +during iteration. In Python a generator can also return a value. To express +this in Rust, PyO3 provides the [`IterNextOutput`] enum to both `Yield` values +and `Return` a final value - see its docs for further details and an example. + +### Awaitable objects - `__await__() -> object` - `__aiter__() -> object` - `__anext__() -> Option or IterANextOutput` -#### Mapping & Sequence types +### Mapping & Sequence types - `__len__() -> usize` + + Implements the built-in function `len()` for the sequence. + - `__contains__(, object) -> bool` + + Implements membership test operators. + Should return true if `item` is in `self`, false otherwise. + For objects that don’t define `__contains__()`, the membership test simply + traverses the sequence until it finds a match. +
Disabling Python's default contains - By default, all `#[pyclass]` types with an `__iter__` method support a default implementation of the `in` operator. Types which do not want this can override this by setting `__contains__` to `None`. This is the same mechanism as for a pure-Python class. This is done like so: + By default, all `#[pyclass]` types with an `__iter__` method support a + default implementation of the `in` operator. Types which do not want this + can override this by setting `__contains__` to `None`. This is the same + mechanism as for a pure-Python class. This is done like so: ```rust # use pyo3::prelude::*; @@ -107,38 +224,62 @@ given signatures should be interpreted as follows: } ```
+ - `__getitem__(, object) -> object` + + Implements retrieval of the `self[a]` element. + + *Note:* Negative integer indexes are not handled specially. + - `__setitem__(, object, object) -> ()` + + Implements assignment to the `self[a]` element. + Should only be implemented if elements can be replaced. + - `__delitem__(, object) -> ()` -#### Descriptors + Implements deletion of the `self[a]` element. + Should only be implemented if elements can be deleted. + + * `fn __concat__(&self, other: impl FromPyObject) -> PyResult` + + Concatenates two sequences. + Used by the `+` operator, after trying the numeric addition via + the `__add__` and `__radd__` methods. + + * `fn __repeat__(&self, count: isize) -> PyResult` + + Repeats the sequence `count` times. + Used by the `*` operator, after trying the numeric multiplication via + the `__mul__` and `__rmul__` methods. + + * `fn __inplace_concat__(&self, other: impl FromPyObject) -> PyResult` + + Concatenates two sequences. + Used by the `+=` operator, after trying the numeric addition via + the `__iadd__` method. + + * `fn __inplace_repeat__(&self, count: isize) -> PyResult` + + Concatenates two sequences. + Used by the `*=` operator, after trying the numeric multiplication via + the `__imul__` method. + + +### Descriptors - `__get__(, object, object) -> object` - `__set__(, object, object) -> ()` - `__delete__(, object) -> ()` -#### Numeric types +### Numeric types + +Binary arithmetic operations (`+`, `-`, `*`, `@`, `/`, `//`, `%`, `divmod()`, +`pow()` and `**`, `<<`, `>>`, `&`, `^`, and `|`) and their reflected versions: + +(If the `object` is not of the type specified in the signature, the generated code +will automatically `return NotImplemented`.) - - `__pos__() -> object` - - `__neg__() -> object` - - `__abs__() -> object` - - `__invert__() -> object` - - `__index__() -> object (int)` - - `__int__() -> object (int)` - - `__float__() -> object (float)` - - `__iadd__(, object) -> ()` - - `__isub__(, object) -> ()` - - `__imul__(, object) -> ()` - - `__imatmul__(, object) -> ()` - - `__itruediv__(, object) -> ()` - - `__ifloordiv__(, object) -> ()` - - `__imod__(, object) -> ()` - - `__ipow__(, object, object) -> ()` - - `__ilshift__(, object) -> ()` - - `__irshift__(, object) -> ()` - - `__iand__(, object) -> ()` - - `__ixor__(, object) -> ()` - - `__ior__(, object) -> ()` - `__add__(, object) -> object` - `__radd__(, object) -> object` - `__sub__(, object) -> object` @@ -168,20 +309,91 @@ given signatures should be interpreted as follows: - `__pow__(, object, object) -> object` - `__rpow__(, object, object) -> object` -#### Buffer objects +In-place assignment operations (`+=`, `-=`, `*=`, `@=`, `/=`, `//=`, `%=`, +`**=`, `<<=`, `>>=`, `&=`, `^=`, `|=`): + + - `__iadd__(, object) -> ()` + - `__isub__(, object) -> ()` + - `__imul__(, object) -> ()` + - `__imatmul__(, object) -> ()` + - `__itruediv__(, object) -> ()` + - `__ifloordiv__(, object) -> ()` + - `__imod__(, object) -> ()` + - `__ipow__(, object, object) -> ()` + - `__ilshift__(, object) -> ()` + - `__irshift__(, object) -> ()` + - `__iand__(, object) -> ()` + - `__ixor__(, object) -> ()` + - `__ior__(, object) -> ()` + +Unary operations (`-`, `+`, `abs()` and `~`): + + - `__pos__() -> object` + - `__neg__() -> object` + - `__abs__() -> object` + - `__invert__() -> object` + +Coercions: + + - `__index__() -> object (int)` + - `__int__() -> object (int)` + - `__float__() -> object (float)` + +### Buffer objects - `__getbuffer__(, *mut ffi::Py_buffer, flags) -> ()` - `__releasebuffer__(, *mut ffi::Py_buffer)` (no return value, not even `PyResult`) -#### Garbage Collector Integration +### Garbage Collector Integration + +If your type owns references to other Python objects, you will need to integrate +with Python's garbage collector so that the GC is aware of those references. To +do this, implement the two methods `__traverse__` and `__clear__`. These +correspond to the slots `tp_traverse` and `tp_clear` in the Python C API. +`__traverse__` must call `visit.call()` for each reference to another Python +object. `__clear__` must clear out any mutable references to other Python +objects (thus breaking reference cycles). Immutable references do not have to be +cleared, as every cycle must contain at least one mutable reference. + + - `__traverse__(, pyo3::class::gc::PyVisit<'_>) -> Result<(), pyo3::class::gc::PyTraverseError>` + - `__clear__() -> ()` + +Example: + +```rust +use pyo3::prelude::*; +use pyo3::PyTraverseError; +use pyo3::gc::PyVisit; + +#[pyclass] +struct ClassWithGCSupport { + obj: Option, +} + +#[pymethods] +impl ClassWithGCSupport { + fn __traverse__(&self, visit: PyVisit<'_>) -> Result<(), PyTraverseError> { + if let Some(obj) = &self.obj { + visit.call(obj)? + } + Ok(()) + } + + fn __clear__(&mut self) { + // Clear reference, this decrements ref counter. + self.obj = None; + } +} +``` + +[`IterNextOutput`]: {{#PYO3_DOCS_URL}}/pyo3/class/iter/enum.IterNextOutput.html -TODO; see [#1884](https://github.com/PyO3/pyo3/issues/1884) ### `#[pyproto]` traits PyO3 can use the `#[pyproto]` attribute in combination with special traits to implement the magic methods which need slots. The special traits are listed below. See also the [documentation for the `pyo3::class` module]({{#PYO3_DOCS_URL}}/pyo3/class/index.html). -Before PyO3 0.15 this was the only supported solution for implementing magic methods. Due to complexity in the implementation and usage, these traits are expected to be deprecated in PyO3 0.16 in favour of the `#[pymethods]` solution. +Due to complexity in the implementation and usage, these traits are deprecated in PyO3 0.16 in favour of the `#[pymethods]` solution. All `#[pyproto]` methods can return `T` instead of `PyResult` if the method implementation is infallible. In addition, if the return type is `()`, it can be omitted altogether. @@ -189,44 +401,15 @@ All `#[pyproto]` methods can return `T` instead of `PyResult` if the method i The [`PyObjectProtocol`] trait provides several basic customizations. -##### Attribute access - -To customize object attribute access, define the following methods: - + * `fn __str__(&self) -> PyResult>` + * `fn __repr__(&self) -> PyResult>` + * `fn __hash__(&self) -> PyResult` + * `fn __richcmp__(&self, other: impl FromPyObject, op: CompareOp) -> PyResult` * `fn __getattr__(&self, name: impl FromPyObject) -> PyResult>` * `fn __setattr__(&mut self, name: impl FromPyObject, value: impl FromPyObject) -> PyResult<()>` * `fn __delattr__(&mut self, name: impl FromPyObject) -> PyResult<()>` - -Each method corresponds to Python's `self.attr`, `self.attr = value` and `del self.attr` code. - -##### String Conversions - - * `fn __repr__(&self) -> PyResult>` - * `fn __str__(&self) -> PyResult>` - - Possible return types for `__str__` and `__repr__` are `PyResult` or `PyResult`. - -##### Comparison operators - - * `fn __richcmp__(&self, other: impl FromPyObject, op: CompareOp) -> PyResult` - - Overloads Python comparison operations (`==`, `!=`, `<`, `<=`, `>`, and `>=`). - The `op` argument indicates the comparison operation being performed. - The return type will normally be `PyResult`, but any Python object can be returned. - If `other` is not of the type specified in the signature, the generated code will - automatically `return NotImplemented`. - - * `fn __hash__(&self) -> PyResult` - - Objects that compare equal must have the same hash value. - The return type must be `PyResult` where `T` is one of Rust's primitive integer types. - -##### Other methods - * `fn __bool__(&self) -> PyResult` - Determines the "truthyness" of the object. - #### Emulating numeric types The [`PyNumberProtocol`] trait can be implemented to emulate [numeric types](https://docs.python.org/3/reference/datamodel.html#emulating-numeric-types). @@ -246,13 +429,7 @@ The [`PyNumberProtocol`] trait can be implemented to emulate [numeric types](htt * `fn __or__(lhs: impl FromPyObject, rhs: impl FromPyObject) -> PyResult` * `fn __xor__(lhs: impl FromPyObject, rhs: impl FromPyObject) -> PyResult` -These methods are called to implement the binary arithmetic operations -(`+`, `-`, `*`, `@`, `/`, `//`, `%`, `divmod()`, `pow()` and `**`, `<<`, `>>`, `&`, `^`, and `|`). - -If `rhs` is not of the type specified in the signature, the generated code -will automatically `return NotImplemented`. This is not the case for `lhs` -which must match signature or else raise a TypeError. - +These methods are called to implement the binary arithmetic operations. The reflected operations are also available: @@ -274,9 +451,6 @@ The reflected operations are also available: The code generated for these methods expect that all arguments match the signature, or raise a TypeError. -This trait also has support the augmented arithmetic assignments (`+=`, `-=`, -`*=`, `@=`, `/=`, `//=`, `%=`, `**=`, `<<=`, `>>=`, `&=`, `^=`, `|=`): - * `fn __iadd__(&'p mut self, other: impl FromPyObject) -> PyResult<()>` * `fn __isub__(&'p mut self, other: impl FromPyObject) -> PyResult<()>` * `fn __imul__(&'p mut self, other: impl FromPyObject) -> PyResult<()>` @@ -293,7 +467,7 @@ This trait also has support the augmented arithmetic assignments (`+=`, `-=`, * `fn __ixor__(&'p mut self, other: impl FromPyObject) -> PyResult<()>` -The following methods implement the unary arithmetic operations (`-`, `+`, `abs()` and `~`): +The following methods implement the unary arithmetic operations: * `fn __neg__(&'p self) -> PyResult` * `fn __pos__(&'p self) -> PyResult` @@ -304,9 +478,6 @@ Support for coercions: * `fn __int__(&'p self) -> PyResult` * `fn __float__(&'p self) -> PyResult` - -Other: - * `fn __index__(&'p self) -> PyResult` #### Emulating sequential containers (such as lists or tuples) @@ -405,152 +576,26 @@ For a mapping, the keys may be Python objects of arbitrary type. The same exceptions should be raised for improper key values as for the `__getitem__()` method. -#### Garbage Collector Integration - -If your type owns references to other Python objects, you will need to -integrate with Python's garbage collector so that the GC is aware of -those references. -To do this, implement the [`PyGCProtocol`] trait for your struct. -It includes two methods `__traverse__` and `__clear__`. -These correspond to the slots `tp_traverse` and `tp_clear` in the Python C API. -`__traverse__` must call `visit.call()` for each reference to another Python object. -`__clear__` must clear out any mutable references to other Python objects -(thus breaking reference cycles). Immutable references do not have to be cleared, -as every cycle must contain at least one mutable reference. -Example: -```rust -use pyo3::prelude::*; -use pyo3::PyTraverseError; -use pyo3::gc::{PyGCProtocol, PyVisit}; - -#[pyclass] -struct ClassWithGCSupport { - obj: Option, -} - -#[pyproto] -impl PyGCProtocol for ClassWithGCSupport { - fn __traverse__(&self, visit: PyVisit) -> Result<(), PyTraverseError> { - if let Some(obj) = &self.obj { - visit.call(obj)? - } - Ok(()) - } - - fn __clear__(&mut self) { - // Clear reference, this decrements ref counter. - self.obj = None; - } -} -``` - -Special protocol trait implementations have to be annotated with the `#[pyproto]` attribute. - -It is also possible to enable GC for custom classes using the `gc` parameter of the `pyclass` attribute. -i.e. `#[pyclass(gc)]`. In that case instances of custom class participate in Python garbage -collection, and it is possible to track them with `gc` module methods. When using the `gc` parameter, -it is *required* to implement the `PyGCProtocol` trait, failure to do so will result in an error -at compile time: - -```compile_fail -#[pyclass(gc)] -struct GCTracked {} // Fails because it does not implement PyGCProtocol -``` - #### Iterator Types -Iterators can be defined using the -[`PyIterProtocol`]({{#PYO3_DOCS_URL}}/pyo3/class/iter/trait.PyIterProtocol.html) trait. +Iterators can be defined using the [`PyIterProtocol`] trait. It includes two methods `__iter__` and `__next__`: - * `fn __iter__(slf: PyRefMut) -> PyResult>` - * `fn __next__(slf: PyRefMut) -> PyResult>>` + * `fn __iter__(slf: PyRefMut<'_, Self>) -> PyResult>` + * `fn __next__(slf: PyRefMut<'_, Self>) -> PyResult>>` -Returning `None` from `__next__` indicates that that there are no further items. -These two methods can be take either `PyRef` or `PyRefMut` as their +These two methods can be take either `PyRef<'_, Self>` or `PyRefMut<'_, Self>` as their first argument, so that mutable borrow can be avoided if needed. -Example: +For details, look at the `#[pymethods]` regarding iterator methods. -```rust -use pyo3::prelude::*; -use pyo3::PyIterProtocol; +#### Garbage Collector Integration -#[pyclass] -struct MyIterator { - iter: Box + Send>, -} - -#[pyproto] -impl PyIterProtocol for MyIterator { - fn __iter__(slf: PyRef) -> PyRef { - slf - } - fn __next__(mut slf: PyRefMut) -> Option { - slf.iter.next() - } -} -``` - -In many cases you'll have a distinction between the type being iterated over (i.e. the *iterable*) and the iterator it -provides. In this case, you should implement `PyIterProtocol` for both the iterable and the iterator, but the iterable -only needs to support `__iter__()` while the iterator must support both `__iter__()` and `__next__()`. The default -implementations in `PyIterProtocol` will ensure that the objects behave correctly in Python. For example: - -```rust -# use pyo3::prelude::*; -# use pyo3::PyIterProtocol; - -#[pyclass] -struct Iter { - inner: std::vec::IntoIter, -} - -#[pyproto] -impl PyIterProtocol for Iter { - fn __iter__(slf: PyRef) -> PyRef { - slf - } - - fn __next__(mut slf: PyRefMut) -> Option { - slf.inner.next() - } -} - -#[pyclass] -struct Container { - iter: Vec, -} - -#[pyproto] -impl PyIterProtocol for Container { - fn __iter__(slf: PyRef) -> PyResult> { - let iter = Iter { - inner: slf.iter.clone().into_iter(), - }; - Py::new(slf.py(), iter) - } -} - -# Python::with_gil(|py| { -# let container = Container { iter: vec![1, 2, 3, 4] }; -# let inst = pyo3::PyCell::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]"); -# }); -``` - -For more details on Python's iteration protocols, check out [the "Iterator Types" section of the library -documentation](https://docs.python.org/3/library/stdtypes.html#iterator-types). - -##### Returning a value from iteration - -This guide has so far shown how to use `Option` to implement yielding values during iteration. -In Python a generator can also return a value. To express this in Rust, PyO3 provides the -[`IterNextOutput`]({{#PYO3_DOCS_URL}}/pyo3/class/iter/enum.IterNextOutput.html) enum to -both `Yield` values and `Return` a final value - see its docs for further details and an example. +Implement the [`PyGCProtocol`] trait for your struct. +For details, look at the `#[pymethods]` regarding GC methods. [`PyGCProtocol`]: {{#PYO3_DOCS_URL}}/pyo3/class/gc/trait.PyGCProtocol.html [`PyMappingProtocol`]: {{#PYO3_DOCS_URL}}/pyo3/class/mapping/trait.PyMappingProtocol.html [`PyNumberProtocol`]: {{#PYO3_DOCS_URL}}/pyo3/class/number/trait.PyNumberProtocol.html [`PyObjectProtocol`]: {{#PYO3_DOCS_URL}}/pyo3/class/basic/trait.PyObjectProtocol.html [`PySequenceProtocol`]: {{#PYO3_DOCS_URL}}/pyo3/class/sequence/trait.PySequenceProtocol.html +[`PyIterProtocol`]: {{#PYO3_DOCS_URL}}/pyo3/class/iter/trait.PyIterProtocol.html diff --git a/guide/src/conversions/traits.md b/guide/src/conversions/traits.md index 619aac80..2705413d 100644 --- a/guide/src/conversions/traits.md +++ b/guide/src/conversions/traits.md @@ -35,7 +35,7 @@ structs is not supported. The derivation generates code that will attempt to access the attribute `my_string` on the Python object, i.e. `obj.getattr("my_string")`, and call `extract()` on the attribute. -``` +```rust use pyo3::prelude::*; #[derive(FromPyObject)] @@ -65,7 +65,7 @@ struct RustyStruct { By setting the `#[pyo3(item)]` attribute on the field, PyO3 will attempt to extract the value by calling the `get_item` method on the Python object. -``` +```rust use pyo3::prelude::*; @@ -90,7 +90,7 @@ struct RustyStruct { The argument passed to `getattr` and `get_item` can also be configured: -``` +```rust use pyo3::prelude::*; #[derive(FromPyObject)] @@ -135,7 +135,7 @@ Tuple structs are also supported but do not allow customizing the extraction. Th always assumed to be a Python tuple with the same length as the Rust type, the `n`th field is extracted from the `n`th item in the Python tuple. -``` +```rust use pyo3::prelude::*; #[derive(FromPyObject)] @@ -158,7 +158,7 @@ struct RustyTuple(String, String); Tuple structs with a single field are treated as wrapper types which are described in the following section. To override this behaviour and ensure that the input is in fact a tuple, specify the struct as -``` +```rust use pyo3::prelude::*; #[derive(FromPyObject)] @@ -184,7 +184,7 @@ in extracting directly from the input object, i.e. `obj.extract()`, rather than an item or attribute. This behaviour is enabled per default for newtype structs and tuple-variants with a single field. -``` +```rust use pyo3::prelude::*; #[derive(FromPyObject)] @@ -223,7 +223,7 @@ i.e. a tuple variant assumes that the input is a Python tuple, and a struct vari extracting fields as attributes but can be configured in the same manner. The `transparent` attribute can be applied to single-field-variants. -``` +```rust use pyo3::prelude::*; #[derive(FromPyObject)] @@ -253,7 +253,7 @@ enum RustyEnum<'a> { # Python::with_gil(|py| -> PyResult<()> { # { # let thing = 42_u8.to_object(py); -# let rust_thing: RustyEnum = thing.extract(py)?; +# let rust_thing: RustyEnum<'_> = thing.extract(py)?; # # assert_eq!( # 42, @@ -265,7 +265,7 @@ enum RustyEnum<'a> { # } # { # let thing = PyString::new(py, "text"); -# let rust_thing: RustyEnum = thing.extract()?; +# let rust_thing: RustyEnum<'_> = thing.extract()?; # # assert_eq!( # "text", @@ -277,7 +277,7 @@ enum RustyEnum<'a> { # } # { # let thing = (32_u8, 73_u8).to_object(py); -# let rust_thing: RustyEnum = thing.extract(py)?; +# let rust_thing: RustyEnum<'_> = thing.extract(py)?; # # assert_eq!( # (32, 73), @@ -289,7 +289,7 @@ enum RustyEnum<'a> { # } # { # let thing = ("foo", 73_u8).to_object(py); -# let rust_thing: RustyEnum = thing.extract(py)?; +# let rust_thing: RustyEnum<'_> = thing.extract(py)?; # # assert_eq!( # (String::from("foo"), 73), @@ -313,7 +313,7 @@ enum RustyEnum<'a> { # # let class = module.getattr("Foo")?; # let instance = class.call0()?; -# let rust_thing: RustyEnum = instance.extract()?; +# let rust_thing: RustyEnum<'_> = instance.extract()?; # # assert_eq!( # (0, 1, 2), @@ -337,7 +337,7 @@ enum RustyEnum<'a> { # # let class = module.getattr("Foo")?; # let instance = class.call0()?; -# let rust_thing: RustyEnum = instance.extract()?; +# let rust_thing: RustyEnum<'_> = instance.extract()?; # # assert_eq!( # (3, 4), @@ -350,7 +350,7 @@ enum RustyEnum<'a> { # # { # let thing = PyBytes::new(py, b"text"); -# let rust_thing: RustyEnum = thing.extract()?; +# let rust_thing: RustyEnum<'_> = thing.extract()?; # # assert_eq!( # b"text", @@ -370,7 +370,7 @@ tested variants is returned. The names reported in the error message can be cust through the `#[pyo3(annotation = "name")]` attribute, e.g. to use conventional Python type names: -``` +```rust use pyo3::prelude::*; #[derive(FromPyObject)] @@ -442,6 +442,10 @@ If the input is neither a string nor an integer, the error message will be: - `pyo3(item)`, `pyo3(item("key"))` - 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. + - 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. ### `IntoPy` @@ -454,13 +458,13 @@ All types in PyO3 implement this trait, as does a `#[pyclass]` which doesn't use Occasionally you may choose to implement this for custom types which are mapped to Python types _without_ having a unique python type. -``` +```rust use pyo3::prelude::*; struct MyPyObjectWrapper(PyObject); impl IntoPy for MyPyObjectWrapper { - fn into_py(self, py: Python) -> PyObject { + fn into_py(self, py: Python<'_>) -> PyObject { self.0 } } diff --git a/guide/src/ecosystem/async-await.md b/guide/src/ecosystem/async-await.md index 37e1333f..262b27e6 100644 --- a/guide/src/ecosystem/async-await.md +++ b/guide/src/ecosystem/async-await.md @@ -118,7 +118,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<&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())) @@ -126,7 +126,7 @@ fn rust_sleep(py: Python) -> PyResult<&PyAny> { } #[pymodule] -fn my_async_module(py: Python, m: &PyModule) -> PyResult<()> { +fn my_async_module(py: Python<'_>, m: &PyModule) -> PyResult<()> { m.add_function(wrap_pyfunction!(rust_sleep, m)?)?; Ok(()) @@ -142,7 +142,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<&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())) @@ -150,7 +150,7 @@ fn rust_sleep(py: Python) -> PyResult<&PyAny> { } #[pymodule] -fn my_async_module(py: Python, m: &PyModule) -> PyResult<()> { +fn my_async_module(py: Python<'_>, m: &PyModule) -> PyResult<()> { m.add_function(wrap_pyfunction!(rust_sleep, m)?)?; Ok(()) } @@ -316,7 +316,7 @@ async fn rust_sleep() { } #[pyfunction] -fn call_rust_sleep(py: Python) -> PyResult<&PyAny> { +fn call_rust_sleep(py: Python<'_>) -> PyResult<&PyAny> { pyo3_asyncio::async_std::future_into_py(py, async move { rust_sleep().await; Ok(Python::with_gil(|py| py.None())) @@ -466,7 +466,7 @@ tokio = "1.4" use pyo3::{prelude::*, wrap_pyfunction}; #[pyfunction] -fn rust_sleep(py: Python) -> PyResult<&PyAny> { +fn rust_sleep(py: Python<'_>) -> PyResult<&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())) @@ -474,7 +474,7 @@ fn rust_sleep(py: Python) -> PyResult<&PyAny> { } #[pymodule] -fn my_async_module(_py: Python, m: &PyModule) -> PyResult<()> { +fn my_async_module(_py: Python<'_>, m: &PyModule) -> PyResult<()> { m.add_function(wrap_pyfunction!(rust_sleep, m)?)?; Ok(()) diff --git a/guide/src/exception.md b/guide/src/exception.md index 251d7f85..f205e035 100644 --- a/guide/src/exception.md +++ b/guide/src/exception.md @@ -38,7 +38,7 @@ the module like this, so that it is importable from Python: create_exception!(mymodule, CustomError, PyException); #[pymodule] -fn mymodule(py: Python, m: &PyModule) -> PyResult<()> { +fn mymodule(py: Python<'_>, m: &PyModule) -> PyResult<()> { // ... other elements added to module ... m.add("CustomError", py.get_type::())?; @@ -151,7 +151,7 @@ struct CustomIOError; impl std::error::Error for CustomIOError {} impl fmt::Display for CustomIOError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "Oh no!") } } diff --git a/guide/src/faq.md b/guide/src/faq.md index 42cb5b27..6ef7fd67 100644 --- a/guide/src/faq.md +++ b/guide/src/faq.md @@ -116,7 +116,7 @@ struct Outer { #[pymethods] impl Outer { #[new] - fn __new__(py: Python) -> PyResult { + fn __new__(py: Python<'_>) -> PyResult { Ok(Self { inner: Py::new(py, Inner {})?, }) diff --git a/guide/src/features.md b/guide/src/features.md index e96b486b..92d04815 100644 --- a/guide/src/features.md +++ b/guide/src/features.md @@ -73,9 +73,7 @@ This feature enables the `#[pyproto]` macro, which is an alternative (older, soo ### `nightly` -The `nightly` feature needs the nightly Rust compiler. This allows PyO3 to use Rust's unstable specialization feature to apply the following optimizations: -- `FromPyObject` for `Vec` and `[T;N]` can perform a `memcpy` when the object supports the Python buffer protocol. -- `ToBorrowedObject` can skip a reference count increase when the provided object is a Python native type. +The `nightly` feature needs the nightly Rust compiler. This allows PyO3 to use the auto_traits and negative_impls features to fix the `Python::allow_threads` function. ### `resolve-config` @@ -117,6 +115,11 @@ Enables (de)serialization of Py objects via [serde](https://serde.rs/). This allows to use [`#[derive(Serialize, Deserialize)`](https://serde.rs/derive.html) on structs that hold references to `#[pyclass]` instances ```rust +# #[cfg(feature = "serde")] +# #[allow(dead_code)] +# mod serde_only { +# use pyo3::prelude::*; +# use serde::{Deserialize, Serialize}; #[pyclass] #[derive(Serialize, Deserialize)] @@ -130,4 +133,5 @@ struct User { username: String, permissions: Vec> } +# } ``` diff --git a/guide/src/function.md b/guide/src/function.md index e027b99c..1fa6c9ca 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(py: Python<'_>, m: &PyModule) -> PyResult<()> { m.add_function(wrap_pyfunction!(double, m)?)?; Ok(()) } @@ -49,7 +49,7 @@ The `#[pyo3]` attribute can be used to modify properties of the generated Python fn no_args_py() -> usize { 42 } #[pymodule] - fn module_with_functions(py: Python, m: &PyModule) -> PyResult<()> { + fn module_with_functions(py: Python<'_>, m: &PyModule) -> PyResult<()> { m.add_function(wrap_pyfunction!(no_args_py, m)?)?; Ok(()) } @@ -113,7 +113,7 @@ The `#[pyo3]` attribute can be used to modify properties of the generated Python } #[pymodule] - fn module_with_fn(py: Python, m: &PyModule) -> PyResult<()> { + fn module_with_fn(py: Python<'_>, m: &PyModule) -> PyResult<()> { m.add_function(wrap_pyfunction!(pyfunction_with_module, m)?) } ``` @@ -132,7 +132,7 @@ fn num_kwds(kwds: Option<&PyDict>) -> usize { } #[pymodule] -fn module_with_functions(py: Python, m: &PyModule) -> PyResult<()> { +fn module_with_functions(py: Python<'_>, m: &PyModule) -> PyResult<()> { m.add_function(wrap_pyfunction!(num_kwds, m)?).unwrap(); Ok(()) } @@ -273,7 +273,7 @@ An example of `#[pyfn]` is below: use pyo3::prelude::*; #[pymodule] -fn my_extension(py: Python, m: &PyModule) -> PyResult<()> { +fn my_extension(py: Python<'_>, m: &PyModule) -> PyResult<()> { #[pyfn(m)] fn double(x: usize) -> usize { @@ -291,7 +291,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(py: Python<'_>, m: &PyModule) -> PyResult<()> { #[pyfunction] fn double(x: usize) -> usize { diff --git a/guide/src/memory.md b/guide/src/memory.md index 137ed3c8..5b43dd78 100644 --- a/guide/src/memory.md +++ b/guide/src/memory.md @@ -23,11 +23,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 +# use pyo3::prelude::*; +# use pyo3::types::PyString; +# fn main() -> PyResult<()> { Python::with_gil(|py| -> PyResult<()> { let hello: &PyString = py.eval("\"Hello World!\"", None, None)?.extract()?; println!("Python says: {}", hello); Ok(()) })?; +# Ok(()) +# } ``` Internally, calling `Python::with_gil()` or `Python::acquire_gil()` creates a @@ -39,6 +44,9 @@ 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 +# 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()?; @@ -47,6 +55,8 @@ Python::with_gil(|py| -> PyResult<()> { // There are 10 copies of `hello` on Python's heap here. Ok(()) })?; +# Ok(()) +# } ``` We might assume that the `hello` variable's memory is freed at the end of each @@ -62,6 +72,9 @@ 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 +# use pyo3::prelude::*; +# use pyo3::types::PyString; +# fn main() -> PyResult<()> { for _ in 0..10 { Python::with_gil(|py| -> PyResult<()> { let hello: &PyString = py.eval("\"Hello World!\"", None, None)?.extract()?; @@ -69,6 +82,8 @@ for _ in 0..10 { Ok(()) })?; // only one copy of `hello` at a time } +# Ok(()) +# } ``` It might not be practical or performant to acquire and release the GIL so many @@ -76,6 +91,9 @@ times. Another workaround is to work with the `GILPool` object directly, but this is unsafe. ```rust +# use pyo3::prelude::*; +# use pyo3::types::PyString; +# fn main() -> PyResult<()> { Python::with_gil(|py| -> PyResult<()> { for _ in 0..10 { let pool = unsafe { py.new_pool() }; @@ -85,6 +103,8 @@ Python::with_gil(|py| -> PyResult<()> { } Ok(()) })?; +# Ok(()) +# } ``` The unsafe method `Python::new_pool` allows you to create a nested `GILPool` @@ -112,11 +132,16 @@ 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 +# use pyo3::prelude::*; +# use pyo3::types::PyString; +# fn main() -> PyResult<()> { Python::with_gil(|py| -> PyResult<()> { - let hello: Py = py.eval("\"Hello World!\"", None, None)?.extract())?; + let hello: Py = py.eval("\"Hello World!\"", None, None)?.extract()?; println!("Python says: {}", hello.as_ref(py)); Ok(()) -}); +})?; +# Ok(()) +# } ``` At the end of the `Python::with_gil()` closure `hello` is dropped, and then the @@ -129,8 +154,11 @@ This example wasn't very interesting. We could have just used a GIL-bound we are *not* holding the GIL? ```rust +# use pyo3::prelude::*; +# use pyo3::types::PyString; +# fn main() -> PyResult<()> { let hello: Py = Python::with_gil(|py| { - Py = py.eval("\"Hello World!\"", None, None)?.extract()) + py.eval("\"Hello World!\"", None, None)?.extract() })?; // Do some stuff... // Now sometime later in the program we want to access `hello`. @@ -142,7 +170,10 @@ drop(hello); // Memory *not* released here. // Sometime later we need the GIL again for something... 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 @@ -154,8 +185,11 @@ We can avoid the delay in releasing memory if we are careful to drop the `Py` while the GIL is held. ```rust +# use pyo3::prelude::*; +# use pyo3::types::PyString; +# fn main() -> PyResult<()> { let hello: Py = Python::with_gil(|py| { - Py = py.eval("\"Hello World!\"", None, None)?.extract()) + py.eval("\"Hello World!\"", None, None)?.extract() })?; // Do some stuff... // Now sometime later in the program: @@ -163,6 +197,8 @@ Python::with_gil(|py| { println!("Python says: {}", hello.as_ref(py)); drop(hello); // Memory released here. }); +# Ok(()) +# } ``` We could also have used `Py::into_ref()`, which consumes `self`, instead of @@ -172,8 +208,11 @@ that rather than being released immediately, the memory will not be released until the GIL is dropped. ```rust +# use pyo3::prelude::*; +# use pyo3::types::PyString; +# fn main() -> PyResult<()> { let hello: Py = Python::with_gil(|py| { - Py = py.eval("\"Hello World!\"", None, None)?.extract()) + py.eval("\"Hello World!\"", None, None)?.extract() })?; // Do some stuff... // Now sometime later in the program: @@ -183,4 +222,6 @@ Python::with_gil(|py| { // 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 46113b60..7a763e66 100644 --- a/guide/src/migration.md +++ b/guide/src/migration.md @@ -7,7 +7,74 @@ For a detailed list of all changes, see the [CHANGELOG](changelog.md). ### Drop support for older technologies -PyO3 0.16 has increased minimum Rust version to 1.48 and minimum Python version to 3.7. This enables ore use of newer language features (enabling some of the other additions in 0.16) and simplifies maintenance of the project. +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 + +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. + +In PyO3 0.16, the `#[pymethods]` implementation has been completed and is now the preferred way to implement magic methods. To allow the PyO3 project to move forward, `#[pyproto]` has been deprecated (with expected removal in PyO3 0.18). + +Migration from `#[pyproto]` to `#[pymethods]` is straightforward; copying the existing methods directly from the `#[pyproto]` trait implementation is all that is needed in most cases. + +Before: + +```rust,ignore +use pyo3::prelude::*; +use pyo3::class::{PyBasicProtocol, PyIterProtocol}; +use pyo3::types::PyString; + +#[pyclass] +struct MyClass { } + +#[pyproto] +impl PyBasicProtocol for MyClass { + fn __str__(&self) -> &'static [u8] { + b"hello, world" + } +} + +#[pyproto] +impl PyIterProtocol for MyClass { + fn __iter__(slf: PyRef) -> PyResult<&PyAny> { + PyString::new(slf.py(), "hello, world").iter() + } +} +``` + +After + +```rust,ignore +use pyo3::prelude::*; +use pyo3::types::PyString; + +#[pyclass] +struct MyClass { } + +#[pymethods] +impl MyClass { + fn __str__(&self) -> &'static [u8] { + b"hello, world" + } + + fn __iter__(slf: PyRef) -> PyResult<&PyAny> { + PyString::new(slf.py(), "hello, world").iter() + } +} +``` + +### Removed `PartialEq` for object wrappers + +The Python object wrappers `Py` and `PyAny` had implementations of `PartialEq` +so that `object_a == object_b` would compare the Python objects for pointer +equality, which corresponds to the `is` operator, not the `==` operator in +Python. This has been removed in favor of a new method: use +`object_a.is(object_b)`. This also has the advantage of not requiring the same +wrapper type for `object_a` and `object_b`; you can now directly compare a +`Py` with a `&PyAny` without having to convert. + +To check for Python object equality (the Python `==` operator), use the new +method `eq()`. ### Container magic methods now match Python behavior @@ -39,6 +106,55 @@ Because there is no such distinction from Python, implementing these methods wil 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 + +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. + +For example, the following code was legal before 0.16, but in 0.16 is rejected because the `wrap_pymodule!` macro cannot access the `private_submodule` function: + +```rust,compile_fail +mod foo { + use pyo3::prelude::*; + + #[pymodule] + fn private_submodule(_py: Python<'_>, m: &PyModule) -> PyResult<()> { + Ok(()) + } +} + +use pyo3::prelude::*; +use foo::*; + +#[pymodule] +fn my_module(_py: Python<'_>, m: &PyModule) -> PyResult<()> { + m.add_wrapped(wrap_pymodule!(private_submodule))?; + Ok(()) +} +``` + +To fix it, make the private submodule visible, e.g. with `pub` or `pub(crate)`. + +```rust +mod foo { + use pyo3::prelude::*; + + #[pymodule] + pub(crate) fn private_submodule(_py: Python<'_>, m: &PyModule) -> PyResult<()> { + Ok(()) + } +} + +use pyo3::prelude::*; +use pyo3::wrap_pymodule; +use foo::*; + +#[pymodule] +fn my_module(_py: Python<'_>, m: &PyModule) -> PyResult<()> { + m.add_wrapped(wrap_pymodule!(private_submodule))?; + Ok(()) +} +``` + ## from 0.14.* to 0.15 ### Changes in sequence indexing @@ -230,7 +346,7 @@ Before: struct MyPyObjectWrapper(PyObject); impl FromPy for PyObject { - fn from_py(other: MyPyObjectWrapper, _py: Python) -> Self { + fn from_py(other: MyPyObjectWrapper, _py: Python<'_>) -> Self { other.0 } } @@ -242,7 +358,7 @@ After struct MyPyObjectWrapper(PyObject); impl IntoPy for MyPyObjectWrapper { - fn into_py(self, _py: Python) -> PyObject { + fn into_py(self, _py: Python<'_>) -> PyObject { self.0 } } @@ -560,10 +676,10 @@ let obj: &PyAny = create_obj(); let obj_cell: &PyCell = obj.extract().unwrap(); let obj_cloned: MyClass = obj.extract().unwrap(); // extracted by cloning the object { - let obj_ref: PyRef = obj.extract().unwrap(); + let obj_ref: PyRef<'_, MyClass> = obj.extract().unwrap(); // we need to drop obj_ref before we can extract a PyRefMut due to Rust's rules of references } -let obj_ref_mut: PyRefMut = obj.extract().unwrap(); +let obj_ref_mut: PyRefMut<'_, MyClass> = obj.extract().unwrap(); # }) ``` @@ -593,7 +709,7 @@ impl PySequenceProtocol for ByteSequence { ``` After: -```rust +```rust,ignore # use pyo3::prelude::*; # use pyo3::class::PySequenceProtocol; #[pyclass] diff --git a/guide/src/module.md b/guide/src/module.md index c85ecc48..6b80b71c 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(py: Python<'_>, m: &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(py: Python<'_>, m: &PyModule) -> PyResult<()> { m.add_function(wrap_pyfunction!(double, m)?)?; Ok(()) } @@ -76,7 +76,7 @@ references to the `PyModule` so that each module registers its own FFI code. For use pyo3::prelude::*; #[pymodule] -fn my_extension(py: Python, m: &PyModule) -> PyResult<()> { +fn my_extension(py: Python<'_>, m: &PyModule) -> PyResult<()> { dirutil::register(py, m)?; osutil::register(py, m)?; Ok(()) @@ -86,7 +86,7 @@ fn my_extension(py: Python, m: &PyModule) -> PyResult<()> { # mod dirutil { use pyo3::prelude::*; -pub(crate) fn register(py: Python, m: &PyModule) -> PyResult<()> { +pub(crate) fn register(py: Python<'_>, m: &PyModule) -> PyResult<()> { m.add_class::()?; Ok(()) } @@ -99,7 +99,7 @@ struct SomeClass {/* ... */} # mod osutil { use pyo3::prelude::*; -pub(crate) fn register(py: Python, m: &PyModule) -> PyResult<()> { +pub(crate) fn register(py: Python<'_>, m: &PyModule) -> PyResult<()> { m.add_function(wrap_pyfunction!(determine_current_os, m)?)?; Ok(()) } @@ -120,7 +120,7 @@ use pyo3::prelude::*; use osutil::*; #[pymodule] -fn my_extension(py: Python, m: &PyModule) -> PyResult<()> { +fn my_extension(py: Python<'_>, m: &PyModule) -> PyResult<()> { m.add_function(wrap_pyfunction!(determine_current_os, m)?)?; Ok(()) } @@ -146,12 +146,12 @@ For example, you could define the modules `parent_module` and `parent_module.chi use pyo3::prelude::*; #[pymodule] -fn parent_module(py: Python, m: &PyModule) -> PyResult<()> { +fn parent_module(py: Python<'_>, m: &PyModule) -> PyResult<()> { register_child_module(py, m)?; Ok(()) } -fn register_child_module(py: Python, parent_module: &PyModule) -> PyResult<()> { +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)?; diff --git a/guide/src/parallelism.md b/guide/src/parallelism.md index cdf2f9bd..f35f1da5 100644 --- a/guide/src/parallelism.md +++ b/guide/src/parallelism.md @@ -23,7 +23,7 @@ fn search_sequential(contents: &str, needle: &str) -> usize { To enable parallel execution of this function, the [`Python::allow_threads`] method can be used to temporarily release the GIL, thus allowing other Python threads to run. We then have a function exposed to the Python runtime which calls `search_sequential` inside a closure passed to [`Python::allow_threads`] to enable true parallelism: ```rust, ignore #[pyfunction] -fn search_sequential_allow_threads(py: Python, contents: &str, needle: &str) -> usize { +fn search_sequential_allow_threads(py: Python<'_>, contents: &str, needle: &str) -> usize { py.allow_threads(|| search_sequential(contents, needle)) } ``` diff --git a/guide/src/python_from_rust.md b/guide/src/python_from_rust.md index 737d1926..98d706df 100644 --- a/guide/src/python_from_rust.md +++ b/guide/src/python_from_rust.md @@ -165,7 +165,7 @@ quickly testing your Python extensions. ```rust use pyo3::prelude::*; -use pyo3::{PyCell, PyObjectProtocol, py_run}; +use pyo3::{PyCell, py_run}; # fn main() { #[pyclass] @@ -179,10 +179,7 @@ impl UserData { fn as_tuple(&self) -> (u32, String) { (self.id, self.name.clone()) } -} -#[pyproto] -impl PyObjectProtocol for UserData { fn __repr__(&self) -> PyResult { Ok(format!("User {}(id: {})", self.name, self.id)) } diff --git a/guide/src/rust_cpython.md b/guide/src/rust_cpython.md index 91f33933..fd3a878b 100644 --- a/guide/src/rust_cpython.md +++ b/guide/src/rust_cpython.md @@ -55,9 +55,9 @@ Here is an example of the PyList API: ```rust,ignore impl PyList { - fn new(py: Python) -> PyList {...} + fn new(py: Python<'_>) -> PyList {...} - fn get_item(&self, py: Python, index: isize) -> PyObject {...} + fn get_item(&self, py: Python<'_>, index: isize) -> PyObject {...} } ``` @@ -66,7 +66,7 @@ impl PyList { ```rust,ignore impl PyList { - fn new(py: Python) -> &PyList {...} + fn new(py: Python<'_>) -> &PyList {...} fn get_item(&self, index: isize) -> &PyAny {...} } diff --git a/guide/src/trait_bounds.md b/guide/src/trait_bounds.md index a57a7580..f08749e0 100644 --- a/guide/src/trait_bounds.md +++ b/guide/src/trait_bounds.md @@ -133,7 +133,7 @@ struct UserModel { } #[pymodule] -fn trait_exposure(_py: Python, m: &PyModule) -> PyResult<()> { +fn trait_exposure(_py: Python<'_>, m: &PyModule) -> PyResult<()> { m.add_class::()?; Ok(()) } @@ -487,7 +487,7 @@ pub struct UserModel { } #[pymodule] -fn trait_exposure(_py: Python, m: &PyModule) -> PyResult<()> { +fn trait_exposure(_py: Python<'_>, m: &PyModule) -> PyResult<()> { m.add_class::()?; m.add_function(wrap_pyfunction!(solve_wrapper, m)?)?; Ok(()) diff --git a/guide/src/types.md b/guide/src/types.md index ed2fda6c..f75ac554 100644 --- a/guide/src/types.md +++ b/guide/src/types.md @@ -103,9 +103,9 @@ let _: Py = obj.extract()?; // To MyClass with PyAny::extract, if MyClass: Clone let _: MyClass = obj.extract()?; -// To PyRef or PyRefMut with PyAny::extract -let _: PyRef = obj.extract()?; -let _: PyRefMut = obj.extract()?; +// To PyRef<'_, MyClass> or PyRefMut<'_, MyClass> with PyAny::extract +let _: PyRef<'_, MyClass> = obj.extract()?; +let _: PyRefMut<'_, MyClass> = obj.extract()?; # Ok(()) # }).unwrap(); ``` @@ -207,11 +207,11 @@ let _: &PyCell = my_class.into_ref(py); let _: Py = my_class.into_py(py); # let my_class = my_class_clone; -// To PyRef with Py::borrow or Py::try_borrow -let _: PyRef = my_class.try_borrow(py)?; +// To PyRef<'_, MyClass> with Py::borrow or Py::try_borrow +let _: PyRef<'_, MyClass> = my_class.try_borrow(py)?; -// To PyRefMut with Py::borrow_mut or Py::try_borrow_mut -let _: PyRefMut = my_class.try_borrow_mut(py)?; +// To PyRefMut<'_, MyClass> with Py::borrow_mut or Py::try_borrow_mut +let _: PyRefMut<'_, MyClass> = my_class.try_borrow_mut(py)?; # Ok(()) # }).unwrap(); # }); @@ -241,12 +241,12 @@ so it also exposes all of the methods on `PyAny`. let cell: &PyCell = PyCell::new(py, MyClass { })?; // To PyRef with .borrow() or .try_borrow() -let py_ref: PyRef = cell.try_borrow()?; +let py_ref: PyRef<'_, MyClass> = cell.try_borrow()?; let _: &MyClass = &*py_ref; # drop(py_ref); // To PyRefMut with .borrow_mut() or .try_borrow_mut() -let mut py_ref_mut: PyRefMut = cell.try_borrow_mut()?; +let mut py_ref_mut: PyRefMut<'_, MyClass> = cell.try_borrow_mut()?; let _: &mut MyClass = &mut *py_ref_mut; # Ok(()) # }).unwrap(); diff --git a/noxfile.py b/noxfile.py new file mode 100644 index 00000000..a15e3fce --- /dev/null +++ b/noxfile.py @@ -0,0 +1,88 @@ +import time +from glob import glob + +import nox + +nox.options.sessions = ["test", "clippy", "fmt"] + + +@nox.session(venv_backend="none") +def test(session: nox.Session): + test_rust(session) + test_py(session) + + +@nox.session(name="test-rust", venv_backend="none") +def test_rust(session: nox.Session): + session.run("cargo", "test", external=True) + session.run("cargo", "test", "--features=abi3", external=True) + session.run("cargo", "test", "--features=full", external=True) + session.run("cargo", "test", "--features=abi3 full", external=True) + + +@nox.session(name="test-py", venv_backend="none") +def test_py(session): + session.run("nox", "-f", "pytests/noxfile.py", external=True) + for example in glob("examples/*/noxfile.py"): + session.run("nox", "-f", example, external=True) + + +@nox.session +def fmt(session: nox.Session): + fmt_rust(session) + fmt_py(session) + + +@nox.session(name="fmt-rust", venv_backend="none") +def fmt_rust(session: nox.Session): + session.run("cargo", "fmt", "--all", "--check", external=True) + + +@nox.session(name="fmt-py") +def fmt_py(session: nox.Session): + session.install("black==22.3.0") + session.run("black", ".", "--check") + + +@nox.session(venv_backend="none") +def clippy(session: nox.Session) -> None: + for feature_set in ["full", "abi3 full"]: + session.run( + "cargo", + "clippy", + f"--features={feature_set}", + "--all-targets", + "--workspace", + "--", + "--deny=warnings", + external=True, + ) + + +@nox.session(venv_backend="none") +def publish(session: nox.Session) -> None: + session.run( + "cargo", + "publish", + "--manifest-path", + "pyo3-build-config/Cargo.toml", + external=True, + ) + time.sleep(10) + session.run( + "cargo", + "publish", + "--manifest-path", + "pyo3-macros-backend/Cargo.toml", + external=True, + ) + time.sleep(10) + session.run( + "cargo", "publish", "--manifest-path", "pyo3-macros/Cargo.toml", external=True + ) + time.sleep(10) + session.run( + "cargo", "publish", "--manifest-path", "pyo3-ffi/Cargo.toml", external=True + ) + time.sleep(10) + session.run("cargo", "publish", external=True) diff --git a/pyo3-build-config/Cargo.toml b/pyo3-build-config/Cargo.toml index 9dfdcd82..2233dd78 100644 --- a/pyo3-build-config/Cargo.toml +++ b/pyo3-build-config/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3-build-config" -version = "0.15.1" +version = "0.16.2" description = "Build configuration for the PyO3 ecosystem" authors = ["PyO3 Project and Contributors "] keywords = ["pyo3", "python", "cpython", "ffi"] @@ -25,3 +25,6 @@ abi3-py37 = ["abi3-py38"] abi3-py38 = ["abi3-py39"] abi3-py39 = ["abi3-py310"] abi3-py310 = ["abi3"] + +[package.metadata.docs.rs] +features = ["resolve-config"] diff --git a/pyo3-build-config/src/errors.rs b/pyo3-build-config/src/errors.rs index 2652da5e..1cbf16ec 100644 --- a/pyo3-build-config/src/errors.rs +++ b/pyo3-build-config/src/errors.rs @@ -2,8 +2,7 @@ #[macro_export] #[doc(hidden)] macro_rules! bail { - ($msg: expr) => { return Err($msg.into()) }; - ($fmt: literal $($args: tt)+) => { return Err(format!($fmt $($args)+).into()) }; + ($($args: tt)+) => { return Err(format!($($args)+).into()) }; } /// A simple macro for checking a condition. Resembles anyhow::ensure. diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index 032aa34b..c3dcff32 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -8,6 +8,7 @@ use std::{ io::{BufRead, BufReader, Read, Write}, path::{Path, PathBuf}, process::{Command, Stdio}, + str, str::FromStr, }; @@ -19,8 +20,9 @@ use crate::{ /// Minimum Python version PyO3 supports. const MINIMUM_SUPPORTED_VERSION: PythonVersion = PythonVersion { major: 3, minor: 7 }; + /// Maximum Python version that can be used as minimum required Python version with abi3. -const ABI3_MAX_MINOR: u8 = 9; +const ABI3_MAX_MINOR: u8 = 10; /// Gets an environment variable owned by cargo. /// @@ -153,7 +155,7 @@ impl InterpreterConfig { } for flag in &self.build_flags.0 { - println!("cargo:rustc-cfg=py_sys_config=\"{}\"", flag) + println!("cargo:rustc-cfg=py_sys_config=\"{}\"", flag); } } @@ -188,7 +190,7 @@ def print_if_set(varname, value): print(varname, value) # Windows always uses shared linking -WINDOWS = hasattr(platform, "win32_ver") +WINDOWS = platform.system() == "Windows" # macOS framework packages use shared linking FRAMEWORK = bool(get_config_var("PYTHONFRAMEWORK")) @@ -299,6 +301,11 @@ print("mingw", get_platform().startswith("mingw")) Some("0") | Some("false") | Some("False") => false, _ => bail!("expected a bool (1/true/True or 0/false/False) for Py_ENABLE_SHARED"), }; + // macOS framework packages use shared linking (PYTHONFRAMEWORK is the framework name, hence the empty check) + let framework = match sysconfigdata.get_value("PYTHONFRAMEWORK") { + Some(s) => !s.is_empty(), + _ => false, + }; let lib_dir = get_key!(sysconfigdata, "LIBDIR").ok().map(str::to_string); let lib_name = Some(default_lib_name_unix( version, @@ -313,7 +320,7 @@ print("mingw", get_platform().startswith("mingw")) Ok(InterpreterConfig { implementation, version, - shared, + shared: shared || framework, abi3: is_abi3(), lib_dir, lib_name, @@ -334,6 +341,12 @@ print("mingw", get_platform().startswith("mingw")) InterpreterConfig::from_reader(reader) } + #[doc(hidden)] + pub fn from_cargo_dep_env() -> Option> { + cargo_env_var("DEP_PYTHON_PYO3_CONFIG") + .map(|buf| InterpreterConfig::from_reader(buf.replace("\\n", "\n").as_bytes())) + } + #[doc(hidden)] pub fn from_reader(reader: impl Read) -> Result { let reader = BufReader::new(reader); @@ -414,6 +427,29 @@ print("mingw", get_platform().startswith("mingw")) }) } + #[doc(hidden)] + /// Serialize the `InterpreterConfig` and print it to the environment for Cargo to pass along + /// to dependent packages during build time. + /// + /// NB: writing to the cargo environment requires the + /// [`links`](https://doc.rust-lang.org/cargo/reference/build-scripts.html#the-links-manifest-key) + /// manifest key to be set. In this case that means this is called by the `pyo3-ffi` crate and + /// available for dependent package build scripts in `DEP_PYTHON_PYO3_CONFIG`. See + /// documentation for the + /// [`DEP__`](https://doc.rust-lang.org/cargo/reference/environment-variables.html#environment-variables-cargo-sets-for-build-scripts) + /// environment variable. + pub fn to_cargo_dep_env(&self) -> Result<()> { + let mut buf = Vec::new(); + self.to_writer(&mut buf)?; + // escape newlines in env var + if let Ok(config) = str::from_utf8(&buf) { + println!("cargo:PYO3_CONFIG={}", config.replace('\n', "\\n")); + } else { + bail!("unable to emit interpreter config to link env for downstream use"); + } + Ok(()) + } + #[doc(hidden)] pub fn to_writer(&self, mut writer: impl Write) -> Result<()> { macro_rules! write_line { @@ -456,6 +492,38 @@ print("mingw", get_platform().startswith("mingw")) } Ok(()) } + + /// Run a python script using the [`InterpreterConfig::executable`]. + /// + /// # Panics + /// + /// This function will panic if the [`executable`](InterpreterConfig::executable) is `None`. + pub fn run_python_script(&self, script: &str) -> Result { + run_python_script_with_envs( + Path::new(self.executable.as_ref().expect("no interpreter executable")), + script, + std::iter::empty::<(&str, &str)>(), + ) + } + + /// Run a python script using the [`InterpreterConfig::executable`] with additional + /// environment variables (e.g. PYTHONPATH) set. + /// + /// # Panics + /// + /// This function will panic if the [`executable`](InterpreterConfig::executable) is `None`. + pub fn run_python_script_with_envs(&self, script: &str, envs: I) -> Result + where + I: IntoIterator, + K: AsRef, + V: AsRef, + { + run_python_script_with_envs( + Path::new(self.executable.as_ref().expect("no interpreter executable")), + script, + envs, + ) + } } #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] @@ -540,6 +608,54 @@ fn is_abi3() -> bool { cargo_env_var("CARGO_FEATURE_ABI3").is_some() } +#[derive(Debug, PartialEq)] +struct TargetInfo { + /// The `arch` component of the compilation target triple. + /// + /// e.g. x86_64, i386, arm, thumb, mips, etc. + arch: String, + + /// The `vendor` component of the compilation target triple. + /// + /// e.g. apple, pc, unknown, etc. + vendor: String, + + /// The `os` component of the compilation target triple. + /// + /// e.g. darwin, freebsd, linux, windows, etc. + os: String, +} + +impl TargetInfo { + fn from_cargo_env() -> Result { + Ok(Self { + arch: cargo_env_var("CARGO_CFG_TARGET_ARCH") + .ok_or("expected CARGO_CFG_TARGET_ARCH env var")?, + vendor: cargo_env_var("CARGO_CFG_TARGET_VENDOR") + .ok_or("expected CARGO_CFG_TARGET_VENDOR env var")?, + os: cargo_env_var("CARGO_CFG_TARGET_OS") + .ok_or("expected CARGO_CFG_TARGET_OS env var")?, + }) + } + + fn to_target_triple(&self) -> String { + format!( + "{}-{}-{}", + if self.arch == "x86" { + "i686" + } else { + &self.arch + }, + self.vendor, + if self.os == "macos" { + "darwin" + } else { + &self.os + } + ) + } +} + /// Configuration needed by PyO3 to cross-compile for a target platform. /// /// Usually this is collected from the environment (i.e. `PYO3_CROSS_*` and `CARGO_CFG_TARGET_*`) @@ -552,51 +668,55 @@ pub struct CrossCompileConfig { /// The version of the Python library to link against. version: Option, - /// The `arch` component of the compilaton target triple. - /// - /// e.g. x86_64, i386, arm, thumb, mips, etc. - arch: String, - - /// The `vendor` component of the compilaton target triple. - /// - /// e.g. apple, pc, unknown, etc. - vendor: String, - - /// The `os` component of the compilaton target triple. - /// - /// e.g. darwin, freebsd, linux, windows, etc. - os: String, + /// The target information + target_info: TargetInfo, } -#[allow(unused)] -pub fn any_cross_compiling_env_vars_set() -> bool { - env::var_os("PYO3_CROSS").is_some() - || env::var_os("PYO3_CROSS_LIB_DIR").is_some() - || env::var_os("PYO3_CROSS_PYTHON_VERSION").is_some() +impl CrossCompileConfig { + fn from_env_vars(env_vars: CrossCompileEnvVars, target_info: TargetInfo) -> Result { + Ok(CrossCompileConfig { + lib_dir: env_vars + .pyo3_cross_lib_dir + .ok_or( + "The PYO3_CROSS_LIB_DIR environment variable must be set when cross-compiling", + )? + .into(), + target_info, + version: env_vars + .pyo3_cross_python_version + .map(|os_string| { + let utf8_str = os_string + .to_str() + .ok_or("PYO3_CROSS_PYTHON_VERSION is not valid utf-8.")?; + utf8_str + .parse() + .context("failed to parse PYO3_CROSS_PYTHON_VERSION") + }) + .transpose()?, + }) + } } -fn cross_compiling_from_cargo_env() -> Result> { - let host = cargo_env_var("HOST").ok_or("expected HOST env var")?; - let target = cargo_env_var("TARGET").ok_or("expected TARGET env var")?; +pub(crate) struct CrossCompileEnvVars { + pyo3_cross: Option, + pyo3_cross_lib_dir: Option, + pyo3_cross_python_version: Option, +} - if host == target { - // Definitely not cross compiling if the host matches the target - return Ok(None); +impl CrossCompileEnvVars { + pub fn any(&self) -> bool { + self.pyo3_cross.is_some() + || self.pyo3_cross_lib_dir.is_some() + || self.pyo3_cross_python_version.is_some() } +} - if target == "i686-pc-windows-msvc" && host == "x86_64-pc-windows-msvc" { - // Not cross-compiling to compile for 32-bit Python from windows 64-bit - return Ok(None); +pub(crate) fn cross_compile_env_vars() -> CrossCompileEnvVars { + CrossCompileEnvVars { + pyo3_cross: env::var_os("PYO3_CROSS"), + pyo3_cross_lib_dir: env::var_os("PYO3_CROSS_LIB_DIR"), + pyo3_cross_python_version: env::var_os("PYO3_CROSS_PYTHON_VERSION"), } - - let target_arch = - cargo_env_var("CARGO_CFG_TARGET_ARCH").ok_or("expected CARGO_CFG_TARGET_ARCH env var")?; - let target_vendor = cargo_env_var("CARGO_CFG_TARGET_VENDOR") - .ok_or("expected CARGO_CFG_TARGET_VENDOR env var")?; - let target_os = - cargo_env_var("CARGO_CFG_TARGET_OS").ok_or("expected CARGO_CFG_TARGET_OS env var")?; - - cross_compiling(&host, &target_arch, &target_vendor, &target_os) } /// Detect whether we are cross compiling and return an assembled CrossCompileConfig if so. @@ -619,53 +739,33 @@ pub fn cross_compiling( target_vendor: &str, target_os: &str, ) -> Result> { - let cross = env_var("PYO3_CROSS"); - let cross_lib_dir = env_var("PYO3_CROSS_LIB_DIR"); - let cross_python_version = env_var("PYO3_CROSS_PYTHON_VERSION"); + let env_vars = cross_compile_env_vars(); - let target_triple = format!("{}-{}-{}", target_arch, target_vendor, target_os); + let target_info = TargetInfo { + arch: target_arch.to_owned(), + vendor: target_vendor.to_owned(), + os: target_os.to_owned(), + }; - if cross.is_none() && cross_lib_dir.is_none() && cross_python_version.is_none() { - // No cross-compiling environment variables set; try to determine if this is a known case - // which is not cross-compilation. - - if target_triple == "x86_64-apple-darwin" && host == "aarch64-apple-darwin" { - // Not cross-compiling to compile for x86-64 Python from macOS arm64 - return Ok(None); - } - - if target_triple == "aarch64-apple-darwin" && host == "x86_64-apple-darwin" { - // Not cross-compiling to compile for arm64 Python from macOS x86_64 - return Ok(None); - } - - if host.starts_with(&target_triple) { - // Not cross-compiling if arch-vendor-os is all the same - // e.g. x86_64-unknown-linux-musl on x86_64-unknown-linux-gnu host - return Ok(None); - } + if !env_vars.any() && is_not_cross_compiling(host, &target_info) { + return Ok(None); } - // At this point we assume that we are cross compiling. + CrossCompileConfig::from_env_vars(env_vars, target_info).map(Some) +} - Ok(Some(CrossCompileConfig { - lib_dir: cross_lib_dir - .ok_or("The PYO3_CROSS_LIB_DIR environment variable must be set when cross-compiling")? - .into(), - arch: target_arch.into(), - vendor: target_vendor.into(), - os: target_os.into(), - version: cross_python_version - .map(|os_string| { - let utf8_str = os_string - .to_str() - .ok_or("PYO3_CROSS_PYTHON_VERSION is not valid utf-8.")?; - utf8_str - .parse() - .context("failed to parse PYO3_CROSS_PYTHON_VERSION") - }) - .transpose()?, - })) +fn is_not_cross_compiling(host: &str, target_info: &TargetInfo) -> bool { + let target_triple = target_info.to_target_triple(); + // Not cross-compiling if arch-vendor-os is all the same + // e.g. x86_64-unknown-linux-musl on x86_64-unknown-linux-gnu host + // x86_64-pc-windows-gnu on x86_64-pc-windows-msvc host + host.starts_with(&target_triple) + // Not cross-compiling to compile for 32-bit Python from windows 64-bit + || (target_triple == "i686-pc-windows" && host.starts_with("x86_64-pc-windows")) + // Not cross-compiling to compile for x86-64 Python from macOS arm64 + || (target_triple == "x86_64-apple-darwin" && host == "aarch64-apple-darwin") + // Not cross-compiling to compile for arm64 Python from macOS x86_64 + || (target_triple == "aarch64-apple-darwin" && host == "x86_64-apple-darwin") } #[allow(non_camel_case_types)] @@ -755,7 +855,7 @@ impl BuildFlags { let mut script = String::from("import sysconfig\n"); script.push_str("config = sysconfig.get_config_vars()\n"); - for k in BuildFlags::ALL.iter() { + for k in &BuildFlags::ALL { script.push_str(&format!("print(config.get('{}', '0'))\n", k)); } @@ -793,10 +893,10 @@ impl Display for BuildFlags { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let mut first = true; for flag in &self.0 { - if !first { - write!(f, ",")?; - } else { + if first { first = false; + } else { + write!(f, ",")?; } write!(f, "{}", flag)?; } @@ -991,15 +1091,15 @@ fn search_lib_dir(path: impl AsRef, cross: &CrossCompileConfig) -> Vec, cross: &CrossCompileConfig) -> Vec 1 { let temp = sysconfig_paths .iter() - .filter(|p| p.to_string_lossy().contains(&cross.arch)) + .filter(|p| p.to_string_lossy().contains(&cross.target_info.arch)) .cloned() .collect::>(); if !temp.is_empty() { @@ -1040,7 +1140,7 @@ fn search_lib_dir(path: impl AsRef, cross: &CrossCompileConfig) -> Vec Result { let sysconfigdata_path = find_sysconfigdata(&cross_compile_config)?; @@ -1081,11 +1181,11 @@ fn load_cross_compile_config( ) -> Result { match cargo_env_var("CARGO_CFG_TARGET_FAMILY") { // Configure for unix platforms using the sysconfigdata file - Some(os) if os == "unix" => load_cross_compile_from_sysconfigdata(cross_compile_config), + Some(os) if os == "unix" => cross_compile_from_sysconfigdata(cross_compile_config), // Use hardcoded interpreter config when targeting Windows Some(os) if os == "windows" => windows_hardcoded_cross_compile(cross_compile_config), // sysconfigdata works fine on wasm/wasi - Some(os) if os == "wasm" => load_cross_compile_from_sysconfigdata(cross_compile_config), + Some(os) if os == "wasm" => cross_compile_from_sysconfigdata(cross_compile_config), // Waiting for users to tell us what they expect on their target platform Some(os) => bail!( "Unknown target OS family for cross-compilation: {:?}.\n\ @@ -1095,7 +1195,7 @@ fn load_cross_compile_config( os ), // Unknown os family - try to do something useful - None => load_cross_compile_from_sysconfigdata(cross_compile_config), + None => cross_compile_from_sysconfigdata(cross_compile_config), } } @@ -1131,14 +1231,35 @@ fn default_lib_name_unix( Some(ld_version) => format!("python{}", ld_version), None => format!("python{}.{}", version.major, version.minor), }, - PythonImplementation::PyPy => format!("pypy{}-c", version.major), + PythonImplementation::PyPy => { + if version >= (PythonVersion { major: 3, minor: 9 }) { + match ld_version { + Some(ld_version) => format!("pypy{}-c", ld_version), + None => format!("pypy{}.{}-c", version.major, version.minor), + } + } else { + format!("pypy{}-c", version.major) + } + } } } /// Run a python script using the specified interpreter binary. fn run_python_script(interpreter: &Path, script: &str) -> Result { + run_python_script_with_envs(interpreter, script, std::iter::empty::<(&str, &str)>()) +} + +/// Run a python script using the specified interpreter binary with additional environment +/// variables (e.g. PYTHONPATH) set. +fn run_python_script_with_envs(interpreter: &Path, script: &str, envs: I) -> Result +where + I: IntoIterator, + K: AsRef, + V: AsRef, +{ let out = Command::new(interpreter) .env("PYTHONIOENCODING", "utf-8") + .envs(envs) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::inherit()) @@ -1237,6 +1358,11 @@ fn fixup_config_for_abi3( config: &mut InterpreterConfig, abi3_version: Option, ) -> Result<()> { + // PyPy doesn't support abi3; don't adjust the version + if config.implementation.is_pypy() { + return Ok(()); + } + if let Some(version) = abi3_version { ensure!( version <= config.version, @@ -1257,13 +1383,30 @@ fn fixup_config_for_abi3( /// This must be called from PyO3's build script, because it relies on environment variables such as /// CARGO_CFG_TARGET_OS which aren't available at any other time. pub fn make_cross_compile_config() -> Result> { - let mut interpreter_config = if let Some(paths) = cross_compiling_from_cargo_env()? { - load_cross_compile_config(paths)? + let env_vars = cross_compile_env_vars(); + + let host = cargo_env_var("HOST").ok_or("expected HOST env var")?; + let target = cargo_env_var("TARGET").ok_or("expected TARGET env var")?; + + let target_info = TargetInfo::from_cargo_env()?; + + let interpreter_config = if env_vars.any() { + let cross_config = CrossCompileConfig::from_env_vars(env_vars, target_info)?; + let mut interpreter_config = load_cross_compile_config(cross_config)?; + fixup_config_for_abi3(&mut interpreter_config, get_abi3_version())?; + Some(interpreter_config) } else { - return Ok(None); + ensure!( + host == target || is_not_cross_compiling(&host, &target_info), + "PyO3 detected compile host {host} and build target {target}, but none of PYO3_CROSS, PYO3_CROSS_LIB_DIR \ + or PYO3_CROSS_PYTHON_VERSION environment variables are set.", + host=host, + target=target, + ); + None }; - fixup_config_for_abi3(&mut interpreter_config, get_abi3_version())?; - Ok(Some(interpreter_config)) + + Ok(interpreter_config) } /// Generates an interpreter config which will be hard-coded into the pyo3-build-config crate. @@ -1467,14 +1610,71 @@ mod tests { ); } + #[test] + fn config_from_sysconfigdata_framework() { + let mut sysconfigdata = Sysconfigdata::new(); + sysconfigdata.insert("SOABI", "cpython-37m-x86_64-linux-gnu"); + sysconfigdata.insert("VERSION", "3.7"); + // PYTHONFRAMEWORK should override Py_ENABLE_SHARED + sysconfigdata.insert("Py_ENABLE_SHARED", "0"); + sysconfigdata.insert("PYTHONFRAMEWORK", "Python"); + sysconfigdata.insert("LIBDIR", "/usr/lib"); + sysconfigdata.insert("LDVERSION", "3.7m"); + sysconfigdata.insert("SIZEOF_VOID_P", "8"); + assert_eq!( + InterpreterConfig::from_sysconfigdata(&sysconfigdata).unwrap(), + InterpreterConfig { + abi3: false, + build_flags: BuildFlags::from_sysconfigdata(&sysconfigdata), + pointer_width: Some(64), + executable: None, + implementation: PythonImplementation::CPython, + lib_dir: Some("/usr/lib".into()), + lib_name: Some("python3.7m".into()), + shared: true, + version: PythonVersion::PY37, + suppress_build_script_link_lines: false, + extra_build_script_lines: vec![], + } + ); + + sysconfigdata = Sysconfigdata::new(); + sysconfigdata.insert("SOABI", "cpython-37m-x86_64-linux-gnu"); + sysconfigdata.insert("VERSION", "3.7"); + // An empty PYTHONFRAMEWORK means it is not a framework + sysconfigdata.insert("Py_ENABLE_SHARED", "0"); + sysconfigdata.insert("PYTHONFRAMEWORK", ""); + sysconfigdata.insert("LIBDIR", "/usr/lib"); + sysconfigdata.insert("LDVERSION", "3.7m"); + sysconfigdata.insert("SIZEOF_VOID_P", "8"); + assert_eq!( + InterpreterConfig::from_sysconfigdata(&sysconfigdata).unwrap(), + InterpreterConfig { + abi3: false, + build_flags: BuildFlags::from_sysconfigdata(&sysconfigdata), + pointer_width: Some(64), + executable: None, + implementation: PythonImplementation::CPython, + lib_dir: Some("/usr/lib".into()), + lib_name: Some("python3.7m".into()), + shared: false, + version: PythonVersion::PY37, + suppress_build_script_link_lines: false, + extra_build_script_lines: vec![], + } + ); + } + #[test] fn windows_hardcoded_cross_compile() { let cross_config = CrossCompileConfig { lib_dir: "C:\\some\\path".into(), version: Some(PythonVersion { major: 3, minor: 7 }), - os: "os".into(), - arch: "arch".into(), - vendor: "vendor".into(), + target_info: TargetInfo { + os: "os".into(), + arch: "arch".into(), + vendor: "vendor".into(), + }, }; assert_eq!( @@ -1567,11 +1767,17 @@ mod tests { "python3.7md", ); - // PyPy ignores ldversion + // PyPy 3.7 ignores ldversion assert_eq!( - super::default_lib_name_unix(PythonVersion { major: 3, minor: 9 }, PyPy, Some("3.7md")), + super::default_lib_name_unix(PythonVersion { major: 3, minor: 7 }, PyPy, Some("3.7md")), "pypy3-c", ); + + // PyPy 3.9 includes ldversion + assert_eq!( + super::default_lib_name_unix(PythonVersion { major: 3, minor: 9 }, PyPy, Some("3.9d")), + "pypy3.9d-c", + ); } #[test] @@ -1640,9 +1846,11 @@ mod tests { let cross = CrossCompileConfig { lib_dir: lib_dir.into(), version: Some(interpreter_config.version), - arch: "x86_64".into(), - vendor: "unknown".into(), - os: "linux".into(), + target_info: TargetInfo { + arch: "x86_64".into(), + vendor: "unknown".into(), + os: "linux".into(), + }, }; let sysconfigdata_path = match find_sysconfigdata(&cross) { @@ -1715,4 +1923,29 @@ mod tests { .is_none() ); } + + #[test] + fn test_run_python_script() { + // as above, this should be okay in CI where Python is presumed installed + let interpreter = make_interpreter_config() + .expect("could not get InterpreterConfig from installed interpreter"); + let out = interpreter + .run_python_script("print(2 + 2)") + .expect("failed to run Python script"); + assert_eq!(out.trim_end(), "4"); + } + + #[test] + fn test_run_python_script_with_envs() { + // as above, this should be okay in CI where Python is presumed installed + let interpreter = make_interpreter_config() + .expect("could not get InterpreterConfig from installed interpreter"); + let out = interpreter + .run_python_script_with_envs( + "import os; print(os.getenv('PYO3_TEST'))", + vec![("PYO3_TEST", "42")], + ) + .expect("failed to run Python script"); + assert_eq!(out.trim_end(), "42"); + } } diff --git a/pyo3-build-config/src/lib.rs b/pyo3-build-config/src/lib.rs index 0a4a147c..4cf49e3b 100644 --- a/pyo3-build-config/src/lib.rs +++ b/pyo3-build-config/src/lib.rs @@ -5,11 +5,14 @@ //! //! It used internally by the PyO3 crate's build script to apply the same configuration. +#![warn(elided_lifetimes_in_paths, unused_lifetimes)] + mod errors; mod impl_; #[cfg(feature = "resolve-config")] use std::io::Cursor; +use std::{env, process::Command}; #[cfg(feature = "resolve-config")] use once_cell::sync::OnceCell; @@ -60,21 +63,22 @@ fn _add_extension_module_link_args(target_os: &str, mut writer: impl std::io::Wr /// Loads the configuration determined from the build environment. /// /// Because this will never change in a given compilation run, this is cached in a `once_cell`. -#[doc(hidden)] #[cfg(feature = "resolve-config")] pub fn get() -> &'static InterpreterConfig { static CONFIG: OnceCell = OnceCell::new(); CONFIG.get_or_init(|| { - if !CONFIG_FILE.is_empty() { + if let Some(interpreter_config) = InterpreterConfig::from_cargo_dep_env() { + interpreter_config + } else if !CONFIG_FILE.is_empty() { InterpreterConfig::from_reader(Cursor::new(CONFIG_FILE)) } else if !ABI3_CONFIG.is_empty() { Ok(abi3_config()) - } else if impl_::any_cross_compiling_env_vars_set() { + } else if impl_::cross_compile_env_vars().any() { InterpreterConfig::from_path(DEFAULT_CROSS_COMPILE_CONFIG_PATH) } else { InterpreterConfig::from_reader(Cursor::new(HOST_CONFIG)) } - .expect("failed to parse PyO3 config file") + .expect("failed to parse PyO3 config") }) } @@ -115,6 +119,36 @@ fn abi3_config() -> InterpreterConfig { interpreter_config } +/// Use certain features if we detect the compiler being used supports them. +/// +/// Features may be removed or added as MSRV gets bumped or new features become available, +/// 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 generics on Rust 1.51 and greater + if rustc_minor_version >= 51 { + println!("cargo:rustc-cfg=min_const_generics"); + } + + // Enable use of std::ptr::addr_of! on Rust 1.51 and greater + if rustc_minor_version >= 51 { + println!("cargo:rustc-cfg=addr_of"); + } +} + /// Private exports used in PyO3's build.rs /// /// Please don't use these - they could change at any time. diff --git a/pyo3-ffi/Cargo.toml b/pyo3-ffi/Cargo.toml index d9d15f9d..81ab18df 100644 --- a/pyo3-ffi/Cargo.toml +++ b/pyo3-ffi/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3-ffi" -version = "0.15.1" +version = "0.16.2" description = "Python-API bindings for the PyO3 ecosystem" authors = ["PyO3 Project and Contributors "] keywords = ["pyo3", "python", "cpython", "ffi"] @@ -9,6 +9,7 @@ repository = "https://github.com/pyo3/pyo3" categories = ["api-bindings", "development-tools::ffi"] license = "Apache-2.0" edition = "2018" +links = "python" [dependencies] libc = "0.2.62" @@ -34,6 +35,4 @@ abi3-py310 = ["abi3", "pyo3-build-config/abi3-py310"] [build-dependencies] -pyo3-build-config = { path = "../pyo3-build-config", version = "0.15.1", features = ["resolve-config"] } - - +pyo3-build-config = { path = "../pyo3-build-config", version = "0.16.2", features = ["resolve-config"] } diff --git a/pyo3-ffi/README.md b/pyo3-ffi/README.md index a7a36133..d1473ee1 100644 --- a/pyo3-ffi/README.md +++ b/pyo3-ffi/README.md @@ -44,8 +44,8 @@ features = ["extension-module"] **`src/lib.rs`** ```rust -use std::mem::transmute; use std::os::raw::c_char; +use std::ptr; use pyo3_ffi::*; @@ -72,13 +72,11 @@ pub unsafe extern "C" fn PyInit_string_sum() -> *mut PyObject { PyUnicode_FromStringAndSize(version.as_ptr() as *const c_char, version.len() as isize), ); - // It's necessary to transmute `sum_as_string` here because functions marked with `METH_FASTCALL` - // have a different signature. However the `PyMethodDef` struct currently represents all - // functions as a `PyCFunction`. The python interpreter will cast the function pointer back - // to `_PyCFunctionFast`. let wrapped_sum_as_string = PyMethodDef { ml_name: "sum_as_string\0".as_ptr() as *const c_char, - ml_meth: Some(transmute::<_PyCFunctionFast, PyCFunction>(sum_as_string)), + ml_meth: PyMethodDefPointer { + _PyCFunctionFast: sum_as_string + }, ml_flags: METH_FASTCALL, ml_doc: "returns the sum of two integers as a string\0".as_ptr() as *const c_char, }; @@ -127,7 +125,7 @@ pub unsafe extern "C" fn sum_as_string( let arg1 = PyLong_AsLong(arg1); if !PyErr_Occurred().is_null() { - return ptr::null() + return ptr::null_mut() } let arg2 = *args.add(1); @@ -137,7 +135,7 @@ pub unsafe extern "C" fn sum_as_string( let arg2 = PyLong_AsLong(arg2); if !PyErr_Occurred().is_null() { - return ptr::null() + return ptr::null_mut() } diff --git a/pyo3-ffi/build.rs b/pyo3-ffi/build.rs index e72cfb97..ddc60e62 100644 --- a/pyo3-ffi/build.rs +++ b/pyo3-ffi/build.rs @@ -1,5 +1,5 @@ use pyo3_build_config::{ - bail, ensure, + bail, ensure, print_feature_cfgs, pyo3_build_script_impl::{ cargo_env_var, env_var, errors::Result, resolve_interpreter_config, InterpreterConfig, PythonVersion, @@ -69,6 +69,9 @@ fn emit_link_config(interpreter_config: &InterpreterConfig) -> Result<()> { } } + // serialize the whole interpreter config in DEP_PYTHON_PYO3_CONFIG + interpreter_config.to_cargo_dep_env()?; + Ok(()) } @@ -100,6 +103,9 @@ fn configure_pyo3() -> Result<()> { println!("{}", line); } + // Emit cfgs like `addr_of` and `min_const_generics` + print_feature_cfgs(); + Ok(()) } diff --git a/pyo3-ffi/src/boolobject.rs b/pyo3-ffi/src/boolobject.rs index 2b72db3f..17af974b 100644 --- a/pyo3-ffi/src/boolobject.rs +++ b/pyo3-ffi/src/boolobject.rs @@ -10,7 +10,7 @@ extern "C" { #[inline] pub unsafe fn PyBool_Check(op: *mut PyObject) -> c_int { - (Py_TYPE(op) == &mut PyBool_Type) as c_int + (Py_TYPE(op) == addr_of_mut_shim!(PyBool_Type)) as c_int } #[cfg_attr(windows, link(name = "pythonXY"))] @@ -23,12 +23,12 @@ extern "C" { #[inline] pub unsafe fn Py_False() -> *mut PyObject { - &mut _Py_FalseStruct as *mut PyLongObject as *mut PyObject + addr_of_mut_shim!(_Py_FalseStruct) as *mut PyLongObject as *mut PyObject } #[inline] pub unsafe fn Py_True() -> *mut PyObject { - &mut _Py_TrueStruct as *mut PyLongObject as *mut PyObject + addr_of_mut_shim!(_Py_TrueStruct) as *mut PyLongObject as *mut PyObject } #[inline] diff --git a/pyo3-ffi/src/bytearrayobject.rs b/pyo3-ffi/src/bytearrayobject.rs index d8df2d6a..7afa7801 100644 --- a/pyo3-ffi/src/bytearrayobject.rs +++ b/pyo3-ffi/src/bytearrayobject.rs @@ -12,12 +12,12 @@ extern "C" { #[inline] pub unsafe fn PyByteArray_Check(op: *mut PyObject) -> c_int { - PyObject_TypeCheck(op, &mut PyByteArray_Type) + PyObject_TypeCheck(op, addr_of_mut_shim!(PyByteArray_Type)) } #[inline] pub unsafe fn PyByteArray_CheckExact(op: *mut PyObject) -> c_int { - (Py_TYPE(op) == &mut PyByteArray_Type) as c_int + (Py_TYPE(op) == addr_of_mut_shim!(PyByteArray_Type)) as c_int } extern "C" { diff --git a/pyo3-ffi/src/bytesobject.rs b/pyo3-ffi/src/bytesobject.rs index 058f402c..8cdcd7df 100644 --- a/pyo3-ffi/src/bytesobject.rs +++ b/pyo3-ffi/src/bytesobject.rs @@ -16,7 +16,7 @@ pub unsafe fn PyBytes_Check(op: *mut PyObject) -> c_int { #[inline] pub unsafe fn PyBytes_CheckExact(op: *mut PyObject) -> c_int { - (Py_TYPE(op) == &mut PyBytes_Type) as c_int + (Py_TYPE(op) == addr_of_mut_shim!(PyBytes_Type)) as c_int } extern "C" { diff --git a/pyo3-ffi/src/complexobject.rs b/pyo3-ffi/src/complexobject.rs index 829adf82..83b7620a 100644 --- a/pyo3-ffi/src/complexobject.rs +++ b/pyo3-ffi/src/complexobject.rs @@ -40,12 +40,12 @@ extern "C" { #[inline] pub unsafe fn PyComplex_Check(op: *mut PyObject) -> c_int { - PyObject_TypeCheck(op, &mut PyComplex_Type) + PyObject_TypeCheck(op, addr_of_mut_shim!(PyComplex_Type)) } #[inline] pub unsafe fn PyComplex_CheckExact(op: *mut PyObject) -> c_int { - (Py_TYPE(op) == &mut PyComplex_Type) as c_int + (Py_TYPE(op) == addr_of_mut_shim!(PyComplex_Type)) as c_int } extern "C" { diff --git a/pyo3-ffi/src/context.rs b/pyo3-ffi/src/context.rs index d720cbe0..a4ee09e3 100644 --- a/pyo3-ffi/src/context.rs +++ b/pyo3-ffi/src/context.rs @@ -12,17 +12,17 @@ extern "C" { #[inline] pub unsafe fn PyContext_CheckExact(op: *mut PyObject) -> c_int { - (Py_TYPE(op) == &mut PyContext_Type) as c_int + (Py_TYPE(op) == addr_of_mut_shim!(PyContext_Type)) as c_int } #[inline] pub unsafe fn PyContextVar_CheckExact(op: *mut PyObject) -> c_int { - (Py_TYPE(op) == &mut PyContextVar_Type) as c_int + (Py_TYPE(op) == addr_of_mut_shim!(PyContextVar_Type)) as c_int } #[inline] pub unsafe fn PyContextToken_CheckExact(op: *mut PyObject) -> c_int { - (Py_TYPE(op) == &mut PyContextToken_Type) as c_int + (Py_TYPE(op) == addr_of_mut_shim!(PyContextToken_Type)) as c_int } extern "C" { diff --git a/pyo3-ffi/src/cpython/abstract_.rs b/pyo3-ffi/src/cpython/abstract_.rs index f495188c..25b1a5fc 100644 --- a/pyo3-ffi/src/cpython/abstract_.rs +++ b/pyo3-ffi/src/cpython/abstract_.rs @@ -9,7 +9,7 @@ use crate::{ pyport::PY_SSIZE_T_MAX, vectorcallfunc, PyCallable_Check, PyThreadState, PyThreadState_GET, PyTuple_Check, PyType_HasFeature, Py_TPFLAGS_HAVE_VECTORCALL, }; -#[cfg(all(Py_3_8, not(PyPy)))] +#[cfg(Py_3_8)] use libc::size_t; extern "C" { @@ -101,17 +101,29 @@ pub unsafe fn PyObject_Vectorcall( } extern "C" { - #[cfg(all(Py_3_8, not(PyPy)))] - #[cfg_attr(not(Py_3_9), link_name = "_PyObject_VectorcallDict")] + #[cfg(all(PyPy, Py_3_8))] + #[cfg_attr(not(Py_3_9), link_name = "_PyPyObject_Vectorcall")] + pub fn PyObject_Vectorcall( + callable: *mut PyObject, + args: *const *mut PyObject, + nargsf: size_t, + kwnames: *mut PyObject, + ) -> *mut PyObject; + + #[cfg(all(Py_3_8))] + #[cfg_attr(all(not(PyPy), 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( callable: *mut PyObject, args: *const *mut PyObject, nargsf: size_t, - kwargs: *mut PyObject, + kwdict: *mut PyObject, ) -> *mut PyObject; - #[cfg(all(Py_3_8, not(PyPy)))] - #[cfg_attr(not(Py_3_9), link_name = "_PyVectorcall_Call")] + #[cfg(all(Py_3_8))] + #[cfg_attr(not(any(Py_3_9, PyPy)), link_name = "_PyVectorcall_Call")] + #[cfg_attr(PyPy, link_name = "PyPyVectorcall_Call")] pub fn PyVectorcall_Call( callable: *mut PyObject, tuple: *mut PyObject, @@ -152,6 +164,12 @@ pub unsafe fn _PyObject_CallNoArg(func: *mut PyObject) -> *mut PyObject { ) } +extern "C" { + #[cfg(PyPy)] + #[link_name = "_PyPyObject_CallNoArg"] + pub fn _PyObject_CallNoArg(func: *mut PyObject) -> *mut PyObject; +} + #[cfg(all(Py_3_8, not(PyPy)))] #[inline(always)] pub unsafe fn PyObject_CallOneArg(func: *mut PyObject, arg: *mut PyObject) -> *mut PyObject { diff --git a/pyo3-ffi/src/cpython/code.rs b/pyo3-ffi/src/cpython/code.rs index 93657855..760afb71 100644 --- a/pyo3-ffi/src/cpython/code.rs +++ b/pyo3-ffi/src/cpython/code.rs @@ -84,7 +84,7 @@ extern "C" { #[inline] #[cfg(not(PyPy))] pub unsafe fn PyCode_Check(op: *mut PyObject) -> c_int { - (Py_TYPE(op) == &mut PyCode_Type) as c_int + (Py_TYPE(op) == addr_of_mut_shim!(PyCode_Type)) as c_int } #[inline] diff --git a/pyo3-ffi/src/cpython/frameobject.rs b/pyo3-ffi/src/cpython/frameobject.rs index 53d2d494..b51a10cb 100644 --- a/pyo3-ffi/src/cpython/frameobject.rs +++ b/pyo3-ffi/src/cpython/frameobject.rs @@ -60,7 +60,7 @@ extern "C" { #[inline] pub unsafe fn PyFrame_Check(op: *mut PyObject) -> c_int { - (Py_TYPE(op) == &mut PyFrame_Type) as c_int + (Py_TYPE(op) == addr_of_mut_shim!(PyFrame_Type)) as c_int } extern "C" { diff --git a/pyo3-ffi/src/datetime.rs b/pyo3-ffi/src/datetime.rs index d6499632..5e064ea2 100644 --- a/pyo3-ffi/src/datetime.rs +++ b/pyo3-ffi/src/datetime.rs @@ -359,7 +359,6 @@ pub struct PyDateTime_CAPI { pub TimeType: *mut PyTypeObject, pub DeltaType: *mut PyTypeObject, pub TZInfoType: *mut PyTypeObject, - #[cfg(not(all(PyPy, not(Py_3_8))))] pub TimeZone_UTC: *mut PyObject, pub Date_FromDate: unsafe extern "C" fn( year: c_int, @@ -393,7 +392,6 @@ pub struct PyDateTime_CAPI { normalize: c_int, cls: *mut PyTypeObject, ) -> *mut PyObject, - #[cfg(not(all(PyPy, not(Py_3_8))))] pub TimeZone_FromTimeZone: unsafe extern "C" fn(offset: *mut PyObject, name: *mut PyObject) -> *mut PyObject, @@ -442,7 +440,6 @@ pub unsafe fn PyDateTimeAPI() -> *mut PyDateTime_CAPI { *PyDateTimeAPI_impl.0.get() } -#[cfg(not(all(PyPy, not(Py_3_8))))] #[inline] pub unsafe fn PyDateTime_TimeZone_UTC() -> *mut PyObject { (*PyDateTimeAPI()).TimeZone_UTC diff --git a/pyo3-ffi/src/dictobject.rs b/pyo3-ffi/src/dictobject.rs index c4cb7193..b03fbb30 100644 --- a/pyo3-ffi/src/dictobject.rs +++ b/pyo3-ffi/src/dictobject.rs @@ -15,7 +15,7 @@ pub unsafe fn PyDict_Check(op: *mut PyObject) -> c_int { #[inline] pub unsafe fn PyDict_CheckExact(op: *mut PyObject) -> c_int { - (Py_TYPE(op) == &mut PyDict_Type) as c_int + (Py_TYPE(op) == addr_of_mut_shim!(PyDict_Type)) as c_int } extern "C" { @@ -76,17 +76,17 @@ extern "C" { #[inline] pub unsafe fn PyDictKeys_Check(op: *mut PyObject) -> c_int { - (Py_TYPE(op) == &mut PyDictKeys_Type) as c_int + (Py_TYPE(op) == addr_of_mut_shim!(PyDictKeys_Type)) as c_int } #[inline] pub unsafe fn PyDictValues_Check(op: *mut PyObject) -> c_int { - (Py_TYPE(op) == &mut PyDictValues_Type) as c_int + (Py_TYPE(op) == addr_of_mut_shim!(PyDictValues_Type)) as c_int } #[inline] pub unsafe fn PyDictItems_Check(op: *mut PyObject) -> c_int { - (Py_TYPE(op) == &mut PyDictItems_Type) as c_int + (Py_TYPE(op) == addr_of_mut_shim!(PyDictItems_Type)) as c_int } #[inline] diff --git a/pyo3-ffi/src/floatobject.rs b/pyo3-ffi/src/floatobject.rs index 476f2145..15f71ba2 100644 --- a/pyo3-ffi/src/floatobject.rs +++ b/pyo3-ffi/src/floatobject.rs @@ -20,12 +20,12 @@ extern "C" { #[inline] pub unsafe fn PyFloat_Check(op: *mut PyObject) -> c_int { - PyObject_TypeCheck(op, &mut PyFloat_Type) + PyObject_TypeCheck(op, addr_of_mut_shim!(PyFloat_Type)) } #[inline] pub unsafe fn PyFloat_CheckExact(op: *mut PyObject) -> c_int { - (Py_TYPE(op) == &mut PyFloat_Type) as c_int + (Py_TYPE(op) == addr_of_mut_shim!(PyFloat_Type)) as c_int } // skipped Py_RETURN_NAN diff --git a/pyo3-ffi/src/funcobject.rs b/pyo3-ffi/src/funcobject.rs index 020d2b49..f3f067e5 100644 --- a/pyo3-ffi/src/funcobject.rs +++ b/pyo3-ffi/src/funcobject.rs @@ -12,7 +12,7 @@ extern "C" { #[inline] pub unsafe fn PyFunction_Check(op: *mut PyObject) -> c_int { - (Py_TYPE(op) == &mut PyFunction_Type) as c_int + (Py_TYPE(op) == addr_of_mut_shim!(PyFunction_Type)) as c_int } extern "C" { diff --git a/pyo3-ffi/src/genobject.rs b/pyo3-ffi/src/genobject.rs index 054104de..c13edb07 100644 --- a/pyo3-ffi/src/genobject.rs +++ b/pyo3-ffi/src/genobject.rs @@ -25,12 +25,12 @@ extern "C" { #[inline] pub unsafe fn PyGen_Check(op: *mut PyObject) -> c_int { - PyObject_TypeCheck(op, &mut PyGen_Type) + PyObject_TypeCheck(op, addr_of_mut_shim!(PyGen_Type)) } #[inline] pub unsafe fn PyGen_CheckExact(op: *mut PyObject) -> c_int { - (Py_TYPE(op) == &mut PyGen_Type) as c_int + (Py_TYPE(op) == addr_of_mut_shim!(PyGen_Type)) as c_int } extern "C" { @@ -55,7 +55,7 @@ extern "C" { #[inline] pub unsafe fn PyCoro_CheckExact(op: *mut PyObject) -> c_int { - PyObject_TypeCheck(op, &mut PyCoro_Type) + PyObject_TypeCheck(op, addr_of_mut_shim!(PyCoro_Type)) } // skipped _PyCoro_GetAwaitableIter @@ -75,7 +75,7 @@ extern "C" { #[inline] pub unsafe fn PyAsyncGen_CheckExact(op: *mut PyObject) -> c_int { - PyObject_TypeCheck(op, &mut PyAsyncGen_Type) + PyObject_TypeCheck(op, addr_of_mut_shim!(PyAsyncGen_Type)) } // skipped _PyAsyncGenValueWrapperNew diff --git a/pyo3-ffi/src/iterobject.rs b/pyo3-ffi/src/iterobject.rs index f83e30bb..657409c7 100644 --- a/pyo3-ffi/src/iterobject.rs +++ b/pyo3-ffi/src/iterobject.rs @@ -9,7 +9,7 @@ extern "C" { #[inline] pub unsafe fn PySeqIter_Check(op: *mut PyObject) -> c_int { - (Py_TYPE(op) == &mut PySeqIter_Type) as c_int + (Py_TYPE(op) == addr_of_mut_shim!(PySeqIter_Type)) as c_int } extern "C" { @@ -19,7 +19,7 @@ extern "C" { #[inline] pub unsafe fn PyCallIter_Check(op: *mut PyObject) -> c_int { - (Py_TYPE(op) == &mut PyCallIter_Type) as c_int + (Py_TYPE(op) == addr_of_mut_shim!(PyCallIter_Type)) as c_int } extern "C" { diff --git a/pyo3-ffi/src/lib.rs b/pyo3-ffi/src/lib.rs index 3564604c..efea47c2 100644 --- a/pyo3-ffi/src/lib.rs +++ b/pyo3-ffi/src/lib.rs @@ -83,8 +83,8 @@ //! //! **`src/lib.rs`** //! ```rust -//! use std::mem::transmute; //! use std::os::raw::c_char; +//! use std::ptr; //! //! use pyo3_ffi::*; //! @@ -111,13 +111,11 @@ //! PyUnicode_FromStringAndSize(version.as_ptr() as *const c_char, version.len() as isize), //! ); //! -//! // It's necessary to transmute `sum_as_string` here because functions marked with `METH_FASTCALL` -//! // have a different signature. However the `PyMethodDef` struct currently represents all -//! // functions as a `PyCFunction`. The python interpreter will cast the function pointer back -//! // to `_PyCFunctionFast`. //! let wrapped_sum_as_string = PyMethodDef { //! ml_name: "sum_as_string\0".as_ptr() as *const c_char, -//! ml_meth: Some(transmute::<_PyCFunctionFast, PyCFunction>(sum_as_string)), +//! ml_meth: PyMethodDefPointer { +//! _PyCFunctionFast: sum_as_string +//! }, //! ml_flags: METH_FASTCALL, //! ml_doc: "returns the sum of two integers as a string\0".as_ptr() as *const c_char, //! }; @@ -166,7 +164,7 @@ //! //! let arg1 = PyLong_AsLong(arg1); //! if !PyErr_Occurred().is_null() { -//! return ptr::null() +//! return ptr::null_mut() //! } //! //! let arg2 = *args.add(1); @@ -176,7 +174,7 @@ //! //! let arg2 = PyLong_AsLong(arg2); //! if !PyErr_Occurred().is_null() { -//! return ptr::null() +//! return ptr::null_mut() //! } //! let res = (arg1 + arg2).to_string(); //! PyUnicode_FromStringAndSize(res.as_ptr() as *const c_char, res.len() as isize) @@ -254,6 +252,7 @@ clippy::upper_case_acronyms, clippy::missing_safety_doc )] +#![warn(elided_lifetimes_in_paths, unused_lifetimes)] // Until `extern type` is stabilized, use the recommended approach to // model opaque types: @@ -265,6 +264,19 @@ macro_rules! opaque_struct { }; } +macro_rules! addr_of_mut_shim { + ($place:expr) => {{ + #[cfg(addr_of)] + { + ::std::ptr::addr_of_mut!($place) + } + #[cfg(not(addr_of))] + { + &mut $place as *mut _ + } + }}; +} + pub use self::abstract_::*; pub use self::bltinmodule::*; pub use self::boolobject::*; diff --git a/pyo3-ffi/src/listobject.rs b/pyo3-ffi/src/listobject.rs index c45b1fcb..9b10a917 100644 --- a/pyo3-ffi/src/listobject.rs +++ b/pyo3-ffi/src/listobject.rs @@ -17,7 +17,7 @@ pub unsafe fn PyList_Check(op: *mut PyObject) -> c_int { #[inline] pub unsafe fn PyList_CheckExact(op: *mut PyObject) -> c_int { - (Py_TYPE(op) == &mut PyList_Type) as c_int + (Py_TYPE(op) == addr_of_mut_shim!(PyList_Type)) as c_int } extern "C" { diff --git a/pyo3-ffi/src/longobject.rs b/pyo3-ffi/src/longobject.rs index f44115cf..72e42b3d 100644 --- a/pyo3-ffi/src/longobject.rs +++ b/pyo3-ffi/src/longobject.rs @@ -20,7 +20,7 @@ pub unsafe fn PyLong_Check(op: *mut PyObject) -> c_int { #[inline] pub unsafe fn PyLong_CheckExact(op: *mut PyObject) -> c_int { - (Py_TYPE(op) == &mut PyLong_Type) as c_int + (Py_TYPE(op) == addr_of_mut_shim!(PyLong_Type)) as c_int } extern "C" { @@ -97,7 +97,7 @@ extern "C" { // skipped non-limited _PyLong_Sign #[cfg_attr(PyPy, link_name = "_PyPyLong_NumBits")] - pub fn _PyLong_NumBits(obj: *mut PyObject) -> c_int; + pub fn _PyLong_NumBits(obj: *mut PyObject) -> size_t; // skipped _PyLong_DivmodNear diff --git a/pyo3-ffi/src/memoryobject.rs b/pyo3-ffi/src/memoryobject.rs index 1bebc881..a6569003 100644 --- a/pyo3-ffi/src/memoryobject.rs +++ b/pyo3-ffi/src/memoryobject.rs @@ -12,7 +12,7 @@ extern "C" { #[inline] pub unsafe fn PyMemoryView_Check(op: *mut PyObject) -> c_int { - (Py_TYPE(op) == &mut PyMemoryView_Type) as c_int + (Py_TYPE(op) == addr_of_mut_shim!(PyMemoryView_Type)) as c_int } // skipped non-limited PyMemoryView_GET_BUFFER diff --git a/pyo3-ffi/src/methodobject.rs b/pyo3-ffi/src/methodobject.rs index 6212090f..d0afa6f5 100644 --- a/pyo3-ffi/src/methodobject.rs +++ b/pyo3-ffi/src/methodobject.rs @@ -13,25 +13,25 @@ extern "C" { #[cfg(Py_3_9)] #[inline] pub unsafe fn PyCFunction_CheckExact(op: *mut PyObject) -> c_int { - (Py_TYPE(op) == &mut PyCFunction_Type) as c_int + (Py_TYPE(op) == addr_of_mut_shim!(PyCFunction_Type)) as c_int } #[cfg(Py_3_9)] #[inline] pub unsafe fn PyCFunction_Check(op: *mut PyObject) -> c_int { - PyObject_TypeCheck(op, &mut PyCFunction_Type) + PyObject_TypeCheck(op, addr_of_mut_shim!(PyCFunction_Type)) } #[cfg(not(Py_3_9))] #[inline] pub unsafe fn PyCFunction_Check(op: *mut PyObject) -> c_int { - (Py_TYPE(op) == &mut PyCFunction_Type) as c_int + (Py_TYPE(op) == addr_of_mut_shim!(PyCFunction_Type)) as c_int } pub type PyCFunction = unsafe extern "C" fn(slf: *mut PyObject, args: *mut PyObject) -> *mut PyObject; -#[cfg(not(Py_LIMITED_API))] +#[cfg(any(Py_3_10, not(Py_LIMITED_API)))] pub type _PyCFunctionFast = unsafe extern "C" fn( slf: *mut PyObject, args: *mut *mut PyObject, @@ -52,7 +52,14 @@ pub type _PyCFunctionFastWithKeywords = unsafe extern "C" fn( kwnames: *mut PyObject, ) -> *mut PyObject; -// skipped PyCMethod (since 3.9) +#[cfg(all(Py_3_9, not(Py_LIMITED_API)))] +pub type PyCMethod = unsafe extern "C" fn( + slf: *mut PyObject, + defining_class: *mut PyTypeObject, + args: *const *mut PyObject, + nargs: crate::pyport::Py_ssize_t, + kwnames: *mut PyObject, +) -> *mut PyObject; extern "C" { #[cfg_attr(PyPy, link_name = "PyPyCFunction_GetFunction")] @@ -71,17 +78,46 @@ extern "C" { #[derive(Copy, Clone)] pub struct PyMethodDef { pub ml_name: *const c_char, - pub ml_meth: Option, + pub ml_meth: PyMethodDefPointer, pub ml_flags: c_int, pub ml_doc: *const c_char, } -impl Default for PyMethodDef { - fn default() -> PyMethodDef { - unsafe { mem::zeroed() } - } +/// Function types used to implement Python callables. +/// +/// This function pointer must be accompanied by the correct [ml_flags](PyMethodDef::ml_flags), +/// otherwise the behavior is undefined. +/// +/// See the [Python C API documentation][1] for more information. +/// +/// [1]: https://docs.python.org/3/c-api/structures.html#implementing-functions-and-methods +#[repr(C)] +#[derive(Copy, Clone)] +pub union PyMethodDefPointer { + /// This variant corresponds with [`METH_VARARGS`] *or* [`METH_NOARGS`] *or* [`METH_O`]. + pub PyCFunction: PyCFunction, + + /// This variant corresponds with [`METH_VARARGS`] | [`METH_KEYWORDS`]. + pub PyCFunctionWithKeywords: PyCFunctionWithKeywords, + + /// This variant corresponds with [`METH_FASTCALL`]. + #[cfg(any(Py_3_10, not(Py_LIMITED_API)))] + pub _PyCFunctionFast: _PyCFunctionFast, + + /// This variant corresponds with [`METH_FASTCALL`] | [`METH_KEYWORDS`]. + #[cfg(not(Py_LIMITED_API))] + pub _PyCFunctionFastWithKeywords: _PyCFunctionFastWithKeywords, + + /// This variant corresponds with [`METH_METHOD`] | [`METH_FASTCALL`] | [`METH_KEYWORDS`]. + #[cfg(all(Py_3_9, not(Py_LIMITED_API)))] + pub PyCMethod: PyCMethod, } +// TODO: This can be a const assert on Rust 1.57 +const _: () = + [()][mem::size_of::() - mem::size_of::>()]; + +#[cfg(not(Py_3_9))] extern "C" { #[cfg_attr(PyPy, link_name = "PyPyCFunction_New")] pub fn PyCFunction_New(ml: *mut PyMethodDef, slf: *mut PyObject) -> *mut PyObject; @@ -94,7 +130,32 @@ extern "C" { ) -> *mut PyObject; } -// skipped non-limited / 3.9 PyCMethod_New +#[cfg(Py_3_9)] +#[inline] +pub unsafe fn PyCFunction_New(ml: *mut PyMethodDef, slf: *mut PyObject) -> *mut PyObject { + PyCFunction_NewEx(ml, slf, std::ptr::null_mut()) +} + +#[cfg(Py_3_9)] +#[inline] +pub unsafe fn PyCFunction_NewEx( + ml: *mut PyMethodDef, + slf: *mut PyObject, + module: *mut PyObject, +) -> *mut PyObject { + PyCMethod_New(ml, slf, module, std::ptr::null_mut()) +} + +#[cfg(Py_3_9)] +extern "C" { + #[cfg_attr(PyPy, link_name = "PyPyCMethod_New")] + pub fn PyCMethod_New( + ml: *mut PyMethodDef, + slf: *mut PyObject, + module: *mut PyObject, + cls: *mut PyTypeObject, + ) -> *mut PyObject; +} /* Flag passed to newmethodobject */ pub const METH_VARARGS: c_int = 0x0001; @@ -118,11 +179,13 @@ pub const METH_COEXIST: c_int = 0x0040; /* METH_FASTCALL indicates the PEP 590 Vectorcall calling format. It may be specified alone or with METH_KEYWORDS. */ -#[cfg(not(Py_LIMITED_API))] +#[cfg(any(Py_3_10, not(Py_LIMITED_API)))] pub const METH_FASTCALL: c_int = 0x0080; // skipped METH_STACKLESS -// skipped METH_METHOD + +#[cfg(all(Py_3_9, not(Py_LIMITED_API)))] +pub const METH_METHOD: c_int = 0x0200; extern "C" { #[cfg(not(Py_3_9))] diff --git a/pyo3-ffi/src/modsupport.rs b/pyo3-ffi/src/modsupport.rs index fc0815a4..fa234016 100644 --- a/pyo3-ffi/src/modsupport.rs +++ b/pyo3-ffi/src/modsupport.rs @@ -77,7 +77,12 @@ extern "C" { name: *const c_char, value: *const c_char, ) -> c_int; - // skipped non-limited / 3.9 PyModule_AddType + #[cfg(any(Py_3_10, all(Py_3_9, not(Py_LIMITED_API))))] + #[cfg_attr(PyPy, link_name = "PyPyModule_AddType")] + pub fn PyModule_AddType( + module: *mut PyObject, + type_: *mut crate::object::PyTypeObject, + ) -> c_int; // skipped PyModule_AddIntMacro // skipped PyModule_AddStringMacro pub fn PyModule_SetDocString(arg1: *mut PyObject, arg2: *const c_char) -> c_int; diff --git a/pyo3-ffi/src/moduleobject.rs b/pyo3-ffi/src/moduleobject.rs index 94939943..63d9f44c 100644 --- a/pyo3-ffi/src/moduleobject.rs +++ b/pyo3-ffi/src/moduleobject.rs @@ -11,12 +11,12 @@ extern "C" { #[inline] pub unsafe fn PyModule_Check(op: *mut PyObject) -> c_int { - PyObject_TypeCheck(op, &mut PyModule_Type) + PyObject_TypeCheck(op, addr_of_mut_shim!(PyModule_Type)) } #[inline] pub unsafe fn PyModule_CheckExact(op: *mut PyObject) -> c_int { - (Py_TYPE(op) == &mut PyModule_Type) as c_int + (Py_TYPE(op) == addr_of_mut_shim!(PyModule_Type)) as c_int } extern "C" { diff --git a/pyo3-ffi/src/object.rs b/pyo3-ffi/src/object.rs index 64d4eec8..408ce861 100644 --- a/pyo3-ffi/src/object.rs +++ b/pyo3-ffi/src/object.rs @@ -63,9 +63,7 @@ pub unsafe fn Py_Is(x: *mut PyObject, y: *mut PyObject) -> c_int { #[inline] pub unsafe fn Py_REFCNT(ob: *mut PyObject) -> Py_ssize_t { - if ob.is_null() { - panic!(); - } + assert!(!ob.is_null()); (*ob).ob_refcnt } @@ -207,9 +205,21 @@ extern "C" { #[cfg_attr(PyPy, link_name = "PyPyType_GetSlot")] pub fn PyType_GetSlot(arg1: *mut PyTypeObject, arg2: c_int) -> *mut c_void; - // skipped non-limited / 3.9 PyType_FromModuleAndSpec - // skipped non-limited / 3.9 PyType_GetModule - // skipped non-limited / 3.9 PyType_GetModuleState + #[cfg(any(Py_3_10, all(Py_3_9, not(Py_LIMITED_API))))] + #[cfg_attr(PyPy, link_name = "PyPyType_FromModuleAndSpec")] + pub fn PyType_FromModuleAndSpec( + module: *mut PyObject, + spec: *mut PyType_Spec, + bases: *mut PyObject, + ) -> *mut PyObject; + + #[cfg(any(Py_3_10, all(Py_3_9, not(Py_LIMITED_API))))] + #[cfg_attr(PyPy, link_name = "PyPyType_GetModule")] + pub fn PyType_GetModule(arg1: *mut PyTypeObject) -> *mut PyObject; + + #[cfg(any(Py_3_10, all(Py_3_9, not(Py_LIMITED_API))))] + #[cfg_attr(PyPy, link_name = "PyPyType_GetModuleState")] + pub fn PyType_GetModuleState(arg1: *mut PyTypeObject) -> *mut c_void; } extern "C" { @@ -412,7 +422,7 @@ pub unsafe fn Py_DECREF(op: *mut PyObject) { } #[inline] -pub unsafe fn Py_CLEAR(op: &mut *mut PyObject) { +pub unsafe fn Py_CLEAR(op: *mut *mut PyObject) { let tmp = *op; if !tmp.is_null() { *op = ptr::null_mut(); @@ -470,7 +480,7 @@ extern "C" { #[inline] pub unsafe fn Py_None() -> *mut PyObject { - &mut _Py_NoneStruct + addr_of_mut_shim!(_Py_NoneStruct) } #[inline] @@ -488,7 +498,7 @@ extern "C" { #[inline] pub unsafe fn Py_NotImplemented() -> *mut PyObject { - &mut _Py_NotImplementedStruct + addr_of_mut_shim!(_Py_NotImplementedStruct) } // skipped Py_RETURN_NOTIMPLEMENTED @@ -536,5 +546,5 @@ pub unsafe fn PyType_Check(op: *mut PyObject) -> c_int { #[inline] pub unsafe fn PyType_CheckExact(op: *mut PyObject) -> c_int { - (Py_TYPE(op) == &mut PyType_Type) as c_int + (Py_TYPE(op) == addr_of_mut_shim!(PyType_Type)) as c_int } diff --git a/pyo3-ffi/src/pycapsule.rs b/pyo3-ffi/src/pycapsule.rs index 0e15d138..382d7eb8 100644 --- a/pyo3-ffi/src/pycapsule.rs +++ b/pyo3-ffi/src/pycapsule.rs @@ -11,7 +11,7 @@ pub type PyCapsule_Destructor = unsafe extern "C" fn(o: *mut PyObject); #[inline] pub unsafe fn PyCapsule_CheckExact(ob: *mut PyObject) -> c_int { - (Py_TYPE(ob) == &mut PyCapsule_Type) as c_int + (Py_TYPE(ob) == addr_of_mut_shim!(PyCapsule_Type)) as c_int } extern "C" { diff --git a/pyo3-ffi/src/rangeobject.rs b/pyo3-ffi/src/rangeobject.rs index af2d3d0b..be617377 100644 --- a/pyo3-ffi/src/rangeobject.rs +++ b/pyo3-ffi/src/rangeobject.rs @@ -11,5 +11,5 @@ extern "C" { #[inline] pub unsafe fn PyRange_Check(op: *mut PyObject) -> c_int { - (Py_TYPE(op) == &mut PyRange_Type) as c_int + (Py_TYPE(op) == addr_of_mut_shim!(PyRange_Type)) as c_int } diff --git a/pyo3-ffi/src/setobject.rs b/pyo3-ffi/src/setobject.rs index b5e48592..c90d26f5 100644 --- a/pyo3-ffi/src/setobject.rs +++ b/pyo3-ffi/src/setobject.rs @@ -82,7 +82,7 @@ extern "C" { #[inline] #[cfg(not(PyPy))] pub unsafe fn PyFrozenSet_CheckExact(ob: *mut PyObject) -> c_int { - (Py_TYPE(ob) == &mut PyFrozenSet_Type) as c_int + (Py_TYPE(ob) == addr_of_mut_shim!(PyFrozenSet_Type)) as c_int } extern "C" { @@ -94,8 +94,8 @@ extern "C" { #[inline] #[cfg(not(PyPy))] pub unsafe fn PyFrozenSet_Check(ob: *mut PyObject) -> c_int { - (Py_TYPE(ob) == &mut PyFrozenSet_Type - || PyType_IsSubtype(Py_TYPE(ob), &mut PyFrozenSet_Type) != 0) as c_int + (Py_TYPE(ob) == addr_of_mut_shim!(PyFrozenSet_Type) + || PyType_IsSubtype(Py_TYPE(ob), addr_of_mut_shim!(PyFrozenSet_Type)) != 0) as c_int } extern "C" { @@ -107,20 +107,21 @@ extern "C" { #[inline] #[cfg(not(PyPy))] pub unsafe fn PyAnySet_CheckExact(ob: *mut PyObject) -> c_int { - (Py_TYPE(ob) == &mut PySet_Type || Py_TYPE(ob) == &mut PyFrozenSet_Type) as c_int + (Py_TYPE(ob) == addr_of_mut_shim!(PySet_Type) + || Py_TYPE(ob) == addr_of_mut_shim!(PyFrozenSet_Type)) as c_int } #[inline] pub unsafe fn PyAnySet_Check(ob: *mut PyObject) -> c_int { (PyAnySet_CheckExact(ob) != 0 - || PyType_IsSubtype(Py_TYPE(ob), &mut PySet_Type) != 0 - || PyType_IsSubtype(Py_TYPE(ob), &mut PyFrozenSet_Type) != 0) as c_int + || PyType_IsSubtype(Py_TYPE(ob), addr_of_mut_shim!(PySet_Type)) != 0 + || PyType_IsSubtype(Py_TYPE(ob), addr_of_mut_shim!(PyFrozenSet_Type)) != 0) as c_int } #[inline] #[cfg(Py_3_10)] pub unsafe fn PySet_CheckExact(op: *mut PyObject) -> c_int { - crate::Py_IS_TYPE(op, &mut PySet_Type) + crate::Py_IS_TYPE(op, addr_of_mut_shim!(PySet_Type)) } extern "C" { @@ -132,5 +133,6 @@ extern "C" { #[inline] #[cfg(not(PyPy))] pub unsafe fn PySet_Check(ob: *mut PyObject) -> c_int { - (Py_TYPE(ob) == &mut PySet_Type || PyType_IsSubtype(Py_TYPE(ob), &mut PySet_Type) != 0) as c_int + (Py_TYPE(ob) == addr_of_mut_shim!(PySet_Type) + || PyType_IsSubtype(Py_TYPE(ob), addr_of_mut_shim!(PySet_Type)) != 0) as c_int } diff --git a/pyo3-ffi/src/sliceobject.rs b/pyo3-ffi/src/sliceobject.rs index 036b4c7d..5f3138ef 100644 --- a/pyo3-ffi/src/sliceobject.rs +++ b/pyo3-ffi/src/sliceobject.rs @@ -9,7 +9,7 @@ extern "C" { #[inline] pub unsafe fn Py_Ellipsis() -> *mut PyObject { - &mut _Py_EllipsisObject + addr_of_mut_shim!(_Py_EllipsisObject) } #[cfg(not(Py_LIMITED_API))] @@ -30,7 +30,7 @@ extern "C" { #[inline] pub unsafe fn PySlice_Check(op: *mut PyObject) -> c_int { - (Py_TYPE(op) == &mut PySlice_Type) as c_int + (Py_TYPE(op) == addr_of_mut_shim!(PySlice_Type)) as c_int } extern "C" { diff --git a/pyo3-ffi/src/traceback.rs b/pyo3-ffi/src/traceback.rs index f9398fb6..b3a25844 100644 --- a/pyo3-ffi/src/traceback.rs +++ b/pyo3-ffi/src/traceback.rs @@ -21,5 +21,5 @@ extern "C" { #[inline] #[cfg(not(PyPy))] pub unsafe fn PyTraceBack_Check(op: *mut PyObject) -> c_int { - (Py_TYPE(op) == &mut PyTraceBack_Type) as c_int + (Py_TYPE(op) == addr_of_mut_shim!(PyTraceBack_Type)) as c_int } diff --git a/pyo3-ffi/src/tupleobject.rs b/pyo3-ffi/src/tupleobject.rs index e4f4b9ea..de3c4c54 100644 --- a/pyo3-ffi/src/tupleobject.rs +++ b/pyo3-ffi/src/tupleobject.rs @@ -16,7 +16,7 @@ pub unsafe fn PyTuple_Check(op: *mut PyObject) -> c_int { #[inline] pub unsafe fn PyTuple_CheckExact(op: *mut PyObject) -> c_int { - (Py_TYPE(op) == &mut PyTuple_Type) as c_int + (Py_TYPE(op) == addr_of_mut_shim!(PyTuple_Type)) as c_int } extern "C" { diff --git a/pyo3-ffi/src/unicodeobject.rs b/pyo3-ffi/src/unicodeobject.rs index 5dd2b3cb..5841fa9e 100644 --- a/pyo3-ffi/src/unicodeobject.rs +++ b/pyo3-ffi/src/unicodeobject.rs @@ -34,7 +34,7 @@ pub unsafe fn PyUnicode_Check(op: *mut PyObject) -> c_int { #[inline] #[cfg(not(PyPy))] pub unsafe fn PyUnicode_CheckExact(op: *mut PyObject) -> c_int { - (Py_TYPE(op) == &mut PyUnicode_Type) as c_int + (Py_TYPE(op) == addr_of_mut_shim!(PyUnicode_Type)) as c_int } pub const Py_UNICODE_REPLACEMENT_CHARACTER: Py_UCS4 = 0xFFFD; diff --git a/pyo3-ffi/src/weakrefobject.rs b/pyo3-ffi/src/weakrefobject.rs index 575bc495..b9506115 100644 --- a/pyo3-ffi/src/weakrefobject.rs +++ b/pyo3-ffi/src/weakrefobject.rs @@ -24,20 +24,20 @@ extern "C" { #[inline] #[cfg(not(PyPy))] pub unsafe fn PyWeakref_CheckRef(op: *mut PyObject) -> c_int { - PyObject_TypeCheck(op, &mut _PyWeakref_RefType) + PyObject_TypeCheck(op, addr_of_mut_shim!(_PyWeakref_RefType)) } #[inline] #[cfg(not(PyPy))] pub unsafe fn PyWeakref_CheckRefExact(op: *mut PyObject) -> c_int { - (Py_TYPE(op) == &mut _PyWeakref_RefType) as c_int + (Py_TYPE(op) == addr_of_mut_shim!(_PyWeakref_RefType)) as c_int } #[inline] #[cfg(not(PyPy))] pub unsafe fn PyWeakref_CheckProxy(op: *mut PyObject) -> c_int { - ((Py_TYPE(op) == &mut _PyWeakref_ProxyType) - || (Py_TYPE(op) == &mut _PyWeakref_CallableProxyType)) as c_int + ((Py_TYPE(op) == addr_of_mut_shim!(_PyWeakref_ProxyType)) + || (Py_TYPE(op) == addr_of_mut_shim!(_PyWeakref_CallableProxyType))) as c_int } #[inline] diff --git a/pyo3-macros-backend/Cargo.toml b/pyo3-macros-backend/Cargo.toml index f46a0848..3b6ddcb4 100644 --- a/pyo3-macros-backend/Cargo.toml +++ b/pyo3-macros-backend/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3-macros-backend" -version = "0.15.1" +version = "0.16.2" description = "Code generation for PyO3 package" authors = ["PyO3 Project and Contributors "] keywords = ["pyo3", "python", "cpython", "ffi"] @@ -16,12 +16,12 @@ edition = "2018" [dependencies] quote = { version = "1", default-features = false } proc-macro2 = { version = "1", default-features = false } -pyo3-build-config = { path = "../pyo3-build-config", version = "0.15.1", features = ["resolve-config"] } [dependencies.syn] -version = "1" +version = "1.0.56" default-features = false features = ["derive", "parsing", "printing", "clone-impls", "full", "extra-traits"] [features] pyproto = [] +abi3 = [] diff --git a/pyo3-macros-backend/src/attributes.rs b/pyo3-macros-backend/src/attributes.rs index e0566076..2b63f0cf 100644 --- a/pyo3-macros-backend/src/attributes.rs +++ b/pyo3-macros-backend/src/attributes.rs @@ -1,77 +1,107 @@ +use proc_macro2::TokenStream; +use quote::ToTokens; use syn::{ parse::{Parse, ParseStream}, punctuated::Punctuated, + spanned::Spanned, token::Comma, - Attribute, ExprPath, Ident, LitStr, Path, Result, Token, + Attribute, Expr, ExprPath, Ident, LitStr, Path, Result, Token, }; pub mod kw { syn::custom_keyword!(annotation); syn::custom_keyword!(attribute); + syn::custom_keyword!(dict); + syn::custom_keyword!(extends); + syn::custom_keyword!(freelist); syn::custom_keyword!(from_py_with); + syn::custom_keyword!(gc); syn::custom_keyword!(get); syn::custom_keyword!(item); - syn::custom_keyword!(pass_module); + syn::custom_keyword!(module); syn::custom_keyword!(name); + syn::custom_keyword!(pass_module); syn::custom_keyword!(set); syn::custom_keyword!(signature); + syn::custom_keyword!(subclass); syn::custom_keyword!(text_signature); syn::custom_keyword!(transparent); + syn::custom_keyword!(unsendable); + syn::custom_keyword!(weakref); } -#[derive(Clone, Debug, PartialEq)] -pub struct FromPyWithAttribute(pub ExprPath); +#[derive(Clone, Debug)] +pub struct KeywordAttribute { + pub kw: K, + pub value: V, +} -impl Parse for FromPyWithAttribute { - fn parse(input: ParseStream) -> Result { - let _: kw::from_py_with = input.parse()?; - let _: Token![=] = input.parse()?; - let string_literal: LitStr = input.parse()?; - string_literal.parse().map(FromPyWithAttribute) +/// A helper type which parses the inner type via a literal string +/// e.g. LitStrValue -> parses "some::path" in quotes. +#[derive(Clone, Debug, PartialEq)] +pub struct LitStrValue(pub T); + +impl Parse for LitStrValue { + fn parse(input: ParseStream<'_>) -> Result { + let lit_str: LitStr = input.parse()?; + lit_str.parse().map(LitStrValue) } } -#[derive(Clone, Debug, PartialEq)] -pub struct NameAttribute(pub Ident); - -impl Parse for NameAttribute { - fn parse(input: ParseStream) -> Result { - let _: kw::name = input.parse()?; - let _: Token![=] = input.parse()?; - let string_literal: LitStr = input.parse()?; - string_literal.parse().map(NameAttribute) +impl ToTokens for LitStrValue { + fn to_tokens(&self, tokens: &mut TokenStream) { + self.0.to_tokens(tokens) } } +/// A helper type which parses a name via a literal string +#[derive(Clone, Debug, PartialEq)] +pub struct NameLitStr(pub Ident); + +impl Parse for NameLitStr { + fn parse(input: ParseStream<'_>) -> Result { + let string_literal: LitStr = input.parse()?; + if let Ok(ident) = string_literal.parse() { + Ok(NameLitStr(ident)) + } else { + bail_spanned!(string_literal.span() => "expected a single identifier in double quotes") + } + } +} + +impl ToTokens for NameLitStr { + fn to_tokens(&self, tokens: &mut TokenStream) { + self.0.to_tokens(tokens) + } +} + +pub type ExtendsAttribute = KeywordAttribute; +pub type FreelistAttribute = KeywordAttribute>; +pub type ModuleAttribute = KeywordAttribute; +pub type NameAttribute = KeywordAttribute; +pub type TextSignatureAttribute = KeywordAttribute; + +impl Parse for KeywordAttribute { + fn parse(input: ParseStream<'_>) -> Result { + let kw: K = input.parse()?; + let _: Token![=] = input.parse()?; + let value = input.parse()?; + Ok(KeywordAttribute { kw, value }) + } +} + +impl ToTokens for KeywordAttribute { + fn to_tokens(&self, tokens: &mut TokenStream) { + self.kw.to_tokens(tokens); + Token![=](self.kw.span()).to_tokens(tokens); + self.value.to_tokens(tokens); + } +} + +pub type FromPyWithAttribute = KeywordAttribute>; + /// For specifying the path to the pyo3 crate. -#[derive(Clone, Debug, PartialEq)] -pub struct CrateAttribute(pub Path); - -impl Parse for CrateAttribute { - fn parse(input: ParseStream) -> Result { - let _: Token![crate] = input.parse()?; - let _: Token![=] = input.parse()?; - let string_literal: LitStr = input.parse()?; - string_literal.parse().map(CrateAttribute) - } -} - -#[derive(Clone, Debug, PartialEq)] -pub struct TextSignatureAttribute { - pub kw: kw::text_signature, - pub eq_token: Token![=], - pub lit: LitStr, -} - -impl Parse for TextSignatureAttribute { - fn parse(input: ParseStream) -> Result { - Ok(TextSignatureAttribute { - kw: input.parse()?, - eq_token: input.parse()?, - lit: input.parse()?, - }) - } -} +pub type CrateAttribute = KeywordAttribute>; pub fn get_pyo3_options(attr: &syn::Attribute) -> Result>> { if is_attribute_ident(attr, "pyo3") { diff --git a/pyo3-macros-backend/src/deprecations.rs b/pyo3-macros-backend/src/deprecations.rs index 3d5fe878..755e8664 100644 --- a/pyo3-macros-backend/src/deprecations.rs +++ b/pyo3-macros-backend/src/deprecations.rs @@ -3,12 +3,14 @@ use quote::{quote_spanned, ToTokens}; pub enum Deprecation { CallAttribute, + PyClassGcOption, } impl Deprecation { fn ident(&self, span: Span) -> syn::Ident { let string = match self { Deprecation::CallAttribute => "CALL_ATTRIBUTE", + Deprecation::PyClassGcOption => "PYCLASS_GC_OPTION", }; syn::Ident::new(string, span) } @@ -33,7 +35,10 @@ impl ToTokens for Deprecations { let ident = deprecation.ident(*span); quote_spanned!( *span => - let _ = _pyo3::impl_::deprecations::#ident; + #[allow(clippy::let_unit_value)] + { + let _ = _pyo3::impl_::deprecations::#ident; + } ) .to_tokens(tokens) } diff --git a/pyo3-macros-backend/src/frompyobject.rs b/pyo3-macros-backend/src/frompyobject.rs index ef72c069..c55f14b3 100644 --- a/pyo3-macros-backend/src/frompyobject.rs +++ b/pyo3-macros-backend/src/frompyobject.rs @@ -106,8 +106,9 @@ enum ContainerType<'a> { StructNewtype(&'a Ident), /// Tuple struct, e.g. `struct Foo(String)`. /// - /// Fields are extracted from a tuple. - Tuple(usize), + /// Variant contains a list of conversion methods for each of the fields that are directly + /// extracted from the tuple. + Tuple(Vec), /// Tuple newtype, e.g. `#[transparent] struct Foo(String)` /// /// The wrapped field is directly extracted from the object. @@ -149,7 +150,15 @@ impl<'a> Container<'a> { (Fields::Unnamed(_), true) => ContainerType::TupleNewtype, (Fields::Unnamed(unnamed), false) => match unnamed.unnamed.len() { 1 => ContainerType::TupleNewtype, - len => ContainerType::Tuple(len), + _ => { + let fields = unnamed + .unnamed + .iter() + .map(|field| FieldPyO3Attributes::from_attrs(&field.attrs)) + .collect::>>()?; + + ContainerType::Tuple(fields) + } }, (Fields::Named(named), true) => { let field = named @@ -196,7 +205,7 @@ impl<'a> Container<'a> { match &self.ty { ContainerType::StructNewtype(ident) => self.build_newtype_struct(Some(ident)), ContainerType::TupleNewtype => self.build_newtype_struct(None), - ContainerType::Tuple(len) => self.build_tuple_struct(*len), + ContainerType::Tuple(tups) => self.build_tuple_struct(tups), ContainerType::Struct(tups) => self.build_struct(tups), } } @@ -233,19 +242,35 @@ impl<'a> Container<'a> { } } - fn build_tuple_struct(&self, len: usize) -> TokenStream { + fn build_tuple_struct(&self, tups: &[FieldPyO3Attributes]) -> TokenStream { let self_ty = &self.path; let mut fields: Punctuated = Punctuated::new(); - for i in 0..len { - let error_msg = format!("failed to extract field {}.{}", quote!(#self_ty), i); - fields.push(quote!( - s.get_item(#i).and_then(_pyo3::types::PyAny::extract).map_err(|inner| { - let py = _pyo3::PyNativeType::py(obj); - let new_err = _pyo3::exceptions::PyTypeError::new_err(#error_msg); - new_err.set_cause(py, ::std::option::Option::Some(inner)); - new_err - })?)); + for (index, attrs) in tups.iter().enumerate() { + let error_msg = format!("failed to extract field {}.{}", quote!(#self_ty), index); + + let parsed_item = match &attrs.from_py_with { + None => quote!( + obj.get_item(#index)?.extract() + ), + Some(FromPyWithAttribute { + value: expr_path, .. + }) => quote! ( + #expr_path(obj.get_item(#index)?) + ), + }; + + let extractor = quote!( + #parsed_item.map_err(|inner| { + let py = _pyo3::PyNativeType::py(obj); + let new_err = _pyo3::exceptions::PyTypeError::new_err(#error_msg); + new_err.set_cause(py, ::std::option::Option::Some(inner)); + new_err + })? + ); + + fields.push(quote!(#extractor)); } + let len = tups.len(); let msg = if self.is_enum_variant { quote!(::std::format!( "expected tuple of length {}, but got length {}", @@ -285,7 +310,9 @@ impl<'a> Container<'a> { new_err.set_cause(py, ::std::option::Option::Some(inner)); new_err })?), - Some(FromPyWithAttribute(expr_path)) => quote! ( + Some(FromPyWithAttribute { + value: expr_path, .. + }) => quote! ( #expr_path(#get_field).map_err(|inner| { let py = _pyo3::PyNativeType::py(obj); let new_err = _pyo3::exceptions::PyTypeError::new_err(#conversion_error_msg); @@ -323,7 +350,7 @@ enum ContainerPyO3Attribute { } impl Parse for ContainerPyO3Attribute { - fn parse(input: ParseStream) -> Result { + fn parse(input: ParseStream<'_>) -> Result { let lookahead = input.lookahead1(); if lookahead.peek(attributes::kw::transparent) { let kw: attributes::kw::transparent = input.parse()?; @@ -365,7 +392,7 @@ impl ContainerOptions { ContainerPyO3Attribute::Crate(path) => { ensure_spanned!( options.krate.is_none(), - path.0.span() => "`crate` may only be provided once" + path.span() => "`crate` may only be provided once" ); options.krate = Some(path); } @@ -396,7 +423,7 @@ enum FieldPyO3Attribute { } impl Parse for FieldPyO3Attribute { - fn parse(input: ParseStream) -> Result { + fn parse(input: ParseStream<'_>) -> Result { let lookahead = input.lookahead1(); if lookahead.peek(attributes::kw::attribute) { let _: attributes::kw::attribute = input.parse()?; diff --git a/pyo3-macros-backend/src/konst.rs b/pyo3-macros-backend/src/konst.rs index c11c828d..30cffe89 100644 --- a/pyo3-macros-backend/src/konst.rs +++ b/pyo3-macros-backend/src/konst.rs @@ -19,9 +19,9 @@ pub struct ConstSpec { } impl ConstSpec { - pub fn python_name(&self) -> Cow { + pub fn python_name(&self) -> Cow<'_, Ident> { if let Some(name) = &self.attributes.name { - Cow::Borrowed(&name.0) + Cow::Borrowed(&name.value.0) } else { Cow::Owned(self.rust_ident.unraw()) } @@ -45,7 +45,7 @@ pub enum PyO3ConstAttribute { } impl Parse for PyO3ConstAttribute { - fn parse(input: ParseStream) -> Result { + fn parse(input: ParseStream<'_>) -> Result { let lookahead = input.lookahead1(); if lookahead.peek(attributes::kw::name) { input.parse().map(PyO3ConstAttribute::Name) @@ -89,7 +89,7 @@ impl ConstAttributes { fn set_name(&mut self, name: NameAttribute) -> Result<()> { ensure_spanned!( self.name.is_none(), - name.0.span() => "`name` may only be specified once" + name.span() => "`name` may only be specified once" ); self.name = Some(name); Ok(()) diff --git a/pyo3-macros-backend/src/lib.rs b/pyo3-macros-backend/src/lib.rs index fd6ec4a4..7ed8e504 100644 --- a/pyo3-macros-backend/src/lib.rs +++ b/pyo3-macros-backend/src/lib.rs @@ -1,6 +1,7 @@ // Copyright (c) 2017-present PyO3 Project and Contributors //! This crate contains the implementation of the proc macro attributes +#![warn(elided_lifetimes_in_paths, unused_lifetimes)] #![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] #![recursion_limit = "1024"] diff --git a/pyo3-macros-backend/src/method.rs b/pyo3-macros-backend/src/method.rs index 8d03d75c..ddf2244f 100644 --- a/pyo3-macros-backend/src/method.rs +++ b/pyo3-macros-backend/src/method.rs @@ -14,7 +14,7 @@ use syn::ext::IdentExt; use syn::spanned::Spanned; use syn::Result; -#[derive(Clone, PartialEq, Debug)] +#[derive(Clone, Debug)] pub struct FnArg<'a> { pub name: &'a syn::Ident, pub by_ref: &'a Option, @@ -157,14 +157,14 @@ impl SelfType { quote! { let _cell = #cell; let _ref = _cell.try_borrow()?; - let _slf = &_ref; + let _slf: &#cls = &*_ref; } } SelfType::Receiver { mutable: true } => { quote! { let _cell = #cell; let mut _ref = _cell.try_borrow_mut()?; - let _slf = &mut _ref; + let _slf: &mut #cls = &mut *_ref; } } SelfType::TryFromPyCell(span) => { @@ -183,7 +183,7 @@ impl SelfType { pub enum CallingConvention { Noargs, // METH_NOARGS Varargs, // METH_VARARGS | METH_KEYWORDS - Fastcall, // METH_FASTCALL | METH_KEYWORDS (Py3.7+ and !abi3) + Fastcall, // METH_FASTCALL | METH_KEYWORDS (not compatible with `abi3` feature) TpNew, // special convention for tp_new } @@ -199,7 +199,8 @@ impl CallingConvention { } else if accept_kwargs { // for functions that accept **kwargs, always prefer varargs Self::Varargs - } else if can_use_fastcall() { + } else if cfg!(not(feature = "abi3")) { + // Not available in the Stable ABI as of Python 3.10 Self::Fastcall } else { Self::Varargs @@ -207,13 +208,6 @@ impl CallingConvention { } } -fn can_use_fastcall() -> bool { - const PY37: pyo3_build_config::PythonVersion = - pyo3_build_config::PythonVersion { major: 3, minor: 7 }; - let config = pyo3_build_config::get(); - config.version >= PY37 && !config.abi3 -} - pub struct FnSpec<'a> { pub tp: FnType, // Rust function name @@ -241,8 +235,15 @@ pub fn get_return_info(output: &syn::ReturnType) -> syn::Type { pub fn parse_method_receiver(arg: &syn::FnArg) -> Result { match arg { - syn::FnArg::Receiver(recv) => Ok(SelfType::Receiver { - mutable: recv.mutability.is_some(), + syn::FnArg::Receiver( + recv @ syn::Receiver { + reference: None, .. + }, + ) => { + bail_spanned!(recv.span() => RECEIVER_BY_VALUE_ERR); + } + syn::FnArg::Receiver(syn::Receiver { mutability, .. }) => Ok(SelfType::Receiver { + mutable: mutability.is_some(), }), syn::FnArg::Typed(syn::PatType { ty, .. }) => { if let syn::Type::ImplTrait(_) = &**ty { @@ -273,7 +274,7 @@ impl<'a> FnSpec<'a> { ty: fn_type_attr, args: fn_attrs, mut python_name, - } = parse_method_attributes(meth_attrs, name.map(|name| name.0), &mut deprecations)?; + } = parse_method_attributes(meth_attrs, name.map(|name| name.value.0), &mut deprecations)?; let (fn_type, skip_first_arg, fixed_convention) = Self::parse_fn_type(sig, fn_type_attr, &mut python_name)?; @@ -420,7 +421,7 @@ impl<'a> FnSpec<'a> { } pub fn default_value(&self, name: &syn::Ident) -> Option { - for s in self.attrs.iter() { + for s in &self.attrs { match s { Argument::Arg(path, opt) | Argument::Kwarg(path, opt) => { if path.is_ident(name) { @@ -437,7 +438,7 @@ impl<'a> FnSpec<'a> { } pub fn is_pos_only(&self, name: &syn::Ident) -> bool { - for s in self.attrs.iter() { + for s in &self.attrs { if let Argument::PosOnlyArg(path, _) = s { if path.is_ident(name) { return true; @@ -448,7 +449,7 @@ impl<'a> FnSpec<'a> { } pub fn is_kw_only(&self, name: &syn::Ident) -> bool { - for s in self.attrs.iter() { + for s in &self.attrs { if let Argument::Kwarg(path, _) = s { if path.is_ident(name) { return true; @@ -490,10 +491,12 @@ impl<'a> FnSpec<'a> { { use #krate as _pyo3; #deprecations - _pyo3::callback::handle_panic(|#py| { + let gil = _pyo3::GILPool::new(); + let #py = gil.python(); + _pyo3::callback::panic_result_into_callback_output(#py, ::std::panic::catch_unwind(move || -> _pyo3::PyResult<_> { #self_conversion #rust_call - }) + })) } } } @@ -508,11 +511,13 @@ impl<'a> FnSpec<'a> { { use #krate as _pyo3; #deprecations - _pyo3::callback::handle_panic(|#py| { + let gil = _pyo3::GILPool::new(); + let #py = gil.python(); + _pyo3::callback::panic_result_into_callback_output(#py, ::std::panic::catch_unwind(move || -> _pyo3::PyResult<_> { #self_conversion #arg_convert #rust_call - }) + })) } } } @@ -526,11 +531,13 @@ impl<'a> FnSpec<'a> { { use #krate as _pyo3; #deprecations - _pyo3::callback::handle_panic(|#py| { + let gil = _pyo3::GILPool::new(); + let #py = gil.python(); + _pyo3::callback::panic_result_into_callback_output(#py, ::std::panic::catch_unwind(move || -> _pyo3::PyResult<_> { #self_conversion #arg_convert #rust_call - }) + })) } } } @@ -546,13 +553,15 @@ impl<'a> FnSpec<'a> { use #krate as _pyo3; #deprecations use _pyo3::callback::IntoPyCallbackOutput; - _pyo3::callback::handle_panic(|#py| { + let gil = _pyo3::GILPool::new(); + let #py = gil.python(); + _pyo3::callback::panic_result_into_callback_output(#py, ::std::panic::catch_unwind(move || -> _pyo3::PyResult<_> { #arg_convert let result = #rust_call; let initializer: _pyo3::PyClassInitializer::<#cls> = result.convert(#py)?; let cell = initializer.create_cell_from_subtype(#py, subtype)?; ::std::result::Result::Ok(cell as *mut _pyo3::ffi::PyObject) - }) + })) } } } @@ -727,3 +736,6 @@ fn parse_method_attributes( } const IMPL_TRAIT_ERR: &str = "Python functions cannot have `impl Trait` arguments"; +const RECEIVER_BY_VALUE_ERR: &str = + "Python objects are shared, so 'self' cannot be moved out of the Python interpreter. +Try `&self`, `&mut self, `slf: PyRef<'_, Self>` or `slf: PyRefMut<'_, Self>`."; diff --git a/pyo3-macros-backend/src/module.rs b/pyo3-macros-backend/src/module.rs index ecfcc9f4..85a94b84 100644 --- a/pyo3-macros-backend/src/module.rs +++ b/pyo3-macros-backend/src/module.rs @@ -31,7 +31,7 @@ impl PyModuleOptions { for option in take_pyo3_options(attrs)? { match option { - PyModulePyO3Option::Name(name) => options.set_name(name.0)?, + PyModulePyO3Option::Name(name) => options.set_name(name.value.0)?, PyModulePyO3Option::Crate(path) => options.set_crate(path)?, } } @@ -52,7 +52,7 @@ impl PyModuleOptions { fn set_crate(&mut self, path: CrateAttribute) -> Result<()> { ensure_spanned!( self.krate.is_none(), - path.0.span() => "`crate` may only be specified once" + path.span() => "`crate` may only be specified once" ); self.krate = Some(path); @@ -80,8 +80,7 @@ pub fn pymodule_impl( /// This autogenerated function is called by the python interpreter when importing /// the module. pub unsafe extern "C" fn #cb_name() -> *mut #krate::ffi::PyObject { - use #krate::{self as _pyo3, IntoPyPointer}; - _pyo3::callback::handle_panic(|_py| ::std::result::Result::Ok(#module_def_name.make_module(_py)?.into_ptr())) + unsafe { #module_def_name.module_init() } } #[doc(hidden)] @@ -121,7 +120,7 @@ pub struct PyFnArgs { } impl Parse for PyFnArgs { - fn parse(input: syn::parse::ParseStream) -> syn::Result { + fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result { let modname = input.parse().map_err( |e| err_spanned!(e.span() => "expected module as first argument to #[pyfn()]"), )?; @@ -174,7 +173,7 @@ enum PyModulePyO3Option { } impl Parse for PyModulePyO3Option { - fn parse(input: ParseStream) -> Result { + fn parse(input: ParseStream<'_>) -> Result { let lookahead = input.lookahead1(); if lookahead.peek(attributes::kw::name) { input.parse().map(PyModulePyO3Option::Name) diff --git a/pyo3-macros-backend/src/params.rs b/pyo3-macros-backend/src/params.rs index 7aad6207..219c1e19 100644 --- a/pyo3-macros-backend/src/params.rs +++ b/pyo3-macros-backend/src/params.rs @@ -81,7 +81,7 @@ pub fn impl_arg_params( let mut required_positional_parameters = 0usize; let mut keyword_only_parameters = Vec::new(); - for arg in spec.args.iter() { + for arg in &spec.args { if arg.py || is_args(&spec.attrs, arg.name) || is_kwargs(&spec.attrs, arg.name) { continue; } @@ -231,7 +231,9 @@ fn impl_arg_param( let arg_value = quote_arg_span!(#args_array[#option_pos]); *option_pos += 1; - let arg_value_or_default = if let Some(FromPyWithAttribute(expr_path)) = &arg.attrs.from_py_with + let arg_value_or_default = if let Some(FromPyWithAttribute { + value: expr_path, .. + }) = &arg.attrs.from_py_with { match (spec.default_value(name), arg.optional.is_some()) { (Some(default), true) if default.to_string() != "None" => { diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index 575e6e28..d5091b8a 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -1,19 +1,20 @@ // Copyright (c) 2017-present PyO3 Project and Contributors use crate::attributes::{ - self, take_pyo3_options, CrateAttribute, NameAttribute, TextSignatureAttribute, + self, kw, take_pyo3_options, CrateAttribute, ExtendsAttribute, FreelistAttribute, + ModuleAttribute, NameAttribute, NameLitStr, TextSignatureAttribute, }; -use crate::deprecations::Deprecations; +use crate::deprecations::{Deprecation, Deprecations}; use crate::konst::{ConstAttributes, ConstSpec}; use crate::pyimpl::{gen_default_items, gen_py_const, PyClassMethodsType}; use crate::pymethod::{impl_py_getter_def, impl_py_setter_def, PropertyType}; -use crate::utils::{self, get_pyo3_crate, unwrap_group, PythonDoc}; +use crate::utils::{self, get_pyo3_crate, PythonDoc}; use proc_macro2::{Span, TokenStream}; use quote::quote; use syn::ext::IdentExt; use syn::parse::{Parse, ParseStream}; use syn::punctuated::Punctuated; -use syn::{parse_quote, spanned::Spanned, Expr, Result, Token}; //unraw +use syn::{parse_quote, spanned::Spanned, Result, Token}; /// If the class is derived from a Rust `struct` or `enum`. #[derive(Copy, Clone, Debug, PartialEq, Eq)] @@ -24,191 +25,85 @@ pub enum PyClassKind { /// The parsed arguments of the pyclass macro pub struct PyClassArgs { - pub freelist: Option, - pub name: Option, - pub base: syn::TypePath, - pub has_dict: bool, - pub has_weaklist: bool, - pub is_gc: bool, - pub is_basetype: bool, - pub has_extends: bool, - pub has_unsendable: bool, - pub is_immutable: bool, - pub module: Option, pub class_kind: PyClassKind, - pub is_immutable: bool, + pub options: PyClassPyO3Options, + pub deprecations: Deprecations, } impl PyClassArgs { - fn parse(input: ParseStream, kind: PyClassKind) -> Result { - let mut slf = PyClassArgs::new(kind); - let vars = Punctuated::::parse_terminated(input)?; - for expr in vars { - slf.add_expr(&expr)?; - } - Ok(slf) + fn parse(input: ParseStream<'_>, kind: PyClassKind) -> Result { + Ok(PyClassArgs { + class_kind: kind, + options: PyClassPyO3Options::parse(input)?, + deprecations: Deprecations::new(), + }) } - pub fn parse_stuct_args(input: ParseStream) -> syn::Result { + pub fn parse_stuct_args(input: ParseStream<'_>) -> syn::Result { Self::parse(input, PyClassKind::Struct) } - pub fn parse_enum_args(input: ParseStream) -> syn::Result { + pub fn parse_enum_args(input: ParseStream<'_>) -> syn::Result { Self::parse(input, PyClassKind::Enum) } - - fn new(class_kind: PyClassKind) -> Self { - PyClassArgs { - freelist: None, - name: None, - module: None, - base: parse_quote! { _pyo3::PyAny }, - has_dict: false, - has_weaklist: false, - is_gc: false, - is_basetype: false, - has_extends: false, - has_unsendable: false, - is_immutable: false, - class_kind, - } - } - - /// Adda single expression from the comma separated list in the attribute, which is - /// either a single word or an assignment expression - fn add_expr(&mut self, expr: &Expr) -> Result<()> { - match expr { - syn::Expr::Path(exp) if exp.path.segments.len() == 1 => self.add_path(exp), - syn::Expr::Assign(assign) => self.add_assign(assign), - _ => bail_spanned!(expr.span() => "failed to parse arguments"), - } - } - - /// Match a key/value flag - fn add_assign(&mut self, assign: &syn::ExprAssign) -> syn::Result<()> { - let syn::ExprAssign { left, right, .. } = assign; - let key = match &**left { - syn::Expr::Path(exp) if exp.path.segments.len() == 1 => { - exp.path.segments.first().unwrap().ident.to_string() - } - _ => bail_spanned!(assign.span() => "failed to parse arguments"), - }; - - macro_rules! expected { - ($expected: literal) => { - expected!($expected, right.span()) - }; - ($expected: literal, $span: expr) => { - bail_spanned!($span => concat!("expected ", $expected)) - }; - } - - match key.as_str() { - "freelist" => { - // We allow arbitrary expressions here so you can e.g. use `8*64` - self.freelist = Some(syn::Expr::clone(right)); - } - "name" => match unwrap_group(&**right) { - syn::Expr::Lit(syn::ExprLit { - lit: syn::Lit::Str(lit), - .. - }) => { - self.name = Some(lit.parse().map_err(|_| { - err_spanned!( - lit.span() => "expected a single identifier in double-quotes") - })?); - } - syn::Expr::Path(exp) if exp.path.segments.len() == 1 => { - bail_spanned!( - exp.span() => format!( - "since PyO3 0.13 a pyclass name should be in double-quotes, \ - e.g. \"{}\"", - exp.path.get_ident().expect("path has 1 segment") - ) - ); - } - _ => expected!("type name (e.g. \"Name\")"), - }, - "extends" => match unwrap_group(&**right) { - syn::Expr::Path(exp) => { - if self.class_kind == PyClassKind::Enum { - bail_spanned!( assign.span() => "enums cannot extend from other classes" ); - } - self.base = syn::TypePath { - path: exp.path.clone(), - qself: None, - }; - self.has_extends = true; - } - _ => expected!("type path (e.g., my_mod::BaseClass)"), - }, - "module" => match unwrap_group(&**right) { - syn::Expr::Lit(syn::ExprLit { - lit: syn::Lit::Str(lit), - .. - }) => { - self.module = Some(lit.clone()); - } - _ => expected!(r#"string literal (e.g., "my_mod")"#), - }, - _ => expected!("one of freelist/name/extends/module", left.span()), - }; - - Ok(()) - } - - /// Match a single flag - fn add_path(&mut self, exp: &syn::ExprPath) -> syn::Result<()> { - let flag = exp.path.segments.first().unwrap().ident.to_string(); - match flag.as_str() { - "gc" => { - self.is_gc = true; - } - "weakref" => { - self.has_weaklist = true; - } - "subclass" => { - if self.class_kind == PyClassKind::Enum { - bail_spanned!(exp.span() => "enums can't be inherited by other classes"); - } - self.is_basetype = true; - } - "dict" => { - self.has_dict = true; - } - "unsendable" => { - self.has_unsendable = true; - } - "immutable" => { - self.is_immutable = true; - } - _ => bail_spanned!( - exp.path.span() => "expected one of gc/weakref/subclass/dict/unsendable" - ), - }; - Ok(()) - } } #[derive(Default)] pub struct PyClassPyO3Options { - pub text_signature: Option, - pub deprecations: Deprecations, pub krate: Option, + pub dict: Option, + pub extends: Option, + pub freelist: Option, + pub module: Option, + pub name: Option, + pub subclass: Option, + pub text_signature: Option, + pub unsendable: Option, + pub weakref: Option, + + pub deprecations: Deprecations, } enum PyClassPyO3Option { - TextSignature(TextSignatureAttribute), Crate(CrateAttribute), + Dict(kw::dict), + Extends(ExtendsAttribute), + Freelist(FreelistAttribute), + Module(ModuleAttribute), + Name(NameAttribute), + Subclass(kw::subclass), + TextSignature(TextSignatureAttribute), + Unsendable(kw::unsendable), + Weakref(kw::weakref), + + DeprecatedGC(kw::gc), } impl Parse for PyClassPyO3Option { - fn parse(input: ParseStream) -> Result { + fn parse(input: ParseStream<'_>) -> Result { let lookahead = input.lookahead1(); - if lookahead.peek(attributes::kw::text_signature) { - input.parse().map(PyClassPyO3Option::TextSignature) - } else if lookahead.peek(Token![crate]) { + if lookahead.peek(Token![crate]) { input.parse().map(PyClassPyO3Option::Crate) + } else if lookahead.peek(kw::dict) { + input.parse().map(PyClassPyO3Option::Dict) + } else if lookahead.peek(kw::extends) { + input.parse().map(PyClassPyO3Option::Extends) + } else if lookahead.peek(attributes::kw::freelist) { + input.parse().map(PyClassPyO3Option::Freelist) + } else if lookahead.peek(attributes::kw::module) { + input.parse().map(PyClassPyO3Option::Module) + } else if lookahead.peek(kw::name) { + input.parse().map(PyClassPyO3Option::Name) + } else if lookahead.peek(attributes::kw::subclass) { + input.parse().map(PyClassPyO3Option::Subclass) + } else if lookahead.peek(attributes::kw::text_signature) { + input.parse().map(PyClassPyO3Option::TextSignature) + } else if lookahead.peek(attributes::kw::unsendable) { + input.parse().map(PyClassPyO3Option::Unsendable) + } else if lookahead.peek(attributes::kw::weakref) { + input.parse().map(PyClassPyO3Option::Weakref) + } else if lookahead.peek(attributes::kw::gc) { + input.parse().map(PyClassPyO3Option::DeprecatedGC) } else { Err(lookahead.error()) } @@ -216,57 +111,69 @@ impl Parse for PyClassPyO3Option { } impl PyClassPyO3Options { - pub fn take_pyo3_options(attrs: &mut Vec) -> syn::Result { + fn parse(input: ParseStream<'_>) -> syn::Result { let mut options: PyClassPyO3Options = Default::default(); - for option in take_pyo3_options(attrs)? { - match option { - PyClassPyO3Option::TextSignature(text_signature) => { - options.set_text_signature(text_signature)?; - } - PyClassPyO3Option::Crate(path) => { - options.set_crate(path)?; - } - } + + for option in Punctuated::::parse_terminated(input)? { + options.set_option(option)?; } + Ok(options) } - pub fn set_text_signature( - &mut self, - text_signature: TextSignatureAttribute, - ) -> syn::Result<()> { - ensure_spanned!( - self.text_signature.is_none(), - text_signature.kw.span() => "`text_signature` may only be specified once" - ); - self.text_signature = Some(text_signature); - Ok(()) + pub fn take_pyo3_options(&mut self, attrs: &mut Vec) -> syn::Result<()> { + take_pyo3_options(attrs)? + .into_iter() + .try_for_each(|option| self.set_option(option)) } - pub fn set_crate(&mut self, path: CrateAttribute) -> syn::Result<()> { - ensure_spanned!( - self.krate.is_none(), - path.0.span() => "`text_signature` may only be specified once" - ); - self.krate = Some(path); + fn set_option(&mut self, option: PyClassPyO3Option) -> syn::Result<()> { + macro_rules! set_option { + ($key:ident) => { + { + ensure_spanned!( + self.$key.is_none(), + $key.span() => concat!("`", stringify!($key), "` may only be specified once") + ); + self.$key = Some($key); + } + }; + } + + match option { + PyClassPyO3Option::Crate(krate) => set_option!(krate), + PyClassPyO3Option::Dict(dict) => set_option!(dict), + PyClassPyO3Option::Extends(extends) => set_option!(extends), + PyClassPyO3Option::Freelist(freelist) => set_option!(freelist), + PyClassPyO3Option::Module(module) => set_option!(module), + PyClassPyO3Option::Name(name) => set_option!(name), + PyClassPyO3Option::Subclass(subclass) => set_option!(subclass), + PyClassPyO3Option::TextSignature(text_signature) => set_option!(text_signature), + PyClassPyO3Option::Unsendable(unsendable) => set_option!(unsendable), + PyClassPyO3Option::Weakref(weakref) => set_option!(weakref), + + PyClassPyO3Option::DeprecatedGC(gc) => self + .deprecations + .push(Deprecation::PyClassGcOption, gc.span()), + } Ok(()) } } pub fn build_py_class( class: &mut syn::ItemStruct, - args: &PyClassArgs, + mut args: PyClassArgs, methods_type: PyClassMethodsType, ) -> syn::Result { - let options = PyClassPyO3Options::take_pyo3_options(&mut class.attrs)?; + args.options.take_pyo3_options(&mut class.attrs)?; let doc = utils::get_doc( &class.attrs, - options + args.options .text_signature .as_ref() - .map(|attr| (get_class_python_name(&class.ident, args), attr)), + .map(|attr| (get_class_python_name(&class.ident, &args), attr)), ); - let krate = get_pyo3_crate(&options.krate); + let krate = get_pyo3_crate(&args.options.krate); ensure_spanned!( class.generics.params.is_empty(), @@ -296,15 +203,7 @@ pub fn build_py_class( } }; - impl_class( - &class.ident, - args, - doc, - field_options, - methods_type, - options.deprecations, - krate, - ) + impl_class(&class.ident, &args, doc, field_options, methods_type, krate) } /// `#[pyo3()]` options for pyclass fields @@ -321,7 +220,7 @@ enum FieldPyO3Option { } impl Parse for FieldPyO3Option { - fn parse(input: ParseStream) -> Result { + fn parse(input: ParseStream<'_>) -> Result { let lookahead = input.lookahead1(); if lookahead.peek(attributes::kw::get) { input.parse().map(FieldPyO3Option::Get) @@ -362,7 +261,7 @@ impl FieldPyO3Options { FieldPyO3Option::Name(name) => { ensure_spanned!( options.name.is_none(), - name.0.span() => "`name` may only be specified once" + name.span() => "`name` may only be specified once" ); options.name = Some(name); } @@ -373,26 +272,30 @@ impl FieldPyO3Options { } } -fn get_class_python_name<'a>(cls: &'a syn::Ident, attr: &'a PyClassArgs) -> &'a syn::Ident { - attr.name.as_ref().unwrap_or(cls) +fn get_class_python_name<'a>(cls: &'a syn::Ident, args: &'a PyClassArgs) -> &'a syn::Ident { + args.options + .name + .as_ref() + .map(|name_attr| &name_attr.value.0) + .unwrap_or(cls) } fn impl_class( cls: &syn::Ident, - attr: &PyClassArgs, + args: &PyClassArgs, doc: PythonDoc, field_options: Vec<(&syn::Field, FieldPyO3Options)>, methods_type: PyClassMethodsType, - deprecations: Deprecations, krate: syn::Path, ) -> syn::Result { - let pytypeinfo_impl = impl_pytypeinfo(cls, attr, Some(&deprecations)); + let pytypeinfo_impl = impl_pytypeinfo(cls, args, Some(&args.options.deprecations)); let py_class_impl = PyClassImplsBuilder::new( cls, - attr, + args, methods_type, descriptors_to_items(cls, field_options)?, + vec![], ) .doc(doc) .impl_all(); @@ -463,63 +366,59 @@ impl<'a> PyClassEnum<'a> { pub fn build_py_enum( enum_: &mut syn::ItemEnum, - args: &PyClassArgs, + mut args: PyClassArgs, method_type: PyClassMethodsType, ) -> syn::Result { - let options = PyClassPyO3Options::take_pyo3_options(&mut enum_.attrs)?; + args.options.take_pyo3_options(&mut enum_.attrs)?; - if enum_.variants.is_empty() { - bail_spanned!(enum_.brace_token.span => "Empty enums can't be #[pyclass]."); + 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 => "#[pyclass] can't be used on enums without any variants"); } + let doc = utils::get_doc( &enum_.attrs, - options + args.options .text_signature .as_ref() - .map(|attr| (get_class_python_name(&enum_.ident, args), attr)), + .map(|attr| (get_class_python_name(&enum_.ident, &args), attr)), ); let enum_ = PyClassEnum::new(enum_)?; - impl_enum(enum_, args, doc, method_type, options) + Ok(impl_enum(enum_, &args, doc, method_type)) } fn impl_enum( - enum_: PyClassEnum, + enum_: PyClassEnum<'_>, args: &PyClassArgs, doc: PythonDoc, methods_type: PyClassMethodsType, - options: PyClassPyO3Options, -) -> syn::Result { - let krate = get_pyo3_crate(&options.krate); +) -> TokenStream { + let krate = get_pyo3_crate(&args.options.krate); impl_enum_class(enum_, args, doc, methods_type, krate) } fn impl_enum_class( - enum_: PyClassEnum, + enum_: PyClassEnum<'_>, args: &PyClassArgs, doc: PythonDoc, methods_type: PyClassMethodsType, krate: syn::Path, -) -> syn::Result { +) -> TokenStream { let cls = enum_.ident; let variants = enum_.variants; let pytypeinfo = impl_pytypeinfo(cls, args, None); - let pyclass_impls = PyClassImplsBuilder::new( - cls, - args, - methods_type, - unit_variants_as_items(cls, variants.iter().map(|v| v.ident)), - ) - .doc(doc) - .impl_all(); - let default_repr_impl = { + let default_repr_impl: syn::ImplItemMethod = { let variants_repr = variants.iter().map(|variant| { let variant_name = variant.ident; // Assuming all variants are unit variants because they are the only type we support. let repr = format!("{}.{}", cls, variant_name); quote! { #cls::#variant_name => #repr, } }); - quote! { + syn::parse_quote! { #[doc(hidden)] #[allow(non_snake_case)] #[pyo3(name = "__repr__")] @@ -539,7 +438,7 @@ fn impl_enum_class( let variant_name = variant.ident; quote! { #cls::#variant_name => #cls::#variant_name as #repr_type, } }); - quote! { + syn::parse_quote! { #[doc(hidden)] #[allow(non_snake_case)] #[pyo3(name = "__int__")] @@ -559,7 +458,7 @@ fn impl_enum_class( Ok(true.to_object(py)), } }); - quote! { + syn::parse_quote! { #[doc(hidden)] #[allow(non_snake_case)] #[pyo3(name = "__richcmp__")] @@ -590,10 +489,19 @@ fn impl_enum_class( } }; - let default_items = - gen_default_items(cls, vec![default_repr_impl, default_richcmp, default_int]); + let mut default_methods = vec![default_repr_impl, default_richcmp, default_int]; - Ok(quote! { + let pyclass_impls = PyClassImplsBuilder::new( + cls, + args, + methods_type, + enum_default_methods(cls, variants.iter().map(|v| v.ident)), + enum_default_slots(cls, &mut default_methods), + ) + .doc(doc) + .impl_all(); + + quote! { const _: () = { use #krate as _pyo3; @@ -601,37 +509,43 @@ fn impl_enum_class( #pyclass_impls - #default_items + impl #cls { + #(#default_methods)* + } }; - }) + } } -fn unit_variants_as_items<'a>( +fn enum_default_methods<'a>( cls: &'a syn::Ident, - variant_names: impl IntoIterator, -) -> TokenStream { + unit_variant_names: impl IntoIterator, +) -> Vec { let cls_type = syn::parse_quote!(#cls); let variant_to_attribute = |ident: &syn::Ident| ConstSpec { rust_ident: ident.clone(), attributes: ConstAttributes { is_class_attr: true, - name: Some(NameAttribute(ident.clone())), + name: Some(NameAttribute { + kw: syn::parse_quote! { name }, + value: NameLitStr(ident.clone()), + }), deprecations: Default::default(), }, }; - let py_methods = variant_names + unit_variant_names .into_iter() - .map(|var| gen_py_const(&cls_type, &variant_to_attribute(var))); - - quote! { - _pyo3::impl_::pyclass::PyClassItems { - methods: &[#(#py_methods),*], - slots: &[] - } - } + .map(|var| gen_py_const(&cls_type, &variant_to_attribute(var))) + .collect() } -fn extract_variant_data(variant: &syn::Variant) -> syn::Result { +fn enum_default_slots( + cls: &syn::Ident, + default_items: &mut [syn::ImplItemMethod], +) -> Vec { + gen_default_items(cls, default_items).collect() +} + +fn extract_variant_data(variant: &syn::Variant) -> syn::Result> { use syn::Fields; let ident = match variant.fields { Fields::Unit => &variant.ident, @@ -643,14 +557,14 @@ fn extract_variant_data(variant: &syn::Variant) -> syn::Result, -) -> syn::Result { +) -> syn::Result> { let ty = syn::parse_quote!(#cls); - let py_methods: Vec = field_options + field_options .into_iter() .enumerate() .flat_map(|(field_index, (field, options))| { let name_err = if options.name.is_some() && !options.get && !options.set { - Some(Err(err_spanned!(options.name.as_ref().unwrap().0.span() => "`name` is useless without `get` or `set`"))) + Some(Err(err_spanned!(options.name.as_ref().unwrap().span() => "`name` is useless without `get` or `set`"))) } else { None }; @@ -677,14 +591,7 @@ fn descriptors_to_items( name_err.into_iter().chain(getter).chain(setter) }) - .collect::>()?; - - Ok(quote! { - _pyo3::impl_::pyclass::PyClassItems { - methods: &[#(#py_methods),*], - slots: &[] - } - }) + .collect::>() } fn impl_pytypeinfo( @@ -694,8 +601,8 @@ fn impl_pytypeinfo( ) -> TokenStream { let cls_name = get_class_python_name(cls, attr).to_string(); - let module = if let Some(m) = &attr.module { - quote! { ::core::option::Option::Some(#m) } + let module = if let Some(ModuleAttribute { value, .. }) = &attr.options.module { + quote! { ::core::option::Option::Some(#value) } } else { quote! { ::core::option::Option::None } }; @@ -728,7 +635,8 @@ struct PyClassImplsBuilder<'a> { cls: &'a syn::Ident, attr: &'a PyClassArgs, methods_type: PyClassMethodsType, - default_items: TokenStream, + default_methods: Vec, + default_slots: Vec, doc: Option, } @@ -737,13 +645,15 @@ impl<'a> PyClassImplsBuilder<'a> { cls: &'a syn::Ident, attr: &'a PyClassArgs, methods_type: PyClassMethodsType, - default_items: TokenStream, + default_methods: Vec, + default_slots: Vec, ) -> Self { Self { cls, attr, methods_type, - default_items, + default_methods, + default_slots, doc: None, } } @@ -763,7 +673,6 @@ impl<'a> PyClassImplsBuilder<'a> { self.impl_pyclassimpl(), self.impl_mutability(), self.impl_freelist(), - self.impl_gc(), ] .into_iter() .collect() @@ -772,14 +681,14 @@ impl<'a> PyClassImplsBuilder<'a> { fn impl_pyclass(&self) -> TokenStream { let cls = self.cls; let attr = self.attr; - let dict = if attr.has_dict { + let dict = if attr.options.dict.is_some() { quote! { _pyo3::impl_::pyclass::PyClassDictSlot } } else { quote! { _pyo3::impl_::pyclass::PyClassDummySlot } }; // insert space for weak ref - let weakref = if attr.has_weaklist { + let weakref = if attr.options.weakref.is_some() { quote! { _pyo3::impl_::pyclass::PyClassWeakRefSlot } } else { quote! { _pyo3::impl_::pyclass::PyClassDummySlot } @@ -828,7 +737,7 @@ impl<'a> PyClassImplsBuilder<'a> { let cls = self.cls; let attr = self.attr; // If #cls is not extended type, we allow Self->PyObject conversion - if !attr.has_extends { + if attr.options.extends.is_none() { quote! { impl _pyo3::IntoPy<_pyo3::PyObject> for #cls { fn into_py(self, py: _pyo3::Python) -> _pyo3::PyObject { @@ -843,12 +752,17 @@ impl<'a> PyClassImplsBuilder<'a> { fn impl_pyclassimpl(&self) -> TokenStream { let cls = self.cls; let doc = self.doc.as_ref().map_or(quote! {"\0"}, |doc| quote! {#doc}); - let is_gc = self.attr.is_gc; - let is_basetype = self.attr.is_basetype; - let base = &self.attr.base; - let is_subclass = self.attr.has_extends; + 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 is_subclass = self.attr.options.extends.is_some(); - let dict_offset = if self.attr.has_dict { + 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::()) @@ -859,7 +773,7 @@ impl<'a> PyClassImplsBuilder<'a> { }; // insert space for weak ref - let weaklist_offset = if self.attr.has_weaklist { + 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::()) @@ -869,9 +783,9 @@ impl<'a> PyClassImplsBuilder<'a> { TokenStream::new() }; - let thread_checker = if self.attr.has_unsendable { + let thread_checker = if self.attr.options.unsendable.is_some() { quote! { _pyo3::impl_::pyclass::ThreadCheckerImpl<#cls> } - } else if self.attr.has_extends { + } else if self.attr.options.extends.is_some() { quote! { _pyo3::impl_::pyclass::ThreadCheckerInherited<#cls, <#cls as _pyo3::impl_::pyclass::PyClassImpl>::BaseType> } @@ -917,7 +831,11 @@ impl<'a> PyClassImplsBuilder<'a> { None }; - let default_items = &self.default_items; + let default_methods = &self.default_methods; + let default_slots = &self.default_slots; + let freelist_slots = self.freelist_slots(); + + let deprecations = &self.attr.deprecations; let mutability = if self.attr.is_immutable { quote! { @@ -942,7 +860,6 @@ impl<'a> PyClassImplsBuilder<'a> { quote! { impl _pyo3::impl_::pyclass::PyClassImpl for #cls { const DOC: &'static str = #doc; - const IS_GC: bool = #is_gc; const IS_BASETYPE: bool = #is_basetype; const IS_SUBCLASS: bool = #is_subclass; @@ -955,29 +872,15 @@ impl<'a> PyClassImplsBuilder<'a> { fn for_all_items(visitor: &mut dyn ::std::ops::FnMut(& _pyo3::impl_::pyclass::PyClassItems)) { use _pyo3::impl_::pyclass::*; let collector = PyClassImplCollector::::new(); - static INTRINSIC_ITEMS: PyClassItems = #default_items; + #deprecations; + static INTRINSIC_ITEMS: PyClassItems = PyClassItems { + methods: &[#(#default_methods),*], + slots: &[#(#default_slots),* #(#freelist_slots),*], + }; visitor(&INTRINSIC_ITEMS); - // This depends on Python implementation detail; - // an old slot entry will be overriden by newer ones. - visitor(collector.pyclass_default_items()); #pymethods_items #pyproto_items } - fn get_new() -> ::std::option::Option<_pyo3::ffi::newfunc> { - use _pyo3::impl_::pyclass::*; - let collector = PyClassImplCollector::::new(); - collector.new_impl() - } - fn get_alloc() -> ::std::option::Option<_pyo3::ffi::allocfunc> { - use _pyo3::impl_::pyclass::*; - let collector = PyClassImplCollector::::new(); - collector.alloc_impl() - } - fn get_free() -> ::std::option::Option<_pyo3::ffi::freefunc> { - use _pyo3::impl_::pyclass::*; - let collector = PyClassImplCollector::::new(); - collector.free_impl() - } #dict_offset @@ -1004,7 +907,8 @@ impl<'a> PyClassImplsBuilder<'a> { fn impl_freelist(&self) -> TokenStream { let cls = self.cls; - self.attr.freelist.as_ref().map_or(quote!{}, |freelist| { + self.attr.options.freelist.as_ref().map_or(quote!{}, |freelist| { + let freelist = &freelist.value; quote! { impl _pyo3::impl_::pyclass::PyClassWithFreeList for #cls { #[inline] @@ -1019,40 +923,30 @@ impl<'a> PyClassImplsBuilder<'a> { } } } - - impl _pyo3::impl_::pyclass::PyClassAllocImpl<#cls> for _pyo3::impl_::pyclass::PyClassImplCollector<#cls> { - #[inline] - fn alloc_impl(self) -> ::std::option::Option<_pyo3::ffi::allocfunc> { - ::std::option::Option::Some(_pyo3::impl_::pyclass::alloc_with_freelist::<#cls>) - } - } - - impl _pyo3::impl_::pyclass::PyClassFreeImpl<#cls> for _pyo3::impl_::pyclass::PyClassImplCollector<#cls> { - #[inline] - fn free_impl(self) -> ::std::option::Option<_pyo3::ffi::freefunc> { - ::std::option::Option::Some(_pyo3::impl_::pyclass::free_with_freelist::<#cls>) - } - } } }) } - /// Enforce at compile time that PyGCProtocol is implemented - fn impl_gc(&self) -> TokenStream { - let cls = self.cls; - let attr = self.attr; - if attr.is_gc { - let closure_name = format!("__assertion_closure_{}", cls); - let closure_token = syn::Ident::new(&closure_name, Span::call_site()); - quote! { - fn #closure_token() { - use _pyo3::class; - fn _assert_implements_protocol<'p, T: _pyo3::class::PyGCProtocol<'p>>() {} - _assert_implements_protocol::<#cls>(); - } - } + fn freelist_slots(&self) -> Vec { + 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 _, + } + }, + quote! { + _pyo3::ffi::PyType_Slot { + slot: _pyo3::ffi::Py_tp_free, + pfunc: _pyo3::impl_::pyclass::free_with_freelist::<#cls> as *mut _, + } + }, + ] } else { - quote! {} + Vec::new() } } } @@ -1064,7 +958,7 @@ fn define_inventory_class(inventory_class_name: &syn::Ident) -> TokenStream { items: _pyo3::impl_::pyclass::PyClassItems, } impl #inventory_class_name { - const fn new(items: _pyo3::impl_::pyclass::PyClassItems) -> Self { + pub const fn new(items: _pyo3::impl_::pyclass::PyClassItems) -> Self { Self { items } } } diff --git a/pyo3-macros-backend/src/pyfunction.rs b/pyo3-macros-backend/src/pyfunction.rs index a45c6a9a..60ad2afe 100644 --- a/pyo3-macros-backend/src/pyfunction.rs +++ b/pyo3-macros-backend/src/pyfunction.rs @@ -40,7 +40,7 @@ pub struct PyFunctionSignature { has_kwargs: bool, } -#[derive(Clone, PartialEq, Debug)] +#[derive(Clone, Debug)] pub struct PyFunctionArgPyO3Attributes { pub from_py_with: Option, } @@ -50,7 +50,7 @@ enum PyFunctionArgPyO3Attribute { } impl Parse for PyFunctionArgPyO3Attribute { - fn parse(input: ParseStream) -> Result { + fn parse(input: ParseStream<'_>) -> Result { let lookahead = input.lookahead1(); if lookahead.peek(attributes::kw::from_py_with) { input.parse().map(PyFunctionArgPyO3Attribute::FromPyWith) @@ -71,7 +71,7 @@ impl PyFunctionArgPyO3Attributes { PyFunctionArgPyO3Attribute::FromPyWith(from_py_with) => { ensure_spanned!( attributes.from_py_with.is_none(), - from_py_with.0.span() => "`from_py_with` may only be specified once per argument" + from_py_with.span() => "`from_py_with` may only be specified once per argument" ); attributes.from_py_with = Some(from_py_with); } @@ -87,7 +87,7 @@ impl PyFunctionArgPyO3Attributes { } impl syn::parse::Parse for PyFunctionSignature { - fn parse(input: &ParseBuffer) -> syn::Result { + fn parse(input: &ParseBuffer<'_>) -> syn::Result { let attr = Punctuated::::parse_terminated(input)?; Self::from_meta(&attr) } @@ -243,7 +243,7 @@ pub struct PyFunctionOptions { } impl Parse for PyFunctionOptions { - fn parse(input: ParseStream) -> Result { + fn parse(input: ParseStream<'_>) -> Result { let mut options = PyFunctionOptions::default(); while !input.is_empty() { @@ -282,7 +282,7 @@ pub enum PyFunctionOption { } impl Parse for PyFunctionOption { - fn parse(input: ParseStream) -> Result { + fn parse(input: ParseStream<'_>) -> Result { let lookahead = input.lookahead1(); if lookahead.peek(attributes::kw::name) { input.parse().map(PyFunctionOption::Name) @@ -339,7 +339,7 @@ impl PyFunctionOptions { PyFunctionOption::Crate(path) => { ensure_spanned!( self.krate.is_none(), - path.0.span() => "`crate` may only be specified once" + path.span() => "`crate` may only be specified once" ); self.krate = Some(path); } @@ -351,7 +351,7 @@ impl PyFunctionOptions { pub fn set_name(&mut self, name: NameAttribute) -> Result<()> { ensure_spanned!( self.name.is_none(), - name.0.span() => "`name` may only be specified once" + name.span() => "`name` may only be specified once" ); self.name = Some(name); Ok(()) @@ -377,7 +377,7 @@ pub fn impl_wrap_pyfunction( let python_name = options .name - .map_or_else(|| func.sig.ident.unraw(), |name| name.0); + .map_or_else(|| func.sig.ident.unraw(), |name| name.value.0); let signature = options.signature.unwrap_or_default(); diff --git a/pyo3-macros-backend/src/pyimpl.rs b/pyo3-macros-backend/src/pyimpl.rs index adc45a62..91bca5ee 100644 --- a/pyo3-macros-backend/src/pyimpl.rs +++ b/pyo3-macros-backend/src/pyimpl.rs @@ -30,7 +30,7 @@ enum PyImplPyO3Option { } impl Parse for PyImplPyO3Option { - fn parse(input: ParseStream) -> Result { + fn parse(input: ParseStream<'_>) -> Result { let lookahead = input.lookahead1(); if lookahead.peek(syn::Token![crate]) { input.parse().map(PyImplPyO3Option::Crate) @@ -61,7 +61,7 @@ impl PyImplOptions { fn set_crate(&mut self, path: CrateAttribute) -> Result<()> { ensure_spanned!( self.krate.is_none(), - path.0.span() => "`crate` may only be specified once" + path.span() => "`crate` may only be specified once" ); self.krate = Some(path); @@ -108,10 +108,6 @@ pub fn impl_methods( let attrs = get_cfg_attributes(&meth.attrs); methods.push(quote!(#(#attrs)* #token_stream)); } - GeneratedPyMethod::TraitImpl(token_stream) => { - let attrs = get_cfg_attributes(&meth.attrs); - trait_impls.push(quote!(#(#attrs)* #token_stream)); - } GeneratedPyMethod::SlotTraitImpl(method_name, token_stream) => { implemented_proto_fragments.insert(method_name); let attrs = get_cfg_attributes(&meth.attrs); @@ -186,49 +182,29 @@ pub fn gen_py_const(cls: &syn::Type, spec: &ConstSpec) -> TokenStream { } } -pub fn gen_default_items(cls: &syn::Ident, method_defs: Vec) -> TokenStream { +pub fn gen_default_items<'a>( + cls: &syn::Ident, + method_defs: &'a mut [syn::ImplItemMethod], +) -> impl Iterator + 'a { // This function uses a lot of `unwrap()`; since method_defs are provided by us, they should // all succeed. let ty: syn::Type = syn::parse_quote!(#cls); - let mut method_defs: Vec<_> = method_defs - .into_iter() - .map(|token| syn::parse2::(token).unwrap()) - .collect(); - - let mut proto_impls = Vec::new(); - - for meth in &mut method_defs { + method_defs.iter_mut().map(move |meth| { let options = PyFunctionOptions::from_attrs(&mut meth.attrs).unwrap(); match pymethod::gen_py_method(&ty, &mut meth.sig, &mut meth.attrs, options).unwrap() { GeneratedPyMethod::Proto(token_stream) => { let attrs = get_cfg_attributes(&meth.attrs); - proto_impls.push(quote!(#(#attrs)* #token_stream)) + quote!(#(#attrs)* #token_stream) } GeneratedPyMethod::SlotTraitImpl(..) => { panic!("SlotFragment methods cannot have default implementation!") } - GeneratedPyMethod::Method(_) | GeneratedPyMethod::TraitImpl(_) => { + GeneratedPyMethod::Method(_) => { panic!("Only protocol methods can have default implementation!") } } - } - - quote! { - impl #cls { - #(#method_defs)* - } - impl _pyo3::impl_::pyclass::PyClassDefaultItems<#cls> - for _pyo3::impl_::pyclass::PyClassImplCollector<#cls> { - fn pyclass_default_items(self) -> &'static _pyo3::impl_::pyclass::PyClassItems { - static ITEMS: _pyo3::impl_::pyclass::PyClassItems = _pyo3::impl_::pyclass::PyClassItems { - methods: &[], - slots: &[#(#proto_impls),*] - }; - &ITEMS - } - } - } + }) } fn impl_py_methods( @@ -266,6 +242,11 @@ fn add_shared_proto_slots( }}; } + try_add_shared_slot!( + "__getattribute__", + "__getattr__", + generate_pyclass_getattro_slot + ); try_add_shared_slot!("__setattr__", "__delattr__", generate_pyclass_setattr_slot); try_add_shared_slot!("__set__", "__delete__", generate_pyclass_setdescr_slot); try_add_shared_slot!("__setitem__", "__delitem__", generate_pyclass_setitem_slot); diff --git a/pyo3-macros-backend/src/pymethod.rs b/pyo3-macros-backend/src/pymethod.rs index c4c901b8..9ed9b430 100644 --- a/pyo3-macros-backend/src/pymethod.rs +++ b/pyo3-macros-backend/src/pymethod.rs @@ -20,7 +20,6 @@ use syn::{ext::IdentExt, spanned::Spanned, Result}; pub enum GeneratedPyMethod { Method(TokenStream), Proto(TokenStream), - TraitImpl(TokenStream), SlotTraitImpl(String, TokenStream), } @@ -37,14 +36,97 @@ enum PyMethodKind { impl PyMethodKind { fn from_name(name: &str) -> Self { - if let Some(slot_def) = pyproto(name) { - PyMethodKind::Proto(PyMethodProtoKind::Slot(slot_def)) - } else if name == "__call__" { - PyMethodKind::Proto(PyMethodProtoKind::Call) - } else if let Some(slot_fragment_def) = pyproto_fragment(name) { - PyMethodKind::Proto(PyMethodProtoKind::SlotFragment(slot_fragment_def)) - } else { - PyMethodKind::Fn + match name { + // Protocol implemented through slots + "__str__" => PyMethodKind::Proto(PyMethodProtoKind::Slot(&__STR__)), + "__repr__" => PyMethodKind::Proto(PyMethodProtoKind::Slot(&__REPR__)), + "__hash__" => PyMethodKind::Proto(PyMethodProtoKind::Slot(&__HASH__)), + "__richcmp__" => PyMethodKind::Proto(PyMethodProtoKind::Slot(&__RICHCMP__)), + "__get__" => PyMethodKind::Proto(PyMethodProtoKind::Slot(&__GET__)), + "__iter__" => PyMethodKind::Proto(PyMethodProtoKind::Slot(&__ITER__)), + "__next__" => PyMethodKind::Proto(PyMethodProtoKind::Slot(&__NEXT__)), + "__await__" => PyMethodKind::Proto(PyMethodProtoKind::Slot(&__AWAIT__)), + "__aiter__" => PyMethodKind::Proto(PyMethodProtoKind::Slot(&__AITER__)), + "__anext__" => PyMethodKind::Proto(PyMethodProtoKind::Slot(&__ANEXT__)), + "__len__" => PyMethodKind::Proto(PyMethodProtoKind::Slot(&__LEN__)), + "__contains__" => PyMethodKind::Proto(PyMethodProtoKind::Slot(&__CONTAINS__)), + "__concat__" => PyMethodKind::Proto(PyMethodProtoKind::Slot(&__CONCAT__)), + "__repeat__" => PyMethodKind::Proto(PyMethodProtoKind::Slot(&__REPEAT__)), + "__inplace_concat__" => { + PyMethodKind::Proto(PyMethodProtoKind::Slot(&__INPLACE_CONCAT__)) + } + "__inplace_repeat__" => { + PyMethodKind::Proto(PyMethodProtoKind::Slot(&__INPLACE_REPEAT__)) + } + "__getitem__" => PyMethodKind::Proto(PyMethodProtoKind::Slot(&__GETITEM__)), + "__pos__" => PyMethodKind::Proto(PyMethodProtoKind::Slot(&__POS__)), + "__neg__" => PyMethodKind::Proto(PyMethodProtoKind::Slot(&__NEG__)), + "__abs__" => PyMethodKind::Proto(PyMethodProtoKind::Slot(&__ABS__)), + "__invert__" => PyMethodKind::Proto(PyMethodProtoKind::Slot(&__INVERT__)), + "__index__" => PyMethodKind::Proto(PyMethodProtoKind::Slot(&__INDEX__)), + "__int__" => PyMethodKind::Proto(PyMethodProtoKind::Slot(&__INT__)), + "__float__" => PyMethodKind::Proto(PyMethodProtoKind::Slot(&__FLOAT__)), + "__bool__" => PyMethodKind::Proto(PyMethodProtoKind::Slot(&__BOOL__)), + "__iadd__" => PyMethodKind::Proto(PyMethodProtoKind::Slot(&__IADD__)), + "__isub__" => PyMethodKind::Proto(PyMethodProtoKind::Slot(&__ISUB__)), + "__imul__" => PyMethodKind::Proto(PyMethodProtoKind::Slot(&__IMUL__)), + "__imatmul__" => PyMethodKind::Proto(PyMethodProtoKind::Slot(&__IMATMUL__)), + "__itruediv__" => PyMethodKind::Proto(PyMethodProtoKind::Slot(&__ITRUEDIV__)), + "__ifloordiv__" => PyMethodKind::Proto(PyMethodProtoKind::Slot(&__IFLOORDIV__)), + "__imod__" => PyMethodKind::Proto(PyMethodProtoKind::Slot(&__IMOD__)), + "__ipow__" => PyMethodKind::Proto(PyMethodProtoKind::Slot(&__IPOW__)), + "__ilshift__" => PyMethodKind::Proto(PyMethodProtoKind::Slot(&__ILSHIFT__)), + "__irshift__" => PyMethodKind::Proto(PyMethodProtoKind::Slot(&__IRSHIFT__)), + "__iand__" => PyMethodKind::Proto(PyMethodProtoKind::Slot(&__IAND__)), + "__ixor__" => PyMethodKind::Proto(PyMethodProtoKind::Slot(&__IXOR__)), + "__ior__" => PyMethodKind::Proto(PyMethodProtoKind::Slot(&__IOR__)), + "__getbuffer__" => PyMethodKind::Proto(PyMethodProtoKind::Slot(&__GETBUFFER__)), + "__releasebuffer__" => PyMethodKind::Proto(PyMethodProtoKind::Slot(&__RELEASEBUFFER__)), + "__clear__" => PyMethodKind::Proto(PyMethodProtoKind::Slot(&__CLEAR__)), + // Protocols implemented through traits + "__getattribute__" => { + PyMethodKind::Proto(PyMethodProtoKind::SlotFragment(&__GETATTRIBUTE__)) + } + "__getattr__" => PyMethodKind::Proto(PyMethodProtoKind::SlotFragment(&__GETATTR__)), + "__setattr__" => PyMethodKind::Proto(PyMethodProtoKind::SlotFragment(&__SETATTR__)), + "__delattr__" => PyMethodKind::Proto(PyMethodProtoKind::SlotFragment(&__DELATTR__)), + "__set__" => PyMethodKind::Proto(PyMethodProtoKind::SlotFragment(&__SET__)), + "__delete__" => PyMethodKind::Proto(PyMethodProtoKind::SlotFragment(&__DELETE__)), + "__setitem__" => PyMethodKind::Proto(PyMethodProtoKind::SlotFragment(&__SETITEM__)), + "__delitem__" => PyMethodKind::Proto(PyMethodProtoKind::SlotFragment(&__DELITEM__)), + "__add__" => PyMethodKind::Proto(PyMethodProtoKind::SlotFragment(&__ADD__)), + "__radd__" => PyMethodKind::Proto(PyMethodProtoKind::SlotFragment(&__RADD__)), + "__sub__" => PyMethodKind::Proto(PyMethodProtoKind::SlotFragment(&__SUB__)), + "__rsub__" => PyMethodKind::Proto(PyMethodProtoKind::SlotFragment(&__RSUB__)), + "__mul__" => PyMethodKind::Proto(PyMethodProtoKind::SlotFragment(&__MUL__)), + "__rmul__" => PyMethodKind::Proto(PyMethodProtoKind::SlotFragment(&__RMUL__)), + "__matmul__" => PyMethodKind::Proto(PyMethodProtoKind::SlotFragment(&__MATMUL__)), + "__rmatmul__" => PyMethodKind::Proto(PyMethodProtoKind::SlotFragment(&__RMATMUL__)), + "__floordiv__" => PyMethodKind::Proto(PyMethodProtoKind::SlotFragment(&__FLOORDIV__)), + "__rfloordiv__" => PyMethodKind::Proto(PyMethodProtoKind::SlotFragment(&__RFLOORDIV__)), + "__truediv__" => PyMethodKind::Proto(PyMethodProtoKind::SlotFragment(&__TRUEDIV__)), + "__rtruediv__" => PyMethodKind::Proto(PyMethodProtoKind::SlotFragment(&__RTRUEDIV__)), + "__divmod__" => PyMethodKind::Proto(PyMethodProtoKind::SlotFragment(&__DIVMOD__)), + "__rdivmod__" => PyMethodKind::Proto(PyMethodProtoKind::SlotFragment(&__RDIVMOD__)), + "__mod__" => PyMethodKind::Proto(PyMethodProtoKind::SlotFragment(&__MOD__)), + "__rmod__" => PyMethodKind::Proto(PyMethodProtoKind::SlotFragment(&__RMOD__)), + "__lshift__" => PyMethodKind::Proto(PyMethodProtoKind::SlotFragment(&__LSHIFT__)), + "__rlshift__" => PyMethodKind::Proto(PyMethodProtoKind::SlotFragment(&__RLSHIFT__)), + "__rshift__" => PyMethodKind::Proto(PyMethodProtoKind::SlotFragment(&__RSHIFT__)), + "__rrshift__" => PyMethodKind::Proto(PyMethodProtoKind::SlotFragment(&__RRSHIFT__)), + "__and__" => PyMethodKind::Proto(PyMethodProtoKind::SlotFragment(&__AND__)), + "__rand__" => PyMethodKind::Proto(PyMethodProtoKind::SlotFragment(&__RAND__)), + "__xor__" => PyMethodKind::Proto(PyMethodProtoKind::SlotFragment(&__XOR__)), + "__rxor__" => PyMethodKind::Proto(PyMethodProtoKind::SlotFragment(&__RXOR__)), + "__or__" => PyMethodKind::Proto(PyMethodProtoKind::SlotFragment(&__OR__)), + "__ror__" => PyMethodKind::Proto(PyMethodProtoKind::SlotFragment(&__ROR__)), + "__pow__" => PyMethodKind::Proto(PyMethodProtoKind::SlotFragment(&__POW__)), + "__rpow__" => PyMethodKind::Proto(PyMethodProtoKind::SlotFragment(&__RPOW__)), + // Some tricky protocols which don't fit the pattern of the rest + "__call__" => PyMethodKind::Proto(PyMethodProtoKind::Call), + "__traverse__" => PyMethodKind::Proto(PyMethodProtoKind::Traverse), + // Not a proto + _ => PyMethodKind::Fn, } } } @@ -52,6 +134,7 @@ impl PyMethodKind { enum PyMethodProtoKind { Slot(&'static SlotDef), Call, + Traverse, SlotFragment(&'static SlotFragmentDef), } @@ -109,6 +192,9 @@ pub fn gen_py_method( PyMethodProtoKind::Call => { GeneratedPyMethod::Proto(impl_call_slot(cls, method.spec)?) } + PyMethodProtoKind::Traverse => { + GeneratedPyMethod::Proto(impl_traverse_slot(cls, method.spec)) + } PyMethodProtoKind::SlotFragment(slot_fragment_def) => { let proto = slot_fragment_def.generate_pyproto_fragment(cls, spec)?; GeneratedPyMethod::SlotTraitImpl(method.method_name, proto) @@ -128,7 +214,7 @@ pub fn gen_py_method( Some(quote!(_pyo3::ffi::METH_STATIC)), )?), // special prototypes - (_, FnType::FnNew) => GeneratedPyMethod::TraitImpl(impl_py_method_def_new(cls, spec)?), + (_, FnType::FnNew) => GeneratedPyMethod::Proto(impl_py_method_def_new(cls, spec)?), (_, FnType::Getter(self_type)) => GeneratedPyMethod::Method(impl_py_getter_def( cls, @@ -163,7 +249,10 @@ fn ensure_function_options_valid(options: &PyFunctionOptions) -> syn::Result<()> Ok(()) } -fn ensure_no_forbidden_protocol_attributes(spec: &FnSpec, method_name: &str) -> syn::Result<()> { +fn ensure_no_forbidden_protocol_attributes( + spec: &FnSpec<'_>, + method_name: &str, +) -> syn::Result<()> { if let Some(text_signature) = &spec.text_signature { bail_spanned!(text_signature.kw.span() => format!("`text_signature` cannot be used with `{}`", method_name)); } @@ -173,7 +262,7 @@ fn ensure_no_forbidden_protocol_attributes(spec: &FnSpec, method_name: &str) -> /// Also used by pyfunction. pub fn impl_py_method_def( cls: &syn::Type, - spec: &FnSpec, + spec: &FnSpec<'_>, flags: Option, ) -> Result { let wrapper_ident = syn::Ident::new("__wrap", Span::call_site()); @@ -190,22 +279,22 @@ pub fn impl_py_method_def( }) } -fn impl_py_method_def_new(cls: &syn::Type, spec: &FnSpec) -> Result { - let wrapper_ident = syn::Ident::new("__wrap", Span::call_site()); +fn impl_py_method_def_new(cls: &syn::Type, spec: &FnSpec<'_>) -> Result { + let wrapper_ident = syn::Ident::new("__pymethod__new__", Span::call_site()); let wrapper = spec.get_wrapper_function(&wrapper_ident, Some(cls))?; - Ok(quote! { - impl _pyo3::impl_::pyclass::PyClassNewImpl<#cls> for _pyo3::impl_::pyclass::PyClassImplCollector<#cls> { - fn new_impl(self) -> ::std::option::Option<_pyo3::ffi::newfunc> { - ::std::option::Option::Some({ - #wrapper - #wrapper_ident - }) - } + Ok(quote! {{ + impl #cls { + #[doc(hidden)] + #wrapper } - }) + _pyo3::ffi::PyType_Slot { + slot: _pyo3::ffi::Py_tp_new, + pfunc: #cls::#wrapper_ident as _pyo3::ffi::newfunc as _ + } + }}) } -fn impl_call_slot(cls: &syn::Type, mut spec: FnSpec) -> Result { +fn impl_call_slot(cls: &syn::Type, mut spec: FnSpec<'_>) -> Result { // 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; @@ -221,7 +310,37 @@ fn impl_call_slot(cls: &syn::Type, mut spec: FnSpec) -> Result { }}) } -fn impl_py_class_attribute(cls: &syn::Type, spec: &FnSpec) -> TokenStream { +fn impl_traverse_slot(cls: &syn::Type, spec: FnSpec<'_>) -> TokenStream { + let ident = spec.name; + quote! {{ + pub unsafe extern "C" fn __wrap_( + slf: *mut _pyo3::ffi::PyObject, + visit: _pyo3::ffi::visitproc, + arg: *mut ::std::os::raw::c_void, + ) -> ::std::os::raw::c_int + { + let pool = _pyo3::GILPool::new(); + let py = pool.python(); + _pyo3::callback::abort_on_traverse_panic(::std::panic::catch_unwind(move || { + let slf = py.from_borrowed_ptr::<_pyo3::PyCell<#cls>>(slf); + + let visit = _pyo3::class::gc::PyVisit::from_raw(visit, arg, py); + let borrow = slf.try_borrow(); + if let ::std::result::Result::Ok(borrow) = borrow { + _pyo3::impl_::pymethods::unwrap_traverse_result(borrow.#ident(visit)) + } else { + 0 + } + })) + } + _pyo3::ffi::PyType_Slot { + slot: _pyo3::ffi::Py_tp_traverse, + pfunc: __wrap_ as _pyo3::ffi::traverseproc as _ + } + }} +} + +fn impl_py_class_attribute(cls: &syn::Type, spec: &FnSpec<'_>) -> TokenStream { let name = &spec.name; let deprecations = &spec.deprecations; let python_name = spec.null_terminated_python_name(); @@ -241,7 +360,7 @@ fn impl_py_class_attribute(cls: &syn::Type, spec: &FnSpec) -> TokenStream { } } -fn impl_call_setter(cls: &syn::Type, spec: &FnSpec) -> syn::Result { +fn impl_call_setter(cls: &syn::Type, spec: &FnSpec<'_>) -> syn::Result { let (py_arg, args) = split_off_python_arg(&spec.args); if args.is_empty() { @@ -264,7 +383,7 @@ fn impl_call_setter(cls: &syn::Type, spec: &FnSpec) -> syn::Result } // Used here for PropertyType::Function, used in pyclass for descriptors. -pub fn impl_py_setter_def(cls: &syn::Type, property_type: PropertyType) -> Result { +pub fn impl_py_setter_def(cls: &syn::Type, property_type: PropertyType<'_>) -> Result { let python_name = property_type.null_terminated_python_name()?; let deprecations = property_type.deprecations(); let doc = property_type.doc(); @@ -305,7 +424,9 @@ pub fn impl_py_setter_def(cls: &syn::Type, property_type: PropertyType) -> Resul _value: *mut _pyo3::ffi::PyObject, _: *mut ::std::os::raw::c_void ) -> ::std::os::raw::c_int { - _pyo3::callback::handle_panic(|_py| { + let gil = _pyo3::GILPool::new(); + let _py = gil.python(); + _pyo3::callback::panic_result_into_callback_output(_py, ::std::panic::catch_unwind(move || -> _pyo3::PyResult<_> { #slf let _value = _py .from_borrowed_ptr_or_opt(_value) @@ -315,7 +436,7 @@ pub fn impl_py_setter_def(cls: &syn::Type, property_type: PropertyType) -> Resul let _val = _pyo3::FromPyObject::extract(_value)?; _pyo3::callback::convert(_py, #setter_impl) - }) + })) } __wrap }), @@ -325,7 +446,7 @@ pub fn impl_py_setter_def(cls: &syn::Type, property_type: PropertyType) -> Resul }) } -fn impl_call_getter(cls: &syn::Type, spec: &FnSpec) -> syn::Result { +fn impl_call_getter(cls: &syn::Type, spec: &FnSpec<'_>) -> syn::Result { let (py_arg, args) = split_off_python_arg(&spec.args); ensure_spanned!( args.is_empty(), @@ -343,7 +464,7 @@ fn impl_call_getter(cls: &syn::Type, spec: &FnSpec) -> syn::Result } // Used here for PropertyType::Function, used in pyclass for descriptors. -pub fn impl_py_getter_def(cls: &syn::Type, property_type: PropertyType) -> Result { +pub fn impl_py_getter_def(cls: &syn::Type, property_type: PropertyType<'_>) -> Result { let python_name = property_type.null_terminated_python_name()?; let deprecations = property_type.deprecations(); let doc = property_type.doc(); @@ -384,10 +505,12 @@ pub fn impl_py_getter_def(cls: &syn::Type, property_type: PropertyType) -> Resul _slf: *mut _pyo3::ffi::PyObject, _: *mut ::std::os::raw::c_void ) -> *mut _pyo3::ffi::PyObject { - _pyo3::callback::handle_panic(|_py| { + let gil = _pyo3::GILPool::new(); + let _py = gil.python(); + _pyo3::callback::panic_result_into_callback_output(_py, ::std::panic::catch_unwind(move || -> _pyo3::PyResult<_> { #slf _pyo3::callback::convert(_py, #getter_impl) - }) + })) } __wrap }), @@ -398,7 +521,7 @@ pub fn impl_py_getter_def(cls: &syn::Type, property_type: PropertyType) -> Resul } /// 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<&FnArg<'_>>, &[FnArg<'_>]) { match args { [py, args @ ..] if utils::is_python(py.ty) => (Some(py), args), args => (None, args), @@ -424,7 +547,7 @@ impl PropertyType<'_> { field, python_name, .. } => { let name = match (python_name, &field.ident) { - (Some(name), _) => name.0.to_string(), + (Some(name), _) => name.value.0.to_string(), (None, Some(field_name)) => format!("{}\0", field_name.unraw()), (None, None) => { bail_spanned!(field.span() => "`get` and `set` with tuple struct fields require `name`"); @@ -443,7 +566,7 @@ impl PropertyType<'_> { } } - fn doc(&self) -> Cow { + fn doc(&self) -> Cow<'_, PythonDoc> { match self { PropertyType::Descriptor { field, .. } => { Cow::Owned(utils::get_doc(&field.attrs, None)) @@ -453,21 +576,6 @@ impl PropertyType<'_> { } } -const __GETATTR__: SlotDef = SlotDef::new("Py_tp_getattro", "getattrofunc") - .arguments(&[Ty::Object]) - .before_call_method(TokenGenerator(|| { - quote! { - // Behave like python's __getattr__ (as opposed to __getattribute__) and check - // for existing fields and methods first - let existing = _pyo3::ffi::PyObject_GenericGetAttr(_slf, arg0); - if existing.is_null() { - // PyObject_HasAttr also tries to get an object and clears the error if it fails - _pyo3::ffi::PyErr_Clear(); - } else { - return existing; - } - } - })); const __STR__: SlotDef = SlotDef::new("Py_tp_str", "reprfunc"); const __REPR__: SlotDef = SlotDef::new("Py_tp_repr", "reprfunc"); const __HASH__: SlotDef = SlotDef::new("Py_tp_hash", "hashfunc") @@ -493,6 +601,12 @@ const __LEN__: SlotDef = SlotDef::new("Py_mp_length", "lenfunc").ret_ty(Ty::PySs const __CONTAINS__: SlotDef = SlotDef::new("Py_sq_contains", "objobjproc") .arguments(&[Ty::Object]) .ret_ty(Ty::Int); +const __CONCAT__: SlotDef = SlotDef::new("Py_sq_concat", "binaryfunc").arguments(&[Ty::Object]); +const __REPEAT__: SlotDef = SlotDef::new("Py_sq_repeat", "ssizeargfunc").arguments(&[Ty::PySsizeT]); +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]); const __POS__: SlotDef = SlotDef::new("Py_nb_positive", "unaryfunc"); @@ -564,49 +678,9 @@ const __RELEASEBUFFER__: SlotDef = SlotDef::new("Py_bf_releasebuffer", "releaseb .arguments(&[Ty::PyBuffer]) .ret_ty(Ty::Void) .require_unsafe(); - -fn pyproto(method_name: &str) -> Option<&'static SlotDef> { - match method_name { - "__getattr__" => Some(&__GETATTR__), - "__str__" => Some(&__STR__), - "__repr__" => Some(&__REPR__), - "__hash__" => Some(&__HASH__), - "__richcmp__" => Some(&__RICHCMP__), - "__get__" => Some(&__GET__), - "__iter__" => Some(&__ITER__), - "__next__" => Some(&__NEXT__), - "__await__" => Some(&__AWAIT__), - "__aiter__" => Some(&__AITER__), - "__anext__" => Some(&__ANEXT__), - "__len__" => Some(&__LEN__), - "__contains__" => Some(&__CONTAINS__), - "__getitem__" => Some(&__GETITEM__), - "__pos__" => Some(&__POS__), - "__neg__" => Some(&__NEG__), - "__abs__" => Some(&__ABS__), - "__invert__" => Some(&__INVERT__), - "__index__" => Some(&__INDEX__), - "__int__" => Some(&__INT__), - "__float__" => Some(&__FLOAT__), - "__bool__" => Some(&__BOOL__), - "__iadd__" => Some(&__IADD__), - "__isub__" => Some(&__ISUB__), - "__imul__" => Some(&__IMUL__), - "__imatmul__" => Some(&__IMATMUL__), - "__itruediv__" => Some(&__ITRUEDIV__), - "__ifloordiv__" => Some(&__IFLOORDIV__), - "__imod__" => Some(&__IMOD__), - "__ipow__" => Some(&__IPOW__), - "__ilshift__" => Some(&__ILSHIFT__), - "__irshift__" => Some(&__IRSHIFT__), - "__iand__" => Some(&__IAND__), - "__ixor__" => Some(&__IXOR__), - "__ior__" => Some(&__IOR__), - "__getbuffer__" => Some(&__GETBUFFER__), - "__releasebuffer__" => Some(&__RELEASEBUFFER__), - _ => None, - } -} +const __CLEAR__: SlotDef = SlotDef::new("Py_tp_clear", "inquiry") + .arguments(&[]) + .ret_ty(Ty::Int); #[derive(Clone, Copy)] enum Ty { @@ -641,7 +715,7 @@ impl Ty { cls: &syn::Type, py: &syn::Ident, ident: &syn::Ident, - arg: &FnArg, + arg: &FnArg<'_>, extract_error_mode: ExtractErrorMode, ) -> TokenStream { match self { @@ -787,7 +861,6 @@ struct SlotDef { func_ty: StaticIdent, arguments: &'static [Ty], ret_ty: Ty, - before_call_method: Option, extract_error_mode: ExtractErrorMode, return_mode: Option, require_unsafe: bool, @@ -802,7 +875,6 @@ impl SlotDef { func_ty: StaticIdent(func_ty), arguments: NO_ARGUMENTS, ret_ty: Ty::Object, - before_call_method: None, extract_error_mode: ExtractErrorMode::Raise, return_mode: None, require_unsafe: false, @@ -819,11 +891,6 @@ impl SlotDef { self } - const fn before_call_method(mut self, before_call_method: TokenGenerator) -> Self { - self.before_call_method = Some(before_call_method); - self - } - const fn return_conversion(mut self, return_conversion: TokenGenerator) -> Self { self.return_mode = Some(ReturnMode::Conversion(return_conversion)); self @@ -847,13 +914,12 @@ impl SlotDef { fn generate_type_slot( &self, cls: &syn::Type, - spec: &FnSpec, + spec: &FnSpec<'_>, method_name: &str, ) -> Result { let SlotDef { slot, func_ty, - before_call_method, arguments, extract_error_mode, ret_ty, @@ -880,10 +946,11 @@ impl SlotDef { Ok(quote!({ unsafe extern "C" fn __wrap(_raw_slf: *mut _pyo3::ffi::PyObject, #(#method_arguments),*) -> #ret_ty { let _slf = _raw_slf; - #before_call_method - _pyo3::callback::handle_panic(|#py| { + let gil = _pyo3::GILPool::new(); + let #py = gil.python(); + _pyo3::callback::panic_result_into_callback_output(#py, ::std::panic::catch_unwind(move || -> _pyo3::PyResult<_> { #body - }) + })) } _pyo3::ffi::PyType_Slot { slot: _pyo3::ffi::#slot, @@ -905,7 +972,7 @@ fn generate_method_arguments(arguments: &[Ty]) -> impl Iterator, py: &syn::Ident, arguments: &[Ty], extract_error_mode: ExtractErrorMode, @@ -958,7 +1025,7 @@ impl SlotFragmentDef { self } - fn generate_pyproto_fragment(&self, cls: &syn::Type, spec: &FnSpec) -> Result { + fn generate_pyproto_fragment(&self, cls: &syn::Type, spec: &FnSpec<'_>) -> Result { let SlotFragmentDef { fragment, arguments, @@ -989,6 +1056,10 @@ impl SlotFragmentDef { } } +const __GETATTRIBUTE__: SlotFragmentDef = + SlotFragmentDef::new("__getattribute__", &[Ty::Object]).ret_ty(Ty::Object); +const __GETATTR__: SlotFragmentDef = + SlotFragmentDef::new("__getattr__", &[Ty::Object]).ret_ty(Ty::Object); const __SETATTR__: SlotFragmentDef = SlotFragmentDef::new("__setattr__", &[Ty::Object, Ty::NonNullObject]); const __DELATTR__: SlotFragmentDef = SlotFragmentDef::new("__delattr__", &[Ty::Object]); @@ -1040,50 +1111,10 @@ const __RPOW__: SlotFragmentDef = SlotFragmentDef::new("__rpow__", &[Ty::Object, .extract_error_mode(ExtractErrorMode::NotImplemented) .ret_ty(Ty::Object); -fn pyproto_fragment(method_name: &str) -> Option<&'static SlotFragmentDef> { - match method_name { - "__setattr__" => Some(&__SETATTR__), - "__delattr__" => Some(&__DELATTR__), - "__set__" => Some(&__SET__), - "__delete__" => Some(&__DELETE__), - "__setitem__" => Some(&__SETITEM__), - "__delitem__" => Some(&__DELITEM__), - "__add__" => Some(&__ADD__), - "__radd__" => Some(&__RADD__), - "__sub__" => Some(&__SUB__), - "__rsub__" => Some(&__RSUB__), - "__mul__" => Some(&__MUL__), - "__rmul__" => Some(&__RMUL__), - "__matmul__" => Some(&__MATMUL__), - "__rmatmul__" => Some(&__RMATMUL__), - "__floordiv__" => Some(&__FLOORDIV__), - "__rfloordiv__" => Some(&__RFLOORDIV__), - "__truediv__" => Some(&__TRUEDIV__), - "__rtruediv__" => Some(&__RTRUEDIV__), - "__divmod__" => Some(&__DIVMOD__), - "__rdivmod__" => Some(&__RDIVMOD__), - "__mod__" => Some(&__MOD__), - "__rmod__" => Some(&__RMOD__), - "__lshift__" => Some(&__LSHIFT__), - "__rlshift__" => Some(&__RLSHIFT__), - "__rshift__" => Some(&__RSHIFT__), - "__rrshift__" => Some(&__RRSHIFT__), - "__and__" => Some(&__AND__), - "__rand__" => Some(&__RAND__), - "__xor__" => Some(&__XOR__), - "__rxor__" => Some(&__RXOR__), - "__or__" => Some(&__OR__), - "__ror__" => Some(&__ROR__), - "__pow__" => Some(&__POW__), - "__rpow__" => Some(&__RPOW__), - _ => None, - } -} - fn extract_proto_arguments( cls: &syn::Type, py: &syn::Ident, - method_args: &[FnArg], + method_args: &[FnArg<'_>], proto_args: &[Ty], extract_error_mode: ExtractErrorMode, ) -> Result<(Vec, usize, TokenStream)> { diff --git a/pyo3-macros-backend/src/utils.rs b/pyo3-macros-backend/src/utils.rs index 9c75239e..471c40d2 100644 --- a/pyo3-macros-backend/src/utils.rs +++ b/pyo3-macros-backend/src/utils.rs @@ -77,7 +77,8 @@ pub fn get_doc( syn::token::Bracket(Span::call_site()).surround(&mut tokens, |tokens| { if let Some((python_name, text_signature)) = text_signature { // create special doc string lines to set `__text_signature__` - let signature_lines = format!("{}{}\n--\n\n", python_name, text_signature.lit.value()); + let signature_lines = + format!("{}{}\n--\n\n", python_name, text_signature.value.value()); signature_lines.to_tokens(tokens); comma.to_tokens(tokens); } @@ -133,7 +134,7 @@ struct DocArgs { } impl syn::parse::Parse for DocArgs { - fn parse(input: syn::parse::ParseStream) -> syn::Result { + fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result { let this = Self { _eq_token: input.parse()?, token_stream: input.parse()?, @@ -154,13 +155,6 @@ pub fn ensure_not_async_fn(sig: &syn::Signature) -> syn::Result<()> { Ok(()) } -pub fn unwrap_group(mut expr: &syn::Expr) -> &syn::Expr { - while let syn::Expr::Group(g) = expr { - expr = &*g.expr; - } - expr -} - pub fn unwrap_ty_group(mut ty: &syn::Type) -> &syn::Type { while let syn::Type::Group(g) = ty { ty = &*g.elem; @@ -193,6 +187,6 @@ pub(crate) fn replace_self(ty: &mut syn::Type, cls: &syn::Type) { /// 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.0.clone()) + .map(|p| p.value.0.clone()) .unwrap_or_else(|| syn::parse_str("::pyo3").unwrap()) } diff --git a/pyo3-macros-backend/src/wrap.rs b/pyo3-macros-backend/src/wrap.rs index 881d65be..99168a2b 100644 --- a/pyo3-macros-backend/src/wrap.rs +++ b/pyo3-macros-backend/src/wrap.rs @@ -8,7 +8,7 @@ pub struct WrapPyFunctionArgs { } impl Parse for WrapPyFunctionArgs { - fn parse(input: syn::parse::ParseStream) -> syn::Result { + fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result { let function = input.parse()?; let comma_and_arg = if !input.is_empty() { Some((input.parse()?, input.parse()?)) diff --git a/pyo3-macros/Cargo.toml b/pyo3-macros/Cargo.toml index a12d7d7c..bda684b2 100644 --- a/pyo3-macros/Cargo.toml +++ b/pyo3-macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3-macros" -version = "0.15.1" +version = "0.16.2" description = "Proc macros for PyO3 package" authors = ["PyO3 Project and Contributors "] keywords = ["pyo3", "python", "cpython", "ffi"] @@ -17,9 +17,10 @@ proc-macro = true multiple-pymethods = [] pyproto = ["pyo3-macros-backend/pyproto"] +abi3 = ["pyo3-macros-backend/abi3"] [dependencies] proc-macro2 = { version = "1", default-features = false } quote = "1" -syn = { version = "1", features = ["full", "extra-traits"] } -pyo3-macros-backend = { path = "../pyo3-macros-backend", version = "=0.15.1" } +syn = { version = "1.0.56", features = ["full", "extra-traits"] } +pyo3-macros-backend = { path = "../pyo3-macros-backend", version = "=0.16.2" } diff --git a/pyo3-macros/docs/pyclass_parameters.md b/pyo3-macros/docs/pyclass_parameters.md new file mode 100644 index 00000000..ae5ef9dd --- /dev/null +++ b/pyo3-macros/docs/pyclass_parameters.md @@ -0,0 +1,28 @@ +`#[pyclass]` can be used with the following parameters: + +| Parameter | Description | +| :- | :- | +| `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] | +| `freelist = N` | Implements a [free list][params-2] of size N. This can improve performance for types that are often created and deleted in quick succession. Profile your code to see whether `freelist` is right for you. | +| `module = "module_name"` | Python code will see the class as being defined in this module. Defaults to `builtins`. | +| `name = "python_name"` | Sets the name that Python sees this class as. Defaults to the name of the Rust struct. | +| `text_signature = "(arg1, arg2, ...)"` | Sets the text signature for the Python class' `__new__` method. | +| `subclass` | Allows other Python classes and `#[pyclass]` to inherit from this class. Enums cannot be subclassed. | +| `unsendable` | Required if your struct is not [`Send`][params-3]. Rather than using `unsendable`, consider implementing your struct in a threadsafe way by e.g. substituting [`Rc`][params-4] with [`Arc`][params-5]. By using `unsendable`, your class will panic when accessed by another thread.| +| `weakref` | Allows this class to be [weakly referenceable][params-6]. | + +All of these parameters can either be passed directly on the `#[pyclass(...)]` annotation, or as one or +more accompanying `#[pyo3(...)]` annotations, e.g.: + +```rust,ignore +// Argument supplied directly to the `#[pyclass]` annotation. +#[pyclass(name = "SomeName", subclass)] +struct MyClass { } + +// Argument supplied as a separate annotation. +#[pyclass] +#[pyo3(name = "SomeName", subclass)] +struct MyClass { } +``` diff --git a/pyo3-macros/src/lib.rs b/pyo3-macros/src/lib.rs index f14e8170..da9784d1 100644 --- a/pyo3-macros/src/lib.rs +++ b/pyo3-macros/src/lib.rs @@ -38,11 +38,11 @@ pub fn pymodule(args: TokenStream, input: TokenStream) -> TokenStream { 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.to_compile_error().into(), + Err(e) => return e.into_compile_error().into(), }; if let Err(err) = process_functions_in_module(&mut ast) { - return err.to_compile_error().into(); + return err.into_compile_error().into(); } let doc = get_doc(&ast.attrs, None); @@ -81,32 +81,18 @@ pub fn pyproto(_: TokenStream, input: TokenStream) -> TokenStream { /// A proc macro used to expose Rust structs and fieldless enums as Python objects. /// -/// `#[pyclass]` accepts the following [parameters][2]: -/// -/// | Parameter | Description | -/// | :- | :- | -/// | `name = "python_name"` | Sets the name that Python sees this class as. Defaults to the name of the Rust struct. | -/// | `freelist = N` | Implements a [free list][10] of size N. This can improve performance for types that are often created and deleted in quick succession. Profile your code to see whether `freelist` is right for you. | -/// | `gc` | Participate in Python's [garbage collection][5]. Required if your type contains references to other Python objects. If you don't (or incorrectly) implement this, contained Python objects may be hidden from Python's garbage collector and you may leak memory. Note that leaking memory, while undesirable, [is safe behavior][7].| -/// | `weakref` | Allows this class to be [weakly referenceable][6]. | -/// | `extends = BaseType` | Use a custom baseclass. Defaults to [`PyAny`][4] | -/// | `subclass` | Allows other Python classes and `#[pyclass]` to inherit from this class. Enums cannot be subclassed. | -/// | `unsendable` | Required if your struct is not [`Send`][3]. Rather than using `unsendable`, consider implementing your struct in a threadsafe way by e.g. substituting [`Rc`][8] with [`Arc`][9]. By using `unsendable`, your class will panic when accessed by another thread.| -/// | `module = "module_name"` | Python code will see the class as being defined in this module. Defaults to `builtins`. | +#[cfg_attr(docsrs, cfg_attr(docsrs, doc = include_str!("../docs/pyclass_parameters.md")))] /// /// For more on creating Python classes, /// see the [class section of the guide][1]. /// /// [1]: https://pyo3.rs/latest/class.html -/// [2]: https://pyo3.rs/latest/class.html#customizing-the-class -/// [3]: std::marker::Send -/// [4]: ../prelude/struct.PyAny.html -/// [5]: https://pyo3.rs/latest/class/protocols.html#garbage-collector-integration -/// [6]: https://docs.python.org/3/library/weakref.html -/// [7]: https://doc.rust-lang.org/nomicon/leaking.html -/// [8]: std::rc::Rc -/// [9]: std::sync::Arc -/// [10]: https://en.wikipedia.org/wiki/Free_list +/// [params-1]: ../prelude/struct.PyAny.html +/// [params-2]: https://en.wikipedia.org/wiki/Free_list +/// [params-3]: std::marker::Send +/// [params-4]: std::rc::Rc +/// [params-5]: std::sync::Arc +/// [params-6]: https://docs.python.org/3/library/weakref.html #[proc_macro_attribute] pub fn pyclass(attr: TokenStream, input: TokenStream) -> TokenStream { use syn::Item; @@ -116,7 +102,7 @@ pub fn pyclass(attr: TokenStream, input: TokenStream) -> TokenStream { Item::Enum(enum_) => pyclass_enum_impl(attr, enum_, methods_type()), unsupported => { syn::Error::new_spanned(unsupported, "#[pyclass] only supports structs and enums.") - .to_compile_error() + .into_compile_error() .into() } } @@ -232,7 +218,7 @@ fn pyclass_impl( methods_type: PyClassMethodsType, ) -> TokenStream { let args = parse_macro_input!(attrs with PyClassArgs::parse_stuct_args); - let expanded = build_py_class(&mut ast, &args, methods_type).unwrap_or_compile_error(); + let expanded = build_py_class(&mut ast, args, methods_type).unwrap_or_compile_error(); quote!( #ast @@ -247,7 +233,7 @@ fn pyclass_enum_impl( methods_type: PyClassMethodsType, ) -> TokenStream { let args = parse_macro_input!(attrs with PyClassArgs::parse_enum_args); - let expanded = build_py_enum(&mut ast, &args, methods_type).unwrap_or_compile_error(); + let expanded = build_py_enum(&mut ast, args, methods_type).unwrap_or_compile_error(); quote!( #ast diff --git a/pytests/src/buf_and_str.rs b/pytests/src/buf_and_str.rs index 47d74d38..b28a24a2 100644 --- a/pytests/src/buf_and_str.rs +++ b/pytests/src/buf_and_str.rs @@ -26,9 +26,9 @@ impl BytesExtractor { Ok(rust_string.len()) } - pub fn from_str_lossy(&mut self, string: &PyString) -> PyResult { + pub fn from_str_lossy(&mut self, string: &PyString) -> usize { let rust_string_lossy: String = string.to_string_lossy().to_string(); - Ok(rust_string_lossy.len()) + rust_string_lossy.len() } pub fn from_buffer(&mut self, buf: &PyAny) -> PyResult { @@ -38,7 +38,7 @@ impl BytesExtractor { } #[pymodule] -pub fn buf_and_str(_py: Python, m: &PyModule) -> PyResult<()> { +pub fn buf_and_str(_py: Python<'_>, m: &PyModule) -> PyResult<()> { m.add_class::()?; Ok(()) } diff --git a/pytests/src/datetime.rs b/pytests/src/datetime.rs index e833e57a..f526ae0a 100644 --- a/pytests/src/datetime.rs +++ b/pytests/src/datetime.rs @@ -7,7 +7,7 @@ use pyo3::types::{ }; #[pyfunction] -fn make_date(py: Python, year: i32, month: u8, day: u8) -> PyResult<&PyDate> { +fn make_date(py: Python<'_>, year: i32, month: u8, day: u8) -> PyResult<&PyDate> { PyDate::new(py, year, month, day) } @@ -20,7 +20,7 @@ fn get_date_tuple<'p>(py: Python<'p>, d: &PyDate) -> &'p PyTuple { } #[pyfunction] -fn date_from_timestamp(py: Python, timestamp: i64) -> PyResult<&PyDate> { +fn date_from_timestamp(py: Python<'_>, timestamp: i64) -> PyResult<&PyDate> { PyDate::from_timestamp(py, timestamp) } @@ -94,7 +94,7 @@ 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> { +fn make_delta(py: Python<'_>, days: i32, seconds: i32, microseconds: i32) -> PyResult<&PyDelta> { PyDelta::new(py, days, seconds, microseconds, true) } diff --git a/pytests/src/lib.rs b/pytests/src/lib.rs index 09549142..7847f0c5 100644 --- a/pytests/src/lib.rs +++ b/pytests/src/lib.rs @@ -14,7 +14,7 @@ pub mod pyfunctions; pub mod subclassing; #[pymodule] -fn pyo3_pytests(py: Python, m: &PyModule) -> PyResult<()> { +fn pyo3_pytests(py: Python<'_>, m: &PyModule) -> PyResult<()> { #[cfg(not(Py_LIMITED_API))] m.add_wrapped(wrap_pymodule!(buf_and_str::buf_and_str))?; #[cfg(not(Py_LIMITED_API))] diff --git a/pytests/src/objstore.rs b/pytests/src/objstore.rs index d68149c7..f7fc66ed 100644 --- a/pytests/src/objstore.rs +++ b/pytests/src/objstore.rs @@ -13,7 +13,7 @@ impl ObjStore { ObjStore::default() } - fn push(&mut self, py: Python, obj: &PyAny) { + fn push(&mut self, py: Python<'_>, obj: &PyAny) { self.obj.push(obj.to_object(py)); } } diff --git a/pytests/src/othermod.rs b/pytests/src/othermod.rs index e439cacf..763d38a8 100644 --- a/pytests/src/othermod.rs +++ b/pytests/src/othermod.rs @@ -1,4 +1,4 @@ -//! https://github.com/PyO3/pyo3/issues/233 +//! //! //! The code below just tries to use the most important code generation paths diff --git a/pytests/src/subclassing.rs b/pytests/src/subclassing.rs index 8fd111e0..0033114c 100644 --- a/pytests/src/subclassing.rs +++ b/pytests/src/subclassing.rs @@ -12,8 +12,8 @@ impl Subclassable { Subclassable {} } - fn __str__(&self) -> PyResult<&'static str> { - Ok("Subclassable") + fn __str__(&self) -> &'static str { + "Subclassable" } } diff --git a/pytests/tests/test_datetime.py b/pytests/tests/test_datetime.py index 67d2da16..e70504e7 100644 --- a/pytests/tests/test_datetime.py +++ b/pytests/tests/test_datetime.py @@ -54,8 +54,9 @@ elif _pointer_size == 4: else: raise RuntimeError("unexpected pointer size: " + repr(_pointer_size)) IS_WINDOWS = sys.platform == "win32" + if IS_WINDOWS: - MIN_DATETIME = pdt.datetime(1970, 1, 2, 0, 0) + MIN_DATETIME = pdt.datetime(1971, 1, 2, 0, 0) if IS_32_BIT: MAX_DATETIME = pdt.datetime(3001, 1, 19, 4, 59, 59) else: @@ -227,7 +228,7 @@ def test_datetime_typeerror(): @given(dt=st.datetimes(MIN_DATETIME, MAX_DATETIME)) -@example(dt=pdt.datetime(1970, 1, 2, 0, 0)) +@example(dt=pdt.datetime(1971, 1, 2, 0, 0)) def test_datetime_from_timestamp(dt): if PYPY and dt < pdt.datetime(1900, 1, 1): pytest.xfail("pdt.datetime.timestamp will raise on PyPy with dates before 1900") diff --git a/pytests/tests/test_othermod.py b/pytests/tests/test_othermod.py index 08ac367a..ff67bba4 100644 --- a/pytests/tests/test_othermod.py +++ b/pytests/tests/test_othermod.py @@ -3,14 +3,14 @@ from hypothesis import strategies as st from pyo3_pytests import othermod -INTEGER32_ST = st.integers(min_value=(-(2 ** 31)), max_value=(2 ** 31 - 1)) +INTEGER32_ST = st.integers(min_value=(-(2**31)), max_value=(2**31 - 1)) USIZE_ST = st.integers(min_value=othermod.USIZE_MIN, max_value=othermod.USIZE_MAX) @given(x=INTEGER32_ST) def test_double(x): expected = x * 2 - assume(-(2 ** 31) <= expected <= (2 ** 31 - 1)) + assume(-(2**31) <= expected <= (2**31 - 1)) assert othermod.double(x) == expected diff --git a/src/buffer.rs b/src/buffer.rs index dbbf27dc..2835713e 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -38,7 +38,7 @@ unsafe impl Send for PyBuffer {} unsafe impl Sync for PyBuffer {} impl Debug for PyBuffer { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("PyBuffer") .field("buf", &self.0.buf) .field("obj", &self.0.obj) @@ -459,7 +459,7 @@ impl PyBuffer { /// To check whether the buffer format is compatible before calling this method, /// you can use `::is_compatible_format(buf.format())`. /// Alternatively, `match buffer::ElementType::from_format(buf.format())`. - pub fn copy_to_slice(&self, py: Python, target: &mut [T]) -> PyResult<()> { + pub fn copy_to_slice(&self, py: Python<'_>, target: &mut [T]) -> PyResult<()> { self.copy_to_slice_impl(py, target, b'C') } @@ -472,11 +472,11 @@ impl PyBuffer { /// To check whether the buffer format is compatible before calling this method, /// you can use `::is_compatible_format(buf.format())`. /// Alternatively, `match buffer::ElementType::from_format(buf.format())`. - pub fn copy_to_fortran_slice(&self, py: Python, target: &mut [T]) -> PyResult<()> { + pub fn copy_to_fortran_slice(&self, py: Python<'_>, target: &mut [T]) -> PyResult<()> { self.copy_to_slice_impl(py, target, b'F') } - fn copy_to_slice_impl(&self, py: Python, target: &mut [T], fort: u8) -> PyResult<()> { + fn copy_to_slice_impl(&self, py: Python<'_>, target: &mut [T], fort: u8) -> PyResult<()> { if mem::size_of_val(target) != self.len_bytes() { return Err(PyBufferError::new_err(format!( "slice to copy to (of length {}) does not match buffer length of {}", @@ -506,7 +506,7 @@ impl PyBuffer { /// If the buffer is multi-dimensional, the elements are written in C-style order. /// /// Fails if the buffer format is not compatible with type `T`. - pub fn to_vec(&self, py: Python) -> PyResult> { + pub fn to_vec(&self, py: Python<'_>) -> PyResult> { self.to_vec_impl(py, b'C') } @@ -514,11 +514,11 @@ impl PyBuffer { /// If the buffer is multi-dimensional, the elements are written in Fortran-style order. /// /// Fails if the buffer format is not compatible with type `T`. - pub fn to_fortran_vec(&self, py: Python) -> PyResult> { + pub fn to_fortran_vec(&self, py: Python<'_>) -> PyResult> { self.to_vec_impl(py, b'F') } - fn to_vec_impl(&self, py: Python, fort: u8) -> PyResult> { + fn to_vec_impl(&self, py: Python<'_>, fort: u8) -> PyResult> { let item_count = self.item_count(); let mut vec: Vec = Vec::with_capacity(item_count); unsafe { @@ -554,7 +554,7 @@ impl PyBuffer { /// To check whether the buffer format is compatible before calling this method, /// use `::is_compatible_format(buf.format())`. /// Alternatively, `match buffer::ElementType::from_format(buf.format())`. - pub fn copy_from_slice(&self, py: Python, source: &[T]) -> PyResult<()> { + pub fn copy_from_slice(&self, py: Python<'_>, source: &[T]) -> PyResult<()> { self.copy_from_slice_impl(py, source, b'C') } @@ -568,11 +568,11 @@ impl PyBuffer { /// To check whether the buffer format is compatible before calling this method, /// use `::is_compatible_format(buf.format())`. /// Alternatively, `match buffer::ElementType::from_format(buf.format())`. - pub fn copy_from_fortran_slice(&self, py: Python, source: &[T]) -> PyResult<()> { + pub fn copy_from_fortran_slice(&self, py: Python<'_>, source: &[T]) -> PyResult<()> { self.copy_from_slice_impl(py, source, b'F') } - fn copy_from_slice_impl(&self, py: Python, source: &[T], fort: u8) -> PyResult<()> { + fn copy_from_slice_impl(&self, py: Python<'_>, source: &[T], fort: u8) -> PyResult<()> { if self.readonly() { return Err(PyBufferError::new_err("cannot write to read-only buffer")); } else if mem::size_of_val(source) != self.len_bytes() { @@ -607,7 +607,7 @@ impl PyBuffer { } } - pub fn release(self, _py: Python) { + pub fn release(self, _py: Python<'_>) { // First move self into a ManuallyDrop, so that PyBuffer::drop will // never be called. (It would acquire the GIL and call PyBuffer_Release // again.) diff --git a/src/callback.rs b/src/callback.rs index 4a8598f3..3794b9e5 100644 --- a/src/callback.rs +++ b/src/callback.rs @@ -37,7 +37,7 @@ impl PyCallbackOutput for () { /// Convert the result of callback function into the appropriate return value. pub trait IntoPyCallbackOutput { - fn convert(self, py: Python) -> PyResult; + fn convert(self, py: Python<'_>) -> PyResult; } impl IntoPyCallbackOutput for Result @@ -46,7 +46,7 @@ where E: Into, { #[inline] - fn convert(self, py: Python) -> PyResult { + fn convert(self, py: Python<'_>) -> PyResult { self.map_err(Into::into).and_then(|t| t.convert(py)) } } @@ -56,42 +56,42 @@ where T: IntoPy, { #[inline] - fn convert(self, py: Python) -> PyResult<*mut ffi::PyObject> { + fn convert(self, py: Python<'_>) -> PyResult<*mut ffi::PyObject> { Ok(self.into_py(py).into_ptr()) } } impl IntoPyCallbackOutput for *mut ffi::PyObject { #[inline] - fn convert(self, _: Python) -> PyResult { + fn convert(self, _: Python<'_>) -> PyResult { Ok(self) } } impl IntoPyCallbackOutput for () { #[inline] - fn convert(self, _: Python) -> PyResult { + fn convert(self, _: Python<'_>) -> PyResult { Ok(0) } } impl IntoPyCallbackOutput for bool { #[inline] - fn convert(self, _: Python) -> PyResult { + fn convert(self, _: Python<'_>) -> PyResult { Ok(self as c_int) } } impl IntoPyCallbackOutput<()> for () { #[inline] - fn convert(self, _: Python) -> PyResult<()> { + fn convert(self, _: Python<'_>) -> PyResult<()> { Ok(()) } } impl IntoPyCallbackOutput for usize { #[inline] - fn convert(self, _py: Python) -> PyResult { + fn convert(self, _py: Python<'_>) -> PyResult { if self <= (isize::MAX as usize) { Ok(self as isize) } else { @@ -104,14 +104,14 @@ impl IntoPyCallbackOutput for usize { impl IntoPyCallbackOutput for bool { #[inline] - fn convert(self, _: Python) -> PyResult { + fn convert(self, _: Python<'_>) -> PyResult { Ok(self) } } impl IntoPyCallbackOutput for usize { #[inline] - fn convert(self, _: Python) -> PyResult { + fn convert(self, _: Python<'_>) -> PyResult { Ok(self) } } @@ -121,7 +121,7 @@ where T: IntoPy, { #[inline] - fn convert(self, py: Python) -> PyResult { + fn convert(self, py: Python<'_>) -> PyResult { Ok(self.into_py(py)) } } @@ -155,7 +155,7 @@ pub struct HashCallbackOutput(Py_hash_t); impl IntoPyCallbackOutput for HashCallbackOutput { #[inline] - fn convert(self, _py: Python) -> PyResult { + fn convert(self, _py: Python<'_>) -> PyResult { let hash = self.0; if hash == -1 { Ok(-2) @@ -170,14 +170,14 @@ where T: WrappingCastTo, { #[inline] - fn convert(self, _py: Python) -> PyResult { + fn convert(self, _py: Python<'_>) -> PyResult { Ok(HashCallbackOutput(self.wrapping_cast())) } } #[doc(hidden)] #[inline] -pub fn convert(py: Python, value: T) -> PyResult +pub fn convert(py: Python<'_>, value: T) -> PyResult where T: IntoPyCallbackOutput, { @@ -237,7 +237,7 @@ macro_rules! callback_body { #[inline] pub unsafe fn handle_panic(body: F) -> R where - F: FnOnce(Python) -> PyResult + UnwindSafe, + F: for<'py> FnOnce(Python<'py>) -> PyResult + UnwindSafe, R: PyCallbackOutput, { let pool = GILPool::new(); @@ -245,8 +245,12 @@ where panic_result_into_callback_output(py, panic::catch_unwind(move || -> PyResult<_> { body(py) })) } -fn panic_result_into_callback_output( - py: Python, +/// Converts the output of std::panic::catch_unwind into a Python function output, either by raising a Python +/// exception or by unwrapping the contained success output. +#[doc(hidden)] +#[inline] +pub fn panic_result_into_callback_output( + py: Python<'_>, panic_result: Result, Box>, ) -> R where @@ -262,3 +266,18 @@ where R::ERR_VALUE }) } + +/// Aborts if panic has occurred. Used inside `__traverse__` implementations, where panicking is not possible. +#[doc(hidden)] +#[inline] +pub fn abort_on_traverse_panic( + panic_result: Result>, +) -> c_int { + match panic_result { + Ok(traverse_result) => traverse_result, + Err(_payload) => { + eprintln!("FATAL: panic inside __traverse__ handler; aborting."); + ::std::process::abort() + } + } +} diff --git a/src/class/basic.rs b/src/class/basic.rs index 90f941f2..be70f8ee 100644 --- a/src/class/basic.rs +++ b/src/class/basic.rs @@ -1,3 +1,4 @@ +#![allow(deprecated)] // Copyright (c) 2017-present PyO3 Project and Contributors //! Basic Python Object customization @@ -16,6 +17,7 @@ use std::os::raw::c_int; /// Basic Python class customization #[allow(unused_variables)] +#[deprecated(since = "0.16.0", note = "prefer `#[pymethods]` to `#[pyproto]`")] pub trait PyObjectProtocol<'p>: PyClass { fn __getattr__(&'p self, name: Self::Name) -> Self::Result where diff --git a/src/class/buffer.rs b/src/class/buffer.rs index be47a5c2..cbfa8fdf 100644 --- a/src/class/buffer.rs +++ b/src/class/buffer.rs @@ -1,3 +1,4 @@ +#![allow(deprecated)] // Copyright (c) 2017-present PyO3 Project and Contributors //! Represent Python Buffer protocol implementation @@ -13,14 +14,19 @@ use std::os::raw::c_int; /// For more information check [buffer protocol](https://docs.python.org/3/c-api/buffer.html) /// c-api. #[allow(unused_variables)] -pub trait PyBufferProtocol<'p>: MutablePyClass { +#[deprecated(since = "0.16.0", note = "prefer `#[pymethods]` to `#[pyproto]`")] +pub trait PyBufferProtocol<'p>: PyClass { // No default implementations so that implementors of this trait provide both methods. - fn bf_getbuffer(slf: PyRefMut, view: *mut ffi::Py_buffer, flags: c_int) -> Self::Result + fn bf_getbuffer( + slf: PyRefMut<'_, Self>, + view: *mut ffi::Py_buffer, + flags: c_int, + ) -> Self::Result where Self: PyBufferGetBufferProtocol<'p>; - fn bf_releasebuffer(slf: PyRefMut, view: *mut ffi::Py_buffer) -> Self::Result + fn bf_releasebuffer(slf: PyRefMut<'_, Self>, view: *mut ffi::Py_buffer) -> Self::Result where Self: PyBufferReleaseBufferProtocol<'p>; } diff --git a/src/class/descr.rs b/src/class/descr.rs index 1c693d44..a50eebf2 100644 --- a/src/class/descr.rs +++ b/src/class/descr.rs @@ -1,3 +1,4 @@ +#![allow(deprecated)] // Copyright (c) 2017-present PyO3 Project and Contributors //! Python Description Interface @@ -11,6 +12,7 @@ use std::os::raw::c_int; /// Descriptor interface #[allow(unused_variables)] +#[deprecated(since = "0.16.0", note = "prefer `#[pymethods]` to `#[pyproto]`")] pub trait PyDescrProtocol<'p>: PyClass { fn __get__( slf: Self::Receiver, diff --git a/src/class/gc.rs b/src/class/gc.rs index 2f1085d0..1f81cda4 100644 --- a/src/class/gc.rs +++ b/src/class/gc.rs @@ -1,16 +1,17 @@ +#![allow(deprecated)] // Copyright (c) 2017-present PyO3 Project and Contributors //! Python GC support -use crate::{ffi, pyclass::MutablePyClass, AsPyPointer, PyCell, Python}; +use crate::{ffi, pyclass::MutablePyClass, AsPyPointer, PyCell, Python, PyClass}; use std::os::raw::{c_int, c_void}; -#[repr(transparent)] -pub struct PyTraverseError(c_int); +pub use crate::impl_::pymethods::{PyTraverseError, PyVisit}; /// GC support -pub trait PyGCProtocol<'p>: MutablePyClass { - fn __traverse__(&'p self, visit: PyVisit) -> Result<(), PyTraverseError>; +#[deprecated(since = "0.16.0", note = "prefer `#[pymethods]` to `#[pyproto]`")] +pub trait PyGCProtocol<'p>: PyClass { + fn __traverse__(&'p self, visit: PyVisit<'_>) -> Result<(), PyTraverseError>; fn __clear__(&'p mut self); } @@ -58,29 +59,3 @@ where slf.borrow_mut().__clear__(); 0 } - -/// Object visitor for GC. -#[derive(Clone)] -pub struct PyVisit<'p> { - visit: ffi::visitproc, - arg: *mut c_void, - /// VisitProc contains a Python instance to ensure that - /// 1) it is cannot be moved out of the traverse() call - /// 2) it cannot be sent to other threads - _py: Python<'p>, -} - -impl<'p> PyVisit<'p> { - /// Visit `obj`. - pub fn call(&self, obj: &T) -> Result<(), PyTraverseError> - where - T: AsPyPointer, - { - let r = unsafe { (self.visit)(obj.as_ptr(), self.arg) }; - if r == 0 { - Ok(()) - } else { - Err(PyTraverseError(r)) - } - } -} diff --git a/src/class/iter.rs b/src/class/iter.rs index 3d980375..f2562ca6 100644 --- a/src/class/iter.rs +++ b/src/class/iter.rs @@ -1,3 +1,4 @@ +#![allow(deprecated)] // Copyright (c) 2017-present PyO3 Project and Contributors //! Python Iterator Interface. //! Trait and support implementation for implementing iterators @@ -16,6 +17,7 @@ use crate::{PyClass, PyObject}; /// the integers 1 to 5, before raising `StopIteration("Ended")`. /// /// ```rust +/// # #![allow(deprecated, elided_lifetimes_in_paths)] /// use pyo3::class::iter::IterNextOutput; /// use pyo3::prelude::*; /// use pyo3::PyIterProtocol; @@ -43,6 +45,7 @@ use crate::{PyClass, PyObject}; /// # }); // test of StopIteration is done in pytests/src/pyclasses.rs /// ``` #[allow(unused_variables)] +#[deprecated(since = "0.16.0", note = "prefer `#[pymethods]` to `#[pyproto]`")] pub trait PyIterProtocol<'p>: PyClass { fn __iter__(slf: Self::Receiver) -> Self::Result where diff --git a/src/class/mapping.rs b/src/class/mapping.rs index 627ef997..413f9719 100644 --- a/src/class/mapping.rs +++ b/src/class/mapping.rs @@ -1,3 +1,4 @@ +#![allow(deprecated)] // Copyright (c) 2017-present PyO3 Project and Contributors //! Python Mapping Interface @@ -8,6 +9,7 @@ use crate::{pyclass::MutablePyClass, FromPyObject, PyClass, PyObject}; /// Mapping interface #[allow(unused_variables)] +#[deprecated(since = "0.16.0", note = "prefer `#[pymethods]` to `#[pyproto]`")] pub trait PyMappingProtocol<'p>: PyClass { fn __len__(&'p self) -> Self::Result where diff --git a/src/class/methods.rs b/src/class/methods.rs deleted file mode 100644 index e69de29b..00000000 diff --git a/src/class/mod.rs b/src/class/mod.rs index a7717abd..aa8f21d8 100644 --- a/src/class/mod.rs +++ b/src/class/mod.rs @@ -1,3 +1,4 @@ +#![allow(deprecated)] // Copyright (c) 2017-present PyO3 Project and Contributors //! Python object protocols diff --git a/src/class/number.rs b/src/class/number.rs index 2d8ebe14..2ae7a813 100644 --- a/src/class/number.rs +++ b/src/class/number.rs @@ -1,3 +1,4 @@ +#![allow(deprecated)] // Copyright (c) 2017-present PyO3 Project and Contributors //! Python Number Interface @@ -8,6 +9,7 @@ use crate::{ffi, pyclass::MutablePyClass, FromPyObject, PyClass, PyObject}; /// Number interface #[allow(unused_variables)] +#[deprecated(since = "0.16.0", note = "prefer `#[pymethods]` to `#[pyproto]`")] pub trait PyNumberProtocol<'p>: PyClass { fn __add__(lhs: Self::Left, rhs: Self::Right) -> Self::Result where diff --git a/src/class/pyasync.rs b/src/class/pyasync.rs index 96053824..411cca4a 100644 --- a/src/class/pyasync.rs +++ b/src/class/pyasync.rs @@ -1,3 +1,4 @@ +#![allow(deprecated)] // Copyright (c) 2017-present PyO3 Project and Contributors //! Python Async/Await Interface. @@ -15,6 +16,7 @@ use crate::{PyClass, PyObject}; /// /// Each method in this trait corresponds to Python async/await implementation. #[allow(unused_variables)] +#[deprecated(since = "0.16.0", note = "prefer `#[pymethods]` to `#[pyproto]`")] pub trait PyAsyncProtocol<'p>: PyClass { fn __await__(slf: Self::Receiver) -> Self::Result where diff --git a/src/class/sequence.rs b/src/class/sequence.rs index 6f453d78..b1b29ea0 100644 --- a/src/class/sequence.rs +++ b/src/class/sequence.rs @@ -11,6 +11,7 @@ use std::os::raw::c_int; /// Sequence interface #[allow(unused_variables)] +#[deprecated(since = "0.16.0", note = "prefer `#[pymethods]` to `#[pyproto]`")] pub trait PySequenceProtocol<'p>: PyClass + Sized { fn __len__(&'p self) -> Self::Result where diff --git a/src/conversion.rs b/src/conversion.rs index f2da392d..6b3e5841 100644 --- a/src/conversion.rs +++ b/src/conversion.rs @@ -45,7 +45,7 @@ where #[inline] fn as_ptr(&self) -> *mut ffi::PyObject { self.as_ref() - .map_or_else(std::ptr::null_mut, |t| t.into_ptr()) + .map_or_else(std::ptr::null_mut, |t| t.as_ptr()) } } @@ -72,7 +72,7 @@ where /// Conversion trait that allows various objects to be converted into `PyObject`. pub trait ToPyObject { /// Converts self into a Python object. - fn to_object(&self, py: Python) -> PyObject; + fn to_object(&self, py: Python<'_>) -> PyObject; } /// This trait has two implementations: The slow one is implemented for @@ -85,7 +85,7 @@ pub trait ToBorrowedObject: ToPyObject { /// /// May be more efficient than `to_object` because it does not need /// to touch any reference counts when the input object already is a Python object. - fn with_borrowed_ptr(&self, py: Python, f: F) -> R + fn with_borrowed_ptr(&self, py: Python<'_>, f: F) -> R where F: FnOnce(*mut ffi::PyObject) -> R, { @@ -98,38 +98,7 @@ pub trait ToBorrowedObject: ToPyObject { } } -impl ToBorrowedObject for T -where - T: ToPyObject, -{ - #[cfg(feature = "nightly")] - #[cfg_attr(docsrs, doc(cfg(feature = "nightly")))] - default fn with_borrowed_ptr(&self, py: Python, f: F) -> R - where - F: FnOnce(*mut ffi::PyObject) -> R, - { - let ptr = self.to_object(py).into_ptr(); - let result = f(ptr); - unsafe { - ffi::Py_XDECREF(ptr); - } - result - } -} - -#[cfg(feature = "nightly")] -#[cfg_attr(docsrs, doc(cfg(feature = "nightly")))] -impl ToBorrowedObject for T -where - T: ToPyObject + AsPyPointer, -{ - fn with_borrowed_ptr(&self, _py: Python, f: F) -> R - where - F: FnOnce(*mut ffi::PyObject) -> R, - { - f(self.as_ptr()) - } -} +impl ToBorrowedObject for T where T: ToPyObject {} /// Defines a conversion from a Rust type to a Python object. /// @@ -166,7 +135,7 @@ where /// } /// /// impl IntoPy for Number { -/// fn into_py(self, py: Python) -> PyObject { +/// fn into_py(self, py: Python<'_>) -> PyObject { /// // delegates to i32's IntoPy implementation. /// self.value.into_py(py) /// } @@ -188,7 +157,7 @@ where /// } /// /// impl IntoPy for Value { -/// fn into_py(self, py: Python) -> PyObject { +/// fn into_py(self, py: Python<'_>) -> PyObject { /// match self { /// Self::Integer(val) => val.into_py(py), /// Self::String(val) => val.into_py(py), @@ -213,7 +182,7 @@ where #[cfg_attr(docsrs, doc(alias = "IntoPyCallbackOutput"))] pub trait IntoPy: Sized { /// Performs the conversion. - fn into_py(self, py: Python) -> T; + fn into_py(self, py: Python<'_>) -> T; } /// `FromPyObject` is implemented by various types that can be extracted from @@ -250,7 +219,7 @@ pub trait FromPyObject<'source>: Sized { /// `T: ToPyObject` is expected. impl ToPyObject for &'_ T { #[inline] - fn to_object(&self, py: Python) -> PyObject { + fn to_object(&self, py: Python<'_>) -> PyObject { ::to_object(*self, py) } } @@ -261,7 +230,7 @@ impl ToPyObject for Option where T: ToPyObject, { - fn to_object(&self, py: Python) -> PyObject { + fn to_object(&self, py: Python<'_>) -> PyObject { self.as_ref() .map_or_else(|| py.None(), |val| val.to_object(py)) } @@ -271,20 +240,20 @@ impl IntoPy for Option where T: IntoPy, { - fn into_py(self, py: Python) -> PyObject { + fn into_py(self, py: Python<'_>) -> PyObject { self.map_or_else(|| py.None(), |val| val.into_py(py)) } } /// `()` is converted to Python `None`. impl ToPyObject for () { - fn to_object(&self, py: Python) -> PyObject { + fn to_object(&self, py: Python<'_>) -> PyObject { py.None() } } impl IntoPy for () { - fn into_py(self, py: Python) -> PyObject { + fn into_py(self, py: Python<'_>) -> PyObject { py.None() } } @@ -294,7 +263,7 @@ where T: AsPyPointer, { #[inline] - fn into_py(self, py: Python) -> PyObject { + fn into_py(self, py: Python<'_>) -> PyObject { unsafe { PyObject::from_borrowed_ptr(py, self.as_ptr()) } } } @@ -375,10 +344,10 @@ pub trait PyTryFrom<'v>: Sized + PyNativeType { /// This trait is similar to `std::convert::TryInto` pub trait PyTryInto: Sized { /// Cast from PyObject to a concrete Python object type. - fn try_into(&self) -> Result<&T, PyDowncastError>; + fn try_into(&self) -> Result<&T, PyDowncastError<'_>>; /// Cast from PyObject to a concrete Python object type. With exact type check. - fn try_into_exact(&self) -> Result<&T, PyDowncastError>; + fn try_into_exact(&self) -> Result<&T, PyDowncastError<'_>>; } // TryFrom implies TryInto @@ -386,10 +355,10 @@ impl PyTryInto for PyAny where U: for<'v> PyTryFrom<'v>, { - fn try_into(&self) -> Result<&U, PyDowncastError> { - U::try_from(self) + fn try_into(&self) -> Result<&U, PyDowncastError<'_>> { + >::try_from(self) } - fn try_into_exact(&self) -> Result<&U, PyDowncastError> { + fn try_into_exact(&self) -> Result<&U, PyDowncastError<'_>> { U::try_from_exact(self) } } @@ -458,7 +427,7 @@ where /// Converts `()` to an empty Python tuple. impl IntoPy> for () { - fn into_py(self, py: Python) -> Py { + fn into_py(self, py: Python<'_>) -> Py { PyTuple::empty(py).into() } } @@ -556,7 +525,7 @@ where #[cfg(test)] mod tests { use crate::types::{IntoPyDict, PyAny, PyDict, PyList}; - use crate::{Python, ToPyObject}; + use crate::{AsPyPointer, PyObject, Python, ToPyObject}; use super::PyTryFrom; @@ -566,11 +535,11 @@ mod tests { let list: &PyAny = vec![3, 6, 5, 4, 7].to_object(py).into_ref(py); let dict: &PyAny = vec![("reverse", true)].into_py_dict(py).as_ref(); - assert!(PyList::try_from(list).is_ok()); - assert!(PyDict::try_from(dict).is_ok()); + assert!(>::try_from(list).is_ok()); + assert!(>::try_from(dict).is_ok()); - assert!(PyAny::try_from(list).is_ok()); - assert!(PyAny::try_from(dict).is_ok()); + assert!(>::try_from(list).is_ok()); + assert!(>::try_from(dict).is_ok()); }); } @@ -593,7 +562,24 @@ mod tests { Python::with_gil(|py| { let list = PyList::new(py, &[1, 2, 3]); let val = unsafe { ::try_from_unchecked(list.as_ref()) }; - assert_eq!(list, val); + assert!(list.is(val)); + }); + } + + #[test] + fn test_option_as_ptr() { + Python::with_gil(|py| { + let mut option: Option = None; + assert_eq!(option.as_ptr(), std::ptr::null_mut()); + + let none = py.None(); + option = Some(none.clone()); + + 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/array.rs b/src/conversions/array.rs index 77a7fde6..1b630a8a 100644 --- a/src/conversions/array.rs +++ b/src/conversions/array.rs @@ -9,7 +9,7 @@ mod min_const_generics { where T: ToPyObject, { - fn into_py(self, py: Python) -> PyObject { + fn into_py(self, py: Python<'_>) -> PyObject { self.as_ref().to_object(py) } } @@ -18,37 +18,9 @@ mod min_const_generics { where T: FromPyObject<'a>, { - #[cfg(not(feature = "nightly"))] fn extract(obj: &'a PyAny) -> PyResult { create_array_from_obj(obj) } - - #[cfg(feature = "nightly")] - default fn extract(obj: &'a PyAny) -> PyResult { - create_array_from_obj(obj) - } - } - - #[cfg(all(feature = "nightly", not(Py_LIMITED_API)))] - impl<'source, T, const N: usize> FromPyObject<'source> for [T; N] - where - for<'a> T: Default + FromPyObject<'a> + crate::buffer::Element, - { - fn extract(obj: &'source PyAny) -> PyResult { - use crate::AsPyPointer; - // first try buffer protocol - if unsafe { crate::ffi::PyObject_CheckBuffer(obj.as_ptr()) } == 1 { - if let Ok(buf) = crate::buffer::PyBuffer::get(obj) { - let mut array = [T::default(); N]; - if buf.dimensions() == 1 && buf.copy_to_slice(obj.py(), &mut array).is_ok() { - buf.release(obj.py()); - return Ok(array); - } - buf.release(obj.py()); - } - } - create_array_from_obj(obj) - } } fn create_array_from_obj<'s, T, const N: usize>(obj: &'s PyAny) -> PyResult<[T; N]> @@ -125,6 +97,7 @@ mod min_const_generics { } let _ = catch_unwind_silent(move || { let _: Result<[CountDrop; 4], ()> = super::array_try_from_fn(|idx| { + #[allow(clippy::manual_assert)] if idx == 2 { panic!("peek a boo"); } @@ -176,7 +149,7 @@ mod array_impls { where T: ToPyObject { - fn into_py(self, py: Python) -> PyObject { + fn into_py(self, py: Python<'_>) -> PyObject { self.as_ref().to_object(py) } } @@ -185,42 +158,11 @@ mod array_impls { where T: Copy + Default + FromPyObject<'a>, { - #[cfg(not(feature = "nightly"))] fn extract(obj: &'a PyAny) -> PyResult { let mut array = [T::default(); $N]; extract_sequence_into_slice(obj, &mut array)?; Ok(array) } - - #[cfg(feature = "nightly")] - default fn extract(obj: &'a PyAny) -> PyResult { - let mut array = [T::default(); $N]; - extract_sequence_into_slice(obj, &mut array)?; - Ok(array) - } - } - - #[cfg(feature = "nightly")] - impl<'source, T> FromPyObject<'source> for [T; $N] - where - for<'a> T: Default + FromPyObject<'a> + crate::buffer::Element, - { - fn extract(obj: &'source PyAny) -> PyResult { - let mut array = [T::default(); $N]; - // first try buffer protocol - if unsafe { crate::ffi::PyObject_CheckBuffer(obj.as_ptr()) } == 1 { - if let Ok(buf) = crate::buffer::PyBuffer::get(obj) { - if buf.dimensions() == 1 && buf.copy_to_slice(obj.py(), &mut array).is_ok() { - buf.release(obj.py()); - return Ok(array); - } - buf.release(obj.py()); - } - } - // fall back to sequence protocol - extract_sequence_into_slice(obj, &mut array)?; - Ok(array) - } } )+ } diff --git a/src/conversions/hashbrown.rs b/src/conversions/hashbrown.rs index 31b8b142..da08e6ad 100644 --- a/src/conversions/hashbrown.rs +++ b/src/conversions/hashbrown.rs @@ -33,7 +33,7 @@ where V: ToPyObject, H: hash::BuildHasher, { - fn to_object(&self, py: Python) -> PyObject { + fn to_object(&self, py: Python<'_>) -> PyObject { IntoPyDict::into_py_dict(self, py).into() } } @@ -44,7 +44,7 @@ where V: IntoPy, H: hash::BuildHasher, { - fn into_py(self, py: Python) -> PyObject { + fn into_py(self, py: Python<'_>) -> PyObject { let iter = self .into_iter() .map(|(k, v)| (k.into_py(py), v.into_py(py))); @@ -72,7 +72,7 @@ impl ToPyObject for hashbrown::HashSet where T: hash::Hash + Eq + ToPyObject, { - fn to_object(&self, py: Python) -> PyObject { + fn to_object(&self, py: Python<'_>) -> PyObject { let set = PySet::new::(py, &[]).expect("Failed to construct empty set"); { for val in self { @@ -88,7 +88,7 @@ where K: IntoPy + Eq + hash::Hash, S: hash::BuildHasher + Default, { - fn into_py(self, py: Python) -> PyObject { + fn into_py(self, py: Python<'_>) -> PyObject { let set = PySet::empty(py).expect("Failed to construct empty set"); { for val in self { diff --git a/src/conversions/indexmap.rs b/src/conversions/indexmap.rs index c7e20e4c..0e18cad7 100644 --- a/src/conversions/indexmap.rs +++ b/src/conversions/indexmap.rs @@ -76,7 +76,7 @@ //! } //! //! #[pymodule] -//! fn my_module(_py: Python, m: &PyModule) -> PyResult<()> { +//! fn my_module(_py: Python<'_>, m: &PyModule) -> PyResult<()> { //! m.add_function(wrap_pyfunction!(calculate_statistics, m)?)?; //! Ok(()) //! } @@ -102,7 +102,7 @@ where V: ToPyObject, H: hash::BuildHasher, { - fn to_object(&self, py: Python) -> PyObject { + fn to_object(&self, py: Python<'_>) -> PyObject { IntoPyDict::into_py_dict(self, py).into() } } @@ -113,7 +113,7 @@ where V: IntoPy, H: hash::BuildHasher, { - fn into_py(self, py: Python) -> PyObject { + fn into_py(self, py: Python<'_>) -> PyObject { let iter = self .into_iter() .map(|(k, v)| (k.into_py(py), v.into_py(py))); diff --git a/src/conversions/num_bigint.rs b/src/conversions/num_bigint.rs index 197f7ec3..b041bc9d 100644 --- a/src/conversions/num_bigint.rs +++ b/src/conversions/num_bigint.rs @@ -40,7 +40,7 @@ //! } //! //! #[pymodule] -//! fn my_module(_py: Python, m: &PyModule) -> PyResult<()> { +//! fn my_module(_py: Python<'_>, m: &PyModule) -> PyResult<()> { //! m.add_function(wrap_pyfunction!(add_one, m)?)?; //! Ok(()) //! } @@ -82,7 +82,7 @@ macro_rules! bigint_conversion { ($rust_ty: ty, $is_signed: expr, $to_bytes: path, $from_bytes: path) => { #[cfg_attr(docsrs, doc(cfg(feature = "num-bigint")))] impl ToPyObject for $rust_ty { - fn to_object(&self, py: Python) -> PyObject { + fn to_object(&self, py: Python<'_>) -> PyObject { unsafe { let bytes = $to_bytes(self); let obj = ffi::_PyLong_FromByteArray( @@ -98,7 +98,7 @@ macro_rules! bigint_conversion { #[cfg_attr(docsrs, doc(cfg(feature = "num-bigint")))] impl IntoPy for $rust_ty { - fn into_py(self, py: Python) -> PyObject { + fn into_py(self, py: Python<'_>) -> PyObject { self.to_object(py) } } @@ -111,12 +111,12 @@ macro_rules! bigint_conversion { let num: Py = Py::from_owned_ptr_or_err(py, ffi::PyNumber_Index(ob.as_ptr()))?; let n_bits = ffi::_PyLong_NumBits(num.as_ptr()); - let n_bytes = if n_bits == -1 { + let n_bytes = if n_bits == (-1isize as usize) { return Err(PyErr::fetch(py)); } else if n_bits == 0 { 0 } else { - (n_bits as usize - 1 + $is_signed) / 8 + 1 + (n_bits - 1 + $is_signed) / 8 + 1 }; if n_bytes <= 128 { let mut buffer = [0; 128]; @@ -146,7 +146,7 @@ mod tests { use crate::types::{PyDict, PyModule}; use indoc::indoc; - fn python_fib(py: Python) -> &PyModule { + fn python_fib(py: Python<'_>) -> &PyModule { let fib_code = indoc!( r#" def fib(n): @@ -224,7 +224,7 @@ mod tests { }) } - fn python_index_class(py: Python) -> &PyModule { + fn python_index_class(py: Python<'_>) -> &PyModule { let index_code = indoc!( r#" class C: diff --git a/src/conversions/num_complex.rs b/src/conversions/num_complex.rs index 5ad88b65..217d862a 100644 --- a/src/conversions/num_complex.rs +++ b/src/conversions/num_complex.rs @@ -49,7 +49,7 @@ //! } //! //! #[pymodule] -//! fn my_module(_py: Python, m: &PyModule) -> PyResult<()> { +//! fn my_module(_py: Python<'_>, m: &PyModule) -> PyResult<()> { //! m.add_function(wrap_pyfunction!(get_eigenvalues, m)?)?; //! Ok(()) //! } @@ -107,7 +107,7 @@ use std::os::raw::c_double; impl PyComplex { /// Creates a new Python `PyComplex` object from `num_complex`'s [`Complex`]. - pub fn from_complex>(py: Python, complex: Complex) -> &PyComplex { + pub fn from_complex>(py: Python<'_>, complex: Complex) -> &PyComplex { unsafe { let ptr = ffi::PyComplex_FromDoubles(complex.re.into(), complex.im.into()); py.from_owned_ptr(ptr) @@ -120,14 +120,14 @@ macro_rules! complex_conversion { #[cfg_attr(docsrs, doc(cfg(feature = "num-complex")))] impl ToPyObject for Complex<$float> { #[inline] - fn to_object(&self, py: Python) -> PyObject { + fn to_object(&self, py: Python<'_>) -> PyObject { crate::IntoPy::::into_py(self.to_owned(), py) } } #[cfg_attr(docsrs, doc(cfg(feature = "num-complex")))] impl crate::IntoPy for Complex<$float> { - fn into_py(self, py: Python) -> PyObject { + fn into_py(self, py: Python<'_>) -> PyObject { unsafe { let raw_obj = ffi::PyComplex_FromDoubles(self.re as c_double, self.im as c_double); diff --git a/src/conversions/osstr.rs b/src/conversions/osstr.rs index b6282f37..21ad0cab 100644 --- a/src/conversions/osstr.rs +++ b/src/conversions/osstr.rs @@ -11,7 +11,7 @@ use std::ffi::{OsStr, OsString}; use std::os::raw::c_char; impl ToPyObject for OsStr { - fn to_object(&self, py: Python) -> PyObject { + fn to_object(&self, py: Python<'_>) -> PyObject { // If the string is UTF-8, take the quick and easy shortcut if let Some(valid_utf8_path) = self.to_str() { return valid_utf8_path.to_object(py); @@ -112,40 +112,40 @@ impl FromPyObject<'_> for OsString { impl IntoPy for &'_ OsStr { #[inline] - fn into_py(self, py: Python) -> PyObject { + fn into_py(self, py: Python<'_>) -> PyObject { self.to_object(py) } } impl ToPyObject for Cow<'_, OsStr> { #[inline] - fn to_object(&self, py: Python) -> PyObject { + fn to_object(&self, py: Python<'_>) -> PyObject { (self as &OsStr).to_object(py) } } impl IntoPy for Cow<'_, OsStr> { #[inline] - fn into_py(self, py: Python) -> PyObject { + fn into_py(self, py: Python<'_>) -> PyObject { self.to_object(py) } } impl ToPyObject for OsString { #[inline] - fn to_object(&self, py: Python) -> PyObject { + fn to_object(&self, py: Python<'_>) -> PyObject { (self as &OsStr).to_object(py) } } impl IntoPy for OsString { - fn into_py(self, py: Python) -> PyObject { + fn into_py(self, py: Python<'_>) -> PyObject { self.to_object(py) } } impl<'a> IntoPy for &'a OsString { - fn into_py(self, py: Python) -> PyObject { + fn into_py(self, py: Python<'_>) -> PyObject { self.to_object(py) } } @@ -182,7 +182,7 @@ mod tests { #[test] fn test_topyobject_roundtrip() { Python::with_gil(|py| { - fn test_roundtrip + Debug>(py: Python, obj: T) { + fn test_roundtrip + Debug>(py: Python<'_>, obj: T) { let pyobject = obj.to_object(py); let pystring: &PyString = pyobject.extract(py).unwrap(); assert_eq!(pystring.to_string_lossy(), obj.as_ref().to_string_lossy()); @@ -201,7 +201,7 @@ mod tests { fn test_intopy_roundtrip() { Python::with_gil(|py| { fn test_roundtrip + AsRef + Debug + Clone>( - py: Python, + py: Python<'_>, obj: T, ) { let pyobject = obj.clone().into_py(py); diff --git a/src/conversions/path.rs b/src/conversions/path.rs index bd0eca69..a5f04a5d 100644 --- a/src/conversions/path.rs +++ b/src/conversions/path.rs @@ -5,7 +5,7 @@ use std::ffi::OsString; use std::path::{Path, PathBuf}; impl ToPyObject for Path { - fn to_object(&self, py: Python) -> PyObject { + fn to_object(&self, py: Python<'_>) -> PyObject { self.as_os_str().to_object(py) } } @@ -34,40 +34,40 @@ impl FromPyObject<'_> for PathBuf { impl<'a> IntoPy for &'a Path { #[inline] - fn into_py(self, py: Python) -> PyObject { + fn into_py(self, py: Python<'_>) -> PyObject { self.as_os_str().to_object(py) } } impl<'a> ToPyObject for Cow<'a, Path> { #[inline] - fn to_object(&self, py: Python) -> PyObject { + fn to_object(&self, py: Python<'_>) -> PyObject { self.as_os_str().to_object(py) } } impl<'a> IntoPy for Cow<'a, Path> { #[inline] - fn into_py(self, py: Python) -> PyObject { + fn into_py(self, py: Python<'_>) -> PyObject { self.to_object(py) } } impl ToPyObject for PathBuf { #[inline] - fn to_object(&self, py: Python) -> PyObject { + fn to_object(&self, py: Python<'_>) -> PyObject { self.as_os_str().to_object(py) } } impl IntoPy for PathBuf { - fn into_py(self, py: Python) -> PyObject { + fn into_py(self, py: Python<'_>) -> PyObject { self.into_os_string().to_object(py) } } impl<'a> IntoPy for &'a PathBuf { - fn into_py(self, py: Python) -> PyObject { + fn into_py(self, py: Python<'_>) -> PyObject { self.as_os_str().to_object(py) } } @@ -103,7 +103,7 @@ mod tests { #[test] fn test_topyobject_roundtrip() { Python::with_gil(|py| { - fn test_roundtrip + Debug>(py: Python, obj: T) { + fn test_roundtrip + Debug>(py: Python<'_>, obj: T) { let pyobject = obj.to_object(py); let pystring: &PyString = pyobject.extract(py).unwrap(); assert_eq!(pystring.to_string_lossy(), obj.as_ref().to_string_lossy()); @@ -122,7 +122,7 @@ mod tests { fn test_intopy_roundtrip() { Python::with_gil(|py| { fn test_roundtrip + AsRef + Debug + Clone>( - py: Python, + py: Python<'_>, obj: T, ) { let pyobject = obj.clone().into_py(py); diff --git a/src/err/err_state.rs b/src/err/err_state.rs index 97a55ec5..ecebeb5a 100644 --- a/src/err/err_state.rs +++ b/src/err/err_state.rs @@ -15,12 +15,12 @@ pub(crate) struct PyErrStateNormalized { pub(crate) enum PyErrState { LazyTypeAndValue { - ptype: fn(Python) -> &PyType, - pvalue: Box PyObject + Send + Sync>, + ptype: for<'py> fn(Python<'py>) -> &PyType, + pvalue: Box FnOnce(Python<'py>) -> PyObject + Send + Sync>, }, LazyValue { ptype: Py, - pvalue: Box PyObject + Send + Sync>, + pvalue: Box FnOnce(Python<'py>) -> PyObject + Send + Sync>, }, FfiTuple { ptype: PyObject, @@ -33,28 +33,28 @@ pub(crate) enum PyErrState { /// Helper conversion trait that allows to use custom arguments for lazy exception construction. pub trait PyErrArguments: Send + Sync { /// Arguments for exception - fn arguments(self, py: Python) -> PyObject; + fn arguments(self, py: Python<'_>) -> PyObject; } impl PyErrArguments for T where T: IntoPy + Send + Sync, { - fn arguments(self, py: Python) -> PyObject { + fn arguments(self, py: Python<'_>) -> PyObject { self.into_py(py) } } pub(crate) fn boxed_args( args: impl PyErrArguments + 'static, -) -> Box PyObject + Send + Sync> { +) -> Box FnOnce(Python<'py>) -> PyObject + Send + Sync> { Box::new(|py| args.arguments(py)) } impl PyErrState { pub(crate) fn into_ffi_tuple( self, - py: Python, + py: Python<'_>, ) -> (*mut ffi::PyObject, *mut ffi::PyObject, *mut ffi::PyObject) { match self { PyErrState::LazyTypeAndValue { ptype, pvalue } => { @@ -88,7 +88,7 @@ impl PyErrState { } #[inline] - pub(crate) fn exceptions_must_derive_from_base_exception(py: Python) -> Self { + pub(crate) fn exceptions_must_derive_from_base_exception(py: Python<'_>) -> Self { PyErrState::LazyValue { ptype: PyTypeError::type_object(py).into(), pvalue: boxed_args("exceptions must derive from BaseException"), diff --git a/src/err/impls.rs b/src/err/impls.rs index 8731b5ce..4d1b740d 100644 --- a/src/err/impls.rs +++ b/src/err/impls.rs @@ -28,7 +28,7 @@ impl std::convert::From for PyErr { } impl PyErrArguments for io::Error { - fn arguments(self, py: Python) -> PyObject { + fn arguments(self, py: Python<'_>) -> PyObject { self.to_string().into_py(py) } } @@ -42,7 +42,7 @@ impl std::convert::From PyErrArguments for std::io::IntoInnerError { - fn arguments(self, py: Python) -> PyObject { + fn arguments(self, py: Python<'_>) -> PyObject { self.to_string().into_py(py) } } @@ -56,7 +56,7 @@ impl std::convert::From for PyErr { macro_rules! impl_to_pyerr { ($err: ty, $pyexc: ty) => { impl PyErrArguments for $err { - fn arguments(self, py: Python) -> PyObject { + fn arguments(self, py: Python<'_>) -> PyObject { self.to_string().into_py(py) } } diff --git a/src/err/mod.rs b/src/err/mod.rs index 5e08bb0e..869ff20a 100644 --- a/src/err/mod.rs +++ b/src/err/mod.rs @@ -38,6 +38,9 @@ pub struct PyErr { state: UnsafeCell>, } +// The inner value is only accessed through ways that require proving the gil is held +#[cfg(feature = "nightly")] +unsafe impl crate::marker::Ungil for PyErr {} unsafe impl Send for PyErr {} unsafe impl Sync for PyErr {} @@ -184,7 +187,7 @@ impl PyErr { /// /// Python::with_gil(|py| { /// let err: PyErr = PyTypeError::new_err(("some type error",)); - /// assert_eq!(err.get_type(py), PyType::new::(py)); + /// assert!(err.get_type(py).is(PyType::new::(py))); /// }); /// ``` pub fn get_type<'py>(&'py self, py: Python<'py>) -> &'py PyType { @@ -211,7 +214,7 @@ impl PyErr { } /// Consumes self to take ownership of the exception value contained in this error. - pub fn into_value(self, py: Python) -> Py { + pub fn into_value(self, py: Python<'_>) -> Py { // NB technically this causes one reference count increase and decrease in quick succession // on pvalue, but it's probably not worth optimizing this right now for the additional code // complexity. @@ -228,7 +231,7 @@ impl PyErr { /// /// Python::with_gil(|py| { /// let err = PyTypeError::new_err(("some type error",)); - /// assert_eq!(err.traceback(py), None); + /// assert!(err.traceback(py).is_none()); /// }); /// ``` pub fn traceback<'py>(&'py self, py: Python<'py>) -> Option<&'py PyTraceback> { @@ -240,7 +243,7 @@ impl PyErr { /// Gets whether an error is present in the Python interpreter's global state. #[inline] - pub fn occurred(_: Python) -> bool { + pub fn occurred(_: Python<'_>) -> bool { unsafe { !ffi::PyErr_Occurred().is_null() } } @@ -253,7 +256,7 @@ impl PyErr { /// Use this function when it is not known if an error should be present. If the error is /// expected to have been set, for example from [`PyErr::occurred`] or by an error return value /// from a C FFI function, use [`PyErr::fetch`]. - pub fn take(py: Python) -> Option { + pub fn take(py: Python<'_>) -> Option { let (ptype, pvalue, ptraceback) = unsafe { let mut ptype: *mut ffi::PyObject = std::ptr::null_mut(); let mut pvalue: *mut ffi::PyObject = std::ptr::null_mut(); @@ -324,7 +327,7 @@ impl PyErr { /// [PyErr::occurred] or by an error return value from a C FFI function. #[cfg_attr(all(debug_assertions, track_caller), track_caller)] #[inline] - pub fn fetch(py: Python) -> PyErr { + pub fn fetch(py: Python<'_>) -> PyErr { const FAILED_TO_FETCH: &str = "attempted to fetch exception but none was set"; match PyErr::take(py) { Some(err) => err, @@ -350,7 +353,7 @@ impl PyErr { /// /// This function will panic if `name` or `doc` cannot be converted to [`CString`]s. pub fn new_type( - py: Python, + py: Python<'_>, name: &str, doc: Option<&str>, base: Option<&PyType>, @@ -390,14 +393,14 @@ impl PyErr { } /// Prints a standard traceback to `sys.stderr`. - pub fn print(&self, py: Python) { + pub fn print(&self, py: Python<'_>) { self.clone_ref(py).restore(py); unsafe { ffi::PyErr_PrintEx(0) } } /// Prints a standard traceback to `sys.stderr`, and sets /// `sys.last_{type,value,traceback}` attributes to this exception's data. - pub fn print_and_set_sys_last_vars(&self, py: Python) { + pub fn print_and_set_sys_last_vars(&self, py: Python<'_>) { self.clone_ref(py).restore(py); unsafe { ffi::PyErr_PrintEx(1) } } @@ -406,7 +409,7 @@ impl PyErr { /// /// If `exc` is a class object, this also returns `true` when `self` is an instance of a subclass. /// If `exc` is a tuple, all exceptions in the tuple (and recursively in subtuples) are searched for a match. - pub fn matches(&self, py: Python, exc: T) -> bool + pub fn matches(&self, py: Python<'_>, exc: T) -> bool where T: ToBorrowedObject, { @@ -417,13 +420,13 @@ impl PyErr { /// Returns true if the current exception is instance of `T`. #[inline] - pub fn is_instance(&self, py: Python, typ: &PyType) -> bool { + pub fn is_instance(&self, py: Python<'_>, typ: &PyType) -> bool { unsafe { ffi::PyErr_GivenExceptionMatches(self.type_ptr(py), typ.as_ptr()) != 0 } } /// Returns true if the current exception is instance of `T`. #[inline] - pub fn is_instance_of(&self, py: Python) -> bool + pub fn is_instance_of(&self, py: Python<'_>) -> bool where T: PyTypeObject, { @@ -433,7 +436,7 @@ impl PyErr { /// Writes the error back to the Python interpreter's global state. /// This is the opposite of `PyErr::fetch()`. #[inline] - pub fn restore(self, py: Python) { + pub fn restore(self, py: Python<'_>) { let (ptype, pvalue, ptraceback) = self .state .into_inner() @@ -444,7 +447,7 @@ impl PyErr { /// Issues a warning message. /// May return a `PyErr` if warnings-as-errors is enabled. - pub fn warn(py: Python, category: &PyAny, message: &str, stacklevel: i32) -> PyResult<()> { + pub fn warn(py: Python<'_>, category: &PyAny, message: &str, stacklevel: i32) -> PyResult<()> { let message = CString::new(message)?; unsafe { error_on_minusone( @@ -466,26 +469,29 @@ impl PyErr { /// Python::with_gil(|py| { /// let err: PyErr = PyTypeError::new_err(("some type error",)); /// let err_clone = err.clone_ref(py); - /// assert_eq!(err.get_type(py), err_clone.get_type(py)); - /// assert_eq!(err.value(py), err_clone.value(py)); - /// assert_eq!(err.traceback(py), err_clone.traceback(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)), + /// } /// }); /// ``` #[inline] - pub fn clone_ref(&self, py: Python) -> PyErr { + pub fn clone_ref(&self, py: Python<'_>) -> PyErr { PyErr::from_state(PyErrState::Normalized(self.normalized(py).clone())) } /// 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 { + pub fn cause(&self, py: Python<'_>) -> Option { let ptr = unsafe { ffi::PyException_GetCause(self.value(py).as_ptr()) }; let obj = unsafe { py.from_owned_ptr_or_opt::(ptr) }; obj.map(Self::from_value) } /// Set the cause associated with the exception, pass `None` to clear it. - pub fn set_cause(&self, py: Python, cause: Option) { + pub fn set_cause(&self, py: Python<'_>, cause: Option) { unsafe { // PyException_SetCause _steals_ a reference to cause, so must use .into_ptr() ffi::PyException_SetCause( @@ -503,7 +509,7 @@ impl PyErr { } /// Returns borrowed reference to this Err's type - fn type_ptr(&self, py: Python) -> *mut ffi::PyObject { + fn type_ptr(&self, py: Python<'_>) -> *mut ffi::PyObject { match unsafe { &*self.state.get() } { // In lazy type case, normalize before returning ptype in case the type is not a valid // exception type. @@ -516,7 +522,7 @@ impl PyErr { } #[inline] - fn normalized(&self, py: Python) -> &PyErrStateNormalized { + fn normalized(&self, py: Python<'_>) -> &PyErrStateNormalized { if let Some(PyErrState::Normalized(n)) = unsafe { // Safety: self.state will never be written again once normalized. &*self.state.get() @@ -528,7 +534,7 @@ impl PyErr { } #[cold] - fn make_normalized(&self, py: Python) -> &PyErrStateNormalized { + fn make_normalized(&self, py: Python<'_>) -> &PyErrStateNormalized { // This process is safe because: // - Access is guaranteed not to be concurrent thanks to `Python` GIL token // - Write happens only once, and then never will change again. @@ -605,13 +611,13 @@ impl PyErr { since = "0.16.0", note = "Use err.into_value(py) instead of err.into_instance(py)" )] - pub fn into_instance(self, py: Python) -> Py { + pub fn into_instance(self, py: Python<'_>) -> Py { self.into_value(py) } } impl std::fmt::Debug for PyErr { - fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { + 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)) @@ -623,7 +629,7 @@ impl std::fmt::Debug for PyErr { } impl std::fmt::Display for PyErr { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { Python::with_gil(|py| { let value = self.value(py); let type_name = value.get_type().name().map_err(|_| std::fmt::Error)?; @@ -640,26 +646,26 @@ impl std::fmt::Display for PyErr { impl std::error::Error for PyErr {} impl IntoPy for PyErr { - fn into_py(self, py: Python) -> PyObject { + fn into_py(self, py: Python<'_>) -> PyObject { self.into_value(py).into() } } impl ToPyObject for PyErr { - fn to_object(&self, py: Python) -> PyObject { + fn to_object(&self, py: Python<'_>) -> PyObject { self.clone_ref(py).into_py(py) } } impl<'a> IntoPy for &'a PyErr { - fn into_py(self, py: Python) -> PyObject { + fn into_py(self, py: Python<'_>) -> PyObject { self.clone_ref(py).into_py(py) } } /// Convert `PyDowncastError` to Python `TypeError`. impl<'a> std::convert::From> for PyErr { - fn from(err: PyDowncastError) -> PyErr { + fn from(err: PyDowncastError<'_>) -> PyErr { exceptions::PyTypeError::new_err(err.to_string()) } } @@ -667,7 +673,7 @@ impl<'a> std::convert::From> for PyErr { impl<'a> std::error::Error for PyDowncastError<'a> {} impl<'a> std::fmt::Display for PyDowncastError<'a> { - fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { write!( f, "'{}' object cannot be converted to '{}'", @@ -677,7 +683,7 @@ impl<'a> std::fmt::Display for PyDowncastError<'a> { } } -pub fn panic_after_error(_py: Python) -> ! { +pub fn panic_after_error(_py: Python<'_>) -> ! { unsafe { ffi::PyErr_Print(); } @@ -686,7 +692,7 @@ pub fn panic_after_error(_py: Python) -> ! { /// Returns Ok if the error code is not -1. #[inline] -pub fn error_on_minusone(py: Python, result: c_int) -> PyResult<()> { +pub fn error_on_minusone(py: Python<'_>, result: c_int) -> PyResult<()> { if result != -1 { Ok(()) } else { @@ -695,7 +701,7 @@ pub fn error_on_minusone(py: Python, result: c_int) -> PyResult<()> { } #[inline] -fn exceptions_must_derive_from_base_exception(py: Python) -> PyErr { +fn exceptions_must_derive_from_base_exception(py: Python<'_>) -> PyErr { PyErr::from_state(PyErrState::exceptions_must_derive_from_base_exception(py)) } @@ -703,7 +709,7 @@ fn exceptions_must_derive_from_base_exception(py: Python) -> PyErr { mod tests { use super::PyErrState; use crate::exceptions; - use crate::{PyErr, Python}; + use crate::{AsPyPointer, PyErr, Python}; #[test] fn no_error() { @@ -854,16 +860,22 @@ mod tests { fn deprecations() { let err = exceptions::PyValueError::new_err("an error"); Python::with_gil(|py| { - assert_eq!(err.ptype(py), err.get_type(py)); - assert_eq!(err.pvalue(py), err.value(py)); - assert_eq!(err.instance(py), err.value(py)); - assert_eq!(err.ptraceback(py), err.traceback(py)); + assert_eq!(err.ptype(py).as_ptr(), err.get_type(py).as_ptr()); + assert_eq!(err.pvalue(py).as_ptr(), err.value(py).as_ptr()); + assert_eq!(err.instance(py).as_ptr(), err.value(py).as_ptr()); + assert_eq!( + err.ptraceback(py).map(|t| t.as_ptr()), + err.traceback(py).map(|t| t.as_ptr()) + ); assert_eq!( - err.clone_ref(py).into_instance(py).as_ref(py), - err.value(py) + err.clone_ref(py).into_instance(py).as_ref(py).as_ptr(), + err.value(py).as_ptr() + ); + assert_eq!( + PyErr::from_instance(err.value(py)).value(py).as_ptr(), + err.value(py).as_ptr() ); - assert_eq!(PyErr::from_instance(err.value(py)).value(py), err.value(py)); }); } } diff --git a/src/exceptions.rs b/src/exceptions.rs index e7335f81..d8d65de5 100644 --- a/src/exceptions.rs +++ b/src/exceptions.rs @@ -98,7 +98,7 @@ macro_rules! import_exception { ); impl $name { - fn type_object_raw(py: $crate::Python) -> *mut $crate::ffi::PyTypeObject { + fn type_object_raw(py: $crate::Python<'_>) -> *mut $crate::ffi::PyTypeObject { use $crate::once_cell::GILOnceCell; use $crate::AsPyPointer; static TYPE_OBJECT: GILOnceCell<$crate::Py<$crate::types::PyType>> = @@ -151,7 +151,7 @@ macro_rules! import_exception { /// } /// /// #[pymodule] -/// fn my_module(py: Python, m: &PyModule) -> PyResult<()> { +/// fn my_module(py: Python<'_>, m: &PyModule) -> PyResult<()> { /// m.add("MyError", py.get_type::())?; /// m.add_function(wrap_pyfunction!(raise_myerror, py)?)?; /// Ok(()) @@ -231,7 +231,7 @@ macro_rules! create_exception_type_object { ); impl $name { - fn type_object_raw(py: $crate::Python) -> *mut $crate::ffi::PyTypeObject { + fn type_object_raw(py: $crate::Python<'_>) -> *mut $crate::ffi::PyTypeObject { use $crate::once_cell::GILOnceCell; use $crate::AsPyPointer; static TYPE_OBJECT: GILOnceCell<$crate::Py<$crate::types::PyType>> = diff --git a/src/ffi/tests.rs b/src/ffi/tests.rs index 15b1eb36..aa523521 100644 --- a/src/ffi/tests.rs +++ b/src/ffi/tests.rs @@ -41,7 +41,6 @@ fn test_date_fromtimestamp() { } #[test] -#[cfg(not(all(PyPy, not(Py_3_8))))] fn test_utc_timezone() { Python::with_gil(|py| { let utc_timezone = unsafe { diff --git a/src/gil.rs b/src/gil.rs index c55db957..111e8215 100644 --- a/src/gil.rs +++ b/src/gil.rs @@ -164,7 +164,7 @@ pub struct GILGuard { impl GILGuard { /// Retrieves the marker type that proves that the GIL was acquired. #[inline] - pub fn python(&self) -> Python { + pub fn python(&self) -> Python<'_> { unsafe { Python::assume_gil_acquired() } } @@ -241,6 +241,7 @@ impl GILGuard { impl Drop for GILGuard { fn drop(&mut self) { // First up, try to detect if the order of destruction is correct. + #[allow(clippy::manual_assert)] let _ = GIL_COUNT.try_with(|c| { if self.gstate == ffi::PyGILState_STATE::PyGILState_UNLOCKED && c.get() != 1 { // XXX: this panic commits to leaking all objects in the pool as well as @@ -297,7 +298,7 @@ impl ReferencePool { self.dirty.store(true, atomic::Ordering::Release); } - fn update_counts(&self, _py: Python) { + fn update_counts(&self, _py: Python<'_>) { let prev = self.dirty.swap(false, atomic::Ordering::Acquire); if !prev { return; @@ -359,7 +360,7 @@ impl GILPool { /// Gets the Python token associated with this [`GILPool`]. #[inline] - pub fn python(&self) -> Python { + pub fn python(&self) -> Python<'_> { unsafe { Python::assume_gil_acquired() } } } @@ -423,7 +424,7 @@ pub unsafe fn register_decref(obj: NonNull) { /// /// # Safety /// The object must be an owned Python reference. -pub unsafe fn register_owned(_py: Python, obj: NonNull) { +pub unsafe fn register_owned(_py: Python<'_>, obj: NonNull) { debug_assert!(gil_is_acquired()); // Ignores the error in case this function called from `atexit`. let _ = OWNED_OBJECTS.try_with(|holder| holder.borrow_mut().push(obj)); @@ -482,7 +483,7 @@ impl EnsureGIL { /// Thus this method could be used to get access to a GIL token while the GIL is not held. /// Care should be taken to only use the returned Python in contexts where it is certain the /// GIL continues to be held. - pub unsafe fn python(&self) -> Python { + pub unsafe fn python(&self) -> Python<'_> { match &self.0 { Some(gil) => gil.python(), None => Python::assume_gil_acquired(), @@ -496,7 +497,7 @@ mod tests { use crate::{ffi, gil, AsPyPointer, IntoPyPointer, PyObject, Python, ToPyObject}; use std::ptr::NonNull; - fn get_object(py: Python) -> PyObject { + 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() }; @@ -675,10 +676,9 @@ mod tests { // Acquiring GIL for the second time should be safe - see #864 let gil = Python::acquire_gil(); let py = gil.python(); - let obj; let gil2 = Python::acquire_gil(); - obj = py.eval("object()", None, None).unwrap(); + let obj = py.eval("object()", None, None).unwrap(); drop(gil2); // After gil2 drops, obj should still have a reference count of one @@ -701,71 +701,119 @@ mod tests { #[test] fn test_clone_without_gil() { - let gil = Python::acquire_gil(); - let py = gil.python(); - let obj = get_object(py); - let count = obj.get_refcnt(py); + use crate::{Py, PyAny}; + use std::sync::atomic::{AtomicBool, Ordering}; + use std::{sync::Arc, thread}; - // Cloning without GIL should not update reference count - drop(gil); - let c = obj.clone(); - assert_eq!( - count, - obj.get_refcnt(unsafe { Python::assume_gil_acquired() }) - ); + // Some spinlocks for synchronizing + static GIL_ACQUIRED: AtomicBool = AtomicBool::new(false); + static OBJECT_CLONED: AtomicBool = AtomicBool::new(false); + static REFCNT_CHECKED: AtomicBool = AtomicBool::new(false); - // Acquring GIL will clear this pending change - let gil = Python::acquire_gil(); - let py = gil.python(); + Python::with_gil(|py| { + let obj: Arc> = Arc::new(get_object(py)); + let thread_obj = Arc::clone(&obj); - // Total reference count should be one higher - assert_eq!(count + 1, obj.get_refcnt(py)); + let count = (&*obj).get_refcnt(py); + println!( + "1: The object has been created and its reference count is {}", + count + ); - // Clone dropped - drop(c); + let handle = thread::spawn(move || { + Python::with_gil(move |py| { + println!("3. The GIL has been acquired on another thread."); + GIL_ACQUIRED.store(true, Ordering::Release); - // Overall count is now back to the original, and should be no pending change - assert_eq!(count, obj.get_refcnt(py)); + // Spin a bit while the main thread registers obj in POOL + while !OBJECT_CLONED.load(Ordering::Acquire) {} + println!("5. Checking refcnt"); + assert_eq!(thread_obj.get_refcnt(py), count); + + REFCNT_CHECKED.store(true, Ordering::Release); + }) + }); + + let cloned = py.allow_threads(|| { + println!("2. The GIL has been released."); + + // spin until the gil has been acquired on the thread. + while !GIL_ACQUIRED.load(Ordering::Acquire) {} + + 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.store(true, Ordering::Release); + cloned + }); + + while !REFCNT_CHECKED.load(Ordering::Acquire) {} + + // Returning from allow_threads doesn't clear the pool + py.allow_threads(|| { + // Acquiring GIL will clear the pending change + Python::with_gil(|_| {}); + }); + + 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] fn test_clone_in_other_thread() { - let gil = Python::acquire_gil(); - let py = gil.python(); - let obj = get_object(py); - let count = obj.get_refcnt(py); + use crate::Py; + use std::sync::atomic::{AtomicBool, Ordering}; + use std::{sync::Arc, thread}; - // Move obj to a thread which does not have the GIL, and clone it - let t = std::thread::spawn(move || { - // Cloning without GIL should not update reference count - #[allow(clippy::redundant_clone)] - let _ = obj.clone(); - assert_eq!( - count, - obj.get_refcnt(unsafe { Python::assume_gil_acquired() }) - ); + // Some spinlocks for synchronizing + static OBJECT_CLONED: AtomicBool = AtomicBool::new(false); - // Return obj so original thread can continue to use - obj + 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.store(true, Ordering::Release); + }); + + while !OBJECT_CLONED.load(Ordering::Acquire) {} + 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) }); - let obj = t.join().unwrap(); - let ptr = NonNull::new(obj.as_ptr()).unwrap(); + 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)); - // The pointer should appear once in the incref pool, and once in the - // decref pool (for the clone being created and also dropped) - assert_eq!(&POOL.pointer_ops.lock().0, &vec![ptr]); - assert_eq!(&POOL.pointer_ops.lock().1, &vec![ptr]); - - // Re-acquring GIL will clear these pending changes - drop(gil); - let gil = Python::acquire_gil(); - - assert!(POOL.pointer_ops.lock().0.is_empty()); - assert!(POOL.pointer_ops.lock().1.is_empty()); - - // Overall count is still unchanged - assert_eq!(count, obj.get_refcnt(gil.python())); + // Overall count is still unchanged + assert_eq!(count, obj.get_refcnt(py)); + }); } #[test] diff --git a/src/impl_/deprecations.rs b/src/impl_/deprecations.rs index c0faa0cf..72c8ff99 100644 --- a/src/impl_/deprecations.rs +++ b/src/impl_/deprecations.rs @@ -2,3 +2,9 @@ #[deprecated(since = "0.15.0", note = "use `fn __call__` instead of `#[call]`")] pub const CALL_ATTRIBUTE: () = (); + +#[deprecated( + since = "0.16.0", + note = "implement a `__traverse__` `#[pymethod]` instead of using `gc` option" +)] +pub const PYCLASS_GC_OPTION: () = (); diff --git a/src/impl_/extract_argument.rs b/src/impl_/extract_argument.rs index 489296bb..b793deef 100644 --- a/src/impl_/extract_argument.rs +++ b/src/impl_/extract_argument.rs @@ -101,9 +101,12 @@ pub fn from_py_with_with_default<'py, T>( /// single string.) #[doc(hidden)] #[cold] -pub fn argument_extraction_error(py: Python, arg_name: &str, error: PyErr) -> PyErr { - if error.get_type(py) == PyTypeError::type_object(py) { - PyTypeError::new_err(format!("argument '{}': {}", arg_name, error.value(py))) +pub fn argument_extraction_error(py: Python<'_>, arg_name: &str, error: PyErr) -> PyErr { + if error.get_type(py).is(PyTypeError::type_object(py)) { + let remapped_error = + PyTypeError::new_err(format!("argument '{}': {}", arg_name, error.value(py))); + remapped_error.set_cause(py, error.cause(py)); + remapped_error } else { error } diff --git a/src/impl_/frompyobject.rs b/src/impl_/frompyobject.rs index 9d859de9..8c0db798 100644 --- a/src/impl_/frompyobject.rs +++ b/src/impl_/frompyobject.rs @@ -2,7 +2,7 @@ use crate::{exceptions::PyTypeError, PyErr, Python}; #[cold] pub fn failed_to_extract_enum( - py: Python, + py: Python<'_>, type_name: &str, variant_names: &[&str], error_names: &[&str], diff --git a/src/impl_/pyclass.rs b/src/impl_/pyclass.rs index ac4d6d88..b2d7db04 100644 --- a/src/impl_/pyclass.rs +++ b/src/impl_/pyclass.rs @@ -6,7 +6,7 @@ use crate::{ pyclass::MutablePyClass, pyclass_init::PyObjectInit, type_object::{PyLayout, PyTypeObject}, - PyCell, PyClass, PyMethodDefType, PyNativeType, PyResult, PyTypeInfo, Python, + Py, PyAny, PyCell, PyClass, PyErr, PyMethodDefType, PyNativeType, PyResult, PyTypeInfo, Python, }; use std::{ marker::PhantomData, @@ -33,7 +33,7 @@ pub trait PyClassDict { fn new() -> Self; /// Empties the dictionary of its key-value pairs. #[inline] - fn clear_dict(&mut self, _py: Python) {} + fn clear_dict(&mut self, _py: Python<'_>) {} private_decl! {} } @@ -47,7 +47,7 @@ pub trait PyClassWeakRef { /// - `_obj` must be a pointer to the pyclass instance which contains `self`. /// - The GIL must be held. #[inline] - unsafe fn clear_weakrefs(&mut self, _obj: *mut ffi::PyObject, _py: Python) {} + unsafe fn clear_weakrefs(&mut self, _obj: *mut ffi::PyObject, _py: Python<'_>) {} private_decl! {} } @@ -83,7 +83,7 @@ impl PyClassDict for PyClassDictSlot { Self(std::ptr::null_mut()) } #[inline] - fn clear_dict(&mut self, _py: Python) { + fn clear_dict(&mut self, _py: Python<'_>) { if !self.0.is_null() { unsafe { ffi::PyDict_Clear(self.0) } } @@ -103,7 +103,7 @@ impl PyClassWeakRef for PyClassWeakRefSlot { Self(std::ptr::null_mut()) } #[inline] - unsafe fn clear_weakrefs(&mut self, obj: *mut ffi::PyObject, _py: Python) { + unsafe fn clear_weakrefs(&mut self, obj: *mut ffi::PyObject, _py: Python<'_>) { if !self.0.is_null() { ffi::PyObject_ClearWeakRefs(obj) } @@ -150,9 +150,6 @@ pub trait PyClassImpl: Sized { /// Class doc string const DOC: &'static str = "\0"; - /// #[pyclass(gc)] - const IS_GC: bool = false; - /// #[pyclass(subclass)] const IS_BASETYPE: bool = false; @@ -182,18 +179,6 @@ pub trait PyClassImpl: Sized { fn for_all_items(visitor: &mut dyn FnMut(&PyClassItems)); - #[inline] - fn get_new() -> Option { - None - } - #[inline] - fn get_alloc() -> Option { - None - } - #[inline] - fn get_free() -> Option { - None - } #[inline] fn dict_offset() -> Option { None @@ -206,16 +191,6 @@ pub trait PyClassImpl: Sized { // Traits describing known special methods. -pub trait PyClassNewImpl { - fn new_impl(self) -> Option; -} - -impl PyClassNewImpl for &'_ PyClassImplCollector { - fn new_impl(self) -> Option { - None - } -} - macro_rules! slot_fragment_trait { ($trait_name:ident, $($default_method:tt)*) => { #[allow(non_camel_case_types)] @@ -227,6 +202,84 @@ macro_rules! slot_fragment_trait { } } +slot_fragment_trait! { + PyClass__getattribute__SlotFragment, + + /// # Safety: _slf and _attr must be valid non-null Python objects + #[inline] + unsafe fn __getattribute__( + self, + py: Python<'_>, + slf: *mut ffi::PyObject, + attr: *mut ffi::PyObject, + ) -> PyResult<*mut ffi::PyObject> { + let res = ffi::PyObject_GenericGetAttr(slf, attr); + if res.is_null() { + Err(PyErr::fetch(py)) + } else { + Ok(res) + } + } +} + +slot_fragment_trait! { + PyClass__getattr__SlotFragment, + + /// # Safety: _slf and _attr must be valid non-null Python objects + #[inline] + unsafe fn __getattr__( + self, + py: Python<'_>, + _slf: *mut ffi::PyObject, + attr: *mut ffi::PyObject, + ) -> PyResult<*mut ffi::PyObject> { + Err(PyErr::new::( + (Py::::from_borrowed_ptr(py, attr),) + )) + } +} + +#[doc(hidden)] +#[macro_export] +macro_rules! generate_pyclass_getattro_slot { + ($cls:ty) => {{ + unsafe extern "C" fn __wrap( + _slf: *mut $crate::ffi::PyObject, + attr: *mut $crate::ffi::PyObject, + ) -> *mut $crate::ffi::PyObject { + use ::std::result::Result::*; + use $crate::impl_::pyclass::*; + let gil = $crate::GILPool::new(); + let py = gil.python(); + $crate::callback::panic_result_into_callback_output( + py, + ::std::panic::catch_unwind(move || -> $crate::PyResult<_> { + let collector = PyClassImplCollector::<$cls>::new(); + + // Strategy: + // - Try __getattribute__ first. Its default is PyObject_GenericGetAttr. + // - If it returns a result, use it. + // - If it fails with AttributeError, try __getattr__. + // - If it fails otherwise, reraise. + match collector.__getattribute__(py, _slf, attr) { + Ok(obj) => Ok(obj), + Err(e) if e.is_instance_of::<$crate::exceptions::PyAttributeError>(py) => { + collector.__getattr__(py, _slf, attr) + } + Err(e) => Err(e), + } + }), + ) + } + $crate::ffi::PyType_Slot { + slot: $crate::ffi::Py_tp_getattro, + pfunc: __wrap as $crate::ffi::getattrofunc as _, + } + }}; +} + +pub use generate_pyclass_getattro_slot; + /// Macro which expands to three items /// - Trait for a __setitem__ dunder /// - Trait for the corresponding __delitem__ dunder @@ -250,7 +303,7 @@ macro_rules! define_pyclass_setattr_slot { #[inline] unsafe fn $set( self, - _py: Python, + _py: Python<'_>, _slf: *mut ffi::PyObject, _attr: *mut ffi::PyObject, _value: NonNull, @@ -266,7 +319,7 @@ macro_rules! define_pyclass_setattr_slot { #[inline] unsafe fn $del( self, - _py: Python, + _py: Python<'_>, _slf: *mut ffi::PyObject, _attr: *mut ffi::PyObject, ) -> PyResult<()> { @@ -286,14 +339,19 @@ macro_rules! define_pyclass_setattr_slot { use ::std::option::Option::*; use $crate::callback::IntoPyCallbackOutput; use $crate::impl_::pyclass::*; - $crate::callback::handle_panic(|py| { - let collector = PyClassImplCollector::<$cls>::new(); - if let Some(value) = ::std::ptr::NonNull::new(value) { - collector.$set(py, _slf, attr, value).convert(py) - } else { - collector.$del(py, _slf, attr).convert(py) - } - }) + let gil = $crate::GILPool::new(); + let py = gil.python(); + $crate::callback::panic_result_into_callback_output( + py, + ::std::panic::catch_unwind(move || -> $crate::PyResult<_> { + let collector = PyClassImplCollector::<$cls>::new(); + if let Some(value) = ::std::ptr::NonNull::new(value) { + collector.$set(py, _slf, attr, value).convert(py) + } else { + collector.$del(py, _slf, attr).convert(py) + } + }), + ) } $crate::ffi::PyType_Slot { slot: $crate::ffi::$slot, @@ -362,7 +420,7 @@ 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> { @@ -377,7 +435,7 @@ 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> { @@ -393,17 +451,22 @@ macro_rules! define_pyclass_binary_operator_slot { _slf: *mut $crate::ffi::PyObject, _other: *mut $crate::ffi::PyObject, ) -> *mut $crate::ffi::PyObject { - $crate::callback::handle_panic(|py| { - use $crate::impl_::pyclass::*; - let collector = PyClassImplCollector::<$cls>::new(); - let lhs_result = collector.$lhs(py, _slf, _other)?; - if lhs_result == $crate::ffi::Py_NotImplemented() { - $crate::ffi::Py_DECREF(lhs_result); - collector.$rhs(py, _other, _slf) - } else { - ::std::result::Result::Ok(lhs_result) - } - }) + let gil = $crate::GILPool::new(); + let py = gil.python(); + $crate::callback::panic_result_into_callback_output( + py, + ::std::panic::catch_unwind(move || -> $crate::PyResult<_> { + use $crate::impl_::pyclass::*; + let collector = PyClassImplCollector::<$cls>::new(); + let lhs_result = collector.$lhs(py, _slf, _other)?; + if lhs_result == $crate::ffi::Py_NotImplemented() { + $crate::ffi::Py_DECREF(lhs_result); + collector.$rhs(py, _other, _slf) + } else { + ::std::result::Result::Ok(lhs_result) + } + }), + ) } $crate::ffi::PyType_Slot { slot: $crate::ffi::$slot, @@ -552,7 +615,7 @@ slot_fragment_trait! { #[inline] unsafe fn __pow__( self, - _py: Python, + _py: Python<'_>, _slf: *mut ffi::PyObject, _other: *mut ffi::PyObject, _mod: *mut ffi::PyObject, @@ -568,7 +631,7 @@ slot_fragment_trait! { #[inline] unsafe fn __rpow__( self, - _py: Python, + _py: Python<'_>, _slf: *mut ffi::PyObject, _other: *mut ffi::PyObject, _mod: *mut ffi::PyObject, @@ -586,17 +649,22 @@ macro_rules! generate_pyclass_pow_slot { _other: *mut $crate::ffi::PyObject, _mod: *mut $crate::ffi::PyObject, ) -> *mut $crate::ffi::PyObject { - $crate::callback::handle_panic(|py| { - use $crate::impl_::pyclass::*; - let collector = PyClassImplCollector::<$cls>::new(); - let lhs_result = collector.__pow__(py, _slf, _other, _mod)?; - if lhs_result == $crate::ffi::Py_NotImplemented() { - $crate::ffi::Py_DECREF(lhs_result); - collector.__rpow__(py, _other, _slf, _mod) - } else { - ::std::result::Result::Ok(lhs_result) - } - }) + let gil = $crate::GILPool::new(); + let py = gil.python(); + $crate::callback::panic_result_into_callback_output( + py, + ::std::panic::catch_unwind(move || -> $crate::PyResult<_> { + use $crate::impl_::pyclass::*; + let collector = PyClassImplCollector::<$cls>::new(); + let lhs_result = collector.__pow__(py, _slf, _other, _mod)?; + if lhs_result == $crate::ffi::Py_NotImplemented() { + $crate::ffi::Py_DECREF(lhs_result); + collector.__rpow__(py, _other, _slf, _mod) + } else { + ::std::result::Result::Ok(lhs_result) + } + }), + ) } $crate::ffi::PyType_Slot { slot: $crate::ffi::Py_nb_power, @@ -606,32 +674,12 @@ macro_rules! generate_pyclass_pow_slot { } pub use generate_pyclass_pow_slot; -pub trait PyClassAllocImpl { - fn alloc_impl(self) -> Option; -} - -impl PyClassAllocImpl for &'_ PyClassImplCollector { - fn alloc_impl(self) -> Option { - None - } -} - -pub trait PyClassFreeImpl { - fn free_impl(self) -> Option; -} - -impl PyClassFreeImpl for &'_ PyClassImplCollector { - fn free_impl(self) -> Option { - None - } -} - /// Implements a freelist. /// /// Do not implement this trait manually. Instead, use `#[pyclass(freelist = N)]` /// on a Rust struct to implement it. pub trait PyClassWithFreeList: PyClass { - fn get_free_list(py: Python) -> &mut FreeList<*mut ffi::PyObject>; + fn get_free_list(py: Python<'_>) -> &mut FreeList<*mut ffi::PyObject>; } /// Implementation of tp_alloc for `freelist` classes. @@ -693,7 +741,7 @@ pub unsafe extern "C" fn free_with_freelist(obj: *mut c_ /// Workaround for Python issue 35810; no longer necessary in Python 3.8 #[inline] #[cfg(not(Py_3_8))] -unsafe fn bpo_35810_workaround(_py: Python, ty: *mut ffi::PyTypeObject) { +unsafe fn bpo_35810_workaround(_py: Python<'_>, ty: *mut ffi::PyTypeObject) { #[cfg(Py_LIMITED_API)] { // Must check version at runtime for abi3 wheels - they could run against a higher version @@ -761,9 +809,6 @@ mod pyproto_traits { #[cfg(feature = "pyproto")] pub use pyproto_traits::*; -// items that PyO3 implements by default, but can be overidden by the users. -items_trait!(PyClassDefaultItems, pyclass_default_items); - // Protocol slots from #[pymethods] if not using inventory. #[cfg(not(feature = "multiple-pymethods"))] items_trait!(PyMethodsProtocolItems, methods_protocol_items); @@ -806,12 +851,12 @@ pub struct ThreadCheckerImpl(thread::ThreadId, PhantomData); impl PyClassThreadChecker for ThreadCheckerImpl { fn ensure(&self) { - if thread::current().id() != self.0 { - panic!( - "{} is unsendable, but sent to another thread!", - std::any::type_name::() - ); - } + assert_eq!( + thread::current().id(), + self.0, + "{} is unsendable, but sent to another thread!", + std::any::type_name::() + ); } fn new() -> Self { ThreadCheckerImpl(thread::current().id(), PhantomData) @@ -857,19 +902,6 @@ impl PyClassBaseType for T { type Initializer = crate::pyclass_init::PyClassInitializer; } -/// Default new implementation -pub(crate) unsafe extern "C" fn fallback_new( - _subtype: *mut ffi::PyTypeObject, - _args: *mut ffi::PyObject, - _kwds: *mut ffi::PyObject, -) -> *mut ffi::PyObject { - crate::callback_body!(py, { - Err::<(), _>(crate::exceptions::PyTypeError::new_err( - "No constructor defined", - )) - }) -} - /// Implementation of tp_dealloc for all pyclasses pub(crate) unsafe extern "C" fn tp_dealloc(obj: *mut ffi::PyObject) { crate::callback_body!(py, T::Layout::tp_dealloc(obj, py)) diff --git a/src/impl_/pymethods.rs b/src/impl_/pymethods.rs index cbaa2994..57024ae7 100644 --- a/src/impl_/pymethods.rs +++ b/src/impl_/pymethods.rs @@ -1,8 +1,8 @@ use crate::internal_tricks::{extract_cstr_or_leak_cstring, NulByteInString}; -use crate::{ffi, FromPyObject, PyAny, PyObject, PyResult, Python}; +use crate::{ffi, AsPyPointer, FromPyObject, PyAny, PyObject, PyResult, Python}; use std::ffi::CStr; use std::fmt; -use std::os::raw::c_int; +use std::os::raw::{c_int, c_void}; /// Python 3.8 and up - __ipow__ has modulo argument correctly populated. #[cfg(Py_3_8)] @@ -163,17 +163,21 @@ impl PyMethodDef { /// Convert `PyMethodDef` to Python method definition struct `ffi::PyMethodDef` pub(crate) fn as_method_def(&self) -> Result { let meth = match self.ml_meth { - PyMethodType::PyCFunction(meth) => meth.0, - PyMethodType::PyCFunctionWithKeywords(meth) => unsafe { std::mem::transmute(meth.0) }, + PyMethodType::PyCFunction(meth) => ffi::PyMethodDefPointer { + PyCFunction: meth.0, + }, + PyMethodType::PyCFunctionWithKeywords(meth) => ffi::PyMethodDefPointer { + PyCFunctionWithKeywords: meth.0, + }, #[cfg(not(Py_LIMITED_API))] - PyMethodType::PyCFunctionFastWithKeywords(meth) => unsafe { - std::mem::transmute(meth.0) + PyMethodType::PyCFunctionFastWithKeywords(meth) => ffi::PyMethodDefPointer { + _PyCFunctionFastWithKeywords: meth.0, }, }; Ok(ffi::PyMethodDef { ml_name: get_name(self.ml_name)?.as_ptr(), - ml_meth: Some(meth), + ml_meth: meth, ml_flags: self.ml_flags, ml_doc: get_doc(self.ml_doc)?.as_ptr(), }) @@ -248,3 +252,48 @@ fn get_name(name: &'static str) -> Result<&'static CStr, NulByteInString> { fn get_doc(doc: &'static str) -> Result<&'static CStr, NulByteInString> { extract_cstr_or_leak_cstring(doc, "Document cannot contain NUL byte.") } + +#[repr(transparent)] +pub struct PyTraverseError(pub(crate) c_int); + +/// Object visitor for GC. +#[derive(Clone)] +pub struct PyVisit<'p> { + pub(crate) visit: ffi::visitproc, + pub(crate) arg: *mut c_void, + /// VisitProc contains a Python instance to ensure that + /// 1) it is cannot be moved out of the traverse() call + /// 2) it cannot be sent to other threads + pub(crate) _py: Python<'p>, +} + +impl<'p> PyVisit<'p> { + /// Visit `obj`. + pub fn call(&self, obj: &T) -> Result<(), PyTraverseError> + where + T: AsPyPointer, + { + let r = unsafe { (self.visit)(obj.as_ptr(), self.arg) }; + if r == 0 { + Ok(()) + } else { + Err(PyTraverseError(r)) + } + } + + /// Creates the PyVisit from the arguments to tp_traverse + #[doc(hidden)] + pub unsafe fn from_raw(visit: ffi::visitproc, arg: *mut c_void, _py: Python<'p>) -> Self { + Self { visit, arg, _py } + } +} + +/// Unwraps the result of __traverse__ for tp_traverse +#[doc(hidden)] +#[inline] +pub fn unwrap_traverse_result(result: Result<(), PyTraverseError>) -> c_int { + match result { + Ok(()) => 0, + Err(PyTraverseError(value)) => value, + } +} diff --git a/src/impl_/pymodule.rs b/src/impl_/pymodule.rs index 11ab08e7..41f89294 100644 --- a/src/impl_/pymodule.rs +++ b/src/impl_/pymodule.rs @@ -1,9 +1,10 @@ //! Implementation details of `#[pymodule]` which need to be accessible from proc-macro generated code. -use std::{cell::UnsafeCell, panic::AssertUnwindSafe}; +use std::cell::UnsafeCell; use crate::{ - callback::handle_panic, ffi, types::PyModule, IntoPyPointer, Py, PyObject, PyResult, Python, + callback::panic_result_into_callback_output, ffi, types::PyModule, GILPool, IntoPyPointer, Py, + PyObject, PyResult, Python, }; /// `Sync` wrapper of `ffi::PyModuleDef`. @@ -14,7 +15,7 @@ pub struct ModuleDef { } /// Wrapper to enable initializer to be used in const fns. -pub struct ModuleInitializer(pub fn(Python, &PyModule) -> PyResult<()>); +pub struct ModuleInitializer(pub for<'py> fn(Python<'py>, &PyModule) -> PyResult<()>); unsafe impl Sync for ModuleDef {} @@ -52,7 +53,7 @@ impl ModuleDef { } } /// Builds a module using user given initializer. Used for [`#[pymodule]`][crate::pymodule]. - pub fn make_module(&'static self, py: Python) -> PyResult { + pub fn make_module(&'static self, py: Python<'_>) -> PyResult { let module = unsafe { Py::::from_owned_ptr_or_err(py, ffi::PyModule_Create(self.ffi_def.get()))? }; @@ -64,8 +65,30 @@ impl ModuleDef { /// # Safety /// The Python GIL must be held. pub unsafe fn module_init(&'static self) -> *mut ffi::PyObject { - let unwind_safe_self = AssertUnwindSafe(self); - handle_panic(|py| Ok(unwind_safe_self.make_module(py)?.into_ptr())) + let pool = GILPool::new(); + let py = pool.python(); + let unwind_safe_self = std::panic::AssertUnwindSafe(self); + panic_result_into_callback_output( + py, + std::panic::catch_unwind(move || -> PyResult<_> { + #[cfg(all(PyPy, not(Py_3_8)))] + { + const PYPY_GOOD_VERSION: [u8; 3] = [7, 3, 8]; + let version = py + .import("sys")? + .getattr("implementation")? + .getattr("version")?; + if version.lt(crate::types::PyTuple::new(py, &PYPY_GOOD_VERSION))? { + let warn = py.import("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.", + ))?; + } + } + Ok(unwind_safe_self.make_module(py)?.into_ptr()) + }), + ) } } @@ -130,7 +153,8 @@ mod tests { static INIT_CALLED: AtomicBool = AtomicBool::new(false); - fn init(_: Python, _: &PyModule) -> PyResult<()> { + #[allow(clippy::unnecessary_wraps)] + fn init(_: Python<'_>, _: &PyModule) -> PyResult<()> { INIT_CALLED.store(true, Ordering::SeqCst); Ok(()) } diff --git a/src/instance.rs b/src/instance.rs index 694e85f5..fe1e1a2e 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -24,7 +24,7 @@ use std::ptr::NonNull; pub unsafe trait PyNativeType: Sized { /// Returns a GIL marker constrained to the lifetime of this type. #[inline] - fn py(&self) -> Python { + fn py(&self) -> Python<'_> { unsafe { Python::assume_gil_acquired() } } /// Cast `&PyAny` to `&Self` without no type checking. @@ -168,7 +168,6 @@ pub unsafe trait PyNativeType: Sized { /// [`Py::clone_ref`] will be faster if you happen to be already holding the GIL. /// /// ```rust -/// use pyo3::conversion::AsPyPointer; /// use pyo3::prelude::*; /// use pyo3::types::PyDict; /// @@ -186,9 +185,9 @@ pub unsafe trait PyNativeType: Sized { /// drop(first); /// /// // They all point to the same object -/// assert_eq!(second.as_ptr(), third.as_ptr()); -/// assert_eq!(fourth.as_ptr(), fifth.as_ptr()); -/// assert_eq!(second.as_ptr(), fourth.as_ptr()); +/// assert!(second.is(&third)); +/// assert!(fourth.is(&fifth)); +/// assert!(second.is(&fourth)); /// }); /// # } /// ``` @@ -197,8 +196,8 @@ pub unsafe trait PyNativeType: Sized { /// /// It is easy to accidentally create reference cycles using [`Py`]``. /// The Python interpreter can break these reference cycles within pyclasses if they -/// implement the [`PyGCProtocol`](crate::class::gc::PyGCProtocol). If your pyclass -/// contains other Python objects you should implement this protocol to avoid leaking memory. +/// [integrate with the garbage collector][gc]. If your pyclass contains other Python +/// objects you should implement it to avoid leaking memory. /// /// # A note on Python reference counts /// @@ -220,9 +219,13 @@ pub unsafe trait PyNativeType: Sized { /// /// [`Rc`]: std::rc::Rc /// [`RefCell`]: std::cell::RefCell +/// [gc]: https://pyo3.rs/main/class/protocols.html#garbage-collector-integration #[repr(transparent)] pub struct Py(NonNull, PhantomData); +// The inner value is only accessed through ways that require proving the gil is held +#[cfg(feature = "nightly")] +unsafe impl crate::marker::Ungil for Py {} unsafe impl Send for Py {} unsafe impl Sync for Py {} @@ -248,7 +251,7 @@ where /// # Ok(()) /// # } /// ``` - pub fn new(py: Python, value: impl Into>) -> PyResult> { + 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 _) }; @@ -348,7 +351,7 @@ where /// obj.into_ref(py) /// } /// ``` - pub fn into_ref(self, py: Python) -> &T::AsRefTarget { + pub fn into_ref(self, py: Python<'_>) -> &T::AsRefTarget { unsafe { py.from_owned_ptr(self.into_ptr()) } } } @@ -466,9 +469,18 @@ where } impl Py { + /// Returns whether `self` and `other` point to the same object. To compare + /// the equality of two objects (the `==` operator), use [`eq`](PyAny::eq). + /// + /// This is equivalent to the Python expression `self is other`. + #[inline] + pub fn is(&self, o: &U) -> bool { + self.as_ptr() == o.as_ptr() + } + /// Gets the reference count of the `ffi::PyObject` pointer. #[inline] - pub fn get_refcnt(&self, _py: Python) -> isize { + pub fn get_refcnt(&self, _py: Python<'_>) -> isize { unsafe { ffi::Py_REFCNT(self.0.as_ptr()) } } @@ -481,7 +493,6 @@ impl Py { /// # Examples /// /// ```rust - /// use pyo3::conversion::AsPyPointer; /// use pyo3::prelude::*; /// use pyo3::types::PyDict; /// @@ -491,26 +502,26 @@ impl Py { /// let second = Py::clone_ref(&first, py); /// /// // Both point to the same object - /// assert_eq!(first.as_ptr(), second.as_ptr()); + /// assert!(first.is(&second)); /// }); /// # } /// ``` #[inline] - pub fn clone_ref(&self, py: Python) -> Py { + pub fn clone_ref(&self, py: Python<'_>) -> Py { unsafe { Py::from_borrowed_ptr(py, self.0.as_ptr()) } } /// Returns whether the object is considered to be None. /// /// This is equivalent to the Python expression `self is None`. - pub fn is_none(&self, _py: Python) -> bool { + pub fn is_none(&self, _py: Python<'_>) -> bool { unsafe { ffi::Py_None() == self.as_ptr() } } /// Returns whether the object is considered to be true. /// /// This is equivalent to the Python expression `bool(self)`. - pub fn is_true(&self, py: Python) -> PyResult { + pub fn is_true(&self, py: Python<'_>) -> PyResult { let v = unsafe { ffi::PyObject_IsTrue(self.as_ptr()) }; err::error_on_minusone(py, v)?; Ok(v != 0) @@ -529,7 +540,7 @@ impl Py { /// Retrieves an attribute value. /// /// This is equivalent to the Python expression `self.attr_name`. - pub fn getattr(&self, py: Python, attr_name: N) -> PyResult + pub fn getattr(&self, py: Python<'_>, attr_name: N) -> PyResult where N: ToPyObject, { @@ -541,7 +552,7 @@ impl Py { /// Sets an attribute value. /// /// This is equivalent to the Python expression `self.attr_name = value`. - pub fn setattr(&self, py: Python, attr_name: N, value: V) -> PyResult<()> + pub fn setattr(&self, py: Python<'_>, attr_name: N, value: V) -> PyResult<()> where N: ToPyObject, V: ToPyObject, @@ -558,7 +569,7 @@ impl Py { /// This is equivalent to the Python expression `self(*args, **kwargs)`. pub fn call( &self, - py: Python, + py: Python<'_>, args: impl IntoPy>, kwargs: Option<&PyDict>, ) -> PyResult { @@ -577,16 +588,16 @@ impl Py { /// 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 { + pub fn call1(&self, py: Python<'_>, args: impl IntoPy>) -> PyResult { self.call(py, args, None) } /// Calls the object without arguments. /// /// This is equivalent to the Python expression `self()`. - pub fn call0(&self, py: Python) -> PyResult { + pub fn call0(&self, py: Python<'_>) -> PyResult { cfg_if::cfg_if! { - if #[cfg(Py_3_9)] { + if #[cfg(all(Py_3_9, not(PyPy)))] { // Optimized path on python 3.9+ unsafe { PyObject::from_owned_ptr_or_err(py, ffi::PyObject_CallNoArgs(self.as_ptr())) @@ -602,7 +613,7 @@ impl Py { /// This is equivalent to the Python expression `self.name(*args, **kwargs)`. pub fn call_method( &self, - py: Python, + py: Python<'_>, name: &str, args: impl IntoPy>, kwargs: Option<&PyDict>, @@ -627,7 +638,7 @@ impl Py { /// This is equivalent to the Python expression `self.name(*args)`. pub fn call_method1( &self, - py: Python, + py: Python<'_>, name: &str, args: impl IntoPy>, ) -> PyResult { @@ -637,9 +648,9 @@ impl Py { /// Calls a method on the object with no arguments. /// /// This is equivalent to the Python expression `self.name()`. - pub fn call_method0(&self, py: Python, name: &str) -> PyResult { + pub fn call_method0(&self, py: Python<'_>, name: &str) -> PyResult { cfg_if::cfg_if! { - if #[cfg(all(Py_3_9, not(Py_LIMITED_API)))] { + if #[cfg(all(Py_3_9, not(any(Py_LIMITED_API, PyPy))))] { // Optimized path on python 3.9+ unsafe { let name = name.into_py(py); @@ -662,7 +673,7 @@ impl Py { /// # Panics /// Panics if `ptr` is null. #[inline] - pub unsafe fn from_owned_ptr(py: Python, ptr: *mut ffi::PyObject) -> Py { + pub unsafe fn from_owned_ptr(py: Python<'_>, ptr: *mut ffi::PyObject) -> Py { match NonNull::new(ptr) { Some(nonnull_ptr) => Py(nonnull_ptr, PhantomData), None => crate::err::panic_after_error(py), @@ -676,7 +687,10 @@ impl Py { /// # Safety /// If non-null, `ptr` must be a pointer to a Python object of type T. #[inline] - pub unsafe fn from_owned_ptr_or_err(py: Python, ptr: *mut ffi::PyObject) -> PyResult> { + pub unsafe fn from_owned_ptr_or_err( + py: Python<'_>, + ptr: *mut ffi::PyObject, + ) -> PyResult> { match NonNull::new(ptr) { Some(nonnull_ptr) => Ok(Py(nonnull_ptr, PhantomData)), None => Err(PyErr::fetch(py)), @@ -690,7 +704,7 @@ impl Py { /// # Safety /// If non-null, `ptr` must be a pointer to a Python object of type T. #[inline] - pub unsafe fn from_owned_ptr_or_opt(_py: Python, ptr: *mut ffi::PyObject) -> Option { + pub unsafe fn from_owned_ptr_or_opt(_py: Python<'_>, ptr: *mut ffi::PyObject) -> Option { NonNull::new(ptr).map(|nonnull_ptr| Py(nonnull_ptr, PhantomData)) } @@ -702,7 +716,7 @@ impl Py { /// # Panics /// Panics if `ptr` is null. #[inline] - pub unsafe fn from_borrowed_ptr(py: Python, ptr: *mut ffi::PyObject) -> Py { + 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, None => crate::err::panic_after_error(py), @@ -716,7 +730,10 @@ impl Py { /// # Safety /// `ptr` must be a pointer to a Python object of type T. #[inline] - pub unsafe fn from_borrowed_ptr_or_err(py: Python, ptr: *mut ffi::PyObject) -> PyResult { + pub unsafe fn from_borrowed_ptr_or_err( + py: Python<'_>, + ptr: *mut ffi::PyObject, + ) -> PyResult { Self::from_borrowed_ptr_or_opt(py, ptr).ok_or_else(|| PyErr::fetch(py)) } @@ -727,7 +744,10 @@ impl Py { /// # Safety /// `ptr` must be a pointer to a Python object of type T. #[inline] - pub unsafe fn from_borrowed_ptr_or_opt(_py: Python, ptr: *mut ffi::PyObject) -> Option { + pub unsafe fn from_borrowed_ptr_or_opt( + _py: Python<'_>, + ptr: *mut ffi::PyObject, + ) -> Option { NonNull::new(ptr).map(|nonnull_ptr| { ffi::Py_INCREF(ptr); Py(nonnull_ptr, PhantomData) @@ -754,7 +774,7 @@ impl Py { impl ToPyObject for Py { /// Converts `Py` instance -> PyObject. - fn to_object(&self, py: Python) -> PyObject { + fn to_object(&self, py: Python<'_>) -> PyObject { unsafe { PyObject::from_borrowed_ptr(py, self.as_ptr()) } } } @@ -763,7 +783,7 @@ impl IntoPy for Py { /// Converts a `Py` instance to `PyObject`. /// Consumes `self` without calling `Py_DECREF()`. #[inline] - fn into_py(self, _py: Python) -> PyObject { + fn into_py(self, _py: Python<'_>) -> PyObject { unsafe { PyObject::from_non_null(self.into_non_null()) } } } @@ -805,7 +825,7 @@ where } // `&PyCell` can be converted to `Py` -impl<'a, T> std::convert::From<&PyCell> for Py +impl std::convert::From<&PyCell> for Py where T: PyClass, { @@ -832,13 +852,6 @@ where } } -impl PartialEq for Py { - #[inline] - fn eq(&self, o: &Py) -> bool { - self.0 == o.0 - } -} - /// 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. @@ -891,13 +904,13 @@ where T: PyTypeInfo, T::AsRefTarget: std::fmt::Display, { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { Python::with_gil(|py| std::fmt::Display::fmt(self.as_ref(py), f)) } } impl std::fmt::Debug for Py { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_tuple("Py").field(&self.0.as_ptr()).finish() } } @@ -915,11 +928,11 @@ impl PyObject { /// /// This can cast only to native Python types, not types implemented in Rust. For a more /// flexible alternative, see [`Py::extract`](struct.Py.html#method.extract). - pub fn cast_as<'p, D>(&'p self, py: Python<'p>) -> Result<&'p D, PyDowncastError> + pub fn cast_as<'p, D>(&'p self, py: Python<'p>) -> Result<&'p D, PyDowncastError<'_>> where D: PyTryFrom<'p>, { - D::try_from(unsafe { py.from_borrowed_ptr::(self.as_ptr()) }) + >::try_from(unsafe { py.from_borrowed_ptr::(self.as_ptr()) }) } } diff --git a/src/lib.rs b/src/lib.rs index 5f11571c..ce2a1e24 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,4 @@ -#![cfg_attr(feature = "nightly", feature(specialization))] +#![cfg_attr(feature = "nightly", feature(auto_traits, negative_impls))] #![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] #![cfg_attr( docsrs, // rustdoc:: is not supported on msrv @@ -8,9 +8,18 @@ rustdoc::bare_urls ) )] +#![warn(elided_lifetimes_in_paths, unused_lifetimes)] // Deny some lints in doctests. // Use `#[allow(...)]` locally to override. -#![doc(test(attr(deny(warnings), allow(unused_variables, unused_assignments))))] +#![doc(test(attr( + deny( + elided_lifetimes_in_paths, + unused_lifetimes, + rust_2021_prelude_collisions, + warnings + ), + allow(unused_variables, unused_assignments) +)))] #![cfg_attr(coverage, feature(no_coverage))] // used in src/test_hygiene.rs //! Rust bindings to the Python interpreter. @@ -62,7 +71,10 @@ //! ## Default feature flags //! //! The following features are turned on by default: -//! - `macros`: Enables various macros, including all the attribute macros. +//! - `macros`: Enables various macros, including all the attribute macros excluding the deprecated +//! `#[pyproto]` attribute. +//! - `pyproto`: Adds the deprecated `#[pyproto]` attribute macro. Likely to become optional and +//! then removed in the future. //! //! ## Optional feature flags //! @@ -94,9 +106,7 @@ //! //! ## Unstable features //! -//! - `nightly`: Gates some optimizations that rely on -//! [`#![feature(specialization)]`](https://github.com/rust-lang/rfcs/blob/master/text/1210-impl-specialization.md), -//! for which you'd also need nightly Rust. You should not use this feature. +//! - `nightly`: Uses `#![feature(auto_traits, negative_impls)]` to define [`Ungil`] as an auto trait. // //! ## `rustc` environment flags //! @@ -159,7 +169,7 @@ //! //! /// A Python module implemented in Rust. //! #[pymodule] -//! fn string_sum(py: Python, m: &PyModule) -> PyResult<()> { +//! fn string_sum(py: Python<'_>, m: &PyModule) -> PyResult<()> { //! m.add_function(wrap_pyfunction!(sum_as_string, m)?)?; //! //! Ok(()) @@ -283,6 +293,7 @@ //! [Python from Rust]: https://github.com/PyO3/pyo3#using-python-from-rust //! [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" +//! [`Ungil`]: crate::marker::Ungil pub use crate::class::*; pub use crate::conversion::{ AsPyPointer, FromPyObject, FromPyPointer, IntoPy, IntoPyPointer, PyTryFrom, PyTryInto, @@ -293,12 +304,13 @@ pub use crate::err::{PyDowncastError, PyErr, PyErrArguments, PyResult}; pub use crate::gil::{prepare_freethreaded_python, with_embedded_python_interpreter}; pub use crate::gil::{GILGuard, GILPool}; pub use crate::instance::{Py, PyNativeType, PyObject}; +pub use crate::marker::Python; pub use crate::pycell::{PyCell, PyRef, PyRefMut}; pub use crate::pyclass::PyClass; pub use crate::pyclass_init::PyClassInitializer; -pub use crate::python::{Python, PythonVersionInfo}; pub use crate::type_object::PyTypeInfo; pub use crate::types::PyAny; +pub use crate::version::PythonVersionInfo; // Old directory layout, to be rethought? #[cfg(not(feature = "pyproto"))] @@ -306,6 +318,8 @@ pub mod class { #[doc(hidden)] pub use crate::impl_::pymethods as methods; + pub use self::gc::{PyTraverseError, PyVisit}; + #[doc(hidden)] pub use self::methods::{ PyClassAttributeDef, PyGetterDef, PyMethodDef, PyMethodDefType, PyMethodType, PySetterDef, @@ -322,6 +336,10 @@ pub mod class { pub mod iter { pub use crate::pyclass::{IterNextOutput, PyIterNextOutput}; } + + pub mod gc { + pub use crate::impl_::pymethods::{PyTraverseError, PyVisit}; + } } #[cfg(feature = "macros")] @@ -355,6 +373,7 @@ mod gil; #[doc(hidden)] pub mod impl_; mod instance; +pub mod marker; pub mod marshal; pub mod once_cell; pub mod panic; @@ -362,9 +381,10 @@ pub mod prelude; pub mod pycell; pub mod pyclass; pub mod pyclass_init; -mod python; + pub mod type_object; pub mod types; +mod version; pub use crate::conversions::*; @@ -406,46 +426,42 @@ pub mod doc_test { }; } - macro_rules! doctest { - ($path:expr, $mod:ident) => { - doctest_impl!(include_str!(concat!("../", $path)), $mod); + macro_rules! doctests { + ($($path:expr => $mod:ident),* $(,)?) => { + $(doctest_impl!(include_str!(concat!("../", $path)), $mod);)* }; } - doctest!("README.md", readme_md); - doctest!("guide/src/advanced.md", guide_advanced_md); - doctest!( - "guide/src/building_and_distribution.md", - guide_building_and_distribution_md - ); - doctest!("guide/src/class.md", guide_class_md); - doctest!("guide/src/class/protocols.md", guide_class_protocols_md); - doctest!("guide/src/conversions.md", guide_conversions_md); - doctest!( - "guide/src/conversions/tables.md", - guide_conversions_tables_md - ); + doctests! { + "README.md" => readme_md, + "guide/src/advanced.md" => guide_advanced_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, + "guide/src/class/numeric.md" => guide_class_numeric, + "guide/src/class/protocols.md" => guide_class_protocols_md, + "guide/src/conversions.md" => guide_conversions_md, + "guide/src/conversions/tables.md" => guide_conversions_tables_md, + "guide/src/conversions/traits.md" => guide_conversions_traits_md, + "guide/src/debugging.md" => guide_debugging_md, - doctest!( - "guide/src/conversions/traits.md", - guide_conversions_traits_md - ); - doctest!("guide/src/debugging.md", guide_debugging_md); - doctest!("guide/src/exception.md", guide_exception_md); - doctest!("guide/src/function.md", guide_function_md); - doctest!("guide/src/migration.md", guide_migration_md); - doctest!("guide/src/module.md", guide_module_md); - doctest!("guide/src/parallelism.md", guide_parallelism_md); - doctest!("guide/src/python_from_rust.md", guide_python_from_rust_md); - doctest!("guide/src/rust_cpython.md", guide_rust_cpython_md); - doctest!("guide/src/trait_bounds.md", guide_trait_bounds_md); - doctest!("guide/src/types.md", guide_types_md); - doctest!("guide/src/faq.md", faq); - doctest!( - "guide/src/python_typing_hints.md", - guide_python_typing_hints - ); + // deliberate choice not to test guide/ecosystem because those pages depend on external + // crates such as pyo3_asyncio. - // deliberate choice not to test guide/ecosystem because those pages depend on external crates - // such as pyo3_asyncio. + "guide/src/exception.md" => guide_exception_md, + "guide/src/faq.md" => guide_faq_md, + "guide/src/features.md" => guide_features_md, + "guide/src/function.md" => guide_function_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/python_from_rust.md" => guide_python_from_rust_md, + "guide/src/python_typing_hints.md" => guide_python_typing_hints_md, + "guide/src/rust_cpython.md" => guide_rust_cpython_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 fafd7386..0d2a668f 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -5,7 +5,7 @@ /// This macro internally calls [`Python::run`](crate::Python::run) and panics /// if it returns `Err`, after printing the error to stdout. /// -/// If you need to handle failures, please use [`Python::run`](crate::python::Python::run) instead. +/// If you need to handle failures, please use [`Python::run`](crate::marker::Python::run) instead. /// /// # Examples /// ``` diff --git a/src/python.rs b/src/marker.rs similarity index 79% rename from src/python.rs rename to src/marker.rs index c410d0d1..f159d89e 100644 --- a/src/python.rs +++ b/src/marker.rs @@ -2,102 +2,188 @@ // // based on Daniel Grunwald's https://github.com/dgrunwald/rust-cpython +//! Fundamental properties of objects tied to the Python interpreter. +//! +//! The Python interpreter is not threadsafe. To protect the Python interpreter in multithreaded +//! scenarios there is a global lock, the *global interpreter lock* (hereafter referred to as *GIL*) +//! that must be held to safely interact with Python objects. This is why in PyO3 when you acquire +//! the GIL you get a [`Python`] marker token that carries the *lifetime* of holding the GIL and all +//! borrowed references to Python objects carry this lifetime as well. This will statically ensure +//! that you can never use Python objects after dropping the lock - if you mess this up it will be +//! caught at compile time and your program will fail to compile. +//! +//! It also supports this pattern that many extension modules employ: +//! - Drop the GIL, so that other Python threads can acquire it and make progress themselves +//! - Do something independently of the Python interpreter, like IO, a long running calculation or +//! awaiting a future +//! - Once that is done, reacquire the GIL +//! +//! That API is provided by [`Python::allow_threads`] and enforced via the [`Ungil`] bound on the +//! closure and the return type. This is done by relying on the [`Send`] auto trait. `Ungil` is +//! defined as the following: +//! +//! ```rust +//! pub unsafe trait Ungil {} +//! +//! unsafe impl Ungil for T {} +//! ``` +//! +//! We piggy-back off the `Send` auto trait because it is not possible to implement custom auto +//! traits on stable Rust. This is the solution which enables it for as many types as possible while +//! making the API usable. +//! +//! In practice this API works quite well, but it comes with some drawbacks: +//! +//! ## Drawbacks +//! +//! There is no reason to prevent `!Send` types like [`Rc`] from crossing the closure. After all, +//! [`Python::allow_threads`] just lets other Python threads run - it does not itself launch a new +//! thread. +//! +//! ```rust, compile_fail +//! # #[cfg(feature = "nightly")] +//! # compile_error!("this actually works on nightly") +//! use pyo3::prelude::*; +//! use std::rc::Rc; +//! +//! fn main() { +//! Python::with_gil(|py| { +//! let rc = Rc::new(5); +//! +//! py.allow_threads(|| { +//! // This would actually be fine... +//! println!("{:?}", *rc); +//! }); +//! }); +//! } +//! ``` +//! +//! Because we are using `Send` for something it's not quite meant for, other code that +//! (correctly) upholds the invariants of [`Send`] can cause problems. +//! +//! [`SendWrapper`] is one of those. Per its documentation: +//! +//! > A wrapper which allows you to move around non-Send-types between threads, as long as you +//! > access the contained value only from within the original thread and make sure that it is +//! > dropped from within the original thread. +//! +//! This will "work" to smuggle Python references across the closure, because we're not actually +//! doing anything with threads: +//! +//! ```rust, no_run +//! use pyo3::prelude::*; +//! use pyo3::types::PyString; +//! use send_wrapper::SendWrapper; +//! +//! Python::with_gil(|py| { +//! let string = PyString::new(py, "foo"); +//! +//! let wrapped = SendWrapper::new(string); +//! +//! py.allow_threads(|| { +//! # #[cfg(not(feature = "nightly"))] +//! # { +//! // 💥 Unsound! 💥 +//! let smuggled: &PyString = *wrapped; +//! println!("{:?}", smuggled); +//! # } +//! }); +//! }); +//! ``` +//! +//! For now the answer to that is "don't do that". +//! +//! # A proper implementation using an auto trait +//! +//! However on nightly Rust and when PyO3's `nightly` feature is +//! enabled, `Ungil` is defined as the following: +//! +//! ```rust +//! # #[cfg(FALSE)] +//! # { +//! #![feature(auto_traits, negative_impls)] +//! +//! pub unsafe auto trait Ungil {} +//! +//! // It is unimplemented for the `Python` struct and Python objects. +//! impl !Ungil for Python<'_> {} +//! impl !Ungil for ffi::PyObject {} +//! +//! // `Py` wraps it in a safe api, so this is OK +//! unsafe impl Ungil for Py {} +//! # } +//! ``` +//! +//! With this feature enabled, the above two examples will start working and not working, respectively. +//! +//! [`SendWrapper`]: https://docs.rs/send_wrapper/latest/send_wrapper/struct.SendWrapper.html +//! [`Rc`]: std::rc::Rc +//! [`Py`]: crate::Py use crate::err::{self, PyDowncastError, PyErr, PyResult}; use crate::gil::{self, GILGuard, GILPool}; use crate::impl_::not_send::NotSend; use crate::type_object::{PyTypeInfo, PyTypeObject}; use crate::types::{PyAny, PyDict, PyModule, PyType}; +use crate::version::PythonVersionInfo; use crate::{ffi, AsPyPointer, FromPyPointer, IntoPyPointer, PyNativeType, PyObject, PyTryFrom}; use std::ffi::{CStr, CString}; use std::marker::PhantomData; use std::os::raw::c_int; -/// Represents the major, minor, and patch (if any) versions of this interpreter. +/// Types that are safe to access while the GIL is not held. /// -/// See [Python::version]. -#[derive(Debug)] -pub struct PythonVersionInfo<'py> { - /// Python major version (e.g. `3`). - pub major: u8, - /// Python minor version (e.g. `11`). - pub minor: u8, - /// Python patch version (e.g. `0`). - pub patch: u8, - /// Python version suffix, if applicable (e.g. `a0`). - pub suffix: Option<&'py str>, -} +/// # Safety +/// +/// The type must not carry borrowed Python references or, if it does, not allow access to them if +/// the GIL is not held. +/// +/// See the [module-level documentation](self) for more information. +#[cfg_attr(docsrs, doc(cfg(all())))] // Hide the cfg flag +#[cfg(not(feature = "nightly"))] +pub unsafe trait Ungil {} -impl<'py> PythonVersionInfo<'py> { - /// Parses a hard-coded Python interpreter version string (e.g. 3.9.0a4+). - /// - /// Panics if the string is ill-formatted. - fn from_str(version_number_str: &'py str) -> Self { - fn split_and_parse_number(version_part: &str) -> (u8, Option<&str>) { - match version_part.find(|c: char| !c.is_ascii_digit()) { - None => (version_part.parse().unwrap(), None), - Some(version_part_suffix_start) => { - let (version_part, version_part_suffix) = - version_part.split_at(version_part_suffix_start); - (version_part.parse().unwrap(), Some(version_part_suffix)) - } - } - } +#[cfg_attr(docsrs, doc(cfg(all())))] // Hide the cfg flag +#[cfg(not(feature = "nightly"))] +unsafe impl Ungil for T {} - let mut parts = version_number_str.split('.'); - let major_str = parts.next().expect("Python major version missing"); - let minor_str = parts.next().expect("Python minor version missing"); - let patch_str = parts.next(); - assert!( - parts.next().is_none(), - "Python version string has too many parts" - ); +/// Types that are safe to access while the GIL is not held. +/// +/// # Safety +/// +/// The type must not carry borrowed Python references or, if it does, not allow access to them if +/// the GIL is not held. +/// +/// See the [module-level documentation](self) for more information. +#[cfg(feature = "nightly")] +pub unsafe auto trait Ungil {} - let major = major_str - .parse() - .expect("Python major version not an integer"); - let (minor, suffix) = split_and_parse_number(minor_str); - if suffix.is_some() { - assert!(patch_str.is_none()); - return PythonVersionInfo { - major, - minor, - patch: 0, - suffix, - }; - } +#[cfg(feature = "nightly")] +mod negative_impls { + use super::Ungil; - let (patch, suffix) = patch_str.map(split_and_parse_number).unwrap_or_default(); - PythonVersionInfo { - major, - minor, - patch, - suffix, - } - } -} + impl !Ungil for crate::Python<'_> {} -impl PartialEq<(u8, u8)> for PythonVersionInfo<'_> { - fn eq(&self, other: &(u8, u8)) -> bool { - self.major == other.0 && self.minor == other.1 - } -} + // This means that PyString, PyList, etc all inherit !Ungil from this. + impl !Ungil for crate::PyAny {} -impl PartialEq<(u8, u8, u8)> for PythonVersionInfo<'_> { - fn eq(&self, other: &(u8, u8, u8)) -> bool { - self.major == other.0 && self.minor == other.1 && self.patch == other.2 - } -} + // All the borrowing wrappers + impl !Ungil for crate::PyCell {} + impl !Ungil for crate::PyRef<'_, T> {} + impl !Ungil for crate::PyRefMut<'_, T> {} -impl PartialOrd<(u8, u8)> for PythonVersionInfo<'_> { - fn partial_cmp(&self, other: &(u8, u8)) -> Option { - (self.major, self.minor).partial_cmp(other) - } -} + // FFI pointees + impl !Ungil for crate::ffi::PyObject {} + impl !Ungil for crate::ffi::PyLongObject {} -impl PartialOrd<(u8, u8, u8)> for PythonVersionInfo<'_> { - fn partial_cmp(&self, other: &(u8, u8, u8)) -> Option { - (self.major, self.minor, self.patch).partial_cmp(other) - } + impl !Ungil for crate::ffi::PyThreadState {} + impl !Ungil for crate::ffi::PyInterpreterState {} + impl !Ungil for crate::ffi::PyWeakReference {} + impl !Ungil for crate::ffi::PyFrameObject {} + impl !Ungil for crate::ffi::PyCodeObject {} + #[cfg(not(Py_LIMITED_API))] + impl !Ungil for crate::ffi::PyDictKeysObject {} + #[cfg(not(Py_LIMITED_API))] + impl !Ungil for crate::ffi::PyArena {} } /// A marker token that represents holding the GIL. @@ -305,12 +391,8 @@ impl<'py> Python<'py> { /// interpreter for some time and have other Python threads around, this will let you run /// Rust-only code while letting those other Python threads make progress. /// - /// The closure is impermeable to types that are tied to holding the GIL, such as `&`[`PyAny`] - /// and its concrete-typed siblings like `&`[`PyString`]. This is achieved via the [`Send`] - /// bound on the closure and the return type. This is slightly - /// more restrictive than necessary, but it's the most fitting solution available in stable - /// Rust. In the future this bound may be relaxed by using an "auto-trait" instead, if - /// [auto-traits] ever become a stable feature of the Rust language. + /// Only types that implement [`Ungil`] can cross the closure. See the + /// [module level documentation](self) for more information. /// /// 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 @@ -365,8 +447,8 @@ impl<'py> Python<'py> { /// [Parallelism]: https://pyo3.rs/main/parallelism.html pub fn allow_threads(self, f: F) -> T where - F: Send + FnOnce() -> T, - T: Send, + F: Ungil + FnOnce() -> T, + T: Ungil, { // Use a guard pattern to handle reacquiring the GIL, so that the GIL will be reacquired // even if `f` panics. @@ -532,7 +614,7 @@ impl<'py> Python<'py> { /// # use pyo3::Python; /// Python::with_gil(|py| { /// // The full string could be, for example: - /// // "3.0a5+ (py3k:63103M, May 12 2008, 00:53:55) \n[GCC 4.2.3]" + /// // "3.10.0 (tags/v3.10.0:b494f59, Oct 4 2021, 19:00:18) [MSC v.1929 64 bit (AMD64)]" /// assert!(py.version().starts_with("3.")); /// }); /// ``` @@ -563,7 +645,7 @@ impl<'py> Python<'py> { // version number. let version_number_str = version_str.split(' ').next().unwrap_or(version_str); - PythonVersionInfo::from_str(version_number_str) + PythonVersionInfo::from_str(version_number_str).unwrap() } /// Registers the object in the release pool, and tries to downcast to specific type. @@ -697,7 +779,7 @@ impl<'py> Python<'py> { /// /// # fn main(){ /// #[pyfunction] - /// fn loop_forever(py: Python) -> PyResult<()> { + /// fn loop_forever(py: Python<'_>) -> PyResult<()> { /// loop { /// // As this loop is infinite it should check for signals every once in a while. /// // Using `?` causes any `PyErr` (potentially containing `KeyboardInterrupt`) @@ -724,22 +806,6 @@ impl<'py> Python<'py> { err::error_on_minusone(self, v) } - /// Retrieves a Python instance under the assumption that the GIL is already - /// acquired at this point, and stays acquired for the lifetime `'py`. - /// - /// Because the output lifetime `'py` is not connected to any input parameter, - /// care must be taken that the compiler infers an appropriate lifetime for `'py` - /// when calling this function. - /// - /// # Safety - /// - /// The lifetime `'py` must be shorter than the period you *assume* that you have GIL. - /// I.e., `Python<'static>` is always *really* unsafe. - #[inline] - pub unsafe fn assume_gil_acquired() -> Python<'py> { - Python(PhantomData) - } - /// Create a new pool for managing PyO3's owned references. /// /// When this `GILPool` is dropped, all PyO3 owned references created after this `GILPool` will @@ -800,10 +866,35 @@ impl<'py> Python<'py> { } } +impl<'unbound> Python<'unbound> { + /// Unsafely creates a Python token with an unbounded lifetime. + /// + /// Many of PyO3 APIs use `Python<'_>` as proof that the GIL is held, but this function can be + /// used to call them unsafely. + /// + /// # Safety + /// + /// - This token and any borrowed Python references derived from it can only be safely used + /// whilst the currently executing thread is actually holding the GIL. + /// - This function creates a token with an *unbounded* lifetime. Safe code can assume that + /// holding a `Python<'py>` token means the GIL is and stays acquired for the lifetime `'py`. + /// If you let it or borrowed Python references escape to safe code you are + /// responsible for bounding the lifetime `'unbound` appropriately. For more on unbounded + /// lifetimes, see the [nomicon]. + /// + /// [nomicon]: https://doc.rust-lang.org/nomicon/unbounded-lifetimes.html + #[inline] + pub unsafe fn assume_gil_acquired() -> Python<'unbound> { + Python(PhantomData) + } +} + #[cfg(test)] mod tests { use super::*; use crate::types::{IntoPyDict, PyList}; + use crate::Py; + use std::sync::Arc; #[test] fn test_eval() { @@ -889,36 +980,19 @@ mod tests { } #[test] - fn test_python_version_info() { - Python::with_gil(|py| { - let version = py.version_info(); - #[cfg(Py_3_7)] - assert!(version >= (3, 7)); - #[cfg(Py_3_7)] - assert!(version >= (3, 7, 0)); - #[cfg(Py_3_8)] - assert!(version >= (3, 8)); - #[cfg(Py_3_8)] - assert!(version >= (3, 8, 0)); - #[cfg(Py_3_9)] - assert!(version >= (3, 9)); - #[cfg(Py_3_9)] - assert!(version >= (3, 9, 0)); + 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 mut v = vec![1, 2, 3]; + let a = Arc::new(String::from("foo")); - #[test] - fn test_python_version_info_parse() { - assert!(PythonVersionInfo::from_str("3.5.0a1") >= (3, 5, 0)); - assert!(PythonVersionInfo::from_str("3.5+") >= (3, 5, 0)); - assert!(PythonVersionInfo::from_str("3.5+") == (3, 5, 0)); - assert!(PythonVersionInfo::from_str("3.5+") != (3, 5, 1)); - assert!(PythonVersionInfo::from_str("3.5.2a1+") < (3, 5, 3)); - assert!(PythonVersionInfo::from_str("3.5.2a1+") == (3, 5, 2)); - assert!(PythonVersionInfo::from_str("3.5.2a1+") == (3, 5)); - assert!(PythonVersionInfo::from_str("3.5+") == (3, 5)); - assert!(PythonVersionInfo::from_str("3.5.2a1+") < (3, 6)); - assert!(PythonVersionInfo::from_str("3.5.2a1+") > (3, 4)); + Python::with_gil(|py| { + py.allow_threads(|| { + drop((list, &mut v, a)); + }); + }); } #[test] diff --git a/src/marshal.rs b/src/marshal.rs index 1f089dfb..343b861d 100644 --- a/src/marshal.rs +++ b/src/marshal.rs @@ -72,7 +72,7 @@ mod tests { }); } - fn equal(_py: Python, a: &impl AsPyPointer, b: &impl AsPyPointer) -> bool { + fn equal(_py: Python<'_>, a: &impl AsPyPointer, b: &impl AsPyPointer) -> bool { unsafe { ffi::PyObject_RichCompareBool(a.as_ptr(), b.as_ptr(), ffi::Py_EQ) != 0 } } } diff --git a/src/once_cell.rs b/src/once_cell.rs index e28a2227..739864c5 100644 --- a/src/once_cell.rs +++ b/src/once_cell.rs @@ -21,7 +21,7 @@ use std::cell::UnsafeCell; /// /// static LIST_CELL: GILOnceCell> = GILOnceCell::new(); /// -/// pub fn get_shared_list(py: Python) -> &PyList { +/// pub fn get_shared_list(py: Python<'_>) -> &PyList { /// LIST_CELL /// .get_or_init(py, || PyList::empty(py).into()) /// .as_ref(py) @@ -42,7 +42,7 @@ impl GILOnceCell { } /// Get a reference to the contained value, or `None` if the cell has not yet been written. - pub fn get(&self, _py: Python) -> Option<&T> { + pub fn get(&self, _py: Python<'_>) -> Option<&T> { // Safe because if the cell has not yet been written, None is returned. unsafe { &*self.0.get() }.as_ref() } @@ -61,7 +61,7 @@ impl GILOnceCell { /// exactly once, even if multiple threads attempt to call `get_or_init` /// 4) if f() can panic but still does not release the GIL, it may be called multiple times, /// but it is guaranteed that f() will never be called concurrently - pub fn get_or_init(&self, py: Python, f: F) -> &T + pub fn get_or_init(&self, py: Python<'_>, f: F) -> &T where F: FnOnce() -> T, { @@ -90,7 +90,7 @@ impl GILOnceCell { /// /// If the cell has already been written, `Err(value)` will be returned containing the new /// value which was not written. - pub fn set(&self, _py: Python, value: T) -> Result<(), T> { + pub fn set(&self, _py: Python<'_>, value: T) -> Result<(), T> { // Safe because GIL is held, so no other thread can be writing to this cell concurrently. let inner = unsafe { &mut *self.0.get() }; if inner.is_some() { diff --git a/src/prelude.rs b/src/prelude.rs index b4dcaedf..d7b65902 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -16,9 +16,9 @@ pub use crate::conversion::{ pub use crate::err::{PyErr, PyResult}; pub use crate::gil::GILGuard; pub use crate::instance::{Py, PyObject}; +pub use crate::marker::Python; pub use crate::pycell::{PyCell, PyRef, PyRefMut}; pub use crate::pyclass_init::PyClassInitializer; -pub use crate::python::Python; pub use crate::types::{PyAny, PyModule}; #[cfg(feature = "macros")] diff --git a/src/pycell.rs b/src/pycell.rs index 863e3d92..8c8569f3 100644 --- a/src/pycell.rs +++ b/src/pycell.rs @@ -107,12 +107,11 @@ //! fn swap_numbers(a: &mut Number, b: &mut Number) { //! std::mem::swap(&mut a.inner, &mut b.inner); //! } -//! # use pyo3::AsPyPointer; //! # fn main() { //! # Python::with_gil(|py|{ //! # let n = Py::new(py, Number{inner: 35}).unwrap(); //! # let n2 = n.clone_ref(py); -//! # assert_eq!(n.as_ptr(), n2.as_ptr()); +//! # assert!(n.is(&n2)); //! # let fun = pyo3::wrap_pyfunction!(swap_numbers, py).unwrap(); //! # fun.call1((n, n2)).expect_err("Managed to create overlapping mutable references. Note: this is undefined behaviour."); //! # }); @@ -138,19 +137,18 @@ //! #[pyfunction] //! fn swap_numbers(a: &PyCell, b: &PyCell) { //! // Check that the pointers are unequal -//! if a.as_ptr() != b.as_ptr() { +//! if !a.is(b) { //! std::mem::swap(&mut a.borrow_mut().inner, &mut b.borrow_mut().inner); //! } else { //! // Do nothing - they are the same object, so don't need swapping. //! } //! } -//! # use pyo3::AsPyPointer; //! # fn main() { //! # // With duplicate numbers //! # Python::with_gil(|py|{ //! # let n = Py::new(py, Number{inner: 35}).unwrap(); //! # let n2 = n.clone_ref(py); -//! # assert_eq!(n.as_ptr(), n2.as_ptr()); +//! # assert!(n.is(&n2)); //! # let fun = pyo3::wrap_pyfunction!(swap_numbers, py).unwrap(); //! # fun.call1((n, n2)).unwrap(); //! # }); @@ -159,7 +157,7 @@ //! # Python::with_gil(|py|{ //! # let n = Py::new(py, Number{inner: 35}).unwrap(); //! # let n2 = Py::new(py, Number{inner: 42}).unwrap(); -//! # assert_ne!(n.as_ptr(), n2.as_ptr()); +//! # assert!(!n.is(&n2)); //! # let fun = pyo3::wrap_pyfunction!(swap_numbers, py).unwrap(); //! # fun.call1((&n, &n2)).unwrap(); //! # let n: u32 = n.borrow(py).inner; @@ -357,7 +355,7 @@ impl PyCell { /// /// 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. - pub fn new(py: Python, value: impl Into>) -> PyResult<&Self> { + pub fn new(py: Python<'_>, value: impl Into>) -> PyResult<&Self> { unsafe { let initializer = value.into(); let self_ = initializer.create_cell(py)?; @@ -671,7 +669,7 @@ impl fmt::Debug for PyCell { /// (Child { name: "Caterpillar" }, Parent { basename: "Butterfly" }) /// } /// -/// fn format(slf: PyRef) -> String { +/// fn format(slf: PyRef<'_, Self>) -> String { /// // We can get *mut ffi::PyObject from PyRef /// use pyo3::AsPyPointer; /// let refcnt = unsafe { pyo3::ffi::Py_REFCNT(slf.as_ptr()) }; @@ -693,7 +691,7 @@ pub struct PyRef<'p, T: PyClass> { impl<'p, T: PyClass> PyRef<'p, T> { /// Returns a `Python` token that is bound to the lifetime of the `PyRef`. - pub fn py(&self) -> Python { + pub fn py(&self) -> Python<'_> { unsafe { Python::assume_gil_acquired() } } } @@ -747,7 +745,7 @@ where /// .add_subclass(Base2 { name2: "base2" }) /// .add_subclass(Self { name3: "sub" }) /// } - /// fn name(slf: PyRef) -> String { + /// fn name(slf: PyRef<'_, Self>) -> String { /// let subname = slf.name3; /// let super_ = slf.into_super(); /// format!("{} {} {}", super_.as_ref().name1, super_.name2, subname) @@ -816,7 +814,7 @@ pub struct PyRefMut<'p, T: MutablePyClass> { impl<'p, T: MutablePyClass> PyRefMut<'p, T> { /// Returns a `Python` token that is bound to the lifetime of the `PyRefMut`. - pub fn py(&self) -> Python { + pub fn py(&self) -> Python<'_> { unsafe { Python::assume_gil_acquired() } } } @@ -880,8 +878,8 @@ impl<'p, T: MutablePyClass> Drop for PyRefMut<'p, T> { } } -impl IntoPy for PyRefMut<'_, T> { - fn into_py(self, py: Python) -> PyObject { +impl IntoPy for PyRefMut<'_, T> { + fn into_py(self, py: Python<'_>) -> PyObject { unsafe { PyObject::from_borrowed_ptr(py, self.inner.as_ptr()) } } } @@ -980,7 +978,7 @@ where /// # 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(slf: *mut ffi::PyObject, py: Python); + unsafe fn tp_dealloc(slf: *mut ffi::PyObject, py: Python<'_>); } impl PyCellLayout for PyCellBase @@ -992,8 +990,7 @@ where fn borrow_checker(&self) -> &M { &self.borrow_impl } - - unsafe fn tp_dealloc(slf: *mut ffi::PyObject, py: Python) { + unsafe fn tp_dealloc(slf: *mut ffi::PyObject, py: Python<'_>) { // For `#[pyclass]` types which inherit from PyAny, we can just call tp_free if T::type_object_raw(py) == &mut PyBaseObject_Type { return get_tp_free(ffi::Py_TYPE(slf))(slf as _); @@ -1023,7 +1020,7 @@ where self.contents.thread_checker.ensure(); self.ob_base.borrow_checker() } - unsafe fn tp_dealloc(slf: *mut ffi::PyObject, py: Python) { + unsafe fn tp_dealloc(slf: *mut ffi::PyObject, py: Python<'_>) { // Safety: Python only calls tp_dealloc when no references to the object remain. let cell = &mut *(slf as *mut PyCell); ManuallyDrop::drop(&mut cell.contents.value); diff --git a/src/pyclass.rs b/src/pyclass.rs index abe3e5ef..59302b52 100644 --- a/src/pyclass.rs +++ b/src/pyclass.rs @@ -2,10 +2,11 @@ use crate::pycell::{Immutable, Mutable}; use crate::{ callback::IntoPyCallbackOutput, + exceptions::PyTypeError, ffi, impl_::pyclass::{ - assign_sequence_item_from_mapping, fallback_new, get_sequence_item_from_mapping, - tp_dealloc, PyClassDict, PyClassImpl, PyClassItems, PyClassWeakRef, + assign_sequence_item_from_mapping, get_sequence_item_from_mapping, tp_dealloc, PyClassDict, + PyClassImpl, PyClassItems, PyClassWeakRef, }, IntoPy, IntoPyPointer, PyCell, PyErr, PyMethodDefType, PyNativeType, PyObject, PyResult, PyTypeInfo, Python, @@ -46,7 +47,7 @@ fn into_raw(vec: Vec) -> *mut c_void { Box::into_raw(vec.into_boxed_slice()) as _ } -pub(crate) fn create_type_object(py: Python) -> *mut ffi::PyTypeObject +pub(crate) fn create_type_object(py: Python<'_>) -> *mut ffi::PyTypeObject where T: PyClass, { @@ -58,14 +59,10 @@ where T::NAME, T::BaseType::type_object_raw(py), std::mem::size_of::(), - T::get_new(), tp_dealloc::, - T::get_alloc(), - T::get_free(), T::dict_offset(), T::weaklist_offset(), &T::for_all_items, - T::IS_GC, T::IS_BASETYPE, ) } { @@ -76,20 +73,16 @@ where #[allow(clippy::too_many_arguments)] unsafe fn create_type_object_impl( - py: Python, + py: Python<'_>, tp_doc: &str, module_name: Option<&str>, name: &str, base_type_object: *mut ffi::PyTypeObject, basicsize: usize, - tp_new: Option, tp_dealloc: ffi::destructor, - tp_alloc: Option, - tp_free: Option, dict_offset: Option, weaklist_offset: Option, for_all_items: &dyn Fn(&mut dyn FnMut(&PyClassItems)), - is_gc: bool, is_basetype: bool, ) -> PyResult<*mut ffi::PyTypeObject> { let mut slots = Vec::new(); @@ -103,20 +96,8 @@ unsafe fn create_type_object_impl( push_slot(&mut slots, ffi::Py_tp_doc, doc as _); } - push_slot( - &mut slots, - ffi::Py_tp_new, - tp_new.unwrap_or(fallback_new) as _, - ); push_slot(&mut slots, ffi::Py_tp_dealloc, tp_dealloc as _); - if let Some(alloc) = tp_alloc { - push_slot(&mut slots, ffi::Py_tp_alloc, alloc as _); - } - if let Some(free) = tp_free { - push_slot(&mut slots, ffi::Py_tp_free, free as _); - } - #[cfg(Py_3_9)] { let members = py_class_members(dict_offset, weaklist_offset); @@ -141,9 +122,11 @@ unsafe fn create_type_object_impl( } // protocol methods + let mut has_new = false; let mut has_getitem = false; let mut has_setitem = false; - let mut has_gc_methods = false; + let mut has_traverse = false; + let mut has_clear = false; // Before Python 3.9, need to patch in buffer methods manually (they don't work in slots) #[cfg(all(not(Py_3_9), not(Py_LIMITED_API)))] @@ -151,19 +134,23 @@ unsafe fn create_type_object_impl( for_all_items(&mut |items| { for slot in items.slots { - has_getitem |= slot.slot == ffi::Py_mp_subscript; - has_setitem |= slot.slot == ffi::Py_mp_ass_subscript; - has_gc_methods |= slot.slot == ffi::Py_tp_clear || slot.slot == ffi::Py_tp_traverse; - #[cfg(all(not(Py_3_9), not(Py_LIMITED_API)))] - if slot.slot == ffi::Py_bf_getbuffer { - // Safety: slot.pfunc is a valid function pointer - buffer_procs.bf_getbuffer = Some(std::mem::transmute(slot.pfunc)); - } - - #[cfg(all(not(Py_3_9), not(Py_LIMITED_API)))] - if slot.slot == ffi::Py_bf_releasebuffer { - // Safety: slot.pfunc is a valid function pointer - buffer_procs.bf_releasebuffer = Some(std::mem::transmute(slot.pfunc)); + match slot.slot { + ffi::Py_tp_new => has_new = true, + ffi::Py_mp_subscript => has_getitem = true, + ffi::Py_mp_ass_subscript => has_setitem = true, + ffi::Py_tp_traverse => has_traverse = true, + ffi::Py_tp_clear => has_clear = true, + #[cfg(all(not(Py_3_9), not(Py_LIMITED_API)))] + ffi::Py_bf_getbuffer => { + // Safety: slot.pfunc is a valid function pointer + buffer_procs.bf_getbuffer = Some(std::mem::transmute(slot.pfunc)); + } + #[cfg(all(not(Py_3_9), not(Py_LIMITED_API)))] + ffi::Py_bf_releasebuffer => { + // Safety: slot.pfunc is a valid function pointer + buffer_procs.bf_releasebuffer = Some(std::mem::transmute(slot.pfunc)); + } + _ => {} } } slots.extend_from_slice(items.slots); @@ -191,6 +178,17 @@ unsafe fn create_type_object_impl( ); } + if !has_new { + push_slot(&mut slots, ffi::Py_tp_new, no_constructor_defined as _); + } + + if has_clear && !has_traverse { + return Err(PyTypeError::new_err(format!( + "`#[pyclass]` {} implements __clear__ without __traverse__", + name + ))); + } + // Add empty sentinel at the end push_slot(&mut slots, 0, ptr::null_mut()); @@ -198,7 +196,7 @@ unsafe fn create_type_object_impl( name: py_class_qualified_name(module_name, name)?, basicsize: basicsize as c_int, itemsize: 0, - flags: py_class_flags(has_gc_methods, is_gc, is_basetype), + flags: py_class_flags(has_traverse, is_basetype), slots: slots.as_mut_ptr(), }; @@ -221,7 +219,7 @@ unsafe fn create_type_object_impl( } #[cold] -fn type_object_creation_failed(py: Python, e: PyErr, name: &'static str) -> ! { +fn type_object_creation_failed(py: Python<'_>, e: PyErr, name: &'static str) -> ! { e.print(py); panic!("An error occurred while initializing class {}", name) } @@ -229,7 +227,7 @@ fn type_object_creation_failed(py: Python, e: PyErr, name: &'static str) -> ! { /// Additional type initializations necessary before Python 3.10 #[cfg(all(not(Py_LIMITED_API), not(Py_3_10)))] unsafe fn tp_init_additional( - type_object: *mut ffi::PyTypeObject, + _type_object: *mut ffi::PyTypeObject, _tp_doc: &str, #[cfg(not(Py_3_9))] buffer_procs: &ffi::PyBufferProcs, #[cfg(not(Py_3_9))] dict_offset: Option, @@ -247,10 +245,10 @@ unsafe fn tp_init_additional( // heap-types, and it removed the text_signature value from it. // We go in after the fact and replace tp_doc with something // that _does_ include the text_signature value! - ffi::PyObject_Free((*type_object).tp_doc as _); + ffi::PyObject_Free((*_type_object).tp_doc as _); let data = ffi::PyObject_Malloc(_tp_doc.len()); data.copy_from(_tp_doc.as_ptr() as _, _tp_doc.len()); - (*type_object).tp_doc = data as _; + (*_type_object).tp_doc = data as _; } } @@ -258,15 +256,15 @@ unsafe fn tp_init_additional( // Python 3.9, so on older versions we must manually fixup the type object. #[cfg(not(Py_3_9))] { - (*(*type_object).tp_as_buffer).bf_getbuffer = buffer_procs.bf_getbuffer; - (*(*type_object).tp_as_buffer).bf_releasebuffer = buffer_procs.bf_releasebuffer; + (*(*_type_object).tp_as_buffer).bf_getbuffer = buffer_procs.bf_getbuffer; + (*(*_type_object).tp_as_buffer).bf_releasebuffer = buffer_procs.bf_releasebuffer; if let Some(dict_offset) = dict_offset { - (*type_object).tp_dictoffset = dict_offset; + (*_type_object).tp_dictoffset = dict_offset; } if let Some(weaklist_offset) = weaklist_offset { - (*type_object).tp_weaklistoffset = weaklist_offset; + (*_type_object).tp_weaklistoffset = weaklist_offset; } } } @@ -308,12 +306,13 @@ fn py_class_qualified_name(module_name: Option<&str>, class_name: &str) -> PyRes .into_raw()) } -fn py_class_flags(has_gc_methods: bool, is_gc: bool, is_basetype: bool) -> c_uint { - let mut flags = if has_gc_methods || is_gc { - ffi::Py_TPFLAGS_DEFAULT | ffi::Py_TPFLAGS_HAVE_GC - } else { - ffi::Py_TPFLAGS_DEFAULT - }; +fn py_class_flags(is_gc: bool, is_basetype: bool) -> c_uint { + let mut flags = ffi::Py_TPFLAGS_DEFAULT; + + if is_gc { + flags |= ffi::Py_TPFLAGS_HAVE_GC; + } + if is_basetype { flags |= ffi::Py_TPFLAGS_BASETYPE; } @@ -489,7 +488,7 @@ pub enum IterNextOutput { pub type PyIterNextOutput = IterNextOutput; impl IntoPyCallbackOutput<*mut ffi::PyObject> for PyIterNextOutput { - fn convert(self, _py: Python) -> PyResult<*mut ffi::PyObject> { + fn convert(self, _py: Python<'_>) -> PyResult<*mut ffi::PyObject> { match self { IterNextOutput::Yield(o) => Ok(o.into_ptr()), IterNextOutput::Return(opt) => Err(crate::exceptions::PyStopIteration::new_err((opt,))), @@ -502,7 +501,7 @@ where T: IntoPy, U: IntoPy, { - fn convert(self, py: Python) -> PyResult { + fn convert(self, py: Python<'_>) -> PyResult { match self { IterNextOutput::Yield(o) => Ok(IterNextOutput::Yield(o.into_py(py))), IterNextOutput::Return(o) => Ok(IterNextOutput::Return(o.into_py(py))), @@ -514,7 +513,7 @@ impl IntoPyCallbackOutput for Option where T: IntoPy, { - fn convert(self, py: Python) -> PyResult { + fn convert(self, py: Python<'_>) -> PyResult { match self { Some(o) => Ok(PyIterNextOutput::Yield(o.into_py(py))), None => Ok(PyIterNextOutput::Return(py.None())), @@ -536,7 +535,7 @@ pub enum IterANextOutput { pub type PyIterANextOutput = IterANextOutput; impl IntoPyCallbackOutput<*mut ffi::PyObject> for PyIterANextOutput { - fn convert(self, _py: Python) -> PyResult<*mut ffi::PyObject> { + fn convert(self, _py: Python<'_>) -> PyResult<*mut ffi::PyObject> { match self { IterANextOutput::Yield(o) => Ok(o.into_ptr()), IterANextOutput::Return(opt) => { @@ -551,7 +550,7 @@ where T: IntoPy, U: IntoPy, { - fn convert(self, py: Python) -> PyResult { + fn convert(self, py: Python<'_>) -> PyResult { match self { IterANextOutput::Yield(o) => Ok(IterANextOutput::Yield(o.into_py(py))), IterANextOutput::Return(o) => Ok(IterANextOutput::Return(o.into_py(py))), @@ -563,10 +562,23 @@ impl IntoPyCallbackOutput for Option where T: IntoPy, { - fn convert(self, py: Python) -> PyResult { + fn convert(self, py: Python<'_>) -> PyResult { match self { Some(o) => Ok(PyIterANextOutput::Yield(o.into_py(py))), None => Ok(PyIterANextOutput::Return(py.None())), } } } + +/// Default new implementation +pub(crate) unsafe extern "C" fn no_constructor_defined( + _subtype: *mut ffi::PyTypeObject, + _args: *mut ffi::PyObject, + _kwds: *mut ffi::PyObject, +) -> *mut ffi::PyObject { + crate::callback_body!(py, { + Err::<(), _>(crate::exceptions::PyTypeError::new_err( + "No constructor defined", + )) + }) +} diff --git a/src/pyclass_init.rs b/src/pyclass_init.rs index 0cfaf84b..807a9a22 100644 --- a/src/pyclass_init.rs +++ b/src/pyclass_init.rs @@ -24,7 +24,7 @@ pub trait PyObjectInit: Sized { /// - `subtype` must be a valid pointer to a type object of T or a subclass. unsafe fn into_new_object( self, - py: Python, + py: Python<'_>, subtype: *mut PyTypeObject, ) -> PyResult<*mut ffi::PyObject>; private_decl! {} @@ -36,7 +36,7 @@ pub struct PyNativeTypeInitializer(PhantomData); impl PyObjectInit for PyNativeTypeInitializer { unsafe fn into_new_object( self, - py: Python, + py: Python<'_>, subtype: *mut PyTypeObject, ) -> PyResult<*mut ffi::PyObject> { let type_object = T::type_object_raw(py); @@ -202,7 +202,7 @@ impl PyClassInitializer { /// Creates a new PyCell and initializes it. #[doc(hidden)] - pub fn create_cell(self, py: Python) -> PyResult<*mut PyCell> + pub fn create_cell(self, py: Python<'_>) -> PyResult<*mut PyCell> where T: PyClass, { @@ -217,7 +217,7 @@ impl PyClassInitializer { #[doc(hidden)] pub unsafe fn create_cell_from_subtype( self, - py: Python, + py: Python<'_>, subtype: *mut crate::ffi::PyTypeObject, ) -> PyResult<*mut PyCell> where @@ -230,7 +230,7 @@ impl PyClassInitializer { impl PyObjectInit for PyClassInitializer { unsafe fn into_new_object( self, - py: Python, + py: Python<'_>, subtype: *mut PyTypeObject, ) -> PyResult<*mut ffi::PyObject> { /// Layout of a PyCellBase after base new has been called, but the borrow flag has not @@ -304,7 +304,7 @@ where U: Into>, { #[inline] - fn convert(self, _py: Python) -> PyResult> { + fn convert(self, _py: Python<'_>) -> PyResult> { Ok(self.into()) } } diff --git a/src/test_hygiene/pyfunction.rs b/src/test_hygiene/pyfunction.rs index 9a7d515e..19fe2739 100644 --- a/src/test_hygiene/pyfunction.rs +++ b/src/test_hygiene/pyfunction.rs @@ -1,5 +1,5 @@ #![no_implicit_prelude] -#![allow(unused_variables)] +#![allow(unused_variables, clippy::unnecessary_wraps)] #[crate::pyfunction] #[pyo3(crate = "crate")] diff --git a/src/test_hygiene/pymethods.rs b/src/test_hygiene/pymethods.rs index e4611228..79fd5a3d 100644 --- a/src/test_hygiene/pymethods.rs +++ b/src/test_hygiene/pymethods.rs @@ -1,5 +1,5 @@ #![no_implicit_prelude] -#![allow(unused_variables)] +#![allow(unused_variables, clippy::unnecessary_wraps)] #[crate::pyclass] #[pyo3(crate = "crate")] @@ -114,7 +114,7 @@ impl Dummy { fn __delitem__(&self, key: u32) {} - fn __iter__(_: crate::pycell::PyRef, py: crate::Python) -> crate::Py { + fn __iter__(_: crate::pycell::PyRef<'_, Self>, py: crate::Python<'_>) -> crate::Py { crate::Py::new(py, DummyIter {}).unwrap() } @@ -122,7 +122,10 @@ impl Dummy { ::std::option::Option::None } - fn __reversed__(slf: crate::pycell::PyRef, py: crate::Python) -> crate::Py { + fn __reversed__( + slf: crate::pycell::PyRef<'_, Self>, + py: crate::Python<'_>, + ) -> crate::Py { crate::Py::new(py, DummyIter {}).unwrap() } @@ -262,19 +265,19 @@ impl Dummy { fn __ior__(&mut self, other: &Self) {} - fn __neg__(slf: crate::pycell::PyRef) -> crate::pycell::PyRef { + fn __neg__(slf: crate::pycell::PyRef<'_, Self>) -> crate::pycell::PyRef<'_, Self> { slf } - fn __pos__(slf: crate::pycell::PyRef) -> crate::pycell::PyRef { + fn __pos__(slf: crate::pycell::PyRef<'_, Self>) -> crate::pycell::PyRef<'_, Self> { slf } - fn __abs__(slf: crate::pycell::PyRef) -> crate::pycell::PyRef { + fn __abs__(slf: crate::pycell::PyRef<'_, Self>) -> crate::pycell::PyRef<'_, Self> { slf } - fn __invert__(slf: crate::pycell::PyRef) -> crate::pycell::PyRef { + fn __invert__(slf: crate::pycell::PyRef<'_, Self>) -> crate::pycell::PyRef<'_, Self> { slf } @@ -328,7 +331,7 @@ impl Dummy { // Awaitable Objects ////////////////////// - fn __await__(slf: crate::pycell::PyRef) -> crate::pycell::PyRef { + fn __await__(slf: crate::pycell::PyRef<'_, Self>) -> crate::pycell::PyRef<'_, Self> { slf } @@ -337,7 +340,10 @@ impl Dummy { // Asynchronous Iterators ////////////////////// - fn __aiter__(slf: crate::pycell::PyRef, py: crate::Python) -> crate::Py { + fn __aiter__( + slf: crate::pycell::PyRef<'_, Self>, + py: crate::Python<'_>, + ) -> crate::Py { crate::Py::new(py, DummyIter {}).unwrap() } @@ -508,7 +514,7 @@ impl Dummy { fn __delitem__(&self, key: u32) {} - fn __iter__(_: crate::pycell::PyRef, py: crate::Python) -> crate::Py { + fn __iter__(_: crate::pycell::PyRef<'_, Self>, py: crate::Python<'_>) -> crate::Py { crate::Py::new(py, DummyIter {}).unwrap() } @@ -516,7 +522,10 @@ impl Dummy { ::std::option::Option::None } - fn __reversed__(slf: crate::pycell::PyRef, py: crate::Python) -> crate::Py { + fn __reversed__( + slf: crate::pycell::PyRef<'_, Self>, + py: crate::Python<'_>, + ) -> crate::Py { crate::Py::new(py, DummyIter {}).unwrap() } @@ -655,19 +664,19 @@ impl Dummy { fn __ior__(&mut self, other: &Self) {} - fn __neg__(slf: crate::pycell::PyRef) -> crate::pycell::PyRef { + fn __neg__(slf: crate::pycell::PyRef<'_, Self>) -> crate::pycell::PyRef<'_, Self> { slf } - fn __pos__(slf: crate::pycell::PyRef) -> crate::pycell::PyRef { + fn __pos__(slf: crate::pycell::PyRef<'_, Self>) -> crate::pycell::PyRef<'_, Self> { slf } - fn __abs__(slf: crate::pycell::PyRef) -> crate::pycell::PyRef { + fn __abs__(slf: crate::pycell::PyRef<'_, Self>) -> crate::pycell::PyRef<'_, Self> { slf } - fn __invert__(slf: crate::pycell::PyRef) -> crate::pycell::PyRef { + fn __invert__(slf: crate::pycell::PyRef<'_, Self>) -> crate::pycell::PyRef<'_, Self> { slf } @@ -721,7 +730,7 @@ impl Dummy { // Awaitable Objects ////////////////////// - fn __await__(slf: crate::pycell::PyRef) -> crate::pycell::PyRef { + fn __await__(slf: crate::pycell::PyRef<'_, Self>) -> crate::pycell::PyRef<'_, Self> { slf } @@ -730,7 +739,10 @@ impl Dummy { // Asynchronous Iterators ////////////////////// - fn __aiter__(slf: crate::pycell::PyRef, py: crate::Python) -> crate::Py { + fn __aiter__( + slf: crate::pycell::PyRef<'_, Self>, + py: crate::Python<'_>, + ) -> crate::Py { crate::Py::new(py, DummyIter {}).unwrap() } diff --git a/src/test_hygiene/pymodule.rs b/src/test_hygiene/pymodule.rs index 478cea3a..0b37c440 100644 --- a/src/test_hygiene/pymodule.rs +++ b/src/test_hygiene/pymodule.rs @@ -1,5 +1,5 @@ #![no_implicit_prelude] -#![allow(unused_variables)] +#![allow(unused_variables, clippy::unnecessary_wraps)] #[crate::pyfunction] #[pyo3(crate = "crate")] @@ -9,13 +9,13 @@ fn do_something(x: i32) -> crate::PyResult { #[crate::pymodule] #[pyo3(crate = "crate")] -fn foo(_py: crate::Python, _m: &crate::types::PyModule) -> crate::PyResult<()> { +fn foo(_py: crate::Python<'_>, _m: &crate::types::PyModule) -> crate::PyResult<()> { ::std::result::Result::Ok(()) } #[crate::pymodule] #[pyo3(crate = "crate")] -fn my_module(_py: crate::Python, m: &crate::types::PyModule) -> crate::PyResult<()> { +fn my_module(_py: crate::Python<'_>, m: &crate::types::PyModule) -> crate::PyResult<()> { m.add_function(crate::wrap_pyfunction!(do_something, m)?)?; m.add_wrapped(crate::wrap_pymodule!(foo))?; diff --git a/src/type_object.rs b/src/type_object.rs index 1022a9d8..b8c2d654 100644 --- a/src/type_object.rs +++ b/src/type_object.rs @@ -52,7 +52,7 @@ pub unsafe trait PyTypeInfo: Sized { type AsRefTarget: PyNativeType; /// PyTypeObject instance for this type. - fn type_object_raw(py: Python) -> *mut ffi::PyTypeObject; + fn type_object_raw(py: Python<'_>) -> *mut ffi::PyTypeObject; /// Checks if `object` is an instance of this type or a subclass of this type. fn is_type_of(object: &PyAny) -> bool { @@ -75,14 +75,14 @@ pub unsafe trait PyTypeInfo: Sized { /// See also [PyTypeInfo::type_object_raw](trait.PyTypeInfo.html#tymethod.type_object_raw). pub unsafe trait PyTypeObject { /// Returns the safe abstraction over the type object. - fn type_object(py: Python) -> &PyType; + fn type_object(py: Python<'_>) -> &PyType; } unsafe impl PyTypeObject for T where T: PyTypeInfo, { - fn type_object(py: Python) -> &PyType { + fn type_object(py: Python<'_>) -> &PyType { unsafe { py.from_borrowed_ptr(Self::type_object_raw(py) as _) } } } @@ -107,7 +107,7 @@ impl LazyStaticType { } } - pub fn get_or_init(&self, py: Python) -> *mut ffi::PyTypeObject { + pub fn get_or_init(&self, py: Python<'_>) -> *mut ffi::PyTypeObject { let type_object = *self.value.get_or_init(py, || create_type_object::(py)); self.ensure_init(py, type_object, T::NAME, &T::for_all_items); type_object @@ -115,7 +115,7 @@ impl LazyStaticType { fn ensure_init( &self, - py: Python, + py: Python<'_>, type_object: *mut ffi::PyTypeObject, name: &str, for_all_items: &dyn Fn(&mut dyn FnMut(&PyClassItems)), @@ -188,7 +188,7 @@ impl LazyStaticType { } fn initialize_tp_dict( - py: Python, + py: Python<'_>, type_object: *mut ffi::PyObject, items: Vec<(&'static std::ffi::CStr, PyObject)>, ) -> PyResult<()> { diff --git a/src/types/any.rs b/src/types/any.rs index b54aa540..4e13b3e9 100644 --- a/src/types/any.rs +++ b/src/types/any.rs @@ -80,13 +80,22 @@ impl PyAny { /// assert!(any.downcast::().is_err()); /// }); /// ``` - pub fn downcast(&self) -> Result<&T, PyDowncastError> + pub fn downcast(&self) -> Result<&T, PyDowncastError<'_>> where for<'py> T: PyTryFrom<'py>, { ::try_from(self) } + /// Returns whether `self` and `other` point to the same object. To compare + /// the equality of two objects (the `==` operator), use [`eq`](PyAny::eq). + /// + /// This is equivalent to the Python expression `self is other`. + #[inline] + pub fn is(&self, other: &T) -> bool { + self.as_ptr() == other.as_ptr() + } + /// Determines whether this object has the given attribute. /// /// This is equivalent to the Python expression `hasattr(self, attr_name)`. @@ -259,6 +268,66 @@ impl PyAny { } } + /// Tests whether this object is less than another. + /// + /// This is equivalent to the Python expression `self < other`. + pub fn lt(&self, other: O) -> PyResult + where + O: ToPyObject, + { + self.rich_compare(other, CompareOp::Lt)?.is_true() + } + + /// Tests whether this object is less than or equal to another. + /// + /// This is equivalent to the Python expression `self <= other`. + pub fn le(&self, other: O) -> PyResult + where + O: ToPyObject, + { + self.rich_compare(other, CompareOp::Le)?.is_true() + } + + /// Tests whether this object is equal to another. + /// + /// This is equivalent to the Python expression `self == other`. + pub fn eq(&self, other: O) -> PyResult + where + O: ToPyObject, + { + self.rich_compare(other, CompareOp::Eq)?.is_true() + } + + /// Tests whether this object is not equal to another. + /// + /// This is equivalent to the Python expression `self != other`. + pub fn ne(&self, other: O) -> PyResult + where + O: ToPyObject, + { + self.rich_compare(other, CompareOp::Ne)?.is_true() + } + + /// Tests whether this object is greater than another. + /// + /// This is equivalent to the Python expression `self > other`. + pub fn gt(&self, other: O) -> PyResult + where + O: ToPyObject, + { + self.rich_compare(other, CompareOp::Gt)?.is_true() + } + + /// Tests whether this object is greater than or equal to another. + /// + /// This is equivalent to the Python expression `self >= other`. + pub fn ge(&self, other: O) -> PyResult + where + O: ToPyObject, + { + self.rich_compare(other, CompareOp::Ge)?.is_true() + } + /// Determines whether this object appears callable. /// /// This is equivalent to Python's [`callable()`][1] function. @@ -333,7 +402,7 @@ impl PyAny { /// This is equivalent to the Python expression `help()`. pub fn call0(&self) -> PyResult<&PyAny> { cfg_if::cfg_if! { - if #[cfg(Py_3_9)] { + if #[cfg(all(Py_3_9, not(PyPy)))] { // Optimized path on python 3.9+ unsafe { self.py().from_owned_ptr_or_err(ffi::PyObject_CallNoArgs(self.as_ptr())) @@ -461,7 +530,7 @@ impl PyAny { /// ``` pub fn call_method0(&self, name: &str) -> PyResult<&PyAny> { cfg_if::cfg_if! { - if #[cfg(all(Py_3_9, not(Py_LIMITED_API)))] { + if #[cfg(all(Py_3_9, not(any(Py_LIMITED_API, PyPy))))] { // Optimized path on python 3.9+ unsafe { let name = name.into_py(self.py()); @@ -589,11 +658,11 @@ impl PyAny { /// Casts the PyObject to a concrete Python object type. /// /// This can cast only to native Python types, not types implemented in Rust. - pub fn cast_as<'a, D>(&'a self) -> Result<&'a D, PyDowncastError> + pub fn cast_as<'a, D>(&'a self) -> Result<&'a D, PyDowncastError<'_>> where D: PyTryFrom<'a>, { - D::try_from(self) + >::try_from(self) } /// Extracts some type from the Python object. @@ -711,7 +780,6 @@ mod tests { types::{IntoPyDict, PyList, PyLong, PyModule}, Python, ToPyObject, }; - #[test] fn test_call_for_non_existing_method() { Python::with_gil(|py| { @@ -834,4 +902,101 @@ class SimpleClass: assert!(bad_haystack.contains(&irrelevant_needle).is_err()); }); } + + // This is intentionally not a test, it's a generic function used by the tests below. + fn test_eq_methods_generic(list: &[T]) + where + T: PartialEq + PartialOrd + ToPyObject, + { + 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); + + assert_eq!( + a.lt(b), + a_py.lt(b_py).unwrap(), + "{} < {} should be {}.", + a_py, + b_py, + a.lt(b) + ); + assert_eq!( + a.le(b), + a_py.le(b_py).unwrap(), + "{} <= {} should be {}.", + a_py, + b_py, + a.le(b) + ); + assert_eq!( + a.eq(b), + a_py.eq(b_py).unwrap(), + "{} == {} should be {}.", + a_py, + b_py, + a.eq(b) + ); + assert_eq!( + a.ne(b), + a_py.ne(b_py).unwrap(), + "{} != {} should be {}.", + a_py, + b_py, + a.ne(b) + ); + assert_eq!( + a.gt(b), + a_py.gt(b_py).unwrap(), + "{} > {} should be {}.", + a_py, + b_py, + a.gt(b) + ); + assert_eq!( + a.ge(b), + a_py.ge(b_py).unwrap(), + "{} >= {} should be {}.", + a_py, + b_py, + a.ge(b) + ); + } + } + }); + } + + #[test] + fn test_eq_methods_integers() { + let ints = [-4, -4, 1, 2, 0, -100, 1_000_000]; + test_eq_methods_generic(&ints); + } + + #[test] + fn test_eq_methods_strings() { + let strings = ["Let's", "test", "some", "eq", "methods"]; + test_eq_methods_generic(&strings); + } + + #[test] + fn test_eq_methods_floats() { + let floats = [ + -1.0, + 2.5, + 0.0, + 3.0, + std::f64::consts::PI, + 10.0, + 10.0 / 3.0, + -1_000_000.0, + ]; + test_eq_methods_generic(&floats); + } + + #[test] + fn test_eq_methods_bools() { + let bools = [true, false]; + test_eq_methods_generic(&bools); + } } diff --git a/src/types/boolobject.rs b/src/types/boolobject.rs index 98fc233c..b7126bc0 100644 --- a/src/types/boolobject.rs +++ b/src/types/boolobject.rs @@ -13,7 +13,7 @@ pyobject_native_type!(PyBool, ffi::PyObject, ffi::PyBool_Type, #checkfunction=ff impl PyBool { /// Depending on `val`, returns `true` or `false`. #[inline] - pub fn new(py: Python, val: bool) -> &PyBool { + pub fn new(py: Python<'_>, val: bool) -> &PyBool { unsafe { py.from_borrowed_ptr(if val { ffi::Py_True() } else { ffi::Py_False() }) } } @@ -27,7 +27,7 @@ impl PyBool { /// Converts a Rust `bool` to a Python `bool`. impl ToPyObject for bool { #[inline] - fn to_object(&self, py: Python) -> PyObject { + fn to_object(&self, py: Python<'_>) -> PyObject { unsafe { PyObject::from_borrowed_ptr( py, @@ -43,7 +43,7 @@ impl ToPyObject for bool { impl IntoPy for bool { #[inline] - fn into_py(self, py: Python) -> PyObject { + fn into_py(self, py: Python<'_>) -> PyObject { PyBool::new(py, self).into() } } @@ -69,7 +69,7 @@ mod tests { assert!(PyBool::new(py, true).is_true()); let t: &PyAny = PyBool::new(py, true).into(); assert!(t.extract::().unwrap()); - assert_eq!(true.to_object(py), PyBool::new(py, true).into()); + assert!(true.to_object(py).is(PyBool::new(py, true))); }); } @@ -79,7 +79,7 @@ mod tests { assert!(!PyBool::new(py, false).is_true()); let t: &PyAny = PyBool::new(py, false).into(); assert!(!t.extract::().unwrap()); - assert_eq!(false.to_object(py), PyBool::new(py, false).into()); + assert!(false.to_object(py).is(PyBool::new(py, false))); }); } } diff --git a/src/types/bytearray.rs b/src/types/bytearray.rs index a63dd4db..1877a7c4 100644 --- a/src/types/bytearray.rs +++ b/src/types/bytearray.rs @@ -44,7 +44,7 @@ impl PyByteArray { /// }) /// # } /// ``` - pub fn new_with(py: Python, len: usize, init: F) -> PyResult<&PyByteArray> + pub fn new_with(py: Python<'_>, len: usize, init: F) -> PyResult<&PyByteArray> where F: FnOnce(&mut [u8]) -> PyResult<()>, { @@ -63,7 +63,7 @@ impl PyByteArray { } } - /// Creates a new Python bytearray object from another PyObject that + /// Creates a new Python `bytearray` object from another Python object that /// implements the buffer protocol. pub fn from<'p, I>(py: Python<'p>, src: &'p I) -> PyResult<&'p PyByteArray> where @@ -84,46 +84,119 @@ impl PyByteArray { self.len() == 0 } - /// Get the start of the buffer containing the contents of the bytearray. + /// Gets the start of the buffer containing the contents of the bytearray. /// - /// Note that this bytearray object is both shared and mutable, and the backing buffer may be - /// reallocated if the bytearray is resized. This can occur from Python code as well as from - /// Rust via [PyByteArray::resize]. + /// # Safety /// - /// As a result, the returned pointer should be dereferenced only if since calling this method - /// no Python code has executed, [PyByteArray::resize] has not been called. + /// See the safety requirements of [`PyByteArray::as_bytes`] and [`PyByteArray::as_bytes_mut`]. pub fn data(&self) -> *mut u8 { unsafe { ffi::PyByteArray_AsString(self.as_ptr()) as *mut u8 } } - /// Get the contents of this buffer as a slice. + /// Extracts a slice of the `ByteArray`'s entire buffer. /// /// # Safety - /// This bytearray must not be resized or edited while holding the slice. /// - /// ## Safety Detail - /// This method is equivalent to `std::slice::from_raw_parts(self.data(), self.len())`, and so - /// all the safety notes of `std::slice::from_raw_parts` apply here. + /// Mutation of the `bytearray` invalidates the slice. If it is used afterwards, the behavior is + /// undefined. /// - /// In particular, note that this bytearray object is both shared and mutable, and the backing - /// buffer may be reallocated if the bytearray is resized. Mutations can occur from Python - /// code as well as from Rust, via [PyByteArray::as_bytes_mut] and [PyByteArray::resize]. + /// These mutations may occur in Python code as well as from Rust: + /// - Calling methods like [`PyByteArray::as_bytes_mut`] and [`PyByteArray::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 + /// reference to the `bytearray` you cannot safely use the vast majority of PyO3's API whilst + /// using the slice. /// - /// Extreme care should be exercised when using this slice, as the Rust compiler will - /// make optimizations based on the assumption the contents of this slice cannot change. This - /// can easily lead to undefined behavior. + /// As a result, this slice should only be used for short-lived operations without executing any + /// Python code, such as copying into a Vec. /// - /// As a result, this slice should only be used for short-lived operations to read this - /// bytearray without executing any Python code, such as copying into a Vec. + /// # Examples + /// + /// ```rust + /// use pyo3::prelude::*; + /// use pyo3::exceptions::PyRuntimeError; + /// use pyo3::types::PyByteArray; + /// + /// #[pyfunction] + /// fn a_valid_function(bytes: &PyByteArray) -> PyResult<()> { + /// let section = { + /// // SAFETY: We promise to not let the interpreter regain control + /// // or invoke any PyO3 APIs while using the slice. + /// let slice = unsafe { bytes.as_bytes() }; + /// + /// // Copy only a section of `bytes` while avoiding + /// // `to_vec` which copies the entire thing. + /// let section = slice + /// .get(6..11) + /// .ok_or_else(|| PyRuntimeError::new_err("input is not long enough"))?; + /// Vec::from(section) + /// }; + /// + /// // Now we can do things with `section` and call PyO3 APIs again. + /// // ... + /// # assert_eq!(§ion, b"world"); + /// + /// Ok(()) + /// } + /// # fn main() -> PyResult<()> { + /// # Python::with_gil(|py| -> PyResult<()> { + /// # let fun = wrap_pyfunction!(a_valid_function, py)?; + /// # let locals = pyo3::types::PyDict::new(py); + /// # locals.set_item("a_valid_function", fun)?; + /// # + /// # py.run( + /// # r#"b = bytearray(b"hello world") + /// # a_valid_function(b) + /// # + /// # try: + /// # a_valid_function(bytearray()) + /// # except RuntimeError as e: + /// # assert str(e) == 'input is not long enough'"#, + /// # None, + /// # Some(locals), + /// # )?; + /// # + /// # Ok(()) + /// # }) + /// # } + /// ``` + /// + /// # Incorrect usage + /// + /// The following `bug` function is unsound ⚠️ + /// + /// ```rust + /// # use pyo3::prelude::*; + /// # use pyo3::types::PyByteArray; + /// + /// #[pyfunction] + /// fn bug(py: Python<'_>, bytes: &PyByteArray) { + /// let slice = unsafe { bytes.as_bytes() }; + /// + /// // This explicitly yields control back to the Python interpreter... + /// // ...but it's not always this obvious. Many things do this implicitly. + /// py.allow_threads(|| { + /// // Python code could be mutating through its handle to `bytes`, + /// // which makes reading it a data race, which is undefined behavior. + /// println!("{:?}", slice[0]); + /// }); + /// + /// // Python code might have mutated it, so we can not rely on the slice + /// // remaining valid. As such this is also undefined behavior. + /// println!("{:?}", slice[0]); + /// } pub unsafe fn as_bytes(&self) -> &[u8] { slice::from_raw_parts(self.data(), self.len()) } - /// Get the contents of this buffer as a mutable slice. + /// Extracts a mutable slice of the `ByteArray`'s entire buffer. /// /// # Safety - /// This slice should only be used for short-lived operations that write to this bytearray - /// without executing any Python code. See the safety note for [PyByteArray::as_bytes]. + /// + /// 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`] + /// apply to this function as well. #[allow(clippy::mut_from_ref)] pub unsafe fn as_bytes_mut(&self) -> &mut [u8] { slice::from_raw_parts_mut(self.data(), self.len()) diff --git a/src/types/bytes.rs b/src/types/bytes.rs index e733582b..44bb2fd9 100644 --- a/src/types/bytes.rs +++ b/src/types/bytes.rs @@ -50,7 +50,7 @@ impl PyBytes { /// }) /// # } /// ``` - pub fn new_with(py: Python, len: usize, init: F) -> PyResult<&PyBytes> + pub fn new_with(py: Python<'_>, len: usize, init: F) -> PyResult<&PyBytes> where F: FnOnce(&mut [u8]) -> PyResult<()>, { @@ -97,6 +97,24 @@ impl PyBytes { } } +impl Py { + /// Gets the Python bytes as a byte slice. Because Python bytes are + /// immutable, the result may be used for as long as the reference to + /// `self` is held, including when the GIL is released. + pub fn as_bytes<'a>(&'a self, _py: Python<'_>) -> &'a [u8] { + // py is required here because `PyBytes_AsString` and `PyBytes_Size` + // can both technically raise exceptions which require the GIL to be + // held. The only circumstance in which they raise is if the value + // isn't really a `PyBytes`, but better safe than sorry. + unsafe { + let buffer = ffi::PyBytes_AsString(self.as_ptr()) as *const u8; + let length = ffi::PyBytes_Size(self.as_ptr()) as usize; + debug_assert!(!buffer.is_null()); + std::slice::from_raw_parts(buffer, length) + } + } +} + /// This is the same way [Vec] is indexed. impl> Index for PyBytes { type Output = I::Output; @@ -107,7 +125,7 @@ impl> Index for PyBytes { } impl<'a> IntoPy for &'a [u8] { - fn into_py(self, py: Python) -> PyObject { + fn into_py(self, py: Python<'_>) -> PyObject { PyBytes::new(py, self).to_object(py) } } diff --git a/src/types/capsule.rs b/src/types/capsule.rs index 931a5355..8add77b4 100644 --- a/src/types/capsule.rs +++ b/src/types/capsule.rs @@ -167,7 +167,7 @@ impl PyCapsule { /// assert_eq!(rx.recv(), Ok("Destructor called!".to_string())); /// ``` #[allow(clippy::not_unsafe_ptr_arg_deref)] - pub fn set_context(&self, py: Python, context: *mut c_void) -> PyResult<()> { + pub fn set_context(&self, py: Python<'_>, context: *mut c_void) -> PyResult<()> { let result = unsafe { ffi::PyCapsule_SetContext(self.as_ptr(), context) as u8 }; if result != 0 { Err(PyErr::fetch(py)) @@ -178,7 +178,7 @@ impl PyCapsule { /// Gets the current context stored in the capsule. If there is no context, the pointer /// will be null. - pub fn get_context(&self, py: Python) -> PyResult<*mut c_void> { + pub fn get_context(&self, py: Python<'_>) -> PyResult<*mut c_void> { let ctx = unsafe { ffi::PyCapsule_GetContext(self.as_ptr()) }; if ctx.is_null() && self.is_valid() && PyErr::occurred(py) { Err(PyErr::fetch(py)) diff --git a/src/types/cls.rs b/src/types/cls.rs new file mode 100644 index 00000000..7b153428 --- /dev/null +++ b/src/types/cls.rs @@ -0,0 +1,584 @@ +// Copyright (c) 2017-present PyO3 Project and Contributors + +//! Defines conversions between Rust and Python types. +use crate::err::{self, PyDowncastError, PyResult}; +use crate::type_object::PyTypeInfo; +use crate::types::PyTuple; +use crate::{ + ffi, gil, Py, PyAny, PyCell, PyClass, PyNativeType, PyObject, PyRef, PyRefMut, Python, +}; +use std::ptr::NonNull; + +/// This trait represents that **we can do zero-cost conversion from the object +/// to a FFI pointer**. +/// +/// This trait is implemented for types that internally wrap a pointer to a Python object. +/// +/// # Examples +/// +/// ``` +/// use pyo3::{prelude::*, AsPyPointer}; +/// Python::with_gil(|py| { +/// let dict = pyo3::types::PyDict::new(py); +/// // All native object wrappers implement AsPyPointer!!! +/// assert_ne!(dict.as_ptr(), std::ptr::null_mut()); +/// }); +/// ``` +pub trait AsPyPointer { + /// Retrieves the underlying FFI pointer (as a borrowed pointer). + fn as_ptr(&self) -> *mut ffi::PyObject; +} + +/// This trait allows retrieving the underlying FFI pointer from Python objects. +pub trait IntoPyPointer { + /// Retrieves the underlying FFI pointer. Whether pointer owned or borrowed + /// depends on implementation. + fn into_ptr(self) -> *mut ffi::PyObject; +} + +/// Convert `None` into a null pointer. +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()) + } +} + +/// Convert `None` into a null pointer. +impl IntoPyPointer for Option +where + T: IntoPyPointer, +{ + #[inline] + fn into_ptr(self) -> *mut ffi::PyObject { + self.map_or_else(std::ptr::null_mut, |t| t.into_ptr()) + } +} + +impl<'a, T> IntoPyPointer for &'a T +where + T: AsPyPointer, +{ + fn into_ptr(self) -> *mut ffi::PyObject { + unsafe { ffi::_Py_XNewRef(self.as_ptr()) } + } +} + +/// Conversion trait that allows various objects to be converted into `PyObject`. +pub trait ToPyObject { + /// Converts self into a Python object. + fn to_object(&self, py: Python<'_>) -> PyObject; +} + +/// This trait has two implementations: The slow one is implemented for +/// all [ToPyObject] and creates a new object using [ToPyObject::to_object], +/// while the fast one is only implemented for AsPyPointer (we know +/// that every AsPyPointer is also ToPyObject) and uses [AsPyPointer::as_ptr()] +pub trait ToBorrowedObject: ToPyObject { + /// Converts self into a Python object and calls the specified closure + /// on the native FFI pointer underlying the Python object. + /// + /// May be more efficient than `to_object` because it does not need + /// to touch any reference counts when the input object already is a Python object. + fn with_borrowed_ptr(&self, py: Python<'_>, f: F) -> R + where + F: FnOnce(*mut ffi::PyObject) -> R, + { + let ptr = self.to_object(py).into_ptr(); + let result = f(ptr); + unsafe { + ffi::Py_XDECREF(ptr); + } + result + } +} + +impl ToBorrowedObject for T where T: ToPyObject {} + +/// Defines a conversion from a Rust type to a Python object. +/// +/// It functions similarly to std's [`Into`](std::convert::Into) trait, +/// but requires a [GIL token](Python) as an argument. +/// Many functions and traits internal to PyO3 require this trait as a bound, +/// so a lack of this trait can manifest itself in different error messages. +/// +/// # Examples +/// ## With `#[pyclass]` +/// The easiest way to implement `IntoPy` is by exposing a struct as a native Python object +/// by annotating it with [`#[pyclass]`](crate::prelude::pyclass). +/// +/// ```rust +/// use pyo3::prelude::*; +/// +/// #[pyclass] +/// struct Number { +/// #[pyo3(get, set)] +/// value: i32, +/// } +/// ``` +/// Python code will see this as an instance of the `Number` class with a `value` attribute. +/// +/// ## Conversion to a Python object +/// +/// However, it may not be desirable to expose the existence of `Number` to Python code. +/// `IntoPy` allows us to define a conversion to an appropriate Python object. +/// ```rust +/// use pyo3::prelude::*; +/// +/// struct Number { +/// value: i32, +/// } +/// +/// impl IntoPy for Number { +/// fn into_py(self, py: Python<'_>) -> PyObject { +/// // delegates to i32's IntoPy implementation. +/// self.value.into_py(py) +/// } +/// } +/// ``` +/// Python code will see this as an `int` object. +/// +/// ## Dynamic conversion into Python objects. +/// It is also possible to return a different Python object depending on some condition. +/// This is useful for types like enums that can carry different types. +/// +/// ```rust +/// use pyo3::prelude::*; +/// +/// enum Value { +/// Integer(i32), +/// String(String), +/// None, +/// } +/// +/// impl IntoPy for Value { +/// fn into_py(self, py: Python<'_>) -> PyObject { +/// match self { +/// Self::Integer(val) => val.into_py(py), +/// Self::String(val) => val.into_py(py), +/// Self::None => py.None(), +/// } +/// } +/// } +/// # fn main() { +/// # Python::with_gil(|py| { +/// # let v = Value::Integer(73).into_py(py); +/// # let v = v.extract::(py).unwrap(); +/// # +/// # let v = Value::String("foo".into()).into_py(py); +/// # let v = v.extract::(py).unwrap(); +/// # +/// # let v = Value::None.into_py(py); +/// # let v = v.extract::>>(py).unwrap(); +/// # }); +/// # } +/// ``` +/// Python code will see this as any of the `int`, `string` or `None` objects. +#[cfg_attr(docsrs, doc(alias = "IntoPyCallbackOutput"))] +pub trait IntoPy: Sized { + /// Performs the conversion. + fn into_py(self, py: Python<'_>) -> T; +} + +/// `FromPyObject` is implemented by various types that can be extracted from +/// a Python object reference. +/// +/// Normal usage is through the helper methods `Py::extract` or `PyAny::extract`: +/// +/// ```rust,ignore +/// let obj: Py = ...; +/// let value: &TargetType = obj.extract(py)?; +/// +/// let any: &PyAny = ...; +/// let value: &TargetType = any.extract()?; +/// ``` +/// +/// 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; +} + +/// 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) + } +} + +/// `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)) + } +} + +/// `()` is converted to Python `None`. +impl ToPyObject for () { + fn to_object(&self, py: Python<'_>) -> PyObject { + py.None() + } +} + +impl IntoPy for () { + fn into_py(self, py: Python<'_>) -> PyObject { + py.None() + } +} + +impl IntoPy for &'_ T +where + T: AsPyPointer, +{ + #[inline] + fn into_py(self, py: Python<'_>) -> PyObject { + unsafe { PyObject::from_borrowed_ptr(py, self.as_ptr()) } + } +} + +impl<'a, T> FromPyObject<'a> for &'a PyCell +where + T: PyClass, +{ + fn extract(obj: &'a PyAny) -> PyResult { + PyTryFrom::try_from(obj).map_err(Into::into) + } +} + +impl<'a, T> FromPyObject<'a> for T +where + T: PyClass + Clone, +{ + fn extract(obj: &'a PyAny) -> PyResult { + let cell: &PyCell = PyTryFrom::try_from(obj)?; + Ok(unsafe { cell.try_borrow_unguarded()?.clone() }) + } +} + +impl<'a, T> FromPyObject<'a> for PyRef<'a, T> +where + T: PyClass, +{ + fn extract(obj: &'a PyAny) -> PyResult { + let cell: &PyCell = PyTryFrom::try_from(obj)?; + cell.try_borrow().map_err(Into::into) + } +} + +impl<'a, T> FromPyObject<'a> for PyRefMut<'a, T> +where + T: PyClass, +{ + fn extract(obj: &'a PyAny) -> PyResult { + let cell: &PyCell = PyTryFrom::try_from(obj)?; + 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) + } + } +} + +/// Trait implemented by Python object types that allow a checked downcast. +/// If `T` implements `PyTryFrom`, we can convert `&PyAny` to `&T`. +/// +/// This trait is similar to `std::convert::TryFrom` +pub trait PyTryFrom<'v>: Sized + PyNativeType { + /// Cast from a concrete Python object type to PyObject. + fn try_from>(value: V) -> Result<&'v Self, PyDowncastError<'v>>; + + /// Cast from a concrete Python object type to PyObject. With exact type check. + fn try_from_exact>(value: V) -> Result<&'v Self, PyDowncastError<'v>>; + + /// Cast a PyAny to a specific type of PyObject. The caller must + /// have already verified the reference is for this type. + /// + /// # Safety + /// + /// Callers must ensure that the type is valid or risk type confusion. + unsafe fn try_from_unchecked>(value: V) -> &'v Self; +} + +/// Trait implemented by Python object types that allow a checked downcast. +/// This trait is similar to `std::convert::TryInto` +pub trait PyTryInto: Sized { + /// Cast from PyObject to a concrete Python object type. + fn try_into(&self) -> Result<&T, PyDowncastError<'_>>; + + /// Cast from PyObject to a concrete Python object type. With exact type check. + fn try_into_exact(&self) -> Result<&T, PyDowncastError<'_>>; +} + +// TryFrom implies TryInto +impl PyTryInto for PyAny +where + U: for<'v> PyTryFrom<'v>, +{ + fn try_into(&self) -> Result<&U, PyDowncastError<'_>> { + >::try_from(self) + } + fn try_into_exact(&self) -> Result<&U, PyDowncastError<'_>> { + U::try_from_exact(self) + } +} + +impl<'v, T> PyTryFrom<'v> for T +where + T: PyTypeInfo + PyNativeType, +{ + fn try_from>(value: V) -> Result<&'v Self, PyDowncastError<'v>> { + let value = value.into(); + unsafe { + if T::is_type_of(value) { + Ok(Self::try_from_unchecked(value)) + } else { + Err(PyDowncastError::new(value, T::NAME)) + } + } + } + + fn try_from_exact>(value: V) -> Result<&'v Self, PyDowncastError<'v>> { + let value = value.into(); + unsafe { + if T::is_exact_type_of(value) { + Ok(Self::try_from_unchecked(value)) + } else { + Err(PyDowncastError::new(value, T::NAME)) + } + } + } + + #[inline] + unsafe fn try_from_unchecked>(value: V) -> &'v Self { + Self::unchecked_downcast(value.into()) + } +} + +impl<'v, T> PyTryFrom<'v> for PyCell +where + T: 'v + PyClass, +{ + fn try_from>(value: V) -> Result<&'v Self, PyDowncastError<'v>> { + let value = value.into(); + unsafe { + if T::is_type_of(value) { + Ok(Self::try_from_unchecked(value)) + } else { + Err(PyDowncastError::new(value, T::NAME)) + } + } + } + fn try_from_exact>(value: V) -> Result<&'v Self, PyDowncastError<'v>> { + let value = value.into(); + unsafe { + if T::is_exact_type_of(value) { + Ok(Self::try_from_unchecked(value)) + } else { + Err(PyDowncastError::new(value, T::NAME)) + } + } + } + #[inline] + unsafe fn try_from_unchecked>(value: V) -> &'v Self { + Self::unchecked_downcast(value.into()) + } +} + +/// Converts `()` to an empty Python tuple. +impl IntoPy> for () { + fn into_py(self, py: Python<'_>) -> Py { + PyTuple::empty(py).into() + } +} + +/// Raw level conversion between `*mut ffi::PyObject` and PyO3 types. +/// +/// # Safety +/// +/// See safety notes on individual functions. +pub unsafe trait FromPyPointer<'p>: Sized { + /// Convert from an arbitrary `PyObject`. + /// + /// # Safety + /// + /// 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`. + 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). + unsafe fn from_owned_ptr_or_panic(py: Python<'p>, ptr: *mut ffi::PyObject) -> &'p Self { + Self::from_owned_ptr_or_opt(py, ptr).unwrap_or_else(|| err::panic_after_error(py)) + } + /// Convert from an arbitrary `PyObject` or panic. + /// + /// # Safety + /// + /// Relies on [`from_owned_ptr_or_opt`](#method.from_owned_ptr_or_opt). + unsafe fn from_owned_ptr(py: Python<'p>, ptr: *mut ffi::PyObject) -> &'p Self { + Self::from_owned_ptr_or_panic(py, ptr) + } + /// Convert from an arbitrary `PyObject`. + /// + /// # Safety + /// + /// Relies on [`from_owned_ptr_or_opt`](#method.from_owned_ptr_or_opt). + unsafe fn from_owned_ptr_or_err(py: Python<'p>, ptr: *mut ffi::PyObject) -> PyResult<&'p Self> { + Self::from_owned_ptr_or_opt(py, ptr).ok_or_else(|| err::PyErr::fetch(py)) + } + /// Convert from an arbitrary borrowed `PyObject`. + /// + /// # Safety + /// + /// Implementations must ensure the object does not get freed during `'p` and avoid type confusion. + unsafe fn from_borrowed_ptr_or_opt(py: Python<'p>, ptr: *mut ffi::PyObject) + -> Option<&'p Self>; + /// Convert from an arbitrary borrowed `PyObject`. + /// + /// # Safety + /// + /// Relies on unsafe fn [`from_borrowed_ptr_or_opt`](#method.from_borrowed_ptr_or_opt). + unsafe fn from_borrowed_ptr_or_panic(py: Python<'p>, ptr: *mut ffi::PyObject) -> &'p Self { + Self::from_borrowed_ptr_or_opt(py, ptr).unwrap_or_else(|| err::panic_after_error(py)) + } + /// Convert from an arbitrary borrowed `PyObject`. + /// + /// # Safety + /// + /// Relies on unsafe fn [`from_borrowed_ptr_or_opt`](#method.from_borrowed_ptr_or_opt). + unsafe fn from_borrowed_ptr(py: Python<'p>, ptr: *mut ffi::PyObject) -> &'p Self { + Self::from_borrowed_ptr_or_panic(py, ptr) + } + /// Convert from an arbitrary borrowed `PyObject`. + /// + /// # Safety + /// + /// Relies on unsafe fn [`from_borrowed_ptr_or_opt`](#method.from_borrowed_ptr_or_opt). + unsafe fn from_borrowed_ptr_or_err( + py: Python<'p>, + ptr: *mut ffi::PyObject, + ) -> PyResult<&'p Self> { + Self::from_borrowed_ptr_or_opt(py, ptr).ok_or_else(|| err::PyErr::fetch(py)) + } +} + +unsafe impl<'p, T> FromPyPointer<'p> for T +where + T: 'p + crate::PyNativeType, +{ + unsafe fn from_owned_ptr_or_opt(py: Python<'p>, ptr: *mut ffi::PyObject) -> Option<&'p Self> { + gil::register_owned(py, NonNull::new(ptr)?); + Some(&*(ptr as *mut Self)) + } + unsafe fn from_borrowed_ptr_or_opt( + _py: Python<'p>, + ptr: *mut ffi::PyObject, + ) -> Option<&'p Self> { + NonNull::new(ptr as *mut Self).map(|p| &*p.as_ptr()) + } +} + +#[cfg(test)] +mod tests { + use crate::types::{IntoPyDict, PyAny, PyDict, PyList}; + use crate::{AsPyPointer, PyObject, Python, ToPyObject}; + + use super::PyTryFrom; + + #[test] + fn test_try_from() { + Python::with_gil(|py| { + let list: &PyAny = vec![3, 6, 5, 4, 7].to_object(py).into_ref(py); + let dict: &PyAny = vec![("reverse", true)].into_py_dict(py).as_ref(); + + assert!(>::try_from(list).is_ok()); + assert!(>::try_from(dict).is_ok()); + + assert!(>::try_from(list).is_ok()); + assert!(>::try_from(dict).is_ok()); + }); + } + + #[test] + fn test_try_from_exact() { + Python::with_gil(|py| { + let list: &PyAny = vec![3, 6, 5, 4, 7].to_object(py).into_ref(py); + let dict: &PyAny = vec![("reverse", true)].into_py_dict(py).as_ref(); + + assert!(PyList::try_from_exact(list).is_ok()); + assert!(PyDict::try_from_exact(dict).is_ok()); + + assert!(PyAny::try_from_exact(list).is_err()); + assert!(PyAny::try_from_exact(dict).is_err()); + }); + } + + #[test] + fn test_try_from_unchecked() { + Python::with_gil(|py| { + let list = PyList::new(py, &[1, 2, 3]); + let val = unsafe { ::try_from_unchecked(list.as_ref()) }; + assert!(list.is(val)); + }); + } + + #[test] + fn test_option_as_ptr() { + Python::with_gil(|py| { + let mut option: Option = None; + assert_eq!(option.as_ptr(), std::ptr::null_mut()); + + let none = py.None(); + option = Some(none.clone()); + + 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/types/complex.rs b/src/types/complex.rs index 73313471..415cc5d2 100644 --- a/src/types/complex.rs +++ b/src/types/complex.rs @@ -20,7 +20,7 @@ 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(py: Python<'_>, real: c_double, imag: c_double) -> &PyComplex { unsafe { let ptr = ffi::PyComplex_FromDoubles(real, imag); py.from_owned_ptr(ptr) diff --git a/src/types/datetime.rs b/src/types/datetime.rs index f6ca1fda..c4053011 100644 --- a/src/types/datetime.rs +++ b/src/types/datetime.rs @@ -26,7 +26,7 @@ use crate::types::PyTuple; use crate::{AsPyPointer, PyAny, PyObject, Python, ToPyObject}; use std::os::raw::c_int; -fn ensure_datetime_api(_py: Python) -> &'static PyDateTime_CAPI { +fn ensure_datetime_api(_py: Python<'_>) -> &'static PyDateTime_CAPI { unsafe { if pyo3_ffi::PyDateTimeAPI().is_null() { PyDateTime_IMPORT() @@ -173,7 +173,7 @@ pyobject_native_type!( impl PyDate { /// Creates a new `datetime.date`. - pub fn new(py: Python, year: i32, month: u8, day: u8) -> PyResult<&PyDate> { + pub fn new(py: Python<'_>, year: i32, month: u8, day: u8) -> PyResult<&PyDate> { unsafe { let ptr = (ensure_datetime_api(py).Date_FromDate)( year, @@ -188,7 +188,7 @@ impl PyDate { /// 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> { + pub fn from_timestamp(py: Python<'_>, timestamp: i64) -> PyResult<&PyDate> { let time_tuple = PyTuple::new(py, &[timestamp]); // safety ensure that the API is loaded @@ -466,7 +466,7 @@ pyobject_native_type!( impl PyDelta { /// Creates a new `timedelta`. pub fn new( - py: Python, + py: Python<'_>, days: i32, seconds: i32, microseconds: i32, @@ -501,7 +501,7 @@ impl PyDeltaAccess for PyDelta { } // Utility function -fn opt_to_pyobj(py: Python, opt: Option<&PyObject>) -> *mut ffi::PyObject { +fn opt_to_pyobj(py: Python<'_>, opt: Option<&PyObject>) -> *mut ffi::PyObject { // Convenience function for unpacking Options to either an Object or None match opt { Some(tzi) => tzi.as_ptr(), diff --git a/src/types/dict.rs b/src/types/dict.rs index 955204eb..794fbea7 100644 --- a/src/types/dict.rs +++ b/src/types/dict.rs @@ -27,7 +27,7 @@ pyobject_native_type!( impl PyDict { /// Creates a new empty dictionary. - pub fn new(py: Python) -> &PyDict { + pub fn new(py: Python<'_>) -> &PyDict { unsafe { py.from_owned_ptr::(ffi::PyDict_New()) } } @@ -39,7 +39,7 @@ 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(py: Python, seq: PyObject) -> PyResult<&PyDict> { + pub fn from_sequence(py: Python<'_>, seq: PyObject) -> PyResult<&PyDict> { unsafe { let dict = py.from_owned_ptr::(ffi::PyDict_New()); err::error_on_minusone( @@ -172,7 +172,7 @@ impl PyDict { /// /// Note that it's unsafe to use when the dictionary might be changed by /// other code. - pub fn iter(&self) -> PyDictIterator { + pub fn iter(&self) -> PyDictIterator<'_> { PyDictIterator { dict: self.as_ref(), pos: 0, @@ -236,7 +236,7 @@ where V: ToPyObject, H: hash::BuildHasher, { - fn to_object(&self, py: Python) -> PyObject { + fn to_object(&self, py: Python<'_>) -> PyObject { IntoPyDict::into_py_dict(self, py).into() } } @@ -246,7 +246,7 @@ where K: cmp::Eq + ToPyObject, V: ToPyObject, { - fn to_object(&self, py: Python) -> PyObject { + fn to_object(&self, py: Python<'_>) -> PyObject { IntoPyDict::into_py_dict(self, py).into() } } @@ -257,7 +257,7 @@ where V: IntoPy, H: hash::BuildHasher, { - fn into_py(self, py: Python) -> PyObject { + fn into_py(self, py: Python<'_>) -> PyObject { let iter = self .into_iter() .map(|(k, v)| (k.into_py(py), v.into_py(py))); @@ -270,7 +270,7 @@ where K: cmp::Eq + IntoPy, V: IntoPy, { - fn into_py(self, py: Python) -> PyObject { + fn into_py(self, py: Python<'_>) -> PyObject { let iter = self .into_iter() .map(|(k, v)| (k.into_py(py), v.into_py(py))); @@ -283,7 +283,7 @@ where pub trait IntoPyDict { /// 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(self, py: Python<'_>) -> &PyDict; } impl IntoPyDict for I @@ -291,7 +291,7 @@ where T: PyDictItem, I: IntoIterator, { - fn into_py_dict(self, py: Python) -> &PyDict { + fn into_py_dict(self, py: Python<'_>) -> &PyDict { let dict = PyDict::new(py); for item in self { dict.set_item(item.key(), item.value()) @@ -387,7 +387,7 @@ mod tests { Python::with_gil(|py| { let dict = [(7, 32)].into_py_dict(py); assert_eq!(32, dict.get_item(7i32).unwrap().extract::().unwrap()); - assert_eq!(None, dict.get_item(8i32)); + assert!(dict.get_item(8i32).is_none()); let map: HashMap = [(7, 32)].iter().cloned().collect(); assert_eq!(map, dict.extract().unwrap()); let map: BTreeMap = [(7, 32)].iter().cloned().collect(); @@ -426,7 +426,7 @@ mod tests { let ndict = dict.copy().unwrap(); assert_eq!(32, ndict.get_item(7i32).unwrap().extract::().unwrap()); - assert_eq!(None, ndict.get_item(8i32)); + assert!(ndict.get_item(8i32).is_none()); }); } @@ -464,7 +464,7 @@ mod tests { let ob = v.to_object(py); let dict = ::try_from(ob.as_ref(py)).unwrap(); assert_eq!(32, dict.get_item(7i32).unwrap().extract::().unwrap()); - assert_eq!(None, dict.get_item(8i32)); + assert!(dict.get_item(8i32).is_none()); }); } @@ -527,7 +527,7 @@ mod tests { let dict = ::try_from(ob.as_ref(py)).unwrap(); assert!(dict.del_item(7i32).is_ok()); assert_eq!(0, dict.len()); - assert_eq!(None, dict.get_item(7i32)); + assert!(dict.get_item(7i32).is_none()); }); } diff --git a/src/types/floatob.rs b/src/types/floatob.rs index 01c33a2d..3079c61b 100644 --- a/src/types/floatob.rs +++ b/src/types/floatob.rs @@ -35,13 +35,13 @@ impl PyFloat { } impl ToPyObject for f64 { - fn to_object(&self, py: Python) -> PyObject { + fn to_object(&self, py: Python<'_>) -> PyObject { PyFloat::new(py, *self).into() } } impl IntoPy for f64 { - fn into_py(self, py: Python) -> PyObject { + fn into_py(self, py: Python<'_>) -> PyObject { PyFloat::new(py, self).into() } } @@ -63,13 +63,13 @@ impl<'source> FromPyObject<'source> for f64 { } impl ToPyObject for f32 { - fn to_object(&self, py: Python) -> PyObject { + fn to_object(&self, py: Python<'_>) -> PyObject { PyFloat::new(py, f64::from(*self)).into() } } impl IntoPy for f32 { - fn into_py(self, py: Python) -> PyObject { + fn into_py(self, py: Python<'_>) -> PyObject { PyFloat::new(py, f64::from(self)).into() } } diff --git a/src/types/function.rs b/src/types/function.rs index 4e72eefe..297b23c9 100644 --- a/src/types/function.rs +++ b/src/types/function.rs @@ -104,7 +104,7 @@ impl PyCFunction { /// py_run!(py, add_one, "assert add_one(42) == 43"); /// }); /// ``` - pub fn new_closure(f: F, py: Python) -> PyResult<&PyCFunction> + pub fn new_closure(f: F, py: Python<'_>) -> PyResult<&PyCFunction> where F: Fn(&types::PyTuple, Option<&types::PyDict>) -> R + Send + 'static, R: crate::callback::IntoPyCallbackOutput<*mut ffi::PyObject>, @@ -131,7 +131,7 @@ impl PyCFunction { #[doc(hidden)] fn internal_new_from_pointers( method_def: PyMethodDef, - py: Python, + py: Python<'_>, mod_ptr: *mut ffi::PyObject, module_name: *mut ffi::PyObject, ) -> PyResult<&Self> { @@ -150,7 +150,7 @@ impl PyCFunction { #[doc(hidden)] pub fn internal_new( method_def: PyMethodDef, - py_or_module: PyFunctionArguments, + py_or_module: PyFunctionArguments<'_>, ) -> PyResult<&Self> { let (py, module) = py_or_module.into_py_and_maybe_module(); let (mod_ptr, module_name) = if let Some(m) = module { diff --git a/src/types/iterator.rs b/src/types/iterator.rs index 02d437c8..924ccea8 100644 --- a/src/types/iterator.rs +++ b/src/types/iterator.rs @@ -99,7 +99,7 @@ impl Py { /// Similar to [`as_ref`](#method.as_ref), and also consumes this `Py` and registers the /// Python object reference in PyO3's object storage. The reference count for the Python /// object will not be decreased until the GIL lifetime ends. - pub fn into_ref(self, py: Python) -> &PyIterator { + pub fn into_ref(self, py: Python<'_>) -> &PyIterator { unsafe { py.from_owned_ptr(self.into_ptr()) } } } @@ -212,8 +212,9 @@ def fibonacci(target): 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 = PyIterator::try_from(obj.as_ref(py)).unwrap(); - assert_eq!(obj, iter.into()); + let iter: &PyIterator = + >::try_from(obj.as_ref(py)).unwrap(); + assert!(obj.is(iter)); }); } diff --git a/src/types/list.rs b/src/types/list.rs index bfd5f8b2..ab69859e 100644 --- a/src/types/list.rs +++ b/src/types/list.rs @@ -21,7 +21,10 @@ pyobject_native_type_core!(PyList, ffi::PyList_Type, #checkfunction=ffi::PyList_ #[inline] #[track_caller] -fn new_from_iter(py: Python, elements: &mut dyn ExactSizeIterator) -> Py { +fn new_from_iter( + py: Python<'_>, + elements: &mut dyn ExactSizeIterator, +) -> Py { unsafe { // PyList_New checks for overflow but has a bad error message, so we check ourselves let len: Py_ssize_t = elements @@ -90,7 +93,7 @@ impl PyList { } /// Constructs a new empty list. - pub fn empty(py: Python) -> &PyList { + pub fn empty(py: Python<'_>) -> &PyList { unsafe { py.from_owned_ptr::(ffi::PyList_New(0)) } } @@ -264,7 +267,7 @@ impl PyList { } /// Returns an iterator over this list's items. - pub fn iter(&self) -> PyListIterator { + pub fn iter(&self) -> PyListIterator<'_> { PyListIterator { list: self, index: 0, @@ -351,7 +354,7 @@ impl IntoPy for Vec where T: IntoPy, { - fn into_py(self, py: Python) -> PyObject { + fn into_py(self, py: Python<'_>) -> PyObject { let mut iter = self.into_iter().map(|e| e.into_py(py)); let list = new_from_iter(py, &mut iter); list.into() @@ -853,7 +856,7 @@ mod tests { } impl ToPyObject for Bad { - fn to_object(&self, py: Python) -> Py { + fn to_object(&self, py: Python<'_>) -> Py { self.to_owned().into_py(py) } } diff --git a/src/types/mapping.rs b/src/types/mapping.rs index 6cb6ff66..8c861ca0 100644 --- a/src/types/mapping.rs +++ b/src/types/mapping.rs @@ -142,7 +142,7 @@ impl Py { /// Similar to [`as_ref`](#method.as_ref), and also consumes this `Py` and registers the /// Python object reference in PyO3's object storage. The reference count for the Python /// object will not be decreased until the GIL lifetime ends. - pub fn into_ref(self, py: Python) -> &PyMapping { + pub fn into_ref(self, py: Python<'_>) -> &PyMapping { unsafe { py.from_owned_ptr(self.into_ptr()) } } } diff --git a/src/types/mod.rs b/src/types/mod.rs index b7404aa9..6879345e 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -39,7 +39,7 @@ macro_rules! pyobject_native_type_base( unsafe impl<$($generics,)*> $crate::PyNativeType for $name {} impl<$($generics,)*> ::std::fmt::Debug for $name { - fn fmt(&self, f: &mut ::std::fmt::Formatter) + 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))?; @@ -48,7 +48,7 @@ macro_rules! pyobject_native_type_base( } impl<$($generics,)*> ::std::fmt::Display for $name { - fn fmt(&self, f: &mut ::std::fmt::Formatter) + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::result::Result<(), ::std::fmt::Error> { let s = self.str().or(::std::result::Result::Err(::std::fmt::Error))?; @@ -59,20 +59,11 @@ macro_rules! pyobject_native_type_base( impl<$($generics,)*> $crate::ToPyObject for $name { #[inline] - fn to_object(&self, py: $crate::Python) -> $crate::PyObject { + fn to_object(&self, py: $crate::Python<'_>) -> $crate::PyObject { use $crate::AsPyPointer; unsafe { $crate::PyObject::from_borrowed_ptr(py, self.as_ptr()) } } } - - impl<$($generics,)*> ::std::cmp::PartialEq for $name { - #[inline] - fn eq(&self, o: &$name) -> bool { - use $crate::AsPyPointer; - - self.as_ptr() == o.as_ptr() - } - } }; ); @@ -110,7 +101,7 @@ macro_rules! pyobject_native_type_named ( impl<$($generics,)*> $crate::IntoPy<$crate::Py<$name>> for &'_ $name { #[inline] - fn into_py(self, py: $crate::Python) -> $crate::Py<$name> { + fn into_py(self, py: $crate::Python<'_>) -> $crate::Py<$name> { use $crate::AsPyPointer; unsafe { $crate::Py::from_borrowed_ptr(py, self.as_ptr()) } } @@ -144,11 +135,15 @@ macro_rules! pyobject_native_type_info( const MODULE: ::std::option::Option<&'static str> = $module; #[inline] - fn type_object_raw(_py: $crate::Python) -> *mut $crate::ffi::PyTypeObject { + fn type_object_raw(_py: $crate::Python<'_>) -> *mut $crate::ffi::PyTypeObject { // Create a very short lived mutable reference and directly // cast it to a pointer: no mutable references can be aliasing // because we hold the GIL. + #[cfg(not(addr_of))] unsafe { &mut $typeobject } + + #[cfg(addr_of)] + unsafe { ::std::ptr::addr_of_mut!($typeobject) } } $( diff --git a/src/types/module.rs b/src/types/module.rs index ca6c2534..facb4524 100644 --- a/src/types/module.rs +++ b/src/types/module.rs @@ -221,7 +221,7 @@ impl PyModule { /// use pyo3::prelude::*; /// /// #[pymodule] - /// fn my_module(_py: Python, module: &PyModule) -> PyResult<()> { + /// fn my_module(_py: Python<'_>, module: &PyModule) -> PyResult<()> { /// module.add("c", 299_792_458)?; /// Ok(()) /// } @@ -265,7 +265,7 @@ impl PyModule { /// struct Foo { /* fields omitted */ } /// /// #[pymodule] - /// fn my_module(_py: Python, module: &PyModule) -> PyResult<()> { + /// fn my_module(_py: Python<'_>, module: &PyModule) -> PyResult<()> { /// module.add_class::()?; /// Ok(()) /// } @@ -324,7 +324,7 @@ impl PyModule { /// use pyo3::prelude::*; /// /// #[pymodule] - /// fn my_module(py: Python, module: &PyModule) -> PyResult<()> { + /// fn my_module(py: Python<'_>, module: &PyModule) -> PyResult<()> { /// let submodule = PyModule::new(py, "submodule")?; /// submodule.add("super_useful_constant", "important")?; /// @@ -367,7 +367,7 @@ impl PyModule { /// println!("Hello world!") /// } /// #[pymodule] - /// fn my_module(_py: Python, module: &PyModule) -> PyResult<()> { + /// fn my_module(_py: Python<'_>, module: &PyModule) -> PyResult<()> { /// module.add_function(wrap_pyfunction!(say_hello, module)?) /// } /// ``` diff --git a/src/types/num.rs b/src/types/num.rs index 7a49b6b5..75af9a19 100644 --- a/src/types/num.rs +++ b/src/types/num.rs @@ -14,12 +14,12 @@ macro_rules! int_fits_larger_int { ($rust_type:ty, $larger_type:ty) => { impl ToPyObject for $rust_type { #[inline] - fn to_object(&self, py: Python) -> PyObject { + fn to_object(&self, py: Python<'_>) -> PyObject { (*self as $larger_type).into_py(py) } } impl IntoPy for $rust_type { - fn into_py(self, py: Python) -> PyObject { + fn into_py(self, py: Python<'_>) -> PyObject { (self as $larger_type).into_py(py) } } @@ -48,12 +48,12 @@ pyobject_native_type_core!(PyLong, ffi::PyLong_Type, #checkfunction=ffi::PyLong_ macro_rules! int_fits_c_long { ($rust_type:ty) => { impl ToPyObject for $rust_type { - fn to_object(&self, py: Python) -> PyObject { + fn to_object(&self, py: Python<'_>) -> PyObject { unsafe { PyObject::from_owned_ptr(py, ffi::PyLong_FromLong(*self as c_long)) } } } impl IntoPy for $rust_type { - fn into_py(self, py: Python) -> PyObject { + fn into_py(self, py: Python<'_>) -> PyObject { unsafe { PyObject::from_owned_ptr(py, ffi::PyLong_FromLong(self as c_long)) } } } @@ -82,13 +82,13 @@ macro_rules! int_convert_u64_or_i64 { ($rust_type:ty, $pylong_from_ll_or_ull:expr, $pylong_as_ll_or_ull:expr) => { impl ToPyObject for $rust_type { #[inline] - fn to_object(&self, py: Python) -> PyObject { + fn to_object(&self, py: Python<'_>) -> PyObject { unsafe { PyObject::from_owned_ptr(py, $pylong_from_ll_or_ull(*self)) } } } impl IntoPy for $rust_type { #[inline] - fn into_py(self, py: Python) -> PyObject { + fn into_py(self, py: Python<'_>) -> PyObject { unsafe { PyObject::from_owned_ptr(py, $pylong_from_ll_or_ull(self)) } } } @@ -152,12 +152,12 @@ mod fast_128bit_int_conversion { ($rust_type: ty, $is_signed: expr) => { impl ToPyObject for $rust_type { #[inline] - fn to_object(&self, py: Python) -> PyObject { + fn to_object(&self, py: Python<'_>) -> PyObject { (*self).into_py(py) } } impl IntoPy for $rust_type { - fn into_py(self, py: Python) -> PyObject { + fn into_py(self, py: Python<'_>) -> PyObject { unsafe { // Always use little endian let bytes = self.to_le_bytes(); @@ -211,13 +211,13 @@ mod slow_128bit_int_conversion { ($rust_type: ty, $half_type: ty) => { impl ToPyObject for $rust_type { #[inline] - fn to_object(&self, py: Python) -> PyObject { + fn to_object(&self, py: Python<'_>) -> PyObject { (*self).into_py(py) } } impl IntoPy for $rust_type { - fn into_py(self, py: Python) -> PyObject { + fn into_py(self, py: Python<'_>) -> PyObject { let lower = self as u64; let upper = (self >> SHIFT) as $half_type; unsafe { @@ -262,7 +262,7 @@ mod slow_128bit_int_conversion { } fn err_if_invalid_value( - py: Python, + py: Python<'_>, invalid_value: T, actual_value: T, ) -> PyResult { diff --git a/src/types/sequence.rs b/src/types/sequence.rs index 61f48569..7cb5ee9d 100644 --- a/src/types/sequence.rs +++ b/src/types/sequence.rs @@ -269,35 +269,9 @@ impl<'a, T> FromPyObject<'a> for Vec where T: FromPyObject<'a>, { - #[cfg(not(feature = "nightly"))] fn extract(obj: &'a PyAny) -> PyResult { extract_sequence(obj) } - #[cfg(feature = "nightly")] - default fn extract(obj: &'a PyAny) -> PyResult { - extract_sequence(obj) - } -} - -#[cfg(all(feature = "nightly", not(Py_LIMITED_API)))] -impl<'source, T> FromPyObject<'source> for Vec -where - for<'a> T: FromPyObject<'a> + crate::buffer::Element, -{ - fn extract(obj: &'source PyAny) -> PyResult { - // first try buffer protocol - if let Ok(buf) = crate::buffer::PyBuffer::get(obj) { - if buf.dimensions() == 1 { - if let Ok(v) = buf.to_vec(obj.py()) { - buf.release(obj.py()); - return Ok(v); - } - } - buf.release(obj.py()); - } - // fall back to sequence protocol - extract_sequence(obj) - } } fn extract_sequence<'s, T>(obj: &'s PyAny) -> PyResult> @@ -356,7 +330,7 @@ impl Py { /// Similar to [`as_ref`](#method.as_ref), and also consumes this `Py` and registers the /// Python object reference in PyO3's object storage. The reference count for the Python /// object will not be decreased until the GIL lifetime ends. - pub fn into_ref(self, py: Python) -> &PySequence { + pub fn into_ref(self, py: Python<'_>) -> &PySequence { unsafe { py.from_owned_ptr(self.into_ptr()) } } } @@ -751,11 +725,11 @@ mod tests { let seq = ob.cast_as::(py).unwrap(); let rep_seq = seq.in_place_repeat(3).unwrap(); assert_eq!(6, seq.len().unwrap()); - assert_eq!(seq, rep_seq); + assert!(seq.is(rep_seq)); let conc_seq = seq.in_place_concat(seq).unwrap(); assert_eq!(12, seq.len().unwrap()); - assert_eq!(seq, conc_seq); + assert!(seq.is(conc_seq)); }); } diff --git a/src/types/set.rs b/src/types/set.rs index 9c52fe2e..63dc7c7e 100644 --- a/src/types/set.rs +++ b/src/types/set.rs @@ -37,7 +37,7 @@ impl PySet { } /// Creates a new empty set. - pub fn empty(py: Python) -> PyResult<&PySet> { + pub fn empty(py: Python<'_>) -> PyResult<&PySet> { unsafe { py.from_owned_ptr_or_err(ffi::PySet_New(ptr::null_mut())) } } @@ -111,7 +111,7 @@ impl PySet { /// Returns an iterator of values in this set. /// /// Note that it can be unsafe to use when the set might be changed by other code. - pub fn iter(&self) -> PySetIterator { + pub fn iter(&self) -> PySetIterator<'_> { PySetIterator::new(self) } } @@ -123,7 +123,7 @@ pub struct PySetIterator<'p> { #[cfg(Py_LIMITED_API)] impl PySetIterator<'_> { - fn new(set: &PyAny) -> PySetIterator { + fn new(set: &PyAny) -> PySetIterator<'_> { PySetIterator { it: PyIterator::from_object(set.py(), set).unwrap(), } @@ -148,7 +148,7 @@ pub struct PySetIterator<'py> { #[cfg(not(Py_LIMITED_API))] impl PySetIterator<'_> { - fn new(set: &PyAny) -> PySetIterator { + fn new(set: &PyAny) -> PySetIterator<'_> { PySetIterator { set, pos: 0 } } } @@ -195,7 +195,7 @@ where T: hash::Hash + Eq + ToPyObject, S: hash::BuildHasher + Default, { - fn to_object(&self, py: Python) -> PyObject { + fn to_object(&self, py: Python<'_>) -> PyObject { let set = PySet::new::(py, &[]).expect("Failed to construct empty set"); { for val in self { @@ -210,7 +210,7 @@ impl ToPyObject for collections::BTreeSet where T: hash::Hash + Eq + ToPyObject, { - fn to_object(&self, py: Python) -> PyObject { + fn to_object(&self, py: Python<'_>) -> PyObject { let set = PySet::new::(py, &[]).expect("Failed to construct empty set"); { for val in self { @@ -226,7 +226,7 @@ where K: IntoPy + Eq + hash::Hash, S: hash::BuildHasher + Default, { - fn into_py(self, py: Python) -> PyObject { + fn into_py(self, py: Python<'_>) -> PyObject { let set = PySet::empty(py).expect("Failed to construct empty set"); { for val in self { @@ -252,7 +252,7 @@ impl IntoPy for BTreeSet where K: IntoPy + cmp::Ord, { - fn into_py(self, py: Python) -> PyObject { + fn into_py(self, py: Python<'_>) -> PyObject { let set = PySet::empty(py).expect("Failed to construct empty set"); { for val in self { @@ -283,7 +283,7 @@ impl PyFrozenSet { } /// Creates a new empty frozen set - pub fn empty(py: Python) -> PyResult<&PyFrozenSet> { + pub fn empty(py: Python<'_>) -> PyResult<&PyFrozenSet> { unsafe { py.from_owned_ptr_or_err(ffi::PyFrozenSet_New(ptr::null_mut())) } } @@ -317,7 +317,7 @@ impl PyFrozenSet { /// Returns an iterator of values in this frozen set. /// /// Note that it can be unsafe to use when the set might be changed by other code. - pub fn iter(&self) -> PySetIterator { + pub fn iter(&self) -> PySetIterator<'_> { PySetIterator::new(self.as_ref()) } } diff --git a/src/types/slice.rs b/src/types/slice.rs index 90cd09da..557cb4cb 100644 --- a/src/types/slice.rs +++ b/src/types/slice.rs @@ -39,7 +39,7 @@ 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(py: Python<'_>, start: isize, stop: isize, step: isize) -> &PySlice { unsafe { let ptr = ffi::PySlice_New( ffi::PyLong_FromLong(start as c_long), @@ -84,7 +84,7 @@ impl PySlice { } impl ToPyObject for PySliceIndices { - fn to_object(&self, py: Python) -> PyObject { + fn to_object(&self, py: Python<'_>) -> PyObject { PySlice::new(py, self.start, self.stop, self.step).into() } } diff --git a/src/types/string.rs b/src/types/string.rs index 22496e26..76610a10 100644 --- a/src/types/string.rs +++ b/src/types/string.rs @@ -68,7 +68,7 @@ impl<'a> PyStringData<'a> { /// storage format. This should only occur for strings that were created via Python /// C APIs that skip input validation (like `PyUnicode_FromKindAndData`) and should /// never occur for strings that were created from Python code. - pub fn to_string(self, py: Python) -> PyResult> { + pub fn to_string(self, py: Python<'_>) -> PyResult> { use std::ffi::CStr; match self { Self::Ucs1(data) => match str::from_utf8(data) { @@ -190,7 +190,7 @@ impl PyString { /// /// Unpaired surrogates invalid UTF-8 sequences are /// replaced with `U+FFFD REPLACEMENT CHARACTER`. - pub fn to_string_lossy(&self) -> Cow { + pub fn to_string_lossy(&self) -> Cow<'_, str> { match self.to_str() { Ok(s) => Cow::Borrowed(s), Err(_) => { @@ -266,30 +266,30 @@ impl PyString { /// See `PyString::new` for details on the conversion. impl ToPyObject for str { #[inline] - fn to_object(&self, py: Python) -> PyObject { + fn to_object(&self, py: Python<'_>) -> PyObject { PyString::new(py, self).into() } } impl<'a> IntoPy for &'a str { #[inline] - fn into_py(self, py: Python) -> PyObject { + fn into_py(self, py: Python<'_>) -> PyObject { PyString::new(py, self).into() } } -/// Converts a Rust `Cow` to a Python object. +/// Converts a Rust `Cow<'_, str>` to a Python object. /// See `PyString::new` for details on the conversion. impl<'a> ToPyObject for Cow<'a, str> { #[inline] - fn to_object(&self, py: Python) -> PyObject { + fn to_object(&self, py: Python<'_>) -> PyObject { PyString::new(py, self).into() } } impl IntoPy for Cow<'_, str> { #[inline] - fn into_py(self, py: Python) -> PyObject { + fn into_py(self, py: Python<'_>) -> PyObject { self.to_object(py) } } @@ -298,33 +298,33 @@ impl IntoPy for Cow<'_, str> { /// See `PyString::new` for details on the conversion. impl ToPyObject for String { #[inline] - fn to_object(&self, py: Python) -> PyObject { + fn to_object(&self, py: Python<'_>) -> PyObject { PyString::new(py, self).into() } } impl ToPyObject for char { - fn to_object(&self, py: Python) -> PyObject { + fn to_object(&self, py: Python<'_>) -> PyObject { self.into_py(py) } } impl IntoPy for char { - fn into_py(self, py: Python) -> PyObject { + fn into_py(self, py: Python<'_>) -> PyObject { let mut bytes = [0u8; 4]; PyString::new(py, self.encode_utf8(&mut bytes)).into() } } impl IntoPy for String { - fn into_py(self, py: Python) -> PyObject { + fn into_py(self, py: Python<'_>) -> PyObject { PyString::new(py, &self).into() } } impl<'a> IntoPy for &'a String { #[inline] - fn into_py(self, py: Python) -> PyObject { + fn into_py(self, py: Python<'_>) -> PyObject { PyString::new(py, self).into() } } @@ -349,7 +349,7 @@ impl FromPyObject<'_> for String { impl FromPyObject<'_> for char { fn extract(obj: &PyAny) -> PyResult { - let s = PyString::try_from(obj)?.to_str()?; + let s = >::try_from(obj)?.to_str()?; let mut iter = s.chars(); if let (Some(ch), None) = (iter.next(), iter.next()) { Ok(ch) @@ -504,7 +504,7 @@ mod tests { let data = unsafe { s.data().unwrap() }; assert_eq!(data, PyStringData::Ucs1(b"f\xfe")); let err = data.to_string(py).unwrap_err(); - assert_eq!(err.get_type(py), PyUnicodeDecodeError::type_object(py)); + assert!(err.get_type(py).is(PyUnicodeDecodeError::type_object(py))); assert!(err .to_string() .contains("'utf-8' codec can't decode byte 0xfe in position 1")); @@ -546,7 +546,7 @@ mod tests { let data = unsafe { s.data().unwrap() }; assert_eq!(data, PyStringData::Ucs2(&[0xff22, 0xd800])); let err = data.to_string(py).unwrap_err(); - assert_eq!(err.get_type(py), PyUnicodeDecodeError::type_object(py)); + assert!(err.get_type(py).is(PyUnicodeDecodeError::type_object(py))); assert!(err .to_string() .contains("'utf-16' codec can't decode bytes in position 0-3")); @@ -585,7 +585,7 @@ mod tests { let data = unsafe { s.data().unwrap() }; assert_eq!(data, PyStringData::Ucs4(&[0x20000, 0xd800])); let err = data.to_string(py).unwrap_err(); - assert_eq!(err.get_type(py), PyUnicodeDecodeError::type_object(py)); + assert!(err.get_type(py).is(PyUnicodeDecodeError::type_object(py))); assert!(err .to_string() .contains("'utf-32' codec can't decode bytes in position 0-7")); diff --git a/src/types/tuple.rs b/src/types/tuple.rs index aace9109..86454542 100644 --- a/src/types/tuple.rs +++ b/src/types/tuple.rs @@ -12,7 +12,10 @@ use crate::{ #[inline] #[track_caller] -fn new_from_iter(py: Python, elements: &mut dyn ExactSizeIterator) -> Py { +fn new_from_iter( + py: Python<'_>, + elements: &mut dyn ExactSizeIterator, +) -> Py { unsafe { // PyTuple_New checks for overflow but has a bad error message, so we check ourselves let len: Py_ssize_t = elements @@ -79,7 +82,10 @@ 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(py: Python, elements: impl IntoIterator) -> &PyTuple + pub fn new( + py: Python<'_>, + elements: impl IntoIterator, + ) -> &PyTuple where T: ToPyObject, U: ExactSizeIterator, @@ -90,7 +96,7 @@ impl PyTuple { } /// Constructs an empty tuple (on the Python side, a singleton object). - pub fn empty(py: Python) -> &PyTuple { + pub fn empty(py: Python<'_>) -> &PyTuple { unsafe { py.from_owned_ptr(ffi::PyTuple_New(0)) } } @@ -226,7 +232,7 @@ impl PyTuple { } /// Returns an iterator over the tuple items. - pub fn iter(&self) -> PyTupleIterator { + pub fn iter(&self) -> PyTupleIterator<'_> { PyTupleIterator { tuple: self, index: 0, @@ -296,7 +302,7 @@ fn wrong_tuple_length(t: &PyTuple, expected_length: usize) -> PyErr { macro_rules! tuple_conversion ({$length:expr,$(($refN:ident, $n:tt, $T:ident)),+} => { impl <$($T: ToPyObject),+> ToPyObject for ($($T,)+) { - fn to_object(&self, py: Python) -> PyObject { + fn to_object(&self, py: Python<'_>) -> PyObject { unsafe { let ptr = ffi::PyTuple_New($length); let ret = PyObject::from_owned_ptr(py, ptr); @@ -306,7 +312,7 @@ macro_rules! tuple_conversion ({$length:expr,$(($refN:ident, $n:tt, $T:ident)),+ } } impl <$($T: IntoPy),+> IntoPy for ($($T,)+) { - fn into_py(self, py: Python) -> PyObject { + fn into_py(self, py: Python<'_>) -> PyObject { unsafe { let ptr = ffi::PyTuple_New($length); let ret = PyObject::from_owned_ptr(py, ptr); @@ -317,7 +323,7 @@ macro_rules! tuple_conversion ({$length:expr,$(($refN:ident, $n:tt, $T:ident)),+ } impl <$($T: IntoPy),+> IntoPy> for ($($T,)+) { - fn into_py(self, py: Python) -> Py { + fn into_py(self, py: Python<'_>) -> Py { unsafe { let ptr = ffi::PyTuple_New($length); let ret = Py::from_owned_ptr(py, ptr); @@ -836,7 +842,7 @@ mod tests { } impl ToPyObject for Bad { - fn to_object(&self, py: Python) -> Py { + fn to_object(&self, py: Python<'_>) -> Py { self.to_owned().into_py(py) } } @@ -905,7 +911,7 @@ mod tests { } impl ToPyObject for Bad { - fn to_object(&self, py: Python) -> Py { + fn to_object(&self, py: Python<'_>) -> Py { self.to_owned().into_py(py) } } diff --git a/src/types/typeobject.rs b/src/types/typeobject.rs index 8156a4f3..dfc97901 100644 --- a/src/types/typeobject.rs +++ b/src/types/typeobject.rs @@ -15,7 +15,7 @@ pyobject_native_type_core!(PyType, ffi::PyType_Type, #checkfunction=ffi::PyType_ impl PyType { /// Creates a new type object. #[inline] - pub fn new(py: Python) -> &PyType { + pub fn new(py: Python<'_>) -> &PyType { T::type_object(py) } @@ -31,7 +31,7 @@ impl PyType { /// - The pointer must be non-null. /// - The pointer must be valid for the entire of the lifetime for which the reference is used. #[inline] - pub unsafe fn from_type_ptr(py: Python, p: *mut ffi::PyTypeObject) -> &PyType { + pub unsafe fn from_type_ptr(py: Python<'_>, p: *mut ffi::PyTypeObject) -> &PyType { py.from_borrowed_ptr(p as *mut ffi::PyObject) } diff --git a/src/version.rs b/src/version.rs new file mode 100644 index 00000000..ea9e9d40 --- /dev/null +++ b/src/version.rs @@ -0,0 +1,143 @@ +/// Represents the major, minor, and patch (if any) versions of this interpreter. +/// +/// This struct is usually created with [`Python::version`]. +/// +/// # Examples +/// +/// ```rust +/// # use pyo3::Python; +/// Python::with_gil(|py| { +/// // PyO3 supports Python 3.7 and up. +/// assert!(py.version_info() >= (3, 7)); +/// assert!(py.version_info() >= (3, 7, 0)); +/// }); +/// ``` +/// +/// [`Python::version`]: crate::marker::Python::version +#[derive(Debug)] +pub struct PythonVersionInfo<'py> { + /// Python major version (e.g. `3`). + pub major: u8, + /// Python minor version (e.g. `11`). + pub minor: u8, + /// Python patch version (e.g. `0`). + pub patch: u8, + /// Python version suffix, if applicable (e.g. `a0`). + pub suffix: Option<&'py str>, +} + +impl<'py> PythonVersionInfo<'py> { + /// Parses a hard-coded Python interpreter version string (e.g. 3.9.0a4+). + pub(crate) fn from_str(version_number_str: &'py str) -> Result { + fn split_and_parse_number(version_part: &str) -> (u8, Option<&str>) { + match version_part.find(|c: char| !c.is_ascii_digit()) { + None => (version_part.parse().unwrap(), None), + Some(version_part_suffix_start) => { + let (version_part, version_part_suffix) = + version_part.split_at(version_part_suffix_start); + (version_part.parse().unwrap(), Some(version_part_suffix)) + } + } + } + + let mut parts = version_number_str.split('.'); + let major_str = parts.next().ok_or("Python major version missing")?; + let minor_str = parts.next().ok_or("Python minor version missing")?; + let patch_str = parts.next(); + if parts.next().is_some() { + return Err("Python version string has too many parts"); + }; + + let major = major_str + .parse() + .map_err(|_| "Python major version not an integer")?; + let (minor, suffix) = split_and_parse_number(minor_str); + if suffix.is_some() { + assert!(patch_str.is_none()); + return Ok(PythonVersionInfo { + major, + minor, + patch: 0, + suffix, + }); + } + + let (patch, suffix) = patch_str.map(split_and_parse_number).unwrap_or_default(); + Ok(PythonVersionInfo { + major, + minor, + patch, + suffix, + }) + } +} + +impl PartialEq<(u8, u8)> for PythonVersionInfo<'_> { + fn eq(&self, other: &(u8, u8)) -> bool { + self.major == other.0 && self.minor == other.1 + } +} + +impl PartialEq<(u8, u8, u8)> for PythonVersionInfo<'_> { + fn eq(&self, other: &(u8, u8, u8)) -> bool { + self.major == other.0 && self.minor == other.1 && self.patch == other.2 + } +} + +impl PartialOrd<(u8, u8)> for PythonVersionInfo<'_> { + fn partial_cmp(&self, other: &(u8, u8)) -> Option { + (self.major, self.minor).partial_cmp(other) + } +} + +impl PartialOrd<(u8, u8, u8)> for PythonVersionInfo<'_> { + fn partial_cmp(&self, other: &(u8, u8, u8)) -> Option { + (self.major, self.minor, self.patch).partial_cmp(other) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::Python; + #[test] + fn test_python_version_info() { + Python::with_gil(|py| { + let version = py.version_info(); + #[cfg(Py_3_7)] + assert!(version >= (3, 7)); + #[cfg(Py_3_7)] + assert!(version >= (3, 7, 0)); + #[cfg(Py_3_8)] + assert!(version >= (3, 8)); + #[cfg(Py_3_8)] + assert!(version >= (3, 8, 0)); + #[cfg(Py_3_9)] + assert!(version >= (3, 9)); + #[cfg(Py_3_9)] + assert!(version >= (3, 9, 0)); + #[cfg(Py_3_10)] + assert!(version >= (3, 10)); + #[cfg(Py_3_10)] + assert!(version >= (3, 10, 0)); + #[cfg(Py_3_11)] + assert!(version >= (3, 11)); + #[cfg(Py_3_11)] + assert!(version >= (3, 11, 0)); + }); + } + + #[test] + fn test_python_version_info_parse() { + assert!(PythonVersionInfo::from_str("3.5.0a1").unwrap() >= (3, 5, 0)); + assert!(PythonVersionInfo::from_str("3.5+").unwrap() >= (3, 5, 0)); + assert!(PythonVersionInfo::from_str("3.5+").unwrap() == (3, 5, 0)); + assert!(PythonVersionInfo::from_str("3.5+").unwrap() != (3, 5, 1)); + assert!(PythonVersionInfo::from_str("3.5.2a1+").unwrap() < (3, 5, 3)); + assert!(PythonVersionInfo::from_str("3.5.2a1+").unwrap() == (3, 5, 2)); + assert!(PythonVersionInfo::from_str("3.5.2a1+").unwrap() == (3, 5)); + assert!(PythonVersionInfo::from_str("3.5+").unwrap() == (3, 5)); + assert!(PythonVersionInfo::from_str("3.5.2a1+").unwrap() < (3, 6)); + assert!(PythonVersionInfo::from_str("3.5.2a1+").unwrap() > (3, 4)); + } +} diff --git a/tests/test_anyhow.rs b/tests/test_anyhow.rs index 33768907..f0df4dcc 100644 --- a/tests/test_anyhow.rs +++ b/tests/test_anyhow.rs @@ -5,6 +5,7 @@ fn test_anyhow_py_function_ok_result() { use pyo3::{py_run, pyfunction, wrap_pyfunction, Python}; #[pyfunction] + #[allow(clippy::unnecessary_wraps)] fn produce_ok_result() -> anyhow::Result { Ok(String::from("OK buddy")) } diff --git a/tests/test_arithmetics.rs b/tests/test_arithmetics.rs index ed1870bb..0c4cd766 100644 --- a/tests/test_arithmetics.rs +++ b/tests/test_arithmetics.rs @@ -52,6 +52,39 @@ fn unary_arithmetic() { py_run!(py, c, "assert repr(round(c, 1)) == 'UA(3)'"); } +#[pyclass] +struct Indexable(i32); + +#[pymethods] +impl Indexable { + fn __index__(&self) -> i32 { + self.0 + } + + fn __int__(&self) -> i32 { + self.0 + } + + fn __float__(&self) -> f64 { + f64::from(self.0) + } + + fn __invert__(&self) -> Self { + Self(!self.0) + } +} + +#[test] +fn indexable() { + Python::with_gil(|py| { + let i = PyCell::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"); + py_run!(py, i, "assert int(~i) == -6"); + }) +} + #[pyclass] struct InPlaceOperations { value: u32, @@ -276,7 +309,7 @@ fn rhs_arithmetic() { struct LhsAndRhs {} impl std::fmt::Debug for LhsAndRhs { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "LR") } } @@ -287,43 +320,43 @@ impl LhsAndRhs { // "BA" // } - fn __add__(lhs: PyRef, rhs: &PyAny) -> String { + fn __add__(lhs: PyRef<'_, Self>, rhs: &PyAny) -> String { format!("{:?} + {:?}", lhs, rhs) } - fn __sub__(lhs: PyRef, rhs: &PyAny) -> String { + fn __sub__(lhs: PyRef<'_, Self>, rhs: &PyAny) -> String { format!("{:?} - {:?}", lhs, rhs) } - fn __mul__(lhs: PyRef, rhs: &PyAny) -> String { + fn __mul__(lhs: PyRef<'_, Self>, rhs: &PyAny) -> String { format!("{:?} * {:?}", lhs, rhs) } - fn __lshift__(lhs: PyRef, rhs: &PyAny) -> String { + fn __lshift__(lhs: PyRef<'_, Self>, rhs: &PyAny) -> String { format!("{:?} << {:?}", lhs, rhs) } - fn __rshift__(lhs: PyRef, rhs: &PyAny) -> String { + fn __rshift__(lhs: PyRef<'_, Self>, rhs: &PyAny) -> String { format!("{:?} >> {:?}", lhs, rhs) } - fn __and__(lhs: PyRef, rhs: &PyAny) -> String { + fn __and__(lhs: PyRef<'_, Self>, rhs: &PyAny) -> String { format!("{:?} & {:?}", lhs, rhs) } - fn __xor__(lhs: PyRef, rhs: &PyAny) -> String { + fn __xor__(lhs: PyRef<'_, Self>, rhs: &PyAny) -> String { format!("{:?} ^ {:?}", lhs, rhs) } - fn __or__(lhs: PyRef, rhs: &PyAny) -> String { + fn __or__(lhs: PyRef<'_, Self>, rhs: &PyAny) -> String { format!("{:?} | {:?}", lhs, rhs) } - fn __pow__(lhs: PyRef, rhs: &PyAny, _mod: Option) -> String { + fn __pow__(lhs: PyRef<'_, Self>, rhs: &PyAny, _mod: Option) -> String { format!("{:?} ** {:?}", lhs, rhs) } - fn __matmul__(lhs: PyRef, rhs: &PyAny) -> String { + fn __matmul__(lhs: PyRef<'_, Self>, rhs: &PyAny) -> String { format!("{:?} @ {:?}", lhs, rhs) } @@ -511,7 +544,7 @@ mod return_not_implemented { "RC_Self" } - fn __richcmp__(&self, other: PyRef, _op: CompareOp) -> PyObject { + fn __richcmp__(&self, other: PyRef<'_, Self>, _op: CompareOp) -> PyObject { other.py().None() } @@ -536,7 +569,7 @@ mod return_not_implemented { fn __mod__<'p>(slf: PyRef<'p, Self>, _other: PyRef<'p, Self>) -> PyRef<'p, Self> { slf } - fn __pow__(slf: PyRef, _other: u8, _modulo: Option) -> PyRef { + fn __pow__(slf: PyRef<'_, Self>, _other: u8, _modulo: Option) -> PyRef<'_, Self> { slf } fn __lshift__<'p>(slf: PyRef<'p, Self>, _other: PyRef<'p, Self>) -> PyRef<'p, Self> { @@ -559,19 +592,19 @@ mod return_not_implemented { } // Inplace assignments - fn __iadd__(&mut self, _other: PyRef) {} - fn __isub__(&mut self, _other: PyRef) {} - fn __imul__(&mut self, _other: PyRef) {} - fn __imatmul__(&mut self, _other: PyRef) {} - fn __itruediv__(&mut self, _other: PyRef) {} - fn __ifloordiv__(&mut self, _other: PyRef) {} - fn __imod__(&mut self, _other: PyRef) {} - fn __ilshift__(&mut self, _other: PyRef) {} - fn __irshift__(&mut self, _other: PyRef) {} - fn __iand__(&mut self, _other: PyRef) {} - fn __ior__(&mut self, _other: PyRef) {} - fn __ixor__(&mut self, _other: PyRef) {} - fn __ipow__(&mut self, _other: PyRef, _modulo: Option) {} + fn __iadd__(&mut self, _other: PyRef<'_, Self>) {} + fn __isub__(&mut self, _other: PyRef<'_, Self>) {} + fn __imul__(&mut self, _other: PyRef<'_, Self>) {} + fn __imatmul__(&mut self, _other: PyRef<'_, Self>) {} + fn __itruediv__(&mut self, _other: PyRef<'_, Self>) {} + fn __ifloordiv__(&mut self, _other: PyRef<'_, Self>) {} + fn __imod__(&mut self, _other: PyRef<'_, Self>) {} + fn __ilshift__(&mut self, _other: PyRef<'_, Self>) {} + fn __irshift__(&mut self, _other: PyRef<'_, Self>) {} + fn __iand__(&mut self, _other: PyRef<'_, Self>) {} + fn __ior__(&mut self, _other: PyRef<'_, Self>) {} + fn __ixor__(&mut self, _other: PyRef<'_, Self>) {} + fn __ipow__(&mut self, _other: PyRef<'_, Self>, _modulo: Option) {} } fn _test_binary_dunder(dunder: &str) { diff --git a/tests/test_arithmetics_protos.rs b/tests/test_arithmetics_protos.rs index b991599a..535b7567 100644 --- a/tests/test_arithmetics_protos.rs +++ b/tests/test_arithmetics_protos.rs @@ -291,7 +291,7 @@ fn rhs_arithmetic() { struct LhsAndRhs {} impl std::fmt::Debug for LhsAndRhs { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "LR") } } diff --git a/tests/test_buffer.rs b/tests/test_buffer.rs index 86a356db..7a5dc41d 100644 --- a/tests/test_buffer.rs +++ b/tests/test_buffer.rs @@ -28,7 +28,7 @@ struct TestBufferErrors { #[pymethods] impl TestBufferErrors { unsafe fn __getbuffer__( - slf: PyRefMut, + slf: PyRefMut<'_, Self>, view: *mut ffi::Py_buffer, flags: c_int, ) -> PyResult<()> { diff --git a/tests/test_buffer_protocol.rs b/tests/test_buffer_protocol.rs index 65c32e2d..a9252901 100644 --- a/tests/test_buffer_protocol.rs +++ b/tests/test_buffer_protocol.rs @@ -24,7 +24,7 @@ struct TestBufferClass { #[pymethods] impl TestBufferClass { unsafe fn __getbuffer__( - mut slf: PyRefMut, + mut slf: PyRefMut<'_, Self>, view: *mut ffi::Py_buffer, flags: c_int, ) -> PyResult<()> { diff --git a/tests/test_buffer_protocol_pyproto.rs b/tests/test_buffer_protocol_pyproto.rs index 2e540b4e..9c1239ed 100644 --- a/tests/test_buffer_protocol_pyproto.rs +++ b/tests/test_buffer_protocol_pyproto.rs @@ -1,6 +1,7 @@ #![cfg(feature = "macros")] #![cfg(feature = "pyproto")] #![cfg(any(not(Py_LIMITED_API), Py_3_11))] +#![allow(deprecated, elided_lifetimes_in_paths)] use pyo3::buffer::PyBuffer; use pyo3::class::PyBufferProtocol; diff --git a/tests/test_bytes.rs b/tests/test_bytes.rs index 710677dd..d2d47e80 100644 --- a/tests/test_bytes.rs +++ b/tests/test_bytes.rs @@ -20,7 +20,7 @@ fn test_pybytes_bytes_conversion() { } #[pyfunction] -fn bytes_vec_conversion(py: Python, bytes: Vec) -> &PyBytes { +fn bytes_vec_conversion(py: Python<'_>, bytes: Vec) -> &PyBytes { PyBytes::new(py, bytes.as_slice()) } @@ -41,3 +41,18 @@ fn test_bytearray_vec_conversion() { let f = wrap_pyfunction!(bytes_vec_conversion)(py).unwrap(); py_assert!(py, f, "f(bytearray(b'Hello World')) == b'Hello World'"); } + +#[test] +fn test_py_as_bytes() { + let pyobj: pyo3::Py; + let data: &[u8]; + + { + let gil = Python::acquire_gil(); + let py = gil.python(); + pyobj = pyo3::types::PyBytes::new(py, b"abc").into_py(py); + data = pyobj.as_bytes(py); + } + + assert_eq!(data, b"abc"); +} diff --git a/tests/test_class_conversion.rs b/tests/test_class_conversion.rs index ef7b403e..a564750a 100644 --- a/tests/test_class_conversion.rs +++ b/tests/test_class_conversion.rs @@ -24,11 +24,11 @@ fn test_cloneable_pyclass() { let c2: Cloneable = py_c.extract(py).unwrap(); assert_eq!(c, c2); { - let rc: PyRef = py_c.extract(py).unwrap(); + let rc: PyRef<'_, Cloneable> = py_c.extract(py).unwrap(); assert_eq!(&c, &*rc); // Drops PyRef before taking PyRefMut } - let mrc: PyRefMut = py_c.extract(py).unwrap(); + let mrc: PyRefMut<'_, Cloneable> = py_c.extract(py).unwrap(); assert_eq!(&c, &*mrc); } @@ -135,16 +135,16 @@ fn test_pyref_as_base() { let cell = PyCell::new(py, (SubClass {}, BaseClass { value: 120 })).unwrap(); // First try PyRefMut - let sub: PyRefMut = cell.borrow_mut(); - let mut base: PyRefMut = sub.into_super(); + let sub: PyRefMut<'_, SubClass> = cell.borrow_mut(); + let mut base: PyRefMut<'_, BaseClass> = sub.into_super(); assert_eq!(120, base.value); base.value = 999; assert_eq!(999, base.value); drop(base); // Repeat for PyRef - let sub: PyRef = cell.borrow(); - let base: PyRef = sub.into_super(); + let sub: PyRef<'_, SubClass> = cell.borrow(); + let base: PyRef<'_, BaseClass> = sub.into_super(); assert_eq!(999, base.value); } diff --git a/tests/test_compile_error.rs b/tests/test_compile_error.rs index db86f230..510561a9 100644 --- a/tests/test_compile_error.rs +++ b/tests/test_compile_error.rs @@ -7,6 +7,7 @@ fn test_compile_errors() { _test_compile_errors() } +#[cfg(not(feature = "nightly"))] #[rustversion::nightly] #[test] fn test_compile_errors() { @@ -15,9 +16,20 @@ fn test_compile_errors() { let _ = std::panic::catch_unwind(_test_compile_errors); } +#[cfg(feature = "nightly")] +#[rustversion::nightly] +#[test] +fn test_compile_errors() { + // nightly - don't care if test output is potentially wrong, to avoid churn in PyO3's CI thanks + // to diagnostics changing on nightly. + _test_compile_errors() +} + +#[cfg(not(feature = "nightly"))] fn _test_compile_errors() { let t = trybuild::TestCases::new(); t.compile_fail("tests/ui/invalid_immutable_pyclass_borrow.rs"); + t.compile_fail("tests/ui/invalid_macro_args.rs"); t.compile_fail("tests/ui/invalid_need_module_arg_position.rs"); t.compile_fail("tests/ui/invalid_property_args.rs"); @@ -74,6 +86,8 @@ fn _test_compile_errors() { t.compile_fail("tests/ui/invalid_pymethods.rs"); t.compile_fail("tests/ui/missing_clone.rs"); t.compile_fail("tests/ui/not_send.rs"); + t.compile_fail("tests/ui/not_send2.rs"); + t.compile_fail("tests/ui/not_send3.rs"); #[cfg(Py_LIMITED_API)] t.compile_fail("tests/ui/abi3_nativetype_inheritance.rs"); } @@ -81,3 +95,12 @@ fn _test_compile_errors() { #[rustversion::before(1.58)] fn tests_rust_1_58(_t: &trybuild::TestCases) {} } + +#[cfg(feature = "nightly")] +fn _test_compile_errors() { + let t = trybuild::TestCases::new(); + + t.compile_fail("tests/ui/not_send_auto_trait.rs"); + t.compile_fail("tests/ui/not_send_auto_trait2.rs"); + t.compile_fail("tests/ui/send_wrapper.rs"); +} diff --git a/tests/test_datetime.rs b/tests/test_datetime.rs index 976c04ef..458e15d3 100644 --- a/tests/test_datetime.rs +++ b/tests/test_datetime.rs @@ -5,14 +5,14 @@ use pyo3::types::IntoPyDict; use pyo3_ffi::PyDateTime_IMPORT; fn _get_subclasses<'p>( - py: &'p Python, + py: Python<'p>, py_type: &str, args: &str, ) -> PyResult<(&'p PyAny, &'p PyAny, &'p PyAny)> { // Import the class from Python and create some subclasses let datetime = py.import("datetime")?; - let locals = [(py_type, datetime.getattr(py_type)?)].into_py_dict(*py); + let locals = [(py_type, datetime.getattr(py_type)?)].into_py_dict(py); let make_subclass_py = format!("class Subklass({}):\n pass", py_type); @@ -57,7 +57,7 @@ macro_rules! assert_check_only { fn test_date_check() { let gil = Python::acquire_gil(); let py = gil.python(); - let (obj, sub_obj, sub_sub_obj) = _get_subclasses(&py, "date", "2018, 1, 1").unwrap(); + let (obj, sub_obj, sub_sub_obj) = _get_subclasses(py, "date", "2018, 1, 1").unwrap(); unsafe { PyDateTime_IMPORT() } assert_check_exact!(PyDate_Check, PyDate_CheckExact, obj); assert_check_only!(PyDate_Check, PyDate_CheckExact, sub_obj); @@ -68,7 +68,7 @@ fn test_date_check() { fn test_time_check() { let gil = Python::acquire_gil(); let py = gil.python(); - let (obj, sub_obj, sub_sub_obj) = _get_subclasses(&py, "time", "12, 30, 15").unwrap(); + let (obj, sub_obj, sub_sub_obj) = _get_subclasses(py, "time", "12, 30, 15").unwrap(); unsafe { PyDateTime_IMPORT() } assert_check_exact!(PyTime_Check, PyTime_CheckExact, obj); @@ -80,7 +80,7 @@ fn test_time_check() { fn test_datetime_check() { let gil = Python::acquire_gil(); let py = gil.python(); - let (obj, sub_obj, sub_sub_obj) = _get_subclasses(&py, "datetime", "2018, 1, 1, 13, 30, 15") + let (obj, sub_obj, sub_sub_obj) = _get_subclasses(py, "datetime", "2018, 1, 1, 13, 30, 15") .map_err(|e| e.print(py)) .unwrap(); unsafe { PyDateTime_IMPORT() } @@ -95,7 +95,7 @@ fn test_datetime_check() { fn test_delta_check() { let gil = Python::acquire_gil(); let py = gil.python(); - let (obj, sub_obj, sub_sub_obj) = _get_subclasses(&py, "timedelta", "1, -3").unwrap(); + let (obj, sub_obj, sub_sub_obj) = _get_subclasses(py, "timedelta", "1, -3").unwrap(); unsafe { PyDateTime_IMPORT() } assert_check_exact!(PyDelta_Check, PyDelta_CheckExact, obj); diff --git a/tests/test_exceptions.rs b/tests/test_exceptions.rs index 50d0f0c7..da42f1b5 100644 --- a/tests/test_exceptions.rs +++ b/tests/test_exceptions.rs @@ -41,7 +41,7 @@ struct CustomError; impl Error for CustomError {} impl fmt::Display for CustomError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "Oh no!") } } diff --git a/tests/test_frompyobject.rs b/tests/test_frompyobject.rs index 17428ced..7215ba82 100644 --- a/tests/test_frompyobject.rs +++ b/tests/test_frompyobject.rs @@ -10,7 +10,7 @@ mod common; /// Helper function that concatenates the error message from /// each error in the traceback into a single string that can /// be tested. -fn extract_traceback(py: Python, mut error: PyErr) -> String { +fn extract_traceback(py: Python<'_>, mut error: PyErr) -> String { let mut error_msg = error.to_string(); while let Some(cause) = error.cause(py) { error_msg.push_str(": "); @@ -57,7 +57,8 @@ 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: A<'_> = + FromPyObject::extract(py_c.as_ref(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()); @@ -481,3 +482,40 @@ fn test_from_py_with() { assert_eq!(zap.some_object_length, 3usize); }); } + +#[derive(Debug, FromPyObject)] +pub struct ZapTuple(String, #[pyo3(from_py_with = "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) + .expect("failed to create tuple"); + + let zap = ZapTuple::extract(py_zap).unwrap(); + + assert_eq!(zap.0, "whatever"); + assert_eq!(zap.1, 3usize); + }); +} + +#[derive(Debug, FromPyObject, PartialEq)] +pub enum ZapEnum { + Zip(#[pyo3(from_py_with = "PyAny::len")] usize), + Zap(String, #[pyo3(from_py_with = "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) + .expect("failed to create tuple"); + + let zap = ZapEnum::extract(py_zap).unwrap(); + let expected_zap = ZapEnum::Zap(String::from("whatever"), 3usize); + + assert_eq!(zap, expected_zap); + }); +} diff --git a/tests/test_gc.rs b/tests/test_gc.rs index 92854473..7f7e5bcf 100644 --- a/tests/test_gc.rs +++ b/tests/test_gc.rs @@ -1,7 +1,5 @@ #![cfg(feature = "macros")] -#![cfg(feature = "pyproto")] // FIXME: #[pymethods] to support gc protocol -use pyo3::class::PyGCProtocol; use pyo3::class::PyTraverseError; use pyo3::class::PyVisit; use pyo3::prelude::*; @@ -90,9 +88,9 @@ struct GcIntegration { dropped: TestDropCall, } -#[pyproto] -impl PyGCProtocol for GcIntegration { - fn __traverse__(&self, visit: PyVisit) -> Result<(), PyTraverseError> { +#[pymethods] +impl GcIntegration { + fn __traverse__(&self, visit: PyVisit<'_>) -> Result<(), PyTraverseError> { visit.call(&self.self_ref) } @@ -122,6 +120,8 @@ fn gc_integration() { let mut borrow = inst.borrow_mut(); borrow.self_ref = inst.to_object(py); + + py_run!(py, inst, "import gc; assert inst in gc.get_objects()"); } let gil = Python::acquire_gil(); @@ -130,25 +130,6 @@ fn gc_integration() { assert!(drop_called.load(Ordering::Relaxed)); } -#[pyclass(gc)] -struct GcIntegration2 {} - -#[pyproto] -impl PyGCProtocol for GcIntegration2 { - fn __traverse__(&self, _visit: PyVisit) -> Result<(), PyTraverseError> { - Ok(()) - } - fn __clear__(&mut self) {} -} - -#[test] -fn gc_integration2() { - let gil = Python::acquire_gil(); - let py = gil.python(); - let inst = PyCell::new(py, GcIntegration2 {}).unwrap(); - py_run!(py, inst, "import gc; assert inst in gc.get_objects()"); -} - #[pyclass(subclass)] struct BaseClassWithDrop { data: Option>, @@ -206,7 +187,7 @@ fn inheritance_with_new_methods_with_drop() { let typeobj = py.get_type::(); let inst = typeobj.call((), None).unwrap(); - let obj: &PyCell = inst.try_into().unwrap(); + let obj: &PyCell = PyTryInto::try_into(&*inst).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(); @@ -217,7 +198,7 @@ fn inheritance_with_new_methods_with_drop() { assert!(drop_called2.load(Ordering::Relaxed)); } -#[pyclass(gc)] +#[pyclass] struct TraversableClass { traversed: AtomicBool, } @@ -230,10 +211,12 @@ impl TraversableClass { } } -#[pyproto] -impl PyGCProtocol for TraversableClass { +#[pymethods] +impl TraversableClass { fn __clear__(&mut self) {} - fn __traverse__(&self, _visit: PyVisit) -> Result<(), PyTraverseError> { + + #[allow(clippy::unnecessary_wraps)] + fn __traverse__(&self, _visit: PyVisit<'_>) -> Result<(), PyTraverseError> { self.traversed.store(true, Ordering::Relaxed); Ok(()) } @@ -280,3 +263,47 @@ fn gc_during_borrow() { drop(guard); } } + +#[pyclass] +struct PanickyTraverse { + member: PyObject, +} + +impl PanickyTraverse { + fn new(py: Python<'_>) -> Self { + Self { member: py.None() } + } +} + +#[pymethods] +impl PanickyTraverse { + fn __traverse__(&self, visit: PyVisit<'_>) -> Result<(), PyTraverseError> { + visit.call(&self.member)?; + // In the test, we expect this to never be hit + unreachable!() + } +} + +#[test] +fn traverse_error() { + Python::with_gil(|py| unsafe { + // declare a visitor function which errors (returns nonzero code) + extern "C" fn visit_error( + _object: *mut pyo3::ffi::PyObject, + _arg: *mut core::ffi::c_void, + ) -> std::os::raw::c_int { + -1 + } + + // get the traverse function + let ty = PanickyTraverse::type_object(py).as_type_ptr(); + let traverse = get_type_traverse(ty).unwrap(); + + // confirm that traversing errors + let obj = Py::new(py, PanickyTraverse::new(py)).unwrap(); + assert_eq!( + traverse(obj.as_ptr(), visit_error, std::ptr::null_mut()), + -1 + ); + }) +} diff --git a/tests/test_gc_pyproto.rs b/tests/test_gc_pyproto.rs index cf6793b7..76419751 100644 --- a/tests/test_gc_pyproto.rs +++ b/tests/test_gc_pyproto.rs @@ -1,5 +1,6 @@ #![cfg(feature = "macros")] #![cfg(feature = "pyproto")] +#![allow(deprecated, elided_lifetimes_in_paths)] use pyo3::class::PyGCProtocol; use pyo3::class::PyTraverseError; @@ -130,7 +131,7 @@ fn gc_integration() { assert!(drop_called.load(Ordering::Relaxed)); } -#[pyclass(gc)] +#[pyclass] struct GcIntegration2 {} #[pyproto] @@ -206,7 +207,7 @@ fn inheritance_with_new_methods_with_drop() { let typeobj = py.get_type::(); let inst = typeobj.call((), None).unwrap(); - let obj: &PyCell = inst.try_into().unwrap(); + let obj: &PyCell = PyTryInto::try_into(&*inst).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(); @@ -217,7 +218,7 @@ fn inheritance_with_new_methods_with_drop() { assert!(drop_called2.load(Ordering::Relaxed)); } -#[pyclass(gc)] +#[pyclass] struct TraversableClass { traversed: AtomicBool, } diff --git a/tests/test_getter_setter.rs b/tests/test_getter_setter.rs index 685c0c79..3cfb28c2 100644 --- a/tests/test_getter_setter.rs +++ b/tests/test_getter_setter.rs @@ -113,12 +113,12 @@ struct RefGetterSetter { #[pymethods] impl RefGetterSetter { #[getter] - fn get_num(slf: PyRef) -> i32 { + fn get_num(slf: PyRef<'_, Self>) -> i32 { slf.num } #[setter] - fn set_num(mut slf: PyRefMut, value: i32) { + fn set_num(mut slf: PyRefMut<'_, Self>, value: i32) { slf.num = value; } } diff --git a/tests/test_mapping.rs b/tests/test_mapping.rs index 0e65f60d..2f916d8e 100644 --- a/tests/test_mapping.rs +++ b/tests/test_mapping.rs @@ -58,7 +58,7 @@ impl Mapping { } /// Return a dict with `m = Mapping(['1', '2', '3'])`. -fn map_dict(py: Python) -> &pyo3::types::PyDict { +fn map_dict(py: Python<'_>) -> &pyo3::types::PyDict { let d = [("Mapping", py.get_type::())].into_py_dict(py); py_run!(py, *d, "m = Mapping(['1', '2', '3'])"); d diff --git a/tests/test_mapping_pyproto.rs b/tests/test_mapping_pyproto.rs index f82b876a..40e07bd2 100644 --- a/tests/test_mapping_pyproto.rs +++ b/tests/test_mapping_pyproto.rs @@ -1,5 +1,6 @@ #![cfg(feature = "macros")] #![cfg(feature = "pyproto")] +#![allow(deprecated, elided_lifetimes_in_paths)] use std::collections::HashMap; @@ -63,7 +64,7 @@ impl PyMappingProtocol for Mapping { } /// Return a dict with `m = Mapping(['1', '2', '3'])`. -fn map_dict(py: Python) -> &pyo3::types::PyDict { +fn map_dict(py: Python<'_>) -> &pyo3::types::PyDict { let d = [("Mapping", py.get_type::())].into_py_dict(py); py_run!(py, *d, "m = Mapping(['1', '2', '3'])"); d diff --git a/tests/test_methods.rs b/tests/test_methods.rs index ac401f03..6b1b3bf1 100644 --- a/tests/test_methods.rs +++ b/tests/test_methods.rs @@ -127,7 +127,7 @@ impl StaticMethod { #[staticmethod] /// Test static method. - fn method(_py: Python) -> &'static str { + fn method(_py: Python<'_>) -> &'static str { "StaticMethod.method()!" } } @@ -152,7 +152,7 @@ struct StaticMethodWithArgs {} #[pymethods] impl StaticMethodWithArgs { #[staticmethod] - fn method(_py: Python, input: i32) -> String { + fn method(_py: Python<'_>, input: i32) -> String { format!("0x{:x}", input) } } @@ -202,14 +202,14 @@ impl MethArgs { test } #[args(args = "*", kwargs = "**")] - fn get_kwargs(&self, py: Python, args: &PyTuple, kwargs: Option<&PyDict>) -> PyObject { + fn get_kwargs(&self, py: Python<'_>, args: &PyTuple, kwargs: Option<&PyDict>) -> PyObject { [args.into(), kwargs.to_object(py)].to_object(py) } #[args(args = "*", kwargs = "**")] fn get_pos_arg_kw( &self, - py: Python, + py: Python<'_>, a: i32, args: &PyTuple, kwargs: Option<&PyDict>, @@ -253,7 +253,12 @@ impl MethArgs { } #[args(a, "/", kwargs = "**")] - fn get_pos_only_with_kwargs(&self, py: Python, a: i32, kwargs: Option<&PyDict>) -> PyObject { + fn get_pos_only_with_kwargs( + &self, + py: Python<'_>, + a: i32, + kwargs: Option<&PyDict>, + ) -> PyObject { [a.to_object(py), kwargs.to_object(py)].to_object(py) } @@ -273,7 +278,7 @@ impl MethArgs { } #[args(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: &PyTuple, a: i32) -> PyObject { (args, a).to_object(py) } @@ -288,7 +293,7 @@ impl MethArgs { } #[args(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<&PyDict>) -> PyObject { [a.to_object(py), kwargs.to_object(py)].to_object(py) } // "args" can be anything that can be extracted from PyTuple @@ -690,7 +695,7 @@ impl MethodWithPyClassArg { value: self.value + other.value, } } - fn add_pyref(&self, other: PyRef) -> MethodWithPyClassArg { + fn add_pyref(&self, other: PyRef<'_, MethodWithPyClassArg>) -> MethodWithPyClassArg { MethodWithPyClassArg { value: self.value + other.value, } @@ -698,7 +703,7 @@ impl MethodWithPyClassArg { fn inplace_add(&self, other: &mut MethodWithPyClassArg) { other.value += self.value; } - fn inplace_add_pyref(&self, mut other: PyRefMut) { + fn inplace_add_pyref(&self, mut other: PyRefMut<'_, MethodWithPyClassArg>) { other.value += self.value; } fn optional_add(&self, other: Option<&MethodWithPyClassArg>) -> MethodWithPyClassArg { @@ -838,16 +843,16 @@ struct r#RawIdents { impl r#RawIdents { #[new] pub fn r#new( - r#_py: Python, + r#_py: Python<'_>, r#type: PyObject, r#subtype: PyObject, r#subsubtype: PyObject, - ) -> PyResult { - Ok(Self { + ) -> Self { + Self { r#type, r#subtype, r#subsubtype, - }) + } } #[getter(r#subtype)] diff --git a/tests/test_module.rs b/tests/test_module.rs index 6bb849b0..7364d7ec 100644 --- a/tests/test_module.rs +++ b/tests/test_module.rs @@ -33,7 +33,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(_py: Python<'_>, m: &PyModule) -> PyResult<()> { #[pyfn(m)] #[pyo3(name = "no_parameters")] fn function_with_name() -> usize { @@ -116,7 +116,7 @@ fn test_module_with_functions() { #[pymodule] #[pyo3(name = "other_name")] -fn some_name(_: Python, m: &PyModule) -> PyResult<()> { +fn some_name(_: Python<'_>, m: &PyModule) -> PyResult<()> { m.add("other_name", "other_name")?; Ok(()) } @@ -166,7 +166,7 @@ fn r#move() -> usize { } #[pymodule] -fn raw_ident_module(_py: Python, module: &PyModule) -> PyResult<()> { +fn raw_ident_module(_py: Python<'_>, module: &PyModule) -> PyResult<()> { module.add_function(wrap_pyfunction!(r#move, module)?) } @@ -189,7 +189,7 @@ fn custom_named_fn() -> usize { } #[pymodule] -fn foobar_module(_py: Python, m: &PyModule) -> PyResult<()> { +fn foobar_module(_py: Python<'_>, m: &PyModule) -> PyResult<()> { m.add_function(wrap_pyfunction!(custom_named_fn, m)?)?; m.dict().set_item("yay", "me")?; Ok(()) @@ -235,7 +235,7 @@ fn submodule(module: &PyModule) -> PyResult<()> { } #[pymodule] -fn submodule_with_init_fn(_py: Python, module: &PyModule) -> PyResult<()> { +fn submodule_with_init_fn(_py: Python<'_>, module: &PyModule) -> PyResult<()> { module.add_function(wrap_pyfunction!(subfunction, module)?)?; Ok(()) } @@ -246,7 +246,7 @@ fn superfunction() -> String { } #[pymodule] -fn supermodule(py: Python, module: &PyModule) -> PyResult<()> { +fn supermodule(py: Python<'_>, module: &PyModule) -> PyResult<()> { module.add_function(wrap_pyfunction!(superfunction, module)?)?; let module_to_add = PyModule::new(py, "submodule")?; submodule(module_to_add)?; @@ -285,14 +285,14 @@ fn test_module_nesting() { // Test that argument parsing specification works for pyfunctions #[pyfunction(a = 5, vararg = "*")] -fn ext_vararg_fn(py: Python, a: i32, vararg: &PyTuple) -> PyObject { +fn ext_vararg_fn(py: Python<'_>, a: i32, vararg: &PyTuple) -> PyObject { [a.to_object(py), vararg.into()].to_object(py) } #[pymodule] -fn vararg_module(_py: Python, m: &PyModule) -> PyResult<()> { +fn vararg_module(_py: Python<'_>, m: &PyModule) -> PyResult<()> { #[pyfn(m, a = 5, vararg = "*")] - fn int_vararg_fn(py: Python, a: i32, vararg: &PyTuple) -> PyObject { + fn int_vararg_fn(py: Python<'_>, a: i32, vararg: &PyTuple) -> PyObject { ext_vararg_fn(py, a, vararg) } @@ -318,7 +318,7 @@ fn test_module_with_constant() { // Regression test for #1102 #[pymodule] - fn module_with_constant(_py: Python, m: &PyModule) -> PyResult<()> { + fn module_with_constant(_py: Python<'_>, m: &PyModule) -> PyResult<()> { const ANON: AnonClass = AnonClass {}; m.add("ANON", ANON)?; @@ -382,7 +382,7 @@ fn pyfunction_with_pass_module_in_attribute(module: &PyModule) -> PyResult<&str> } #[pymodule] -fn module_with_functions_with_module(_py: Python, m: &PyModule) -> PyResult<()> { +fn module_with_functions_with_module(_py: Python<'_>, m: &PyModule) -> PyResult<()> { m.add_function(wrap_pyfunction!(pyfunction_with_module, m)?)?; m.add_function(wrap_pyfunction!(pyfunction_with_module_and_py, m)?)?; m.add_function(wrap_pyfunction!(pyfunction_with_module_and_arg, m)?)?; @@ -432,8 +432,9 @@ fn test_module_functions_with_module() { #[test] fn test_module_doc_hidden() { #[doc(hidden)] + #[allow(clippy::unnecessary_wraps)] #[pymodule] - fn my_module(_py: Python, _m: &PyModule) -> PyResult<()> { + fn my_module(_py: Python<'_>, _m: &PyModule) -> PyResult<()> { Ok(()) } diff --git a/tests/test_proto_methods.rs b/tests/test_proto_methods.rs index a6d621c9..68881d6f 100644 --- a/tests/test_proto_methods.rs +++ b/tests/test_proto_methods.rs @@ -1,9 +1,8 @@ #![cfg(feature = "macros")] -use pyo3::exceptions::{PyIndexError, PyValueError}; +use pyo3::exceptions::{PyAttributeError, PyIndexError, PyValueError}; use pyo3::types::{PyDict, PyList, PyMapping, PySequence, PySlice, PyType}; -use pyo3::{exceptions::PyAttributeError, prelude::*}; -use pyo3::{py_run, PyCell}; +use pyo3::{prelude::*, py_run, PyCell}; use std::{isize, iter}; mod common; @@ -20,7 +19,7 @@ struct ExampleClass { #[pymethods] impl ExampleClass { - fn __getattr__(&self, py: Python, attr: &str) -> PyResult { + fn __getattr__(&self, py: Python<'_>, attr: &str) -> PyResult { if attr == "special_custom_attr" { Ok(self._custom_attr.into_py(py)) } else { @@ -64,7 +63,7 @@ impl ExampleClass { } } -fn make_example(py: Python) -> &PyCell { +fn make_example(py: Python<'_>) -> &PyCell { Py::new( py, ExampleClass { @@ -190,7 +189,7 @@ pub struct Mapping { #[pymethods] impl Mapping { - fn __len__(&self, py: Python) -> usize { + fn __len__(&self, py: Python<'_>) -> usize { self.values.as_ref(py).len() } @@ -263,7 +262,7 @@ impl Sequence { self.values.len() } - fn __getitem__(&self, index: SequenceIndex) -> PyResult { + fn __getitem__(&self, index: SequenceIndex<'_>) -> PyResult { match index { SequenceIndex::Integer(index) => { let uindex = self.usize_index(index)?; @@ -367,11 +366,11 @@ struct Iterator { #[pymethods] impl Iterator { - fn __iter__(slf: PyRef) -> PyRef { + fn __iter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> { slf } - fn __next__(mut slf: PyRefMut) -> Option { + fn __next__(mut slf: PyRefMut<'_, Self>) -> Option { slf.iter.next() } } @@ -606,8 +605,66 @@ fn getattr_doesnt_override_member() { py_assert!(py, inst, "inst.a == 8"); } +#[pyclass] +struct ClassWithGetAttribute { + #[pyo3(get, set)] + data: u32, +} + +#[pymethods] +impl ClassWithGetAttribute { + fn __getattribute__(&self, _name: &str) -> u32 { + self.data * 2 + } +} + +#[test] +fn getattribute_overrides_member() { + let gil = Python::acquire_gil(); + let py = gil.python(); + let inst = PyCell::new(py, ClassWithGetAttribute { data: 4 }).unwrap(); + py_assert!(py, inst, "inst.data == 8"); + py_assert!(py, inst, "inst.y == 8"); +} + +#[pyclass] +struct ClassWithGetAttrAndGetAttribute; + +#[pymethods] +impl ClassWithGetAttrAndGetAttribute { + fn __getattribute__(&self, name: &str) -> PyResult { + if name == "exists" { + Ok(42) + } else if name == "error" { + Err(PyValueError::new_err("bad")) + } else { + Err(PyAttributeError::new_err("fallback")) + } + } + + fn __getattr__(&self, name: &str) -> PyResult { + if name == "lucky" { + Ok(57) + } else { + Err(PyAttributeError::new_err("no chance")) + } + } +} + +#[test] +fn getattr_and_getattribute() { + let gil = Python::acquire_gil(); + let py = gil.python(); + let inst = PyCell::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); + py_expect_exception!(py, inst, "inst.unlucky", PyAttributeError); +} + /// Wraps a Python future and yield it once. #[pyclass] +#[derive(Debug)] struct OnceFuture { future: PyObject, polled: bool, @@ -623,14 +680,14 @@ impl OnceFuture { } } - fn __await__(slf: PyRef) -> PyRef { + fn __await__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> { slf } - fn __iter__(slf: PyRef) -> PyRef { + fn __iter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> { slf } - fn __next__(mut slf: PyRefMut) -> Option { + fn __next__(mut slf: PyRefMut<'_, Self>) -> Option { if !slf.polled { slf.polled = true; Some(slf.future.clone()) @@ -645,24 +702,20 @@ fn test_await() { let gil = Python::acquire_gil(); let py = gil.python(); let once = py.get_type::(); - let source = pyo3::indoc::indoc!( - r#" + let source = r#" import asyncio import sys async def main(): res = await Once(await asyncio.sleep(0.1)) - return res + assert res is None + # For an odd error similar to https://bugs.python.org/issue38563 if sys.platform == "win32" and sys.version_info >= (3, 8, 0): asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) -# get_event_loop can raise an error: https://github.com/PyO3/pyo3/pull/961#issuecomment-645238579 -loop = asyncio.new_event_loop() -asyncio.set_event_loop(loop) -assert loop.run_until_complete(main()) is None -loop.close() -"# - ); + +asyncio.run(main()) +"#; let globals = PyModule::import(py, "__main__").unwrap().dict(); globals.set_item("Once", once).unwrap(); py.run(source, Some(globals), None) @@ -670,6 +723,62 @@ loop.close() .unwrap(); } +#[pyclass] +struct AsyncIterator { + future: Option>, +} + +#[pymethods] +impl AsyncIterator { + #[new] + fn new(future: Py) -> Self { + Self { + future: Some(future), + } + } + + fn __aiter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> { + slf + } + + fn __anext__(&mut self) -> Option> { + self.future.take() + } +} + +#[test] +fn test_anext_aiter() { + let gil = Python::acquire_gil(); + let py = gil.python(); + let once = py.get_type::(); + let source = r#" +import asyncio +import sys + +async def main(): + count = 0 + async for result in AsyncIterator(Once(await asyncio.sleep(0.1))): + # The Once is awaited as part of the `async for` and produces None + assert result is None + count +=1 + assert count == 1 + +# For an odd error similar to https://bugs.python.org/issue38563 +if sys.platform == "win32" and sys.version_info >= (3, 8, 0): + asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) + +asyncio.run(main()) +"#; + let globals = PyModule::import(py, "__main__").unwrap().dict(); + globals.set_item("Once", once).unwrap(); + globals + .set_item("AsyncIterator", py.get_type::()) + .unwrap(); + py.run(source, Some(globals), None) + .map_err(|e| e.print(py)) + .unwrap(); +} + /// Increment the count when `__get__` is called. #[pyclass] struct DescrCounter { @@ -767,7 +876,7 @@ struct DefaultedContains; #[pymethods] impl DefaultedContains { - fn __iter__(&self, py: Python) -> PyObject { + fn __iter__(&self, py: Python<'_>) -> PyObject { PyList::new(py, &["a", "b", "c"]) .as_ref() .iter() @@ -781,7 +890,7 @@ struct NoContains; #[pymethods] impl NoContains { - fn __iter__(&self, py: Python) -> PyObject { + fn __iter__(&self, py: Python<'_>) -> PyObject { PyList::new(py, &["a", "b", "c"]) .as_ref() .iter() diff --git a/tests/test_pyfunction.rs b/tests/test_pyfunction.rs index 930fe780..ab80d4a0 100644 --- a/tests/test_pyfunction.rs +++ b/tests/test_pyfunction.rs @@ -29,7 +29,7 @@ fn test_optional_bool() { #[cfg(not(Py_LIMITED_API))] #[pyfunction] -fn buffer_inplace_add(py: Python, x: PyBuffer, y: PyBuffer) { +fn buffer_inplace_add(py: Python<'_>, x: PyBuffer, y: PyBuffer) { let x = x.as_mut_slice(py).unwrap(); let y = y.as_slice(py).unwrap(); for (xi, yi) in x.iter().zip(y) { @@ -165,11 +165,24 @@ fn test_function_with_custom_conversion_error() { ); } +#[pyclass] +#[derive(Debug, FromPyObject)] +struct ValueClass { + #[pyo3(get)] + value: usize, +} + #[pyfunction] -fn conversion_error(str_arg: &str, int_arg: i64, tuple_arg: (&str, f64), option_arg: Option) { +fn conversion_error( + str_arg: &str, + int_arg: i64, + tuple_arg: (&str, f64), + option_arg: Option, + struct_arg: Option, +) { println!( - "{:?} {:?} {:?} {:?}", - str_arg, int_arg, tuple_arg, option_arg + "{:?} {:?} {:?} {:?} {:?}", + str_arg, int_arg, tuple_arg, option_arg, struct_arg ); } @@ -182,38 +195,82 @@ fn test_conversion_error() { py_expect_exception!( py, conversion_error, - "conversion_error(None, None, None, None)", + "conversion_error(None, None, None, None, None)", PyTypeError, "argument 'str_arg': 'NoneType' object cannot be converted to 'PyString'" ); py_expect_exception!( py, conversion_error, - "conversion_error(100, None, None, None)", + "conversion_error(100, None, None, None, None)", PyTypeError, "argument 'str_arg': 'int' object cannot be converted to 'PyString'" ); py_expect_exception!( py, conversion_error, - "conversion_error('string1', 'string2', None, None)", + "conversion_error('string1', 'string2', None, None, None)", PyTypeError, "argument 'int_arg': 'str' object cannot be interpreted as an integer" ); py_expect_exception!( py, conversion_error, - "conversion_error('string1', -100, 'string2', None)", + "conversion_error('string1', -100, 'string2', None, None)", PyTypeError, "argument 'tuple_arg': 'str' object cannot be converted to 'PyTuple'" ); py_expect_exception!( py, conversion_error, - "conversion_error('string1', -100, ('string2', 10.), 'string3')", + "conversion_error('string1', -100, ('string2', 10.), 'string3', None)", PyTypeError, "argument 'option_arg': 'str' object cannot be interpreted as an integer" ); + let exception = py_expect_exception!( + py, + conversion_error, + " +class ValueClass: + def __init__(self, value): + self.value = value +conversion_error('string1', -100, ('string2', 10.), None, ValueClass(\"no_expected_type\"))", + PyTypeError + ); + assert_eq!( + extract_traceback(py, exception), + "TypeError: argument 'struct_arg': failed to \ + extract field ValueClass.value: TypeError: 'str' object cannot be interpreted as an integer" + ); + + let exception = py_expect_exception!( + py, + conversion_error, + " +class ValueClass: + def __init__(self, value): + self.value = value +conversion_error('string1', -100, ('string2', 10.), None, ValueClass(-5))", + PyTypeError + ); + assert_eq!( + extract_traceback(py, exception), + "TypeError: argument 'struct_arg': failed to \ + extract field ValueClass.value: OverflowError: can't convert negative int to unsigned" + ); +} + +/// Helper function that concatenates the error message from +/// each error in the traceback into a single string that can +/// be tested. +fn extract_traceback(py: Python<'_>, mut error: PyErr) -> String { + let mut error_msg = error.to_string(); + while let Some(cause) = error.cause(py) { + error_msg.push_str(": "); + error_msg.push_str(&cause.to_string()); + error = cause + } + error_msg } #[test] diff --git a/tests/test_pyproto.rs b/tests/test_pyproto.rs index 0d963c1a..240c52ed 100644 --- a/tests/test_pyproto.rs +++ b/tests/test_pyproto.rs @@ -1,5 +1,6 @@ #![cfg(feature = "macros")] #![cfg(feature = "pyproto")] +#![allow(deprecated)] use pyo3::class::{ PyAsyncProtocol, PyDescrProtocol, PyIterProtocol, PyMappingProtocol, PyObjectProtocol, diff --git a/tests/test_pyself.rs b/tests/test_pyself.rs index 48196300..5195f5e1 100644 --- a/tests/test_pyself.rs +++ b/tests/test_pyself.rs @@ -32,9 +32,9 @@ impl Reader { } } fn get_iter_and_reset( - mut slf: PyRefMut, + mut slf: PyRefMut<'_, Self>, keys: Py, - py: Python, + py: Python<'_>, ) -> PyResult { let reader = Py::new(py, slf.clone())?; slf.inner.clear(); @@ -57,11 +57,11 @@ struct Iter { #[pymethods] impl Iter { #[allow(clippy::self_named_constructors)] - fn __iter__(slf: PyRef) -> PyRef { + fn __iter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> { slf } - fn __next__(mut slf: PyRefMut) -> PyResult> { + fn __next__(mut slf: PyRefMut<'_, Self>) -> PyResult> { let bytes = slf.keys.as_ref(slf.py()).as_bytes(); match bytes.get(slf.idx) { Some(&b) => { diff --git a/tests/test_sequence.rs b/tests/test_sequence.rs index e156b9ff..c87247a2 100644 --- a/tests/test_sequence.rs +++ b/tests/test_sequence.rs @@ -1,7 +1,5 @@ #![cfg(feature = "macros")] -#![cfg(feature = "pyproto")] // FIXME: change this to use #[pymethods] once supports sequence protocol -use pyo3::class::PySequenceProtocol; use pyo3::exceptions::{PyIndexError, PyValueError}; use pyo3::prelude::*; use pyo3::types::{IntoPyDict, PyList}; @@ -21,7 +19,7 @@ impl ByteSequence { fn new(elements: Option<&PyList>) -> PyResult { if let Some(pylist) = elements { let mut elems = Vec::with_capacity(pylist.len()); - for pyelem in pylist.into_iter() { + for pyelem in pylist { let elem = u8::extract(pyelem)?; elems.push(elem); } @@ -32,10 +30,7 @@ impl ByteSequence { }) } } -} -#[pyproto] -impl PySequenceProtocol for ByteSequence { fn __len__(&self) -> usize { self.elements.len() } @@ -51,8 +46,12 @@ impl PySequenceProtocol for ByteSequence { self.elements[idx as usize] = value; } - fn __delitem__(&mut self, idx: isize) -> PyResult<()> { - if (idx < self.elements.len() as isize) && (idx >= 0) { + fn __delitem__(&mut self, mut idx: isize) -> PyResult<()> { + let self_len = self.elements.len() as isize; + if idx < 0 { + idx += self_len; + } + if (idx < self_len) && (idx >= 0) { self.elements.remove(idx as usize); Ok(()) } else { @@ -67,12 +66,17 @@ impl PySequenceProtocol for ByteSequence { } } - fn __concat__(&self, other: PyRef<'p, Self>) -> Self { + fn __concat__(&self, other: &Self) -> Self { let mut elements = self.elements.clone(); elements.extend_from_slice(&other.elements); Self { elements } } + fn __inplace_concat__(mut slf: PyRefMut<'_, Self>, other: &Self) -> Py { + slf.elements.extend_from_slice(&other.elements); + slf.into() + } + fn __repeat__(&self, count: isize) -> PyResult { if count >= 0 { let mut elements = Vec::with_capacity(self.elements.len() * count as usize); @@ -84,10 +88,23 @@ impl PySequenceProtocol for ByteSequence { Err(PyValueError::new_err("invalid repeat count")) } } + + fn __inplace_repeat__(mut slf: PyRefMut<'_, Self>, count: isize) -> PyResult> { + if count >= 0 { + let mut elements = Vec::with_capacity(slf.elements.len() * count as usize); + for _ in 0..count { + elements.extend(&slf.elements); + } + slf.elements = elements; + Ok(slf.into()) + } else { + Err(PyValueError::new_err("invalid repeat count")) + } + } } /// Return a dict with `s = ByteSequence([1, 2, 3])`. -fn seq_dict(py: Python) -> &pyo3::types::PyDict { +fn seq_dict(py: Python<'_>) -> &pyo3::types::PyDict { let d = [("ByteSequence", py.get_type::())].into_py_dict(py); // Though we can construct `s` in Rust, let's test `__new__` works. py_run!(py, *d, "s = ByteSequence([1, 2, 3])"); @@ -262,10 +279,12 @@ fn test_generic_list_set() { let list = PyCell::new(py, GenericList { items: vec![] }).unwrap(); py_run!(py, list, "list.items = [1, 2, 3]"); - assert_eq!( - list.borrow().items, - vec![1.to_object(py), 2.to_object(py), 3.to_object(py)] - ); + assert!(list + .borrow() + .items + .iter() + .zip(&[1u32, 2, 3]) + .all(|(a, b)| a.as_ref(py).eq(&b.into_py(py)).unwrap())); } #[pyclass] @@ -274,8 +293,8 @@ struct OptionList { items: Vec>, } -#[pyproto] -impl PySequenceProtocol for OptionList { +#[pymethods] +impl OptionList { fn __getitem__(&self, idx: isize) -> PyResult> { match self.items.get(idx as usize) { Some(x) => Ok(*x), diff --git a/tests/test_sequence_pyproto.rs b/tests/test_sequence_pyproto.rs index db4b6d41..d61b3ad3 100644 --- a/tests/test_sequence_pyproto.rs +++ b/tests/test_sequence_pyproto.rs @@ -1,5 +1,6 @@ #![cfg(feature = "macros")] #![cfg(feature = "pyproto")] +#![allow(deprecated)] use pyo3::class::PySequenceProtocol; use pyo3::exceptions::{PyIndexError, PyValueError}; @@ -21,7 +22,7 @@ impl ByteSequence { fn new(elements: Option<&PyList>) -> PyResult { if let Some(pylist) = elements { let mut elems = Vec::with_capacity(pylist.len()); - for pyelem in pylist.into_iter() { + for pyelem in pylist { let elem = u8::extract(pyelem)?; elems.push(elem); } @@ -87,7 +88,7 @@ impl PySequenceProtocol for ByteSequence { } /// Return a dict with `s = ByteSequence([1, 2, 3])`. -fn seq_dict(py: Python) -> &pyo3::types::PyDict { +fn seq_dict(py: Python<'_>) -> &pyo3::types::PyDict { let d = [("ByteSequence", py.get_type::())].into_py_dict(py); // Though we can construct `s` in Rust, let's test `__new__` works. py_run!(py, *d, "s = ByteSequence([1, 2, 3])"); @@ -262,10 +263,12 @@ fn test_generic_list_set() { let list = PyCell::new(py, GenericList { items: vec![] }).unwrap(); py_run!(py, list, "list.items = [1, 2, 3]"); - assert_eq!( - list.borrow().items, - vec![1.to_object(py), 2.to_object(py), 3.to_object(py)] - ); + assert!(list + .borrow() + .items + .iter() + .zip(&[1u32, 2, 3]) + .all(|(a, b)| a.as_ref(py).eq(&b.into_py(py)).unwrap())); } #[pyclass] diff --git a/tests/test_serde.rs b/tests/test_serde.rs index cb58528a..f9c96598 100644 --- a/tests/test_serde.rs +++ b/tests/test_serde.rs @@ -59,12 +59,12 @@ mod test_serde { #[test] fn test_deserialize() { - let serialized = r#"{"username": "danya", "friends": + let serialized = r#"{"username": "danya", "friends": [{"username": "friend", "group": {"name": "danya's friends"}, "friends": []}]}"#; let user: User = serde_json::from_str(serialized).expect("failed to deserialize"); assert_eq!(user.username, "danya"); - assert_eq!(user.group, None); + assert!(user.group.is_none()); assert_eq!(user.friends.len(), 1usize); let friend = user.friends.get(0).unwrap(); diff --git a/tests/test_text_signature.rs b/tests/test_text_signature.rs index 47ea328f..f3368bd9 100644 --- a/tests/test_text_signature.rs +++ b/tests/test_text_signature.rs @@ -120,7 +120,7 @@ fn test_function() { #[test] fn test_pyfn() { #[pymodule] - fn my_module(_py: Python, m: &PyModule) -> PyResult<()> { + fn my_module(_py: Python<'_>, m: &PyModule) -> PyResult<()> { #[pyfn(m, a, b = "None", "*", c = 42)] #[pyo3(text_signature = "(a, b=None, *, c=42)")] fn my_function(a: i32, b: Option, c: i32) { diff --git a/tests/test_various.rs b/tests/test_various.rs index e830d8ca..b0053d10 100644 --- a/tests/test_various.rs +++ b/tests/test_various.rs @@ -18,7 +18,7 @@ impl MutRefArg { fn get(&self) -> i32 { self.n } - fn set_other(&self, mut other: PyRefMut) { + fn set_other(&self, mut other: PyRefMut<'_, MutRefArg>) { other.n = 100; } } @@ -137,7 +137,7 @@ impl PickleSupport { } } -fn add_module(py: Python, module: &PyModule) -> PyResult<()> { +fn add_module(py: Python<'_>, module: &PyModule) -> PyResult<()> { py.import("sys")? .dict() .get_item("modules") @@ -182,7 +182,7 @@ struct MyError { } impl fmt::Display for MyError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "My error message: {}", self.descr) } } diff --git a/tests/test_wrap_pyfunction_deduction.rs b/tests/test_wrap_pyfunction_deduction.rs index 4adb3975..8723ad43 100644 --- a/tests/test_wrap_pyfunction_deduction.rs +++ b/tests/test_wrap_pyfunction_deduction.rs @@ -5,7 +5,7 @@ use pyo3::{prelude::*, types::PyCFunction}; #[pyfunction] fn f() {} -pub fn add_wrapped(wrapper: &impl Fn(Python) -> PyResult<&PyCFunction>) { +pub fn add_wrapped(wrapper: &impl Fn(Python<'_>) -> PyResult<&PyCFunction>) { let _ = wrapper; } diff --git a/tests/ui/deprecations.rs b/tests/ui/deprecations.rs index 5ad70f9c..7ad3f98e 100644 --- a/tests/ui/deprecations.rs +++ b/tests/ui/deprecations.rs @@ -11,6 +11,9 @@ impl DeprecatedCall { fn deprecated_call(&self) {} } +#[pyclass(gc)] +struct DeprecatedGc; + fn main() { } diff --git a/tests/ui/deprecations.stderr b/tests/ui/deprecations.stderr index d71c1927..c5d6aba3 100644 --- a/tests/ui/deprecations.stderr +++ b/tests/ui/deprecations.stderr @@ -9,3 +9,9 @@ note: the lint level is defined here | 1 | #![deny(deprecated)] | ^^^^^^^^^^ + +error: use of deprecated constant `pyo3::impl_::deprecations::PYCLASS_GC_OPTION`: implement a `__traverse__` `#[pymethod]` instead of using `gc` option + --> tests/ui/deprecations.rs:14:11 + | +14 | #[pyclass(gc)] + | ^^ diff --git a/tests/ui/invalid_macro_args.rs b/tests/ui/invalid_macro_args.rs index 677c0cfb..f64ed4b5 100644 --- a/tests/ui/invalid_macro_args.rs +++ b/tests/ui/invalid_macro_args.rs @@ -1,32 +1,32 @@ use pyo3::prelude::*; #[pyfunction(a = 5, b)] -fn pos_after_kw(py: Python, a: i32, b: i32) -> PyObject { +fn pos_after_kw(py: Python<'_>, a: i32, b: i32) -> PyObject { [a.to_object(py), vararg.into()].to_object(py) } #[pyfunction(kwargs = "**", a = 5)] -fn kw_after_kwargs(py: Python, kwargs: &PyDict, a: i32) -> PyObject { +fn kw_after_kwargs(py: Python<'_>, kwargs: &PyDict, a: i32) -> PyObject { [a.to_object(py), vararg.into()].to_object(py) } #[pyfunction(a, "*", b, "/", c)] -fn pos_only_after_kw_only(py: Python, a: i32, b: i32, c: i32) -> i32 { +fn pos_only_after_kw_only(py: Python<'_>, a: i32, b: i32, c: i32) -> i32 { a + b + c } #[pyfunction(a, args="*", "/", b)] -fn pos_only_after_args(py: Python, a: i32, args: Vec, b: i32) -> i32 { +fn pos_only_after_args(py: Python<'_>, a: i32, args: Vec, b: i32) -> i32 { a + b + c } #[pyfunction(a, kwargs="**", "/", b)] -fn pos_only_after_kwargs(py: Python, a: i32, args: Vec, b: i32) -> i32 { +fn pos_only_after_kwargs(py: Python<'_>, a: i32, args: Vec, b: i32) -> i32 { a + b } #[pyfunction(kwargs = "**", "*", a)] -fn kw_only_after_kwargs(py: Python, kwargs: &PyDict, a: i32) -> PyObject { +fn kw_only_after_kwargs(py: Python<'_>, kwargs: &PyDict, a: i32) -> PyObject { [a.to_object(py), vararg.into()].to_object(py) } diff --git a/tests/ui/invalid_need_module_arg_position.rs b/tests/ui/invalid_need_module_arg_position.rs index b8837031..b3722ae4 100644 --- a/tests/ui/invalid_need_module_arg_position.rs +++ b/tests/ui/invalid_need_module_arg_position.rs @@ -1,7 +1,7 @@ use pyo3::prelude::*; #[pymodule] -fn module(_py: Python, m: &PyModule) -> PyResult<()> { +fn module(_py: Python<'_>, m: &PyModule) -> PyResult<()> { #[pyfn(m, pass_module)] fn fail(string: &str, module: &PyModule) -> PyResult<&str> { module.name() diff --git a/tests/ui/invalid_property_args.rs b/tests/ui/invalid_property_args.rs index c1049946..3f335952 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 2147682c..bf8976a6 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:50 + --> tests/ui/invalid_property_args.rs:9:54 | -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:72 + --> tests/ui/invalid_property_args.rs:24:76 | -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 @@ -35,13 +35,13 @@ error: `set` may only be specified once | ^^^ error: `name` may only be specified once - --> tests/ui/invalid_property_args.rs:37:49 + --> tests/ui/invalid_property_args.rs:37:42 | 37 | struct MultipleName(#[pyo3(name = "foo", name = "bar")] i32); - | ^^^^^ + | ^^^^ error: `name` is useless without `get` or `set` - --> tests/ui/invalid_property_args.rs:40:40 + --> tests/ui/invalid_property_args.rs:40:33 | 40 | struct NameWithoutGetSet(#[pyo3(name = "value")] i32); - | ^^^^^^^ + | ^^^^ diff --git a/tests/ui/invalid_pyclass_args.stderr b/tests/ui/invalid_pyclass_args.stderr index d166cc81..fd64e06e 100644 --- a/tests/ui/invalid_pyclass_args.stderr +++ b/tests/ui/invalid_pyclass_args.stderr @@ -1,40 +1,40 @@ -error: expected one of freelist/name/extends/module +error: expected one of: `crate`, `dict`, `extends`, `freelist`, `module`, `name`, `subclass`, `text_signature`, `unsendable`, `weakref`, `gc` --> tests/ui/invalid_pyclass_args.rs:3:11 | 3 | #[pyclass(extend=pyo3::types::PyDict)] | ^^^^^^ -error: expected type path (e.g., my_mod::BaseClass) +error: expected identifier --> tests/ui/invalid_pyclass_args.rs:6:21 | 6 | #[pyclass(extends = "PyDict")] | ^^^^^^^^ -error: expected type name (e.g. "Name") +error: expected string literal --> tests/ui/invalid_pyclass_args.rs:9:18 | 9 | #[pyclass(name = m::MyClass)] | ^ -error: expected a single identifier in double-quotes +error: expected a single identifier in double quotes --> tests/ui/invalid_pyclass_args.rs:12:18 | 12 | #[pyclass(name = "Custom Name")] | ^^^^^^^^^^^^^ -error: since PyO3 0.13 a pyclass name should be in double-quotes, e.g. "CustomName" +error: expected string literal --> tests/ui/invalid_pyclass_args.rs:15:18 | 15 | #[pyclass(name = CustomName)] | ^^^^^^^^^^ -error: expected string literal (e.g., "my_mod") +error: expected string literal --> tests/ui/invalid_pyclass_args.rs:18:20 | 18 | #[pyclass(module = my_module)] | ^^^^^^^^^ -error: expected one of gc/weakref/subclass/dict/unsendable +error: expected one of: `crate`, `dict`, `extends`, `freelist`, `module`, `name`, `subclass`, `text_signature`, `unsendable`, `weakref`, `gc` --> tests/ui/invalid_pyclass_args.rs:21:11 | 21 | #[pyclass(weakrev)] diff --git a/tests/ui/invalid_pyclass_enum.rs b/tests/ui/invalid_pyclass_enum.rs index a12accbf..4bc53238 100644 --- a/tests/ui/invalid_pyclass_enum.rs +++ b/tests/ui/invalid_pyclass_enum.rs @@ -2,14 +2,14 @@ use pyo3::prelude::*; #[pyclass(subclass)] enum NotBaseClass { - x, - y, + X, + Y, } #[pyclass(extends = PyList)] enum NotDrivedClass { - x, - y, + X, + Y, } #[pyclass] diff --git a/tests/ui/invalid_pyclass_enum.stderr b/tests/ui/invalid_pyclass_enum.stderr index eea36e5c..8f340a76 100644 --- a/tests/ui/invalid_pyclass_enum.stderr +++ b/tests/ui/invalid_pyclass_enum.stderr @@ -4,13 +4,13 @@ error: enums can't be inherited by other classes 3 | #[pyclass(subclass)] | ^^^^^^^^ -error: enums cannot extend from other classes +error: enums can't extend from other classes --> tests/ui/invalid_pyclass_enum.rs:9:11 | 9 | #[pyclass(extends = PyList)] | ^^^^^^^ -error: Empty enums can't be #[pyclass]. +error: #[pyclass] can't be used on enums without any variants --> tests/ui/invalid_pyclass_enum.rs:16:18 | 16 | enum NoEmptyEnum {} diff --git a/tests/ui/invalid_pymethod_names.stderr b/tests/ui/invalid_pymethod_names.stderr index 8aed1d41..c99c692c 100644 --- a/tests/ui/invalid_pymethod_names.stderr +++ b/tests/ui/invalid_pymethod_names.stderr @@ -5,10 +5,10 @@ error: `name` may only be specified once | ^^^^^ error: `name` may only be specified once - --> tests/ui/invalid_pymethod_names.rs:18:19 + --> tests/ui/invalid_pymethod_names.rs:18:12 | 18 | #[pyo3(name = "bar")] - | ^^^^^ + | ^^^^ error: `name` not allowed with `#[new]` --> tests/ui/invalid_pymethod_names.rs:24:19 diff --git a/tests/ui/invalid_pymethod_proto_args_py.rs b/tests/ui/invalid_pymethod_proto_args_py.rs index e5f9345f..33c6fe82 100644 --- a/tests/ui/invalid_pymethod_proto_args_py.rs +++ b/tests/ui/invalid_pymethod_proto_args_py.rs @@ -5,7 +5,7 @@ struct MyClass {} #[pymethods] impl MyClass { - fn __truediv__(&self, _py: Python) -> PyResult<()> { + fn __truediv__(&self, _py: Python<'_>) -> PyResult<()> { Ok(()) } } diff --git a/tests/ui/invalid_pymethod_proto_args_py.stderr b/tests/ui/invalid_pymethod_proto_args_py.stderr index 5661ed45..5b0aca19 100644 --- a/tests/ui/invalid_pymethod_proto_args_py.stderr +++ b/tests/ui/invalid_pymethod_proto_args_py.stderr @@ -1,5 +1,5 @@ error: Expected 1 arguments, got 0 --> tests/ui/invalid_pymethod_proto_args_py.rs:8:8 | -8 | fn __truediv__(&self, _py: Python) -> PyResult<()> { +8 | fn __truediv__(&self, _py: Python<'_>) -> PyResult<()> { | ^^^^^^^^^^^ diff --git a/tests/ui/invalid_pymethod_receiver.rs b/tests/ui/invalid_pymethod_receiver.rs index f85d30d5..77832e12 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 09057ae7..fec45af8 100644 --- a/tests/ui/invalid_pymethod_receiver.stderr +++ b/tests/ui/invalid_pymethod_receiver.stderr @@ -1,7 +1,7 @@ error[E0277]: the trait bound `i32: From<&PyCell>` is not satisfied --> tests/ui/invalid_pymethod_receiver.rs:8:43 | -8 | fn method_with_invalid_self_type(slf: i32, py: Python, index: u32) {} +8 | fn method_with_invalid_self_type(slf: i32, py: Python<'_>, index: u32) {} | ^^^ the trait `From<&PyCell>` is not implemented for `i32` | = help: the following implementations were found: diff --git a/tests/ui/invalid_pymethods.rs b/tests/ui/invalid_pymethods.rs index ed45bac0..cad832b1 100644 --- a/tests/ui/invalid_pymethods.rs +++ b/tests/ui/invalid_pymethods.rs @@ -119,4 +119,20 @@ impl MyClass { fn default_arg_before_required(&self, has_default: isize, required: isize) {} } +#[pymethods] +impl MyClass { + fn method_self_by_value(self){} +} + +struct TwoNew { } + +#[pymethods] +impl TwoNew { + #[new] + fn new_1() -> Self { Self { } } + + #[new] + fn new_2() -> Self { Self { } } +} + fn main() {} diff --git a/tests/ui/invalid_pymethods.stderr b/tests/ui/invalid_pymethods.stderr index bc11faba..06306bca 100644 --- a/tests/ui/invalid_pymethods.stderr +++ b/tests/ui/invalid_pymethods.stderr @@ -101,3 +101,21 @@ error: `pass_module` cannot be used on Python methods | 112 | #[pyo3(pass_module)] | ^^^^^^^^^^^ + +error: Python objects are shared, so 'self' cannot be moved out of the Python interpreter. + Try `&self`, `&mut self, `slf: PyRef<'_, Self>` or `slf: PyRefMut<'_, Self>`. + --> tests/ui/invalid_pymethods.rs:124:29 + | +124 | fn method_self_by_value(self){} + | ^^^^ + +error[E0592]: duplicate definitions with name `__pymethod__new__` + --> tests/ui/invalid_pymethods.rs:129:1 + | +129 | #[pymethods] + | ^^^^^^^^^^^^ + | | + | duplicate definitions for `__pymethod__new__` + | other definition for `__pymethod__new__` + | + = 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_pymodule_args.rs b/tests/ui/invalid_pymodule_args.rs index a11ec937..ebd229ee 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(_py: Python<'_>, m: &PyModule) -> PyResult<()> { Ok(()) } diff --git a/tests/ui/invalid_result_conversion.rs b/tests/ui/invalid_result_conversion.rs index c053edea..53770248 100644 --- a/tests/ui/invalid_result_conversion.rs +++ b/tests/ui/invalid_result_conversion.rs @@ -13,7 +13,7 @@ struct MyError { } impl fmt::Display for MyError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "My error message: {}", self.descr) } } diff --git a/tests/ui/not_send.rs b/tests/ui/not_send.rs index d7c47dec..6566f2d7 100644 --- a/tests/ui/not_send.rs +++ b/tests/ui/not_send.rs @@ -1,6 +1,6 @@ use pyo3::prelude::*; -fn test_not_send_allow_threads(py: Python) { +fn test_not_send_allow_threads(py: Python<'_>) { py.allow_threads(|| { drop(py); }); } diff --git a/tests/ui/not_send.stderr b/tests/ui/not_send.stderr index d4a0d4d4..69e07ac0 100644 --- a/tests/ui/not_send.stderr +++ b/tests/ui/not_send.stderr @@ -12,8 +12,9 @@ error[E0277]: `*mut pyo3::Python<'static>` cannot be shared between threads safe = note: required because it appears within the type `pyo3::Python<'_>` = note: required because of the requirements on the impl of `Send` for `&pyo3::Python<'_>` = note: required because it appears within the type `[closure@$DIR/tests/ui/not_send.rs:4:22: 4:38]` + = note: required because of the requirements on the impl of `Ungil` for `[closure@$DIR/tests/ui/not_send.rs:4:22: 4:38]` note: required by a bound in `pyo3::Python::<'py>::allow_threads` - --> src/python.rs + --> src/marker.rs | - | F: Send + FnOnce() -> T, - | ^^^^ required by this bound in `pyo3::Python::<'py>::allow_threads` + | F: Ungil + FnOnce() -> T, + | ^^^^^ required by this bound in `pyo3::Python::<'py>::allow_threads` diff --git a/tests/ui/not_send2.rs b/tests/ui/not_send2.rs new file mode 100644 index 00000000..4eb0a9f0 --- /dev/null +++ b/tests/ui/not_send2.rs @@ -0,0 +1,12 @@ +use pyo3::prelude::*; +use pyo3::types::PyString; + +fn main() { + Python::with_gil(|py| { + let string = PyString::new(py, "foo"); + + py.allow_threads(|| { + println!("{:?}", string); + }); + }); +} \ No newline at end of file diff --git a/tests/ui/not_send2.stderr b/tests/ui/not_send2.stderr new file mode 100644 index 00000000..c33ee4fc --- /dev/null +++ b/tests/ui/not_send2.stderr @@ -0,0 +1,18 @@ +error[E0277]: `UnsafeCell` cannot be shared between threads safely + --> tests/ui/not_send2.rs:8:12 + | +8 | py.allow_threads(|| { + | ^^^^^^^^^^^^^ `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` + = note: required because it appears within the type `PyString` + = note: required because it appears within the type `&PyString` + = note: required because of the requirements on the impl of `Send` for `&&PyString` + = note: required because it appears within the type `[closure@$DIR/tests/ui/not_send2.rs:8:26: 10:10]` + = note: required because of the requirements on the impl of `Ungil` for `[closure@$DIR/tests/ui/not_send2.rs:8:26: 10:10]` +note: required by a bound in `pyo3::Python::<'py>::allow_threads` + --> src/marker.rs + | + | F: Ungil + FnOnce() -> T, + | ^^^^^ required by this bound in `pyo3::Python::<'py>::allow_threads` diff --git a/tests/ui/not_send3.rs b/tests/ui/not_send3.rs new file mode 100644 index 00000000..5941d10a --- /dev/null +++ b/tests/ui/not_send3.rs @@ -0,0 +1,12 @@ +use pyo3::prelude::*; +use std::rc::Rc; + +fn main() { + Python::with_gil(|py| { + let rc = Rc::new(5); + + py.allow_threads(|| { + println!("{:?}", rc); + }); + }); +} \ No newline at end of file diff --git a/tests/ui/not_send3.stderr b/tests/ui/not_send3.stderr new file mode 100644 index 00000000..b152d076 --- /dev/null +++ b/tests/ui/not_send3.stderr @@ -0,0 +1,15 @@ +error[E0277]: `Rc` cannot be shared between threads safely + --> tests/ui/not_send3.rs:8:12 + | +8 | py.allow_threads(|| { + | ^^^^^^^^^^^^^ `Rc` cannot be shared between threads safely + | + = help: the trait `Sync` is not implemented for `Rc` + = note: required because of the requirements on the impl of `Send` for `&Rc` + = note: required because it appears within the type `[closure@$DIR/tests/ui/not_send3.rs:8:26: 10:10]` + = note: required because of the requirements on the impl of `Ungil` for `[closure@$DIR/tests/ui/not_send3.rs:8:26: 10:10]` +note: required by a bound in `pyo3::Python::<'py>::allow_threads` + --> src/marker.rs + | + | F: Ungil + FnOnce() -> T, + | ^^^^^ required by this bound in `pyo3::Python::<'py>::allow_threads` diff --git a/tests/ui/not_send_auto_trait.rs b/tests/ui/not_send_auto_trait.rs new file mode 100644 index 00000000..6566f2d7 --- /dev/null +++ b/tests/ui/not_send_auto_trait.rs @@ -0,0 +1,11 @@ +use pyo3::prelude::*; + +fn test_not_send_allow_threads(py: Python<'_>) { + py.allow_threads(|| { drop(py); }); +} + +fn main() { + Python::with_gil(|py| { + test_not_send_allow_threads(py); + }) +} diff --git a/tests/ui/not_send_auto_trait.stderr b/tests/ui/not_send_auto_trait.stderr new file mode 100644 index 00000000..c124fd4a --- /dev/null +++ b/tests/ui/not_send_auto_trait.stderr @@ -0,0 +1,15 @@ +error[E0277]: the trait bound `pyo3::Python<'_>: Ungil` is not satisfied in `[closure@$DIR/tests/ui/not_send_auto_trait.rs:4:22: 4:38]` + --> tests/ui/not_send_auto_trait.rs:4:8 + | +4 | py.allow_threads(|| { drop(py); }); + | ^^^^^^^^^^^^^ ---------------- within this `[closure@$DIR/tests/ui/not_send_auto_trait.rs:4:22: 4:38]` + | | + | within `[closure@$DIR/tests/ui/not_send_auto_trait.rs:4:22: 4:38]`, the trait `Ungil` is not implemented for `pyo3::Python<'_>` + | + = note: required because it appears within the type `&pyo3::Python<'_>` + = note: required because it appears within the type `[closure@$DIR/tests/ui/not_send_auto_trait.rs:4:22: 4:38]` +note: required by a bound in `pyo3::Python::<'py>::allow_threads` + --> src/marker.rs + | + | F: Ungil + FnOnce() -> T, + | ^^^^^ required by this bound in `pyo3::Python::<'py>::allow_threads` diff --git a/tests/ui/not_send_auto_trait2.rs b/tests/ui/not_send_auto_trait2.rs new file mode 100644 index 00000000..4eb0a9f0 --- /dev/null +++ b/tests/ui/not_send_auto_trait2.rs @@ -0,0 +1,12 @@ +use pyo3::prelude::*; +use pyo3::types::PyString; + +fn main() { + Python::with_gil(|py| { + let string = PyString::new(py, "foo"); + + py.allow_threads(|| { + println!("{:?}", string); + }); + }); +} \ No newline at end of file diff --git a/tests/ui/not_send_auto_trait2.stderr b/tests/ui/not_send_auto_trait2.stderr new file mode 100644 index 00000000..e549f7e2 --- /dev/null +++ b/tests/ui/not_send_auto_trait2.stderr @@ -0,0 +1,20 @@ +error[E0277]: the trait bound `PyAny: Ungil` is not satisfied in `[closure@$DIR/tests/ui/not_send_auto_trait2.rs:8:26: 10:10]` + --> tests/ui/not_send_auto_trait2.rs:8:12 + | +8 | py.allow_threads(|| { + | ____________^^^^^^^^^^^^^_- + | | | + | | within `[closure@$DIR/tests/ui/not_send_auto_trait2.rs:8:26: 10:10]`, the trait `Ungil` is not implemented for `PyAny` +9 | | println!("{:?}", string); +10 | | }); + | |_________- within this `[closure@$DIR/tests/ui/not_send_auto_trait2.rs:8:26: 10:10]` + | + = note: required because it appears within the type `PyString` + = note: required because it appears within the type `&PyString` + = note: required because it appears within the type `&&PyString` + = note: required because it appears within the type `[closure@$DIR/tests/ui/not_send_auto_trait2.rs:8:26: 10:10]` +note: required by a bound in `pyo3::Python::<'py>::allow_threads` + --> src/marker.rs + | + | F: Ungil + FnOnce() -> T, + | ^^^^^ required by this bound in `pyo3::Python::<'py>::allow_threads` diff --git a/tests/ui/send_wrapper.rs b/tests/ui/send_wrapper.rs new file mode 100644 index 00000000..7e7f00ea --- /dev/null +++ b/tests/ui/send_wrapper.rs @@ -0,0 +1,16 @@ +use pyo3::prelude::*; +use pyo3::types::PyString; +use send_wrapper::SendWrapper; + +fn main() { + Python::with_gil(|py| { + let string = PyString::new(py, "foo"); + + let wrapped = SendWrapper::new(string); + + py.allow_threads(|| { + let smuggled: &PyString = *wrapped; + println!("{:?}", smuggled); + }); + }); +} \ No newline at end of file diff --git a/tests/ui/send_wrapper.stderr b/tests/ui/send_wrapper.stderr new file mode 100644 index 00000000..2c91e3ba --- /dev/null +++ b/tests/ui/send_wrapper.stderr @@ -0,0 +1,23 @@ +error[E0277]: the trait bound `PyAny: Ungil` is not satisfied in `[closure@$DIR/tests/ui/send_wrapper.rs:11:26: 14:10]` + --> tests/ui/send_wrapper.rs:11:12 + | +11 | py.allow_threads(|| { + | ____________^^^^^^^^^^^^^_- + | | | + | | within `[closure@$DIR/tests/ui/send_wrapper.rs:11:26: 14:10]`, the trait `Ungil` is not implemented for `PyAny` +12 | | let smuggled: &PyString = *wrapped; +13 | | println!("{:?}", smuggled); +14 | | }); + | |_________- within this `[closure@$DIR/tests/ui/send_wrapper.rs:11:26: 14:10]` + | + = note: required because it appears within the type `PyString` + = note: required because it appears within the type `&PyString` + = note: required because it appears within the type `*mut &PyString` + = note: required because it appears within the type `SendWrapper<&PyString>` + = note: required because it appears within the type `&SendWrapper<&PyString>` + = note: required because it appears within the type `[closure@$DIR/tests/ui/send_wrapper.rs:11:26: 14:10]` +note: required by a bound in `pyo3::Python::<'py>::allow_threads` + --> src/marker.rs + | + | F: Ungil + FnOnce() -> T, + | ^^^^^ required by this bound in `pyo3::Python::<'py>::allow_threads` diff --git a/tests/ui/static_ref.stderr b/tests/ui/static_ref.stderr index e948a2ff..f087904c 100644 --- a/tests/ui/static_ref.stderr +++ b/tests/ui/static_ref.stderr @@ -1,25 +1,11 @@ -error[E0495]: cannot infer an appropriate lifetime for lifetime parameter `'py` due to conflicting requirements +error[E0597]: `gil` does not live long enough --> tests/ui/static_ref.rs:4:1 | 4 | #[pyfunction] - | ^^^^^^^^^^^^^ + | ^^^^^^^^^^^^- + | | | + | | `gil` dropped here while still borrowed + | borrowed value does not live long enough + | cast requires that `gil` is borrowed for `'static` | -note: first, the lifetime cannot outlive the anonymous lifetime #1 defined here... - --> tests/ui/static_ref.rs:4:1 - | -4 | #[pyfunction] - | ^^^^^^^^^^^^^ -note: ...so that the expression is assignable - --> tests/ui/static_ref.rs:4:1 - | -4 | #[pyfunction] - | ^^^^^^^^^^^^^ - = note: expected `pyo3::Python<'_>` - found `pyo3::Python<'_>` - = note: but, the lifetime must be valid for the static lifetime... -note: ...so that reference does not outlive borrowed content - --> tests/ui/static_ref.rs:4:1 - | -4 | #[pyfunction] - | ^^^^^^^^^^^^^ = note: this error originates in the attribute macro `pyfunction` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml index f6008412..b3560ae1 100644 --- a/xtask/Cargo.toml +++ b/xtask/Cargo.toml @@ -3,9 +3,13 @@ name = "xtask" version = "0.1.0" edition = "2018" +[[bin]] +name = "xtask" + [dependencies] anyhow = "1.0.51" # Clap 3 requires MSRV 1.54 rustversion = "1.0" structopt = { version = "0.3", default-features = false } +clap = { version = "2" } diff --git a/xtask/README.md b/xtask/README.md new file mode 100644 index 00000000..68d078e5 --- /dev/null +++ b/xtask/README.md @@ -0,0 +1,23 @@ +## Commands to test PyO3. + +To run these commands, you should be in PyO3's root directory, and run (for example) `cargo xtask ci`. + +``` +USAGE: + xtask.exe + +FLAGS: + -h, --help Prints help information + -V, --version Prints version information + +SUBCOMMANDS: + ci Runs everything + clippy Runs `clippy`, denying all warnings + coverage Runs `cargo llvm-cov` for the PyO3 codebase + default Only runs the fast things (this is used if no command is specified) + doc Attempts to render the documentation + fmt Checks Rust and Python code formatting with `rustfmt` and `black` + help Prints this message or the help of the given subcommand(s) + test Runs various variations on `cargo test` + test-py Runs the tests in examples/ and pytests/ +``` \ No newline at end of file diff --git a/xtask/src/cli.rs b/xtask/src/cli.rs new file mode 100644 index 00000000..9dc16744 --- /dev/null +++ b/xtask/src/cli.rs @@ -0,0 +1,204 @@ +use crate::utils::*; +use anyhow::{ensure, Result}; +use std::io; +use std::process::Command; +use std::time::Instant; +use structopt::StructOpt; + +pub const MSRV: &str = "1.48"; + +#[derive(StructOpt)] +pub enum Subcommand { + /// Only runs the fast things (this is used if no command is specified) + Default, + /// Runs everything + Ci, + /// Checks Rust and Python code formatting with `rustfmt` and `black` + Fmt, + /// Runs `clippy`, denying all warnings. + Clippy, + /// Runs `cargo llvm-cov` for the PyO3 codebase. + Coverage(CoverageOpts), + /// Attempts to render the documentation. + Doc(DocOpts), + /// Runs various variations on `cargo test` + Test, + /// Runs the tests in examples/ and pytests/ + TestPy, +} + +impl Default for Subcommand { + fn default() -> Self { + Self::Default + } +} + +#[derive(StructOpt, Default)] +pub struct CoverageOpts { + /// Creates an lcov output instead of printing to the terminal. + #[structopt(long)] + pub output_lcov: Option, +} + +#[derive(StructOpt)] +pub struct DocOpts { + /// Whether to run the docs using nightly rustdoc + #[structopt(long)] + pub stable: bool, + /// Whether to open the docs after rendering. + #[structopt(long)] + pub open: bool, + /// Whether to show the private and hidden API. + #[structopt(long)] + pub internal: bool, +} + +impl Default for DocOpts { + fn default() -> Self { + Self { + stable: true, + open: false, + internal: false, + } + } +} + +impl Subcommand { + pub fn execute(self) -> Result<()> { + print_metadata()?; + + let start = Instant::now(); + + match self { + Subcommand::Default => { + crate::fmt::rust::run()?; + crate::clippy::run()?; + crate::test::run()?; + crate::doc::run(DocOpts::default())?; + } + Subcommand::Ci => { + let installed = Installed::new()?; + crate::fmt::rust::run()?; + if installed.black { + crate::fmt::python::run()?; + } else { + Installed::warn_black() + }; + crate::clippy::run()?; + crate::test::run()?; + crate::doc::run(DocOpts::default())?; + if installed.nox { + crate::pytests::run(None)?; + } else { + Installed::warn_nox() + }; + crate::llvm_cov::run(CoverageOpts::default())?; + installed.assert()? + } + + Subcommand::Doc(opts) => crate::doc::run(opts)?, + Subcommand::Fmt => { + crate::fmt::rust::run()?; + crate::fmt::python::run()?; + } + Subcommand::Clippy => crate::clippy::run()?, + Subcommand::Coverage(opts) => crate::llvm_cov::run(opts)?, + Subcommand::TestPy => crate::pytests::run(None)?, + Subcommand::Test => crate::test::run()?, + }; + + let dt = start.elapsed().as_secs(); + let minutes = dt / 60; + let seconds = dt % 60; + println!("\nxtask finished in {}m {}s.", minutes, seconds); + + Ok(()) + } +} + +pub fn run(command: &mut Command) -> Result<()> { + let command_str = format_command(command); + let github_actions = std::env::var_os("GITHUB_ACTIONS").is_some(); + if github_actions { + println!("::group::Running: {}", command_str); + } else { + println!("Running: {}", command_str); + } + + let status = command.spawn()?.wait()?; + + ensure! { + status.success(), + "process did not run successfully ({exit}): {command}", + exit = match status.code() { + Some(code) => format!("exit code {}", code), + None => "terminated by signal".into(), + }, + command = command_str, + }; + + if github_actions { + println!("::endgroup::") + } + Ok(()) +} + +#[derive(Copy, Clone, Debug)] +pub struct Installed { + pub nox: bool, + pub black: bool, +} + +impl Installed { + pub fn new() -> anyhow::Result { + Ok(Self { + nox: Self::nox()?, + black: Self::black()?, + }) + } + + pub fn nox() -> anyhow::Result { + let output = std::process::Command::new("nox").arg("--version").output(); + match output { + Ok(_) => Ok(true), + Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(false), + Err(other) => Err(other.into()), + } + } + + pub fn warn_nox() { + eprintln!("Skipping: formatting Python code, because `nox` was not found"); + } + + pub fn black() -> anyhow::Result { + let output = std::process::Command::new("black") + .arg("--version") + .output(); + match output { + Ok(_) => Ok(true), + Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(false), + Err(other) => Err(other.into()), + } + } + + pub fn warn_black() { + eprintln!("Skipping: Python code formatting, because `black` was not found."); + } + + pub fn assert(&self) -> anyhow::Result<()> { + if self.nox && self.black { + Ok(()) + } else { + let mut err = + String::from("\n\nxtask was unable to run all tests due to some missing programs:"); + if !self.black { + err.push_str("\n`black` was not installed. (`pip install black`)"); + } + if !self.nox { + err.push_str("\n`nox` was not installed. (`pip install nox`)"); + } + + Err(anyhow::anyhow!(err)) + } + } +} diff --git a/xtask/src/clippy.rs b/xtask/src/clippy.rs new file mode 100644 index 00000000..eec5f5fd --- /dev/null +++ b/xtask/src/clippy.rs @@ -0,0 +1,25 @@ +use crate::cli; +use std::process::Command; + +pub fn run() -> anyhow::Result<()> { + cli::run( + Command::new("cargo") + .arg("clippy") + .arg("--features=full") + .arg("--all-targets") + .arg("--workspace") + .arg("--") + .arg("-Dwarnings"), + )?; + cli::run( + Command::new("cargo") + .arg("clippy") + .arg("--all-targets") + .arg("--workspace") + .arg("--features=abi3,full") + .arg("--") + .arg("-Dwarnings"), + )?; + + Ok(()) +} diff --git a/xtask/src/doc.rs b/xtask/src/doc.rs new file mode 100644 index 00000000..9fe49f30 --- /dev/null +++ b/xtask/src/doc.rs @@ -0,0 +1,47 @@ +use crate::cli; +use crate::cli::DocOpts; +use std::process::Command; +//--cfg docsrs --Z unstable-options --document-hidden-items + +pub fn run(opts: DocOpts) -> anyhow::Result<()> { + let mut flags = Vec::new(); + + if !opts.stable { + flags.push("--cfg docsrs"); + } + if opts.internal { + flags.push("--Z unstable-options"); + flags.push("--document-hidden-items"); + } + flags.push("-Dwarnings"); + + std::env::set_var("RUSTDOCFLAGS", flags.join(" ")); + cli::run( + Command::new("cargo") + .args(if opts.stable { None } else { Some("+nightly") }) + .arg("doc") + .arg("--lib") + .arg("--no-default-features") + .arg("--features=full") + .arg("--no-deps") + .arg("--workspace") + .args(if opts.internal { + &["--document-private-items"][..] + } else { + &["--exclude=pyo3-macros", "--exclude=pyo3-macros-backend"][..] + }) + .args(if opts.stable { + &[][..] + } else { + &[ + "-Z", + "unstable-options", + "-Z", + "rustdoc-scrape-examples=examples", + ] + }) + .args(if opts.open { Some("--open") } else { None }), + )?; + + Ok(()) +} diff --git a/xtask/src/fmt.rs b/xtask/src/fmt.rs new file mode 100644 index 00000000..8bc74524 --- /dev/null +++ b/xtask/src/fmt.rs @@ -0,0 +1,23 @@ +pub mod rust { + use crate::cli; + use std::process::Command; + pub fn run() -> anyhow::Result<()> { + cli::run( + Command::new("cargo") + .arg("fmt") + .arg("--all") + .arg("--") + .arg("--check"), + )?; + Ok(()) + } +} + +pub mod python { + use crate::cli; + use std::process::Command; + pub fn run() -> anyhow::Result<()> { + cli::run(Command::new("black").arg(".").arg("--check"))?; + Ok(()) + } +} diff --git a/xtask/src/llvm_cov.rs b/xtask/src/llvm_cov.rs new file mode 100644 index 00000000..50ea70e6 --- /dev/null +++ b/xtask/src/llvm_cov.rs @@ -0,0 +1,100 @@ +use crate::cli; +use crate::cli::CoverageOpts; +use crate::utils::*; +use anyhow::{Context, Result}; +use std::{collections::HashMap, process::Command}; + +/// Runs `cargo llvm-cov` for the PyO3 codebase. +pub fn run(opts: CoverageOpts) -> Result<()> { + let env = get_coverage_env()?; + + cli::run(llvm_cov_command(&["clean", "--workspace"]).envs(&env))?; + + cli::run( + Command::new("cargo") + .args(&["test", "--manifest-path", "pyo3-build-config/Cargo.toml"]) + .envs(&env), + )?; + cli::run( + Command::new("cargo") + .args(&["test", "--manifest-path", "pyo3-macros-backend/Cargo.toml"]) + .envs(&env), + )?; + cli::run( + Command::new("cargo") + .args(&["test", "--manifest-path", "pyo3-macros/Cargo.toml"]) + .envs(&env), + )?; + + cli::run(Command::new("cargo").arg("test").envs(&env))?; + cli::run( + Command::new("cargo") + .args(&["test", "--features", "abi3"]) + .envs(&env), + )?; + cli::run( + Command::new("cargo") + .args(&["test", "--features", "full"]) + .envs(&env), + )?; + cli::run( + Command::new("cargo") + .args(&["test", "--features", "abi3 full"]) + .envs(&env), + )?; + + crate::pytests::run(&env)?; + + match opts.output_lcov { + Some(path) => { + cli::run(llvm_cov_command(&["--no-run", "--lcov", "--output-path", &path]).envs(&env))? + } + None => cli::run(llvm_cov_command(&["--no-run", "--summary-only"]).envs(&env))?, + } + + Ok(()) +} + +fn llvm_cov_command(args: &[&str]) -> Command { + let mut command = Command::new("cargo"); + command + .args(&[ + "llvm-cov", + "--package=pyo3", + "--package=pyo3-build-config", + "--package=pyo3-macros-backend", + "--package=pyo3-macros", + "--package=pyo3-ffi", + ]) + .args(args); + command +} + +fn get_coverage_env() -> Result> { + let mut env = HashMap::new(); + + let output = String::from_utf8(llvm_cov_command(&["show-env"]).output()?.stdout)?; + + for line in output.trim().split('\n') { + let (key, value) = split_once(line, '=') + .context("expected '=' in each line of output from llvm-cov show-env")?; + env.insert(key.to_owned(), value.trim_matches('"').to_owned()); + } + + // Ensure that examples/ and pytests/ all build to the correct target directory to collect + // coverage artifacts. + env.insert( + "CARGO_TARGET_DIR".to_owned(), + env.get("CARGO_LLVM_COV_TARGET_DIR").unwrap().to_owned(), + ); + + // Coverage only works on nightly. + let rustc_version = + String::from_utf8(get_output(Command::new("rustc").arg("--version"))?.stdout) + .context("failed to parse rust version as utf8")?; + if !rustc_version.contains("nightly") { + env.insert("RUSTUP_TOOLCHAIN".to_owned(), "nightly".to_owned()); + } + + Ok(env) +} diff --git a/xtask/src/main.rs b/xtask/src/main.rs index cdbfbf14..ab7afafa 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -1,196 +1,24 @@ -use anyhow::{ensure, Context, Result}; -use std::{collections::HashMap, path::Path, process::Command}; +use clap::ErrorKind::MissingArgumentOrSubcommand; use structopt::StructOpt; -#[derive(StructOpt)] -enum Subcommand { - /// Runs `cargo llvm-cov` for the PyO3 codebase. - Coverage(CoverageOpts), - /// Runs tests in examples/ and pytests/ - TestPy, -} +pub mod cli; +pub mod clippy; +pub mod doc; +pub mod fmt; +pub mod llvm_cov; +pub mod pytests; +pub mod test; +pub mod utils; -#[derive(StructOpt)] -struct CoverageOpts { - /// Creates an lcov output instead of printing to the terminal. - #[structopt(long)] - output_lcov: Option, -} +fn main() -> anyhow::Result<()> { + // Avoid spewing backtraces all over the command line + // For some reason this is automatically enabled on nightly compilers... + std::env::set_var("RUST_LIB_BACKTRACE", "0"); -impl Subcommand { - fn execute(self) -> Result<()> { - match self { - Subcommand::Coverage(opts) => subcommand_coverage(opts), - Subcommand::TestPy => run_python_tests(None), - } - } -} - -fn main() -> Result<()> { - Subcommand::from_args().execute() -} - -/// Runs `cargo llvm-cov` for the PyO3 codebase. -fn subcommand_coverage(opts: CoverageOpts) -> Result<()> { - let env = get_coverage_env()?; - - run(llvm_cov_command(&["clean", "--workspace"]).envs(&env))?; - - run(Command::new("cargo") - .args(&["test", "--manifest-path", "pyo3-build-config/Cargo.toml"]) - .envs(&env))?; - run(Command::new("cargo") - .args(&["test", "--manifest-path", "pyo3-macros-backend/Cargo.toml"]) - .envs(&env))?; - run(Command::new("cargo") - .args(&["test", "--manifest-path", "pyo3-macros/Cargo.toml"]) - .envs(&env))?; - - run(Command::new("cargo").arg("test").envs(&env))?; - run(Command::new("cargo") - .args(&["test", "--features", "abi3"]) - .envs(&env))?; - run(Command::new("cargo") - .args(&["test", "--features", "full"]) - .envs(&env))?; - run(Command::new("cargo") - .args(&["test", "--features", "abi3 full"]) - .envs(&env))?; - - run_python_tests(&env)?; - - match opts.output_lcov { - Some(path) => { - run(llvm_cov_command(&["--no-run", "--lcov", "--output-path", &path]).envs(&env))? - } - None => run(llvm_cov_command(&["--no-run", "--summary-only"]).envs(&env))?, - } - - Ok(()) -} - -fn run(command: &mut Command) -> Result<()> { - println!("running: {}", format_command(command)); - let status = command.spawn()?.wait()?; - ensure! { - status.success(), - "process did not run successfully ({exit}): {command}", - exit = match status.code() { - Some(code) => format!("exit code {}", code), - None => "terminated by signal".into(), - }, - command = format_command(command), - }; - Ok(()) -} - -fn get_output(command: &mut Command) -> Result { - let output = command.output()?; - ensure! { - output.status.success(), - "process did not run successfully ({exit}): {command}", - exit = match output.status.code() { - Some(code) => format!("exit code {}", code), - None => "terminated by signal".into(), - }, - command = format_command(command), - }; - Ok(output) -} - -fn llvm_cov_command(args: &[&str]) -> Command { - let mut command = Command::new("cargo"); - command - .args(&[ - "llvm-cov", - "--package=pyo3", - "--package=pyo3-build-config", - "--package=pyo3-macros-backend", - "--package=pyo3-macros", - "--package=pyo3-ffi", - ]) - .args(args); - command -} - -fn run_python_tests<'a>( - env: impl IntoIterator + Copy, -) -> Result<()> { - run(Command::new("nox") - .arg("--non-interactive") - .arg("-f") - .arg(Path::new("pytests").join("noxfile.py")) - .envs(env))?; - - for entry in std::fs::read_dir("examples")? { - let path = entry?.path(); - if path.is_dir() && path.join("noxfile.py").exists() { - run(Command::new("nox") - .arg("--non-interactive") - .arg("-f") - .arg(path.join("noxfile.py")) - .envs(env))?; - } + match cli::Subcommand::from_args_safe() { + Ok(c) => c.execute()?, + Err(e) if e.kind == MissingArgumentOrSubcommand => cli::Subcommand::default().execute()?, + Err(e) => return Err(e.into()), } Ok(()) } - -fn get_coverage_env() -> Result> { - let mut env = HashMap::new(); - - let output = String::from_utf8(llvm_cov_command(&["show-env"]).output()?.stdout)?; - - for line in output.trim().split('\n') { - let (key, value) = split_once(line, '=') - .context("expected '=' in each line of output from llvm-cov show-env")?; - env.insert(key.to_owned(), value.trim_matches('"').to_owned()); - } - - // Ensure that examples/ and pytests/ all build to the correct target directory to collect - // coverage artifacts. - env.insert( - "CARGO_TARGET_DIR".to_owned(), - env.get("CARGO_LLVM_COV_TARGET_DIR").unwrap().to_owned(), - ); - - // Coverage only works on nightly. - let rustc_version = - String::from_utf8(get_output(Command::new("rustc").arg("--version"))?.stdout) - .context("failed to parse rust version as utf8")?; - if !rustc_version.contains("nightly") { - env.insert("RUSTUP_TOOLCHAIN".to_owned(), "nightly".to_owned()); - } - - Ok(env) -} - -// Replacement for str.split_once() on Rust older than 1.52 -#[rustversion::before(1.52)] -fn split_once(s: &str, pat: char) -> Option<(&str, &str)> { - let mut iter = s.splitn(2, pat); - Some((iter.next()?, iter.next()?)) -} - -#[rustversion::since(1.52)] -fn split_once(s: &str, pat: char) -> Option<(&str, &str)> { - s.split_once(pat) -} - -#[rustversion::since(1.57)] -fn format_command(command: &Command) -> String { - let mut buf = String::new(); - buf.push('`'); - buf.push_str(&command.get_program().to_string_lossy()); - for arg in command.get_args() { - buf.push(' '); - buf.push_str(&arg.to_string_lossy()); - } - buf.push('`'); - buf -} - -#[rustversion::before(1.57)] -fn format_command(command: &Command) -> String { - // Debug impl isn't as nice as the above, but will do on < 1.57 - format!("{:?}", command) -} diff --git a/xtask/src/pytests.rs b/xtask/src/pytests.rs new file mode 100644 index 00000000..78744c69 --- /dev/null +++ b/xtask/src/pytests.rs @@ -0,0 +1,27 @@ +use crate::cli; +use anyhow::Result; +use std::{path::Path, process::Command}; + +pub fn run<'a>(env: impl IntoIterator + Copy) -> Result<()> { + cli::run( + Command::new("nox") + .arg("--non-interactive") + .arg("-f") + .arg(Path::new("pytests").join("noxfile.py")) + .envs(env), + )?; + + for entry in std::fs::read_dir("examples")? { + let path = entry?.path(); + if path.is_dir() && path.join("noxfile.py").exists() { + cli::run( + Command::new("nox") + .arg("--non-interactive") + .arg("-f") + .arg(path.join("noxfile.py")) + .envs(env), + )?; + } + } + Ok(()) +} diff --git a/xtask/src/test.rs b/xtask/src/test.rs new file mode 100644 index 00000000..c383140a --- /dev/null +++ b/xtask/src/test.rs @@ -0,0 +1,73 @@ +use crate::cli::{self, MSRV}; +use std::process::Command; + +pub fn run() -> anyhow::Result<()> { + cli::run( + Command::new("cargo") + .arg("test") + .arg("--lib") + .arg("--no-default-features") + .arg("--tests") + .arg("--quiet"), + )?; + + cli::run( + Command::new("cargo") + .arg("test") + .arg("--no-default-features") + .arg("--features=full") + .arg("--quiet"), + )?; + + cli::run( + Command::new("cargo") + .arg("test") + .arg("--no-default-features") + .arg("--features=abi3,full") + .arg("--quiet"), + )?; + + // If the MSRV toolchain is not installed, this will install it + cli::run( + Command::new("rustup") + .arg("toolchain") + .arg("install") + .arg(MSRV), + )?; + + // Test MSRV + cli::run( + Command::new("cargo") + .arg(format!("+{}", MSRV)) + .arg("test") + .arg("--no-default-features") + .arg("--features=full,auto-initialize") + .arg("--quiet"), + )?; + + cli::run( + Command::new("cargo") + .arg("+nightly") + .arg("test") + .arg("--no-default-features") + .arg("--features=full,nightly") + .arg("--quiet"), + )?; + + cli::run( + Command::new("cargo") + .arg("test") + .arg("--manifest-path=pyo3-ffi/Cargo.toml") + .arg("--quiet"), + )?; + + cli::run( + Command::new("cargo") + .arg("test") + .arg("--no-default-features") + .arg("--manifest-path=pyo3-build-config/Cargo.toml") + .arg("--quiet"), + )?; + + Ok(()) +} diff --git a/xtask/src/utils.rs b/xtask/src/utils.rs new file mode 100644 index 00000000..045697e7 --- /dev/null +++ b/xtask/src/utils.rs @@ -0,0 +1,65 @@ +use anyhow::ensure; +use std::process::Command; + +// Replacement for str.split_once() on Rust older than 1.52 +#[rustversion::before(1.52)] +pub fn split_once(s: &str, pat: char) -> Option<(&str, &str)> { + let mut iter = s.splitn(2, pat); + Some((iter.next()?, iter.next()?)) +} + +#[rustversion::since(1.52)] +pub fn split_once(s: &str, pat: char) -> Option<(&str, &str)> { + s.split_once(pat) +} + +#[rustversion::since(1.57)] +pub fn format_command(command: &Command) -> String { + let mut buf = String::new(); + buf.push('`'); + buf.push_str(&command.get_program().to_string_lossy()); + for arg in command.get_args() { + buf.push(' '); + buf.push_str(&arg.to_string_lossy()); + } + buf.push('`'); + buf +} + +#[rustversion::before(1.57)] +pub fn format_command(command: &Command) -> String { + // Debug impl isn't as nice as the above, but will do on < 1.57 + format!("{:?}", command) +} + +pub fn get_output(command: &mut Command) -> anyhow::Result { + let output = command.output()?; + ensure! { + output.status.success(), + "process did not run successfully ({exit}): {command}", + exit = match output.status.code() { + Some(code) => format!("exit code {}", code), + None => "terminated by signal".into(), + }, + command = format_command(command), + }; + Ok(output) +} + +pub fn print_metadata() -> anyhow::Result<()> { + let rustc_output = std::process::Command::new("rustc") + .arg("--version") + .arg("--verbose") + .output()?; + let rustc_version = core::str::from_utf8(&rustc_output.stdout).unwrap(); + println!("Metadata: \n\n{}", rustc_version); + + let py_output = std::process::Command::new("python") + .arg("--version") + .arg("-V") + .output()?; + let py_version = core::str::from_utf8(&py_output.stdout).unwrap(); + println!("{}", py_version); + + Ok(()) +}