diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b20a26a4..f8b7b732 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,16 +15,16 @@ jobs: steps: - uses: actions/checkout@v2 - uses: actions/setup-python@v2 - - run: pip install black==20.8b1 + - run: pip install black==21.12b0 - uses: actions-rs/toolchain@v1 with: toolchain: stable profile: minimal components: rustfmt - name: Check python formatting (black) - run: black --check . + run: make fmt_py - name: Check rust formatting (rustfmt) - run: cargo fmt --all -- --check + run: make fmt_rust clippy: runs-on: ubuntu-latest @@ -58,7 +58,7 @@ jobs: - name: Run cargo checks run: | set -x - VERSIONS=("3.6" "3.7" "3.8" "3.9" "3.10") + VERSIONS=("3.7" "3.8" "3.9" "3.10") for VERSION in ${VERSIONS[@]}; do echo "version=$VERSION" > config.txt echo "suppress_build_script_link_lines=true" >> config.txt @@ -76,7 +76,7 @@ jobs: fail-fast: false # If one platform fails, allow the rest to keep testing. matrix: rust: [stable] - python-version: [3.6, 3.7, 3.8, 3.9, "3.10", pypy-3.6, pypy-3.7, pypy-3.8] + python-version: [3.7, 3.8, 3.9, "3.10", pypy-3.7, pypy-3.8] platform: [ { @@ -101,29 +101,14 @@ jobs: }, ] exclude: - # PyPy 3.6 is EOL and not working on macos-latest (now macos-11) - - python-version: pypy-3.6 - platform: { os: "macos-latest", python-architecture: "x64" } - # There is no 64-bit pypy on windows for pypy-3.6 - - python-version: pypy-3.6 - platform: { os: "windows-latest", python-architecture: "x64" } - # PyPy 3.7 on Windows doesn't release 32-bit builds any more + # 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: - # PyPy3.6 still runs on macos-10.15 - - rust: stable - python-version: pypy-3.6 - platform: - { - os: "macos-10.15", - python-architecture: "x64", - rust-target: "x86_64-apple-darwin", - } # Test minimal supported Rust version - - rust: 1.41.1 + - rust: 1.48.0 python-version: "3.10" platform: { @@ -200,7 +185,7 @@ jobs: # Run tests (except on PyPy, because no embedding API). - if: ${{ !startsWith(matrix.python-version, 'pypy') }} name: Test (no features) - run: cargo test --no-default-features + run: cargo test --no-default-features --lib --tests # --no-default-features when used with `cargo build/test -p` doesn't seem to work! - name: Test pyo3-build-config (no features) @@ -211,6 +196,10 @@ jobs: - name: Build (all additive features) run: cargo build --lib --tests --no-default-features --features "${{ steps.settings.outputs.all_additive_features }}" + - if: ${{ startsWith(matrix.python-version, 'pypy') }} + name: Build PyPy (abi3-py37) + run: cargo build --lib --tests --no-default-features --features "abi3-py37 ${{ steps.settings.outputs.all_additive_features }}" + # Run tests (except on PyPy, because no embedding API). - if: ${{ !startsWith(matrix.python-version, 'pypy') }} name: Test @@ -221,10 +210,10 @@ jobs: name: Test (abi3) run: cargo test --no-default-features --features "abi3 ${{ steps.settings.outputs.all_additive_features }}" - # Run tests again, for abi3-py36 (the minimal Python version) - - if: ${{ (!startsWith(matrix.python-version, 'pypy')) && (matrix.python-version != '3.6') }} - name: Test (abi3-py36) - run: cargo test --no-default-features --features "abi3-py36 ${{ steps.settings.outputs.all_additive_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 ${{ steps.settings.outputs.all_additive_features }}" - name: Test proc-macro code run: cargo test --manifest-path=pyo3-macros-backend/Cargo.toml @@ -263,6 +252,10 @@ jobs: # TODO: this is a hack to workaround compile_error! warnings about auto-initialize on PyPy # Once cargo's `resolver = "2"` is stable (~ MSRV Rust 1.52), remove this. PYO3_CI: 1 + # This is a hack to make CARGO_PRIMARY_PACKAGE always set even for the + # msrv job. MSRV is currently 1.48, but CARGO_PRIMARY_PACKAGE only came in + # 1.49. + CARGO_PRIMARY_PACKAGE: 1 coverage: needs: [fmt] diff --git a/CHANGELOG.md b/CHANGELOG.md index b4e86580..de1f6233 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,11 +6,52 @@ 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 + +### Packaging + +- Update MSRV to Rust 1.48. [#2004](https://github.com/PyO3/pyo3/pull/2004) +- Update `indoc` optional dependency to 1.0. [#2004](https://github.com/PyO3/pyo3/pull/2004) +- Update `paste` optional dependency to 1.0. [#2004](https://github.com/PyO3/pyo3/pull/2004) +- Drop support for Python 3.6, remove `abi3-py36` feature. [#2006](https://github.com/PyO3/pyo3/pull/2006) +- `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) + +### Added + +- 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) +- 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) + +### Changed + +- `PyType::is_subclass`, `PyErr::is_instance` and `PyAny::is_instance` now operate run-time type object instead of a type known at compile-time. The old behavior is still available as `PyType::is_subclass_of`, `PyErr::is_instance_of` and `PyAny::is_instance_of`. [#1985](https://github.com/PyO3/pyo3/pull/1985) +- Rename some methods on `PyErr` (the old names are just marked deprecated for now): [#2026](https://github.com/PyO3/pyo3/pull/2026) + - `pytype` -> `get_type` + - `pvalue` -> `value` (and deprecate equivalent `instance`) + - `ptraceback` -> `traceback` + - `from_instance` -> `from_value` + - `into_instance` -> `into_value` +- 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) +- Optional parameters of `#[pymethods]` and `#[pyfunction]`s cannot be followed by required parameters, i.e. `fn opt_first(a: Option, b: i32) {}` is not allowed, while `fn opt_last(a:i32, b: Option) {}` is. [#2041](https://github.com/PyO3/pyo3/pull/2041) + +### Removed + +- Remove all functionality deprecated in PyO3 0.14. [#2007](https://github.com/PyO3/pyo3/pull/2007) + +### Fixed + +- Fix undefined symbol for `PyObject_HasAttr` on PyPy. [#2025](https://github.com/PyO3/pyo3/pull/2025) +- 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) + +## [0.15.1] - 2021-11-19 ### Added - Add implementations for `Py::as_ref()` and `Py::into_ref()` for `Py`, `Py` and `Py`. [#1682](https://github.com/PyO3/pyo3/pull/1682) +- Add `PyTraceback` type to represent and format Python tracebacks. [#1977](https://github.com/PyO3/pyo3/pull/1977) ### Changed @@ -19,6 +60,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Fix creating `#[classattr]` by functions with the name of a known magic method. [#1969](https://github.com/PyO3/pyo3/pull/1969) +- Fix use of `catch_unwind` in `allow_threads` which can cause fatal crashes. [#1989](https://github.com/PyO3/pyo3/pull/1989) +- Fix build failure on PyPy when abi3 features are activated. [#1991](https://github.com/PyO3/pyo3/pull/1991) +- Fix mingw platform detection. [#1993](https://github.com/PyO3/pyo3/pull/1993) +- Fix panic in `__get__` implementation when accessing descriptor on type object. [#1997](https://github.com/PyO3/pyo3/pull/1997) ## [0.15.0] - 2021-11-03 @@ -982,7 +1027,8 @@ Yanked - Initial release -[unreleased]: https://github.com/pyo3/pyo3/compare/v0.15.0...HEAD +[unreleased]: https://github.com/pyo3/pyo3/compare/v0.15.1...HEAD +[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 [0.14.4]: https://github.com/pyo3/pyo3/compare/v0.14.3...v0.14.4 diff --git a/Cargo.toml b/Cargo.toml index b7790271..6c106cdc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3" -version = "0.15.0" +version = "0.15.1" description = "Bindings to Python interpreter" authors = ["PyO3 Project and Contributors "] readme = "README.md" @@ -20,19 +20,17 @@ libc = "0.2.62" parking_lot = "0.11.0" # support crates for macros feature -pyo3-macros = { path = "pyo3-macros", version = "=0.15.0", optional = true } -# indoc must stay at 0.3.x for Rust 1.41 compatibility -indoc = { version = "0.3.6", optional = true } -paste = { version = "0.1.18", optional = true } +pyo3-macros = { path = "pyo3-macros", version = "=0.15.1", optional = true } +indoc = { version = "1.0.3", optional = true } +paste = { version = "1.0.6", optional = true } unindent = { version = "0.1.4", optional = true } # support crate for multiple-pymethods feature -# must stay at 0.1.x for Rust 1.41 compatibility -inventory = { version = "0.1.4", optional = true } +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 } +eyre = { version = ">= 0.4, < 0.7", optional = true } hashbrown = { version = ">= 0.9, < 0.12", optional = true } indexmap = { version = ">= 1.6, < 1.8", optional = true } num-bigint = { version = "0.4", optional = true } @@ -41,22 +39,15 @@ serde = { version = "1.0", optional = true } [dev-dependencies] assert_approx_eq = "1.1.0" -# O.3.5 uses the matches! macro, which isn't compatible with Rust 1.41 -criterion = "=0.3.4" -# half and bitflags use if/match in const fn, which isn't compatible with Rust 1.41 -half = "=1.7.1" -bitflags = "=1.2.1" +criterion = "0.3.5" trybuild = "1.0.49" rustversion = "1.0" # 1.0.0 requires Rust 1.50 proptest = { version = "0.10.1", default-features = false, features = ["std"] } serde_json = "1.0.61" -# features needed to run the PyO3 test suite -pyo3 = { path = ".", default-features = false, features = ["macros", "auto-initialize"] } - [build-dependencies] -pyo3-build-config = { path = "pyo3-build-config", version = "0.15.0", features = ["resolve-config"] } +pyo3-build-config = { path = "pyo3-build-config", version = "0.15.1", features = ["resolve-config"] } [features] default = ["macros"] @@ -76,7 +67,6 @@ extension-module = [] abi3 = ["pyo3-build-config/abi3"] # With abi3, we can manually set the minimum Python version. -abi3-py36 = ["abi3-py37", "pyo3-build-config/abi3-py36"] abi3-py37 = ["abi3-py38", "pyo3-build-config/abi3-py37"] abi3-py38 = ["abi3-py39", "pyo3-build-config/abi3-py38"] abi3-py39 = ["abi3-py310", "pyo3-build-config/abi3-py39"] diff --git a/Makefile b/Makefile index 485cbe0c..fcf40243 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: test test_py publish clippy lint fmt +.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 @@ -14,13 +14,18 @@ test: lint test_py test_py: for example in examples/*/; do TOX_TESTENV_PASSENV=RUSTUP_HOME tox -e py -c $$example || exit 1; done -fmt: - cargo fmt --all -- --check +fmt_py: black . --check +fmt_rust: + cargo fmt --all -- --check + +fmt: fmt_rust fmt_py + @true + clippy: - cargo clippy --features="$(ALL_ADDITIVE_FEATURES)" --tests -- -Dwarnings - cargo clippy --features="abi3 $(ALL_ADDITIVE_FEATURES)" --tests -- -Dwarnings + 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 diff --git a/README.md b/README.md index fdfb1c2a..90ff1f7d 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![benchmark](https://github.com/PyO3/pyo3/actions/workflows/bench.yml/badge.svg)](https://pyo3.rs/dev/bench/) [![codecov](https://codecov.io/gh/PyO3/pyo3/branch/main/graph/badge.svg)](https://codecov.io/gh/PyO3/pyo3) [![crates.io](https://img.shields.io/crates/v/pyo3)](https://crates.io/crates/pyo3) -[![minimum rustc 1.41](https://img.shields.io/badge/rustc-1.41+-blue.svg)](https://rust-lang.github.io/rfcs/2495-min-rust-version.html) +[![minimum rustc 1.48](https://img.shields.io/badge/rustc-1.48+-blue.svg)](https://rust-lang.github.io/rfcs/2495-min-rust-version.html) [![dev chat](https://img.shields.io/gitter/room/nwjs/nw.js.svg)](https://gitter.im/PyO3/Lobby) [![contributing notes](https://img.shields.io/badge/contribute-on%20github-Green)](https://github.com/PyO3/pyo3/blob/main/Contributing.md) @@ -17,22 +17,41 @@ ## Usage PyO3 supports the following software versions: - - Python 3.6 and up (CPython and PyPy) - - Rust 1.41 and up + - Python 3.7 and up (CPython and PyPy) + - Rust 1.48 and up You can use PyO3 to write a native Python module in Rust, or to embed Python in a Rust binary. The following sections explain each of these in turn. ### Using Rust from Python -PyO3 can be used to generate a native Python module. The easiest way to try this out for the first time is to use [`maturin`](https://github.com/PyO3/maturin). `maturin` is a tool for building and publishing Rust-based Python packages with minimal configuration. The following steps set up some files for an example Python module, install `maturin`, and then show how build and import the Python module. +PyO3 can be used to generate a native Python module. The easiest way to try this out for the first time is to use [`maturin`](https://github.com/PyO3/maturin). `maturin` is a tool for building and publishing Rust-based Python packages with minimal configuration. The following steps install `maturin`, use it to generate and build a new Python package, and then launch Python to import and execute a function from the package. -First, create a new folder (let's call it `string_sum`) containing the following two files: +First, follow the commands below to create a new directory containing a new Python `virtualenv`, and install `maturin` into the virtualenv using Python's package manager, `pip`: + +```bash +# (replace string_sum with the desired package name) +$ mkdir string_sum +$ cd string_sum +$ python -m venv .env +$ source .env/bin/activate +$ pip install maturin +``` + +Still inside this `string_sum` directory, now run `maturin init`. This will generate the new package source. When given the choice of bindings to use, select pyo3 bindings: + +```bash +$ maturin init +✔ 🤷 What kind of bindings to use? · pyo3 + ✨ Done! New project created string_sum +``` + +The most important files generated by this command are `Cargo.toml` and `lib.rs`, which will look roughly like the following: **`Cargo.toml`** ```toml [package] -name = "string-sum" +name = "string_sum" version = "0.1.0" edition = "2018" @@ -45,9 +64,8 @@ name = "string_sum" # crate-type = ["cdylib", "rlib"] crate-type = ["cdylib"] -[dependencies.pyo3] -version = "0.15.0" -features = ["extension-module"] +[dependencies] +pyo3 = { version = "0.15.1", features = ["extension-module"] } ``` **`src/lib.rs`** @@ -67,21 +85,11 @@ fn sum_as_string(a: usize, b: usize) -> PyResult { #[pymodule] fn string_sum(_py: Python, m: &PyModule) -> PyResult<()> { m.add_function(wrap_pyfunction!(sum_as_string, m)?)?; - Ok(()) } ``` -With those two files in place, now `maturin` needs to be installed. This can be done using Python's package manager `pip`. First, load up a new Python `virtualenv`, and install `maturin` into it: - -```bash -$ cd string_sum -$ python -m venv .env -$ source .env/bin/activate -$ pip install maturin -``` - -Now build and execute the module: +Finally, run `maturin develop`. This will build the package and install it into the Python virtualenv previously created and activated. The package is then ready to be used from `python`: ```bash $ maturin develop @@ -92,7 +100,20 @@ $ python '25' ``` -As well as with `maturin`, it is possible to build using [`setuptools-rust`](https://github.com/PyO3/setuptools-rust) or [manually](https://pyo3.rs/latest/building_and_distribution.html#manual-builds). Both offer more flexibility than `maturin` but require further configuration. +To make changes to the package, just edit the Rust source code and then re-run `maturin develop` to recompile. + +To run this all as a single copy-and-paste, use the bash script below (replace `string_sum` in the first command with the desired package name): + +```bash +mkdir string_sum && cd "$_" +python -m venv .env +source .env/bin/activate +pip install maturin +maturin init --bindings pyo3 +maturin develop +``` + +As well as with `maturin`, it is possible to build using [`setuptools-rust`](https://github.com/PyO3/setuptools-rust) or [manually](https://pyo3.rs/latest/building_and_distribution.html#manual-builds). Both offer more flexibility than `maturin` but require more configuration to get started. ### Using Python from Rust @@ -108,7 +129,7 @@ Start a new project with `cargo new` and add `pyo3` to the `Cargo.toml` like th ```toml [dependencies.pyo3] -version = "0.15.0" +version = "0.15.1" features = ["auto-initialize"] ``` @@ -171,6 +192,7 @@ about this topic. ## Articles and other media +- [Calling Rust from Python using PyO3](https://saidvandeklundert.net/learn/2021-11-18-calling-rust-from-python-using-pyo3/) - Nov 18, 2021 - [davidhewitt's 2021 talk at Rust Manchester meetup](https://www.youtube.com/watch?v=-XyWG_klSAw&t=320s) - Aug 19, 2021 - [Incrementally porting a small Python project to Rust](https://blog.waleedkhan.name/port-python-to-rust/) - Apr 29, 2021 - [Vortexa - Integrating Rust into Python](https://www.vortexa.com/insight/integrating-rust-into-python) - Apr 12, 2021 diff --git a/benches/bench_call.rs b/benches/bench_call.rs index 08b19496..90c1a7ff 100644 --- a/benches/bench_call.rs +++ b/benches/bench_call.rs @@ -4,19 +4,13 @@ use pyo3::prelude::*; macro_rules! test_module { ($py:ident, $code:literal) => { - PyModule::from_code($py, indoc::indoc!($code), file!(), "test_module") - .expect("module creation failed") + PyModule::from_code($py, $code, file!(), "test_module").expect("module creation failed") }; } fn bench_call_0(b: &mut Bencher) { Python::with_gil(|py| { - let module = test_module!( - py, - r#" - def foo(): pass - "# - ); + let module = test_module!(py, "def foo(): pass"); let foo_module = module.getattr("foo").unwrap(); @@ -32,10 +26,11 @@ fn bench_call_method_0(b: &mut Bencher) { Python::with_gil(|py| { let module = test_module!( py, - r#" - class Foo: - def foo(self): pass - "# + " +class Foo: + def foo(self): + pass +" ); let foo_module = module.getattr("Foo").unwrap().call0().unwrap(); diff --git a/benches/bench_pyclass.rs b/benches/bench_pyclass.rs index 4cfe737b..03a8c07a 100644 --- a/benches/bench_pyclass.rs +++ b/benches/bench_pyclass.rs @@ -1,49 +1,64 @@ -use criterion::{criterion_group, criterion_main, Bencher, Criterion}; +#[cfg(feature = "macros")] +use criterion::{criterion_group, criterion_main, Criterion}; -use pyo3::{class::PyObjectProtocol, prelude::*, type_object::LazyStaticType}; +#[cfg(feature = "macros")] +mod m { + use pyo3::{class::PyObjectProtocol, prelude::*, type_object::LazyStaticType}; -/// This is a feature-rich class instance used to benchmark various parts of the pyclass lifecycle. -#[pyclass] -struct MyClass { - #[pyo3(get, set)] - elements: Vec, -} - -#[pymethods] -impl MyClass { - #[new] - fn new(elements: Vec) -> Self { - Self { elements } + /// This is a feature-rich class instance used to benchmark various parts of the pyclass lifecycle. + #[pyclass] + struct MyClass { + #[pyo3(get, set)] + elements: Vec, } - fn __call__(&mut self, new_element: i32) -> usize { - self.elements.push(new_element); - self.elements.len() + #[pymethods] + impl MyClass { + #[new] + fn new(elements: Vec) -> Self { + Self { elements } + } + + fn __call__(&mut self, new_element: i32) -> usize { + 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) { + let gil = Python::acquire_gil(); + let py = gil.python(); + b.iter(|| { + // This is using an undocumented internal PyO3 API to measure pyclass performance; please + // don't use this in your own code! + let ty = LazyStaticType::new(); + ty.get_or_init::(py); + }); } } -#[pyproto] -impl PyObjectProtocol for MyClass { - /// A basic __str__ implementation. - fn __str__(&self) -> &'static str { - "MyClass" - } -} - -fn first_time_init(b: &mut Bencher) { - let gil = Python::acquire_gil(); - let py = gil.python(); - b.iter(|| { - // This is using an undocumented internal PyO3 API to measure pyclass performance; please - // don't use this in your own code! - let ty = LazyStaticType::new(); - ty.get_or_init::(py); - }); -} - +#[cfg(feature = "macros")] fn criterion_benchmark(c: &mut Criterion) { - c.bench_function("first_time_init", first_time_init); + c.bench_function("first_time_init", m::first_time_init); } +#[cfg(feature = "macros")] criterion_group!(benches, criterion_benchmark); + +#[cfg(feature = "macros")] criterion_main!(benches); + +#[cfg(not(feature = "macros"))] +fn main() { + unimplemented!( + "benchmarking `bench_pyclass` is only available with the `macros` feature enabled" + ); +} diff --git a/examples/pyo3-benchmarks/requirements-dev.txt b/examples/pyo3-benchmarks/requirements-dev.txt index 8f7900ee..8005ca5e 100644 --- a/examples/pyo3-benchmarks/requirements-dev.txt +++ b/examples/pyo3-benchmarks/requirements-dev.txt @@ -1,4 +1,4 @@ pytest>=3.5.0 -setuptools-rust>=0.10.2 +setuptools_rust~=1.0.0 pytest-benchmark~=3.2 pip>=21.3 diff --git a/examples/pyo3-pytests/src/buf_and_str.rs b/examples/pyo3-pytests/src/buf_and_str.rs index 76e53093..47d74d38 100644 --- a/examples/pyo3-pytests/src/buf_and_str.rs +++ b/examples/pyo3-pytests/src/buf_and_str.rs @@ -1,3 +1,5 @@ +#![cfg(not(Py_LIMITED_API))] + //! Objects related to PyBuffer and PyStr use pyo3::buffer::PyBuffer; use pyo3::prelude::*; diff --git a/examples/pyo3-pytests/src/datetime.rs b/examples/pyo3-pytests/src/datetime.rs index a3522b48..11e33d11 100644 --- a/examples/pyo3-pytests/src/datetime.rs +++ b/examples/pyo3-pytests/src/datetime.rs @@ -1,3 +1,5 @@ +#![cfg(not(Py_LIMITED_API))] + use pyo3::prelude::*; use pyo3::types::{ PyDate, PyDateAccess, PyDateTime, PyDelta, PyDeltaAccess, PyTime, PyTimeAccess, PyTuple, @@ -49,8 +51,8 @@ fn time_with_fold<'p>( minute: u8, second: u8, microsecond: u32, - tzinfo: Option<&PyTzInfo>, fold: bool, + tzinfo: Option<&PyTzInfo>, ) -> PyResult<&'p PyTime> { PyTime::new_with_fold( py, diff --git a/examples/pyo3-pytests/src/lib.rs b/examples/pyo3-pytests/src/lib.rs index ed0b8180..3501e84f 100644 --- a/examples/pyo3-pytests/src/lib.rs +++ b/examples/pyo3-pytests/src/lib.rs @@ -12,7 +12,9 @@ pub mod path; pub mod pyclass_iter; pub mod subclassing; +#[cfg(not(Py_LIMITED_API))] use buf_and_str::*; +#[cfg(not(Py_LIMITED_API))] use datetime::*; use dict_iter::*; use misc::*; @@ -24,7 +26,9 @@ use subclassing::*; #[pymodule] fn pyo3_pytests(py: Python, m: &PyModule) -> PyResult<()> { + #[cfg(not(Py_LIMITED_API))] m.add_wrapped(wrap_pymodule!(buf_and_str))?; + #[cfg(not(Py_LIMITED_API))] m.add_wrapped(wrap_pymodule!(datetime))?; m.add_wrapped(wrap_pymodule!(dict_iter))?; m.add_wrapped(wrap_pymodule!(misc))?; diff --git a/examples/pyo3-pytests/tests/test_datetime.py b/examples/pyo3-pytests/tests/test_datetime.py index c8659835..f9966f63 100644 --- a/examples/pyo3-pytests/tests/test_datetime.py +++ b/examples/pyo3-pytests/tests/test_datetime.py @@ -139,7 +139,7 @@ def test_time_fold(t): @pytest.mark.xfail(PYPY, reason="Feature not available on PyPy") @pytest.mark.parametrize("fold", [False, True]) def test_time_fold(fold): - t = rdt.time_with_fold(0, 0, 0, 0, None, fold) + t = rdt.time_with_fold(0, 0, 0, 0, fold, None) assert t.fold == fold diff --git a/examples/setuptools-rust-starter/requirements-dev.txt b/examples/setuptools-rust-starter/requirements-dev.txt index 678a0650..c52fb607 100644 --- a/examples/setuptools-rust-starter/requirements-dev.txt +++ b/examples/setuptools-rust-starter/requirements-dev.txt @@ -1,3 +1,3 @@ pytest>=3.5.0 -setuptools_rust~=0.11.0 +setuptools_rust~=1.0.0 pip>=21.3 diff --git a/examples/word-count/requirements-dev.txt b/examples/word-count/requirements-dev.txt index f55c7ca8..dbc0f652 100644 --- a/examples/word-count/requirements-dev.txt +++ b/examples/word-count/requirements-dev.txt @@ -1,3 +1,3 @@ pytest>=3.5.0 -setuptools-rust>=0.10.2 +setuptools_rust~=1.0.0 pytest-benchmark>=3.1.1 diff --git a/guide/src/building_and_distribution.md b/guide/src/building_and_distribution.md index 0d63dd6c..0644859d 100644 --- a/guide/src/building_and_distribution.md +++ b/guide/src/building_and_distribution.md @@ -13,7 +13,7 @@ PyO3 uses a build script (backed by the [`pyo3-build-config`] crate) to determin - The `python` executable (if it's a Python 3 interpreter). - The `python3` executable. -You can override the Python interpreter by setting the `PYO3_PYTHON` environment variable, e.g. `PYO3_PYTHON=python3.6`, `PYO3_PYTHON=/usr/bin/python3.9`, or even a PyPy interpreter `PYO3_PYTHON=pypy3`. +You can override the Python interpreter by setting the `PYO3_PYTHON` environment variable, e.g. `PYO3_PYTHON=python3.7`, `PYO3_PYTHON=/usr/bin/python3.9`, or even a PyPy interpreter `PYO3_PYTHON=pypy3`. Once the Python interpreter is located, `pyo3-build-config` executes it to query the information in the `sysconfig` module which is needed to configure the rest of the compilation. @@ -145,16 +145,16 @@ See the [corresponding](https://github.com/PyO3/maturin/pull/353) [PRs](https:// #### Minimum Python version for `abi3` -Because a single `abi3` wheel can be used with many different Python versions, PyO3 has feature flags `abi3-py36`, `abi3-py37`, `abi-py38` etc. to set the minimum required Python version for your `abi3` wheel. -For example, if you set the `abi3-py36` feature, your extension wheel can be used on all Python 3 versions from Python 3.6 and up. `maturin` and `setuptools-rust` will give the wheel a name like `my-extension-1.0-cp36-abi3-manylinux2020_x86_64.whl`. +Because a single `abi3` wheel can be used with many different Python versions, PyO3 has feature flags `abi3-py37`, `abi3-py38`, `abi3-py39` etc. to set the minimum required Python version for your `abi3` wheel. +For example, if you set the `abi3-py37` feature, your extension wheel can be used on all Python 3 versions from Python 3.7 and up. `maturin` and `setuptools-rust` will give the wheel a name like `my-extension-1.0-cp37-abi3-manylinux2020_x86_64.whl`. As your extension module may be run with multiple different Python versions you may occasionally find you need to check the Python version at runtime to customize behavior. See [the relevant section of this guide](./building_and_distribution/multiple_python_versions.html#checking-the-python-version-at-runtime) on supporting multiple Python versions at runtime. -PyO3 is only able to link your extension module to api3 version up to and including your host Python version. E.g., if you set `abi3-py38` and try to compile the crate with a host of Python 3.6, the build will fail. +PyO3 is only able to link your extension module to api3 version up to and including your host Python version. E.g., if you set `abi3-py38` and try to compile the crate with a host of Python 3.7, the build will fail. As an advanced feature, you can build PyO3 wheel without calling Python interpreter with the environment variable `PYO3_NO_PYTHON` set. On unix systems this works unconditionally; on Windows you must also set the `RUSTFLAGS` evironment variable to contain `-L native=/path/to/python/libs` so that the linker can find `python3.lib`. -> Note: If you set more that one of these api version feature flags the highest version always wins. For example, with both `abi3-py36` and `abi3-py38` set, PyO3 would build a wheel which supports Python 3.8 and up. +> Note: If you set more that one of these api version feature flags the lowest version always wins. For example, with both `abi3-py37` and `abi3-py38` set, PyO3 would build a wheel which supports Python 3.7 and up. #### Missing features diff --git a/guide/src/class.md b/guide/src/class.md index 9ab5bf96..9ba39ed7 100644 --- a/guide/src/class.md +++ b/guide/src/class.md @@ -865,7 +865,6 @@ impl pyo3::class::impl_::PyClassImpl for MyClass { visitor(collector.py_class_descriptors()); visitor(collector.object_protocol_methods()); visitor(collector.async_protocol_methods()); - visitor(collector.context_protocol_methods()); visitor(collector.descr_protocol_methods()); visitor(collector.mapping_protocol_methods()); visitor(collector.number_protocol_methods()); diff --git a/guide/src/class/protocols.md b/guide/src/class/protocols.md index cc3e11c3..d8dcda1c 100644 --- a/guide/src/class/protocols.md +++ b/guide/src/class/protocols.md @@ -98,7 +98,7 @@ impl PyCounter { fn __new__(wraps: Py) -> Self { PyCounter { count: 0, wraps } } - + #[args(args="*", kwargs="**")] fn __call__( &mut self, py: Python, diff --git a/guide/src/conversions/traits.md b/guide/src/conversions/traits.md index a102d47f..619aac80 100644 --- a/guide/src/conversions/traits.md +++ b/guide/src/conversions/traits.md @@ -17,7 +17,7 @@ variety of Rust types, which you can check out in the implementor list of [`FromPyObject`]. [`FromPyObject`] is also implemented for your own Rust types wrapped as Python -objects (see [the chapter about classes](class.md)). There, in order to both be +objects (see [the chapter about classes](../class.md)). There, in order to both be able to operate on mutable references *and* satisfy Rust's rules of non-aliasing mutable references, you have to extract the PyO3 reference wrappers [`PyRef`] and [`PyRefMut`]. They work like the reference wrappers of @@ -413,7 +413,7 @@ enum RustyEnum { # { # let thing = b"foo".to_object(py); # let error = thing.extract::(py).unwrap_err(); -# assert!(error.is_instance::(py)); +# assert!(error.is_instance_of::(py)); # } # # Ok(()) diff --git a/guide/src/debugging.md b/guide/src/debugging.md index b7df5b40..3a624d2d 100644 --- a/guide/src/debugging.md +++ b/guide/src/debugging.md @@ -24,7 +24,7 @@ Valgrind is a tool to detect memory management bugs such as memory leaks. You first need to install a debug build of Python, otherwise Valgrind won't produce usable results. In Ubuntu there's e.g. a `python3-dbg` package. -Activate an environment with the debug interpreter and recompile. If you're on Linux, use `ldd` with the name of your binary and check that you're linking e.g. `libpython3.6dm.so.1.0` instead of `libpython3.6m.so.1.0`. +Activate an environment with the debug interpreter and recompile. If you're on Linux, use `ldd` with the name of your binary and check that you're linking e.g. `libpython3.7d.so.1.0` instead of `libpython3.7.so.1.0`. [Download the suppressions file for cpython](https://raw.githubusercontent.com/python/cpython/master/Misc/valgrind-python.supp). diff --git a/guide/src/ecosystem/async-await.md b/guide/src/ecosystem/async-await.md index 7c92ea46..37e1333f 100644 --- a/guide/src/ecosystem/async-await.md +++ b/guide/src/ecosystem/async-await.md @@ -536,12 +536,13 @@ fn main() -> PyResult<()> { pyo3_asyncio::async_std::run(py, async move { // verify that we are on a uvloop.Loop Python::with_gil(|py| -> PyResult<()> { - assert!(uvloop - .as_ref(py) - .getattr("Loop")? - .downcast::() - .unwrap() - .is_instance(pyo3_asyncio::async_std::get_current_loop(py)?)?); + assert!(pyo3_asyncio::async_std::get_current_loop(py)?.is_instance( + uvloop + .as_ref(py) + .getattr("Loop")? + .downcast::() + .unwrap() + )?); Ok(()) })?; diff --git a/guide/src/exception.md b/guide/src/exception.md index 63ee6dda..251d7f85 100644 --- a/guide/src/exception.md +++ b/guide/src/exception.md @@ -90,31 +90,29 @@ Python::with_gil(|py| { }); ``` -If you already have a Python exception instance, you can simply call [`PyErr::from_instance`]. +If you already have a Python exception object, you can simply call [`PyErr::from_value`]. ```rust,ignore -PyErr::from_instance(py, err).restore(py); +PyErr::from_value(py, err).restore(py); ``` ## Checking exception types Python has an [`isinstance`](https://docs.python.org/3/library/functions.html#isinstance) method to check an object's type. -In PyO3 every native type has access to the [`PyAny::is_instance`] method which does the same thing. +In PyO3 every object has the [`PyAny::is_instance`] and [`PyAny::is_instance_of`] methods which do the same thing. ```rust use pyo3::Python; use pyo3::types::{PyBool, PyList}; Python::with_gil(|py| { - assert!(PyBool::new(py, true).is_instance::().unwrap()); + assert!(PyBool::new(py, true).is_instance_of::().unwrap()); let list = PyList::new(py, &[1, 2, 3, 4]); - assert!(!list.is_instance::().unwrap()); - assert!(list.is_instance::().unwrap()); + assert!(!list.is_instance_of::().unwrap()); + assert!(list.is_instance_of::().unwrap()); }); ``` -[`PyAny::is_instance`] calls the underlying [`PyType::is_instance`]({{#PYO3_DOCS_URL}}/pyo3/types/struct.PyType.html#method.is_instance) -method to do the actual work. To check the type of an exception, you can similarly do: @@ -123,7 +121,7 @@ To check the type of an exception, you can similarly do: # use pyo3::prelude::*; # Python::with_gil(|py| { # let err = PyTypeError::new_err(()); -err.is_instance::(py); +err.is_instance_of::(py); # }); ``` @@ -136,7 +134,7 @@ which is an alias for the type `Result`. A [`PyErr`] represents a Python exception. Errors within the PyO3 library are also exposed as Python exceptions. -If your code has a custom error type, adding an implementation of `std::convert::From for PyErr` +If your code has a custom error type, adding an implementation of `std::convert::From for PyErr` is usually enough. PyO3 will then automatically convert your error to a Python exception when needed. The following code snippet defines a Rust error named `CustomIOError`. In its `From for PyErr` @@ -184,13 +182,13 @@ fn main() { Python::with_gil(|py| { let fun = pyo3::wrap_pyfunction!(connect, py).unwrap(); let err = fun.call1(("0.0.0.0",)).unwrap_err(); - assert!(err.is_instance::(py)); + assert!(err.is_instance_of::(py)); }); } ``` This has been implemented for most of Rust's standard library errors, so that you can use the `?` -("try") operator with them. The following code snippet will raise a `ValueError` in Python if +("try") operator with them. The following code snippet will raise a `ValueError` in Python if `String::parse()` returns an error. ```rust @@ -201,21 +199,21 @@ fn parse_int(s: String) -> PyResult { } # # use pyo3::exceptions::PyValueError; -# +# # fn main() { # Python::with_gil(|py| { # assert_eq!(parse_int(String::from("1")).unwrap(), 1); # assert_eq!(parse_int(String::from("1337")).unwrap(), 1337); -# +# # assert!(parse_int(String::from("-1")) # .unwrap_err() -# .is_instance::(py)); +# .is_instance_of::(py)); # assert!(parse_int(String::from("foo")) # .unwrap_err() -# .is_instance::(py)); +# .is_instance_of::(py)); # assert!(parse_int(String::from("13.37")) # .unwrap_err() -# .is_instance::(py)); +# .is_instance_of::(py)); # }) # } ``` @@ -256,6 +254,6 @@ defines exceptions for several standard library modules. [`PyErr`]: {{#PYO3_DOCS_URL}}/pyo3/struct.PyErr.html [`PyResult`]: {{#PYO3_DOCS_URL}}/pyo3/type.PyResult.html -[`PyErr::from_instance`]: {{#PYO3_DOCS_URL}}/pyo3/struct.PyErr.html#method.from_instance -[`Python::is_instance`]: {{#PYO3_DOCS_URL}}/pyo3/struct.Python.html#method.is_instance +[`PyErr::from_value`]: {{#PYO3_DOCS_URL}}/pyo3/struct.PyErr.html#method.from_value [`PyAny::is_instance`]: {{#PYO3_DOCS_URL}}/pyo3/struct.PyAny.html#method.is_instance +[`PyAny::is_instance_of`]: {{#PYO3_DOCS_URL}}/pyo3/struct.PyAny.html#method.is_instance_of diff --git a/guide/src/faq.md b/guide/src/faq.md index 4f2786aa..42cb5b27 100644 --- a/guide/src/faq.md +++ b/guide/src/faq.md @@ -139,3 +139,23 @@ a: b: ``` The downside to this approach is that any Rust code working on the `Outer` struct now has to acquire the GIL to do anything with its field. + +## I want to use the `pyo3` crate re-exported from from dependency but the proc-macros fail! + +All PyO3 proc-macros (`#[pyclass]`, `#[pyfunction]`, `#[derive(FromPyObject)]` +and so on) expect the `pyo3` crate to be available under that name in your crate +root, which is the normal situation when `pyo3` is a direct dependency of your +crate. + +However, when the dependency is renamed, or your crate only indirectly depends +on `pyo3`, you need to let the macro code know where to find the crate. This is +done with the `crate` attribute: + +```rust +# use pyo3::prelude::*; +# pub extern crate pyo3; +# mod reexported { pub use ::pyo3; } +#[pyclass] +#[pyo3(crate = "reexported::pyo3")] +struct MyClass; +``` diff --git a/guide/src/features.md b/guide/src/features.md index 86447c9d..5af4f3ec 100644 --- a/guide/src/features.md +++ b/guide/src/features.md @@ -24,7 +24,7 @@ See the [building and distribution](building_and_distribution.md#py_limited_apia ### The `abi3-pyXY` features -(`abi3-py36`, `abi3-py37`, `abi3-py38`, `abi3-py39`, and `abi3-py310`) +(`abi3-py37`, `abi3-py38`, `abi3-py39`, and `abi3-py310`) These features are extensions of the `abi3` feature to specify the exact minimum Python version which the multiple-version-wheel will support. @@ -74,10 +74,8 @@ The `nightly` feature needs the nightly Rust compiler. This allows PyO3 to use R ### `resolve-config` The `resolve-config` feature of the `pyo3-build-config` crate controls whether that crate's -build script automatically resolves a Python interpreter / build configuration. Disabling -this feature enables this crate to be used in *library mode*. This may be desirable for -use cases where you want to read or write PyO3 build configuration files or resolve -metadata about a Python interpreter. +build script automatically resolves a Python interpreter / build configuration. This feature is primarily useful when building PyO3 +itself. By default this feature is not enabled, meaning you can freely use `pyo3-build-config` as a standalone library to read or write PyO3 build configuration files or resolve metadata about a Python interpreter. ## Optional Dependencies @@ -127,5 +125,3 @@ struct User { permissions: Vec> } ``` - - diff --git a/guide/src/migration.md b/guide/src/migration.md index 56a714ac..4c298bf2 100644 --- a/guide/src/migration.md +++ b/guide/src/migration.md @@ -3,6 +3,12 @@ This guide can help you upgrade code through breaking changes from one PyO3 version to the next. For a detailed list of all changes, see the [CHANGELOG](changelog.md). +## from 0.15.* to 0.16 + +### Drop support for older technogies + +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. + ## from 0.14.* to 0.15 ### Changes in sequence indexing @@ -164,7 +170,7 @@ let err: PyErr = TypeError::py_err("error message"); After: -```rust +```rust,ignore # use pyo3::{PyErr, PyResult, Python, type_object::PyTypeObject}; # use pyo3::exceptions::{PyBaseException, PyTypeError}; # Python::with_gil(|py| -> PyResult<()> { diff --git a/guide/src/python_from_rust.md b/guide/src/python_from_rust.md index f5648213..8b3e9d55 100644 --- a/guide/src/python_from_rust.md +++ b/guide/src/python_from_rust.md @@ -280,7 +280,7 @@ class House(object): Err(e) => { house.call_method1( "__exit__", - (e.ptype(py), e.pvalue(py), e.ptraceback(py)) + (e.get_type(py), e.value(py), e.traceback(py)) ).unwrap(); } } diff --git a/pyo3-build-config/Cargo.toml b/pyo3-build-config/Cargo.toml index 2ade560e..9dfdcd82 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.0" +version = "0.15.1" description = "Build configuration for the PyO3 ecosystem" authors = ["PyO3 Project and Contributors "] keywords = ["pyo3", "python", "cpython", "ffi"] @@ -14,14 +14,13 @@ edition = "2018" once_cell = "1" [features] -default = ["resolve-config"] +default = [] # Attempt to resolve a Python interpreter config for building in the build # script. If this feature isn't enabled, the build script no-ops. resolve-config = [] abi3 = [] -abi3-py36 = ["abi3-py37"] abi3-py37 = ["abi3-py38"] abi3-py38 = ["abi3-py39"] abi3-py39 = ["abi3-py310"] diff --git a/pyo3-build-config/build.rs b/pyo3-build-config/build.rs index 7e2cae9b..45a5bb71 100644 --- a/pyo3-build-config/build.rs +++ b/pyo3-build-config/build.rs @@ -63,7 +63,7 @@ pub fn abi3_config() -> Option { abi3: true, lib_name: None, lib_dir: None, - build_flags: BuildFlags::abi3(), + build_flags: BuildFlags::default(), pointer_width: None, executable: None, shared: true, diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index 4690b6e7..c48d5569 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -18,7 +18,7 @@ use crate::{ }; /// Minimum Python version PyO3 supports. -const MINIMUM_SUPPORTED_VERSION: PythonVersion = PythonVersion { major: 3, minor: 6 }; +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; @@ -131,12 +131,11 @@ impl InterpreterConfig { pub fn emit_pyo3_cfgs(&self) { // This should have been checked during pyo3-build-config build time. assert!(self.version >= MINIMUM_SUPPORTED_VERSION); - for i in MINIMUM_SUPPORTED_VERSION.minor..=self.version.minor { - println!("cargo:rustc-cfg=Py_3_{}", i); - } - if self.abi3 { - println!("cargo:rustc-cfg=Py_LIMITED_API"); + // pyo3-build-config was released when Python 3.6 was supported, so minimum flag to emit is + // Py_3_6 (to avoid silently breaking users who depend on this cfg). + for i in 6..=self.version.minor { + println!("cargo:rustc-cfg=Py_3_{}", i); } if self.implementation.is_pypy() { @@ -147,7 +146,9 @@ impl InterpreterConfig { See https://foss.heptapod.net/pypy/pypy/-/issues/3397 for more information." ); } - }; + } else if self.abi3 { + println!("cargo:rustc-cfg=Py_LIMITED_API"); + } for flag in &self.build_flags.0 { println!("cargo:rustc-cfg=py_sys_config=\"{}\"", flag) @@ -202,7 +203,7 @@ print_if_set("libdir", get_config_var("LIBDIR")) print_if_set("base_prefix", base_prefix) print("executable", sys.executable) print("calcsize_pointer", struct.calcsize("P")) -print("mingw", get_platform() == "mingw") +print("mingw", get_platform().startswith("mingw")) "#; let output = run_python_script(interpreter.as_ref(), SCRIPT)?; let map: HashMap = parse_script_output(&output); @@ -221,7 +222,12 @@ print("mingw", get_platform() == "mingw") let implementation = map["implementation"].parse()?; let lib_name = if cfg!(windows) { - default_lib_name_windows(version, abi3, map["mingw"].as_str() == "True") + default_lib_name_windows( + version, + implementation, + abi3, + map["mingw"].as_str() == "True", + ) } else { default_lib_name_unix( version, @@ -255,7 +261,7 @@ print("mingw", get_platform() == "mingw") lib_dir, executable: map.get("executable").cloned(), pointer_width: Some(calcsize_pointer * 8), - build_flags: BuildFlags::from_interpreter(interpreter)?.fixup(version, implementation), + build_flags: BuildFlags::from_interpreter(interpreter)?.fixup(version), suppress_build_script_link_lines: false, extra_build_script_lines: vec![], }) @@ -277,7 +283,7 @@ print("mingw", get_platform() == "mingw") macro_rules! parse_value { ($variable:ident, $value:ident) => { - $variable = Some($value.parse().context(format!( + $variable = Some($value.trim().parse().context(format!( concat!( "failed to parse ", stringify!($variable), @@ -344,14 +350,7 @@ print("mingw", get_platform() == "mingw") lib_dir, executable, pointer_width, - build_flags: build_flags.unwrap_or_else(|| { - if abi3 { - BuildFlags::abi3() - } else { - BuildFlags::default() - } - .fixup(version, implementation) - }), + build_flags: build_flags.unwrap_or_default(), suppress_build_script_link_lines: suppress_build_script_link_lines.unwrap_or(false), extra_build_script_lines, }) @@ -554,7 +553,6 @@ fn cross_compiling() -> Result> { #[allow(non_camel_case_types)] #[derive(Debug, Clone, Hash, PartialEq, Eq)] pub enum BuildFlag { - WITH_THREAD, Py_DEBUG, Py_REF_DEBUG, Py_TRACE_REFS, @@ -575,7 +573,6 @@ impl FromStr for BuildFlag { type Err = std::convert::Infallible; fn from_str(s: &str) -> Result { match s { - "WITH_THREAD" => Ok(BuildFlag::WITH_THREAD), "Py_DEBUG" => Ok(BuildFlag::Py_DEBUG), "Py_REF_DEBUG" => Ok(BuildFlag::Py_REF_DEBUG), "Py_TRACE_REFS" => Ok(BuildFlag::Py_TRACE_REFS), @@ -602,9 +599,7 @@ impl FromStr for BuildFlag { pub struct BuildFlags(pub HashSet); impl BuildFlags { - const ALL: [BuildFlag; 5] = [ - // TODO: Remove WITH_THREAD once Python 3.6 support dropped (as it's always on). - BuildFlag::WITH_THREAD, + const ALL: [BuildFlag; 4] = [ BuildFlag::Py_DEBUG, BuildFlag::Py_REF_DEBUG, BuildFlag::Py_TRACE_REFS, @@ -633,9 +628,10 @@ impl BuildFlags { /// the interpreter and printing variables of interest from /// sysconfig.get_config_vars. fn from_interpreter(interpreter: impl AsRef) -> Result { - // If we're on a Windows host, then Python won't have any useful config vars + // sysconfig is missing all the flags on windows, so we can't actually + // query the interpreter directly for its build flags. if cfg!(windows) { - return Ok(Self::windows_hardcoded()); + return Ok(Self::new()); } let mut script = String::from("import sysconfig\n"); @@ -662,21 +658,7 @@ impl BuildFlags { Ok(Self(flags)) } - fn windows_hardcoded() -> Self { - // sysconfig is missing all the flags on windows, so we can't actually - // query the interpreter directly for its build flags. - let mut flags = HashSet::new(); - flags.insert(BuildFlag::WITH_THREAD); - Self(flags) - } - - pub fn abi3() -> Self { - let mut flags = HashSet::new(); - flags.insert(BuildFlag::WITH_THREAD); - Self(flags) - } - - fn fixup(mut self, version: PythonVersion, implementation: PythonImplementation) -> Self { + fn fixup(mut self, version: PythonVersion) -> Self { if self.0.contains(&BuildFlag::Py_DEBUG) { self.0.insert(BuildFlag::Py_REF_DEBUG); if version <= PythonVersion::PY37 { @@ -685,11 +667,6 @@ impl BuildFlags { } } - // WITH_THREAD is always on for Python 3.7, and for PyPy. - if implementation == PythonImplementation::PyPy || version >= PythonVersion::PY37 { - self.0.insert(BuildFlag::WITH_THREAD); - } - self } } @@ -714,7 +691,7 @@ impl FromStr for BuildFlags { fn from_str(value: &str) -> Result { let mut flags = HashSet::new(); - for flag in value.split(',') { + for flag in value.split_terminator(',') { flags.insert(flag.parse().unwrap()); } Ok(BuildFlags(flags)) @@ -818,7 +795,7 @@ for key in KEYS: )), executable: None, pointer_width, - build_flags: BuildFlags::from_config_map(&sysconfigdata).fixup(version, implementation), + build_flags: BuildFlags::from_config_map(&sysconfigdata).fixup(version), suppress_build_script_link_lines: false, extra_build_script_lines: vec![], }) @@ -908,10 +885,8 @@ fn search_lib_dir(path: impl AsRef, cross: &CrossCompileConfig) -> Vec vec![f.path()], // Python 3.7+ sysconfigdata with platform specifics Ok(f) if starts_with(f, "_sysconfigdata_") && ends_with(f, "py") => vec![f.path()], Ok(f) if f.metadata().map_or(false, |metadata| metadata.is_dir()) => { @@ -966,7 +941,6 @@ fn search_lib_dir(path: impl AsRef, cross: &CrossCompileConfig) -> Vec String { - if abi3 { +fn default_lib_name_windows( + version: PythonVersion, + implementation: PythonImplementation, + abi3: bool, + mingw: bool, +) -> String { + if abi3 && !implementation.is_pypy() { WINDOWS_ABI3_LIB_NAME.to_owned() } else if mingw { // https://packages.msys2.org/base/mingw-w64-python @@ -1203,7 +1187,7 @@ mod tests { fn test_config_file_roundtrip() { let config = InterpreterConfig { abi3: true, - build_flags: BuildFlags::abi3(), + build_flags: BuildFlags::default(), pointer_width: Some(32), executable: Some("executable".into()), implementation: PythonImplementation::CPython, @@ -1258,9 +1242,9 @@ mod tests { fn test_config_file_defaults() { // Only version is required assert_eq!( - InterpreterConfig::from_reader(Cursor::new("version=3.6")).unwrap(), + InterpreterConfig::from_reader(Cursor::new("version=3.7")).unwrap(), InterpreterConfig { - version: PythonVersion { major: 3, minor: 6 }, + version: PythonVersion { major: 3, minor: 7 }, implementation: PythonImplementation::CPython, shared: true, abi3: false, @@ -1302,45 +1286,26 @@ mod tests { } #[test] - fn build_flags_fixup_py36_debug() { + fn build_flags_fixup_py37_debug() { let mut build_flags = BuildFlags::new(); build_flags.0.insert(BuildFlag::Py_DEBUG); - build_flags = build_flags.fixup( - PythonVersion { major: 3, minor: 6 }, - PythonImplementation::CPython, - ); + build_flags = build_flags.fixup(PythonVersion { major: 3, minor: 7 }); - // On 3.6, Py_DEBUG implies Py_REF_DEBUG and Py_TRACE_REFS + // On 3.7, Py_DEBUG implies Py_REF_DEBUG and Py_TRACE_REFS assert!(build_flags.0.contains(&BuildFlag::Py_REF_DEBUG)); assert!(build_flags.0.contains(&BuildFlag::Py_TRACE_REFS)); } #[test] - fn build_flags_fixup_py37_debug() { + fn build_flags_fixup_py38_debug() { let mut build_flags = BuildFlags::new(); build_flags.0.insert(BuildFlag::Py_DEBUG); - build_flags = build_flags.fixup(PythonVersion::PY37, PythonImplementation::CPython); + build_flags = build_flags.fixup(PythonVersion { major: 3, minor: 8 }); - // On 3.7, Py_DEBUG implies Py_REF_DEBUG + // On 3.8, Py_DEBUG implies Py_REF_DEBUG assert!(build_flags.0.contains(&BuildFlag::Py_REF_DEBUG)); - - // 3.7 always has WITH_THREAD - assert!(build_flags.0.contains(&BuildFlag::WITH_THREAD)); - } - - #[test] - fn build_flags_fixup_pypy() { - let mut build_flags = BuildFlags::new(); - - build_flags = build_flags.fixup( - PythonVersion { major: 3, minor: 6 }, - PythonImplementation::PyPy, - ); - - // PyPy always has WITH_THREAD - assert!(build_flags.0.contains(&BuildFlag::WITH_THREAD)); } #[test] @@ -1364,7 +1329,7 @@ mod tests { fn windows_hardcoded_cross_compile() { let cross_config = CrossCompileConfig { lib_dir: "C:\\some\\path".into(), - version: Some(PythonVersion { major: 3, minor: 6 }), + version: Some(PythonVersion { major: 3, minor: 7 }), os: "os".into(), arch: "arch".into(), }; @@ -1373,14 +1338,14 @@ mod tests { super::windows_hardcoded_cross_compile(cross_config).unwrap(), InterpreterConfig { implementation: PythonImplementation::CPython, - version: PythonVersion { major: 3, minor: 6 }, + version: PythonVersion { major: 3, minor: 7 }, shared: true, abi3: false, - lib_name: Some("python36".into()), + lib_name: Some("python37".into()), lib_dir: Some("C:\\some\\path".into()), executable: None, pointer_width: None, - build_flags: BuildFlags::windows_hardcoded(), + build_flags: BuildFlags::default(), suppress_build_script_link_lines: false, extra_build_script_lines: vec![], } @@ -1389,22 +1354,52 @@ mod tests { #[test] fn default_lib_name_windows() { + use PythonImplementation::*; assert_eq!( - super::default_lib_name_windows(PythonVersion { major: 3, minor: 6 }, false, false), - "python36", + super::default_lib_name_windows( + PythonVersion { major: 3, minor: 7 }, + CPython, + false, + false + ), + "python37", ); assert_eq!( - super::default_lib_name_windows(PythonVersion { major: 3, minor: 6 }, true, false), + super::default_lib_name_windows( + PythonVersion { major: 3, minor: 7 }, + CPython, + true, + false + ), "python3", ); assert_eq!( - super::default_lib_name_windows(PythonVersion { major: 3, minor: 6 }, false, true), - "python3.6", + super::default_lib_name_windows( + PythonVersion { major: 3, minor: 7 }, + CPython, + false, + true + ), + "python3.7", ); assert_eq!( - super::default_lib_name_windows(PythonVersion { major: 3, minor: 6 }, true, true), + super::default_lib_name_windows( + PythonVersion { major: 3, minor: 7 }, + CPython, + true, + true + ), "python3", ); + assert_eq!( + super::default_lib_name_windows( + PythonVersion { major: 3, minor: 7 }, + PyPy, + true, + false + ), + "python37", + ); } #[test] @@ -1412,8 +1407,8 @@ mod tests { use PythonImplementation::*; // Defaults to pythonX.Y for CPython assert_eq!( - super::default_lib_name_unix(PythonVersion { major: 3, minor: 6 }, CPython, None), - "python3.6", + super::default_lib_name_unix(PythonVersion { major: 3, minor: 7 }, CPython, None), + "python3.7", ); assert_eq!( super::default_lib_name_unix(PythonVersion { major: 3, minor: 9 }, CPython, None), @@ -1440,7 +1435,7 @@ mod tests { fn interpreter_version_reduced_to_abi3() { let mut config = InterpreterConfig { abi3: true, - build_flags: BuildFlags::new(), + build_flags: BuildFlags::default(), pointer_width: None, executable: None, implementation: PythonImplementation::CPython, @@ -1452,8 +1447,8 @@ mod tests { extra_build_script_lines: vec![], }; - fixup_config_for_abi3(&mut config, Some(PythonVersion { major: 3, minor: 6 })).unwrap(); - assert_eq!(config.version, PythonVersion { major: 3, minor: 6 }); + fixup_config_for_abi3(&mut config, Some(PythonVersion { major: 3, minor: 7 })).unwrap(); + assert_eq!(config.version, PythonVersion { major: 3, minor: 7 }); } #[test] @@ -1467,16 +1462,16 @@ mod tests { lib_dir: None, lib_name: None, shared: true, - version: PythonVersion { major: 3, minor: 6 }, + version: PythonVersion { major: 3, minor: 7 }, suppress_build_script_link_lines: false, extra_build_script_lines: vec![], }; assert!( - fixup_config_for_abi3(&mut config, Some(PythonVersion { major: 3, minor: 7 })) + fixup_config_for_abi3(&mut config, Some(PythonVersion { major: 3, minor: 8 })) .unwrap_err() .to_string() - .contains("cannot set a minimum Python version 3.7 higher than the interpreter version 3.6") + .contains("cannot set a minimum Python version 3.8 higher than the interpreter version 3.7") ); } @@ -1535,11 +1530,11 @@ mod tests { fn test_venv_interpreter() { let base = OsStr::new("base"); assert_eq!( - venv_interpreter(&base, true), + venv_interpreter(base, true), PathBuf::from_iter(&["base", "Scripts", "python.exe"]) ); assert_eq!( - venv_interpreter(&base, false), + venv_interpreter(base, false), PathBuf::from_iter(&["base", "bin", "python"]) ); } @@ -1548,11 +1543,11 @@ mod tests { fn test_conda_env_interpreter() { let base = OsStr::new("base"); assert_eq!( - conda_env_interpreter(&base, true), + conda_env_interpreter(base, true), PathBuf::from_iter(&["base", "python.exe"]) ); assert_eq!( - conda_env_interpreter(&base, false), + conda_env_interpreter(base, false), PathBuf::from_iter(&["base", "bin", "python"]) ); } diff --git a/pyo3-build-config/src/lib.rs b/pyo3-build-config/src/lib.rs index 80219526..28a611bd 100644 --- a/pyo3-build-config/src/lib.rs +++ b/pyo3-build-config/src/lib.rs @@ -25,7 +25,7 @@ pub use impl_::{BuildFlag, BuildFlags, InterpreterConfig, PythonImplementation, /// /// | Flag | Description | /// | ---- | ----------- | -/// | `#[cfg(Py_3_6)]`, `#[cfg(Py_3_7)]`, `#[cfg(Py_3_8)]`, `#[cfg(Py_3_9)]`, `#[cfg(Py_3_10)]` | These attributes mark code only for a given Python version and up. For example, `#[cfg(Py_3_6)]` marks code which can run on Python 3.6 **and newer**. | +/// | `#[cfg(Py_3_7)]`, `#[cfg(Py_3_8)]`, `#[cfg(Py_3_9)]`, `#[cfg(Py_3_10)]` | These attributes mark code only for a given Python version and up. For example, `#[cfg(Py_3_7)]` marks code which can run on Python 3.7 **and newer**. | /// | `#[cfg(Py_LIMITED_API)]` | This marks code which is run when compiling with PyO3's `abi3` feature enabled. | /// | `#[cfg(PyPy)]` | This marks code which is run when compiling for PyPy. | /// diff --git a/pyo3-macros-backend/Cargo.toml b/pyo3-macros-backend/Cargo.toml index a72239e5..5abd99af 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.0" +version = "0.15.1" description = "Code generation for PyO3 package" authors = ["PyO3 Project and Contributors "] keywords = ["pyo3", "python", "cpython", "ffi"] @@ -16,7 +16,7 @@ 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.0", features = ["resolve-config"] } +pyo3-build-config = { path = "../pyo3-build-config", version = "0.15.1", features = ["resolve-config"] } [dependencies.syn] version = "1" diff --git a/pyo3-macros-backend/src/attributes.rs b/pyo3-macros-backend/src/attributes.rs index 04dcc054..e0566076 100644 --- a/pyo3-macros-backend/src/attributes.rs +++ b/pyo3-macros-backend/src/attributes.rs @@ -1,13 +1,10 @@ use syn::{ parse::{Parse, ParseStream}, punctuated::Punctuated, - spanned::Spanned, token::Comma, - Attribute, ExprPath, Ident, LitStr, Result, Token, + Attribute, ExprPath, Ident, LitStr, Path, Result, Token, }; -use crate::deprecations::{Deprecation, Deprecations}; - pub mod kw { syn::custom_keyword!(annotation); syn::custom_keyword!(attribute); @@ -46,6 +43,19 @@ impl Parse for NameAttribute { } } +/// 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, @@ -113,66 +123,3 @@ pub fn take_pyo3_options(attrs: &mut Vec) -> Result syn::Result> { - match attr.parse_meta() { - Ok(syn::Meta::NameValue(syn::MetaNameValue { - path, - lit: syn::Lit::Str(s), - .. - })) if path.is_ident("name") => { - deprecations.push(Deprecation::NameAttribute, attr.span()); - Ok(Some(NameAttribute(s.parse()?))) - } - _ => Ok(None), - } -} - -pub fn get_deprecated_text_signature_attribute( - attr: &syn::Attribute, - deprecations: &mut Deprecations, -) -> syn::Result> { - match attr.parse_meta() { - Ok(syn::Meta::NameValue(syn::MetaNameValue { - path, - lit: syn::Lit::Str(lit), - .. - })) if path.is_ident("text_signature") => { - let text_signature = TextSignatureAttribute { - kw: syn::parse_quote!(text_signature), - eq_token: syn::parse_quote!(=), - lit, - }; - deprecations.push( - crate::deprecations::Deprecation::TextSignatureAttribute, - attr.span(), - ); - Ok(Some(text_signature)) - } - _ => Ok(None), - } -} - -pub fn take_deprecated_text_signature_attribute( - attrs: &mut Vec, - deprecations: &mut Deprecations, -) -> syn::Result> { - let mut text_signature = None; - let mut attrs_out = Vec::with_capacity(attrs.len()); - for attr in attrs.drain(..) { - if let Some(value) = get_deprecated_text_signature_attribute(&attr, deprecations)? { - ensure_spanned!( - text_signature.is_none(), - attr.span() => "text_signature attribute already specified previously" - ); - text_signature = Some(value); - } else { - attrs_out.push(attr); - } - } - *attrs = attrs_out; - Ok(text_signature) -} diff --git a/pyo3-macros-backend/src/defs.rs b/pyo3-macros-backend/src/defs.rs index bdda83b6..be90f137 100644 --- a/pyo3-macros-backend/src/defs.rs +++ b/pyo3-macros-backend/src/defs.rs @@ -107,13 +107,6 @@ impl PyMethod { can_coexist: true, } } - const fn new(name: &'static str, proto: &'static str) -> Self { - PyMethod { - name, - proto, - can_coexist: false, - } - } } /// Represents a slot definition. @@ -156,20 +149,13 @@ pub const OBJECT: Proto = Proto { .has_self(), MethodProto::new("__str__", "PyObjectStrProtocol").has_self(), MethodProto::new("__repr__", "PyObjectReprProtocol").has_self(), - MethodProto::new("__format__", "PyObjectFormatProtocol") - .args(&["Format"]) - .has_self(), MethodProto::new("__hash__", "PyObjectHashProtocol").has_self(), - MethodProto::new("__bytes__", "PyObjectBytesProtocol").has_self(), MethodProto::new("__richcmp__", "PyObjectRichcmpProtocol") .args(&["Other"]) .has_self(), MethodProto::new("__bool__", "PyObjectBoolProtocol").has_self(), ], - py_methods: &[ - PyMethod::new("__format__", "FormatProtocolImpl"), - PyMethod::new("__bytes__", "BytesProtocolImpl"), - ], + py_methods: &[], slot_defs: &[ SlotDef::new(&["__str__"], "Py_tp_str", "str"), SlotDef::new(&["__repr__"], "Py_tp_repr", "repr"), @@ -194,15 +180,8 @@ pub const ASYNC: Proto = Proto { MethodProto::new("__await__", "PyAsyncAwaitProtocol").args(&["Receiver"]), MethodProto::new("__aiter__", "PyAsyncAiterProtocol").args(&["Receiver"]), MethodProto::new("__anext__", "PyAsyncAnextProtocol").args(&["Receiver"]), - MethodProto::new("__aenter__", "PyAsyncAenterProtocol").has_self(), - MethodProto::new("__aexit__", "PyAsyncAexitProtocol") - .args(&["ExcType", "ExcValue", "Traceback"]) - .has_self(), - ], - py_methods: &[ - PyMethod::new("__aenter__", "PyAsyncAenterProtocolImpl"), - PyMethod::new("__aexit__", "PyAsyncAexitProtocolImpl"), ], + py_methods: &[], slot_defs: &[ SlotDef::new(&["__await__"], "Py_am_await", "await_"), SlotDef::new(&["__aiter__"], "Py_am_aiter", "aiter"), @@ -228,22 +207,6 @@ pub const BUFFER: Proto = Proto { ], }; -pub const CONTEXT: Proto = Proto { - name: "Context", - module: "::pyo3::class::context", - methods: &[ - MethodProto::new("__enter__", "PyContextEnterProtocol").has_self(), - MethodProto::new("__exit__", "PyContextExitProtocol") - .args(&["ExcType", "ExcValue", "Traceback"]) - .has_self(), - ], - py_methods: &[ - PyMethod::new("__enter__", "PyContextEnterProtocolImpl"), - PyMethod::new("__exit__", "PyContextExitProtocolImpl"), - ], - slot_defs: &[], -}; - pub const GC: Proto = Proto { name: "GC", module: "::pyo3::class::gc", @@ -268,17 +231,8 @@ pub const DESCR: Proto = Proto { methods: &[ MethodProto::new("__get__", "PyDescrGetProtocol").args(&["Receiver", "Inst", "Owner"]), MethodProto::new("__set__", "PyDescrSetProtocol").args(&["Receiver", "Inst", "Value"]), - MethodProto::new("__delete__", "PyDescrDelProtocol") - .args(&["Inst"]) - .has_self(), - MethodProto::new("__set_name__", "PyDescrSetNameProtocol") - .args(&["Inst"]) - .has_self(), - ], - py_methods: &[ - PyMethod::new("__delete__", "PyDescrDelProtocolImpl"), - PyMethod::new("__set_name__", "PyDescrNameProtocolImpl"), ], + py_methods: &[], slot_defs: &[ SlotDef::new(&["__get__"], "Py_tp_descr_get", "descr_get"), SlotDef::new(&["__set__"], "Py_tp_descr_set", "descr_set"), @@ -313,12 +267,8 @@ pub const MAPPING: Proto = Proto { MethodProto::new("__delitem__", "PyMappingDelItemProtocol") .args(&["Key"]) .has_self(), - MethodProto::new("__reversed__", "PyMappingReversedProtocol").has_self(), ], - py_methods: &[PyMethod::new( - "__reversed__", - "PyMappingReversedProtocolImpl", - )], + py_methods: &[], slot_defs: &[ SlotDef::new(&["__len__"], "Py_mp_length", "len"), SlotDef::new(&["__getitem__"], "Py_mp_subscript", "getitem"), @@ -492,13 +442,9 @@ pub const NUM: Proto = Proto { MethodProto::new("__pos__", "PyNumberPosProtocol").has_self(), MethodProto::new("__abs__", "PyNumberAbsProtocol").has_self(), MethodProto::new("__invert__", "PyNumberInvertProtocol").has_self(), - MethodProto::new("__complex__", "PyNumberComplexProtocol").has_self(), MethodProto::new("__int__", "PyNumberIntProtocol").has_self(), MethodProto::new("__float__", "PyNumberFloatProtocol").has_self(), MethodProto::new("__index__", "PyNumberIndexProtocol").has_self(), - MethodProto::new("__round__", "PyNumberRoundProtocol") - .args(&["NDigits"]) - .has_self(), ], py_methods: &[ PyMethod::coexist("__radd__", "PyNumberRAddProtocolImpl"), @@ -515,8 +461,6 @@ pub const NUM: Proto = Proto { PyMethod::coexist("__rand__", "PyNumberRAndProtocolImpl"), PyMethod::coexist("__rxor__", "PyNumberRXorProtocolImpl"), PyMethod::coexist("__ror__", "PyNumberROrProtocolImpl"), - PyMethod::new("__complex__", "PyNumberComplexProtocolImpl"), - PyMethod::new("__round__", "PyNumberRoundProtocolImpl"), ], slot_defs: &[ SlotDef::new(&["__add__", "__radd__"], "Py_nb_add", "add_radd"), diff --git a/pyo3-macros-backend/src/deprecations.rs b/pyo3-macros-backend/src/deprecations.rs index b178d4e8..3d5fe878 100644 --- a/pyo3-macros-backend/src/deprecations.rs +++ b/pyo3-macros-backend/src/deprecations.rs @@ -2,20 +2,12 @@ use proc_macro2::{Span, TokenStream}; use quote::{quote_spanned, ToTokens}; pub enum Deprecation { - NameAttribute, - PyfnNameArgument, - PyModuleNameArgument, - TextSignatureAttribute, CallAttribute, } impl Deprecation { fn ident(&self, span: Span) -> syn::Ident { let string = match self { - Deprecation::NameAttribute => "NAME_ATTRIBUTE", - Deprecation::PyfnNameArgument => "PYFN_NAME_ARGUMENT", - Deprecation::PyModuleNameArgument => "PYMODULE_NAME_ARGUMENT", - Deprecation::TextSignatureAttribute => "TEXT_SIGNATURE_ATTRIBUTE", Deprecation::CallAttribute => "CALL_ATTRIBUTE", }; syn::Ident::new(string, span) @@ -41,7 +33,7 @@ impl ToTokens for Deprecations { let ident = deprecation.ident(*span); quote_spanned!( *span => - let _ = ::pyo3::impl_::deprecations::#ident; + let _ = _pyo3::impl_::deprecations::#ident; ) .to_tokens(tokens) } diff --git a/pyo3-macros-backend/src/from_pyobject.rs b/pyo3-macros-backend/src/from_pyobject.rs index 5f4811eb..c0560fb8 100644 --- a/pyo3-macros-backend/src/from_pyobject.rs +++ b/pyo3-macros-backend/src/from_pyobject.rs @@ -1,4 +1,7 @@ -use crate::attributes::{self, get_pyo3_options, FromPyWithAttribute}; +use crate::{ + attributes::{self, get_pyo3_options, CrateAttribute, FromPyWithAttribute}, + utils::get_pyo3_crate, +}; use proc_macro2::TokenStream; use quote::quote; use syn::{ @@ -55,15 +58,15 @@ impl<'a> Enum<'a> { for (i, var) in self.variants.iter().enumerate() { let struct_derive = var.build(); let ext = quote!( - let maybe_ret = || -> ::pyo3::PyResult { + let maybe_ret = || -> _pyo3::PyResult { #struct_derive }(); match maybe_ret { ok @ ::std::result::Result::Ok(_) => return ok, - ::std::result::Result::Err(inner) => { - let py = ::pyo3::PyNativeType::py(obj); - err_reasons.push_str(&::std::format!("{}\n", inner.instance(py).str()?)); + ::std::result::Result::Err(err) => { + let py = _pyo3::PyNativeType::py(obj); + err_reasons.push_str(&::std::format!("{}\n", err.value(py).str()?)); } } ); @@ -82,7 +85,7 @@ impl<'a> Enum<'a> { #ty_name, #error_names, &err_reasons); - ::std::result::Result::Err(::pyo3::exceptions::PyTypeError::new_err(err_msg)) + ::std::result::Result::Err(_pyo3::exceptions::PyTypeError::new_err(err_msg)) ) } } @@ -207,8 +210,8 @@ impl<'a> Container<'a> { ); quote!( ::std::result::Result::Ok(#self_ty{#ident: obj.extract().map_err(|inner| { - let py = ::pyo3::PyNativeType::py(obj); - let new_err = ::pyo3::exceptions::PyTypeError::new_err(#error_msg); + 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 })?}) @@ -221,12 +224,12 @@ impl<'a> Container<'a> { format!("failed to extract inner field of {}", quote!(#self_ty)) }; quote!( - ::std::result::Result::Ok(#self_ty(obj.extract().map_err(|inner| { - let py = ::pyo3::PyNativeType::py(obj); + ::std::result::Result::Ok(#self_ty(obj.extract().map_err(|err| { + let py = _pyo3::PyNativeType::py(obj); let err_msg = ::std::format!("{}: {}", #error_msg, - inner.instance(py).str().unwrap()); - ::pyo3::exceptions::PyTypeError::new_err(err_msg) + err.value(py).str().unwrap()); + _pyo3::exceptions::PyTypeError::new_err(err_msg) })?)) ) } @@ -238,9 +241,9 @@ impl<'a> Container<'a> { 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); + 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 })?)); @@ -255,9 +258,9 @@ impl<'a> Container<'a> { quote!("") }; quote!( - let s = <::pyo3::types::PyTuple as ::pyo3::conversion::PyTryFrom>::try_from(obj)?; + let s = <_pyo3::types::PyTuple as _pyo3::conversion::PyTryFrom>::try_from(obj)?; if s.len() != #len { - return ::std::result::Result::Err(::pyo3::exceptions::PyValueError::new_err(#msg)) + return ::std::result::Result::Err(_pyo3::exceptions::PyValueError::new_err(#msg)) } ::std::result::Result::Ok(#self_ty(#fields)) ) @@ -279,15 +282,15 @@ impl<'a> Container<'a> { let extractor = match &attrs.from_py_with { None => quote!( #get_field.extract().map_err(|inner| { - let py = ::pyo3::PyNativeType::py(obj); - let new_err = ::pyo3::exceptions::PyTypeError::new_err(#conversion_error_msg); + let py = _pyo3::PyNativeType::py(obj); + let new_err = _pyo3::exceptions::PyTypeError::new_err(#conversion_error_msg); new_err.set_cause(py, ::std::option::Option::Some(inner)); new_err })?), Some(FromPyWithAttribute(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); + let py = _pyo3::PyNativeType::py(obj); + let new_err = _pyo3::exceptions::PyTypeError::new_err(#conversion_error_msg); new_err.set_cause(py, ::std::option::Option::Some(inner)); new_err })? @@ -300,20 +303,25 @@ impl<'a> Container<'a> { } } +#[derive(Default)] struct ContainerOptions { /// Treat the Container as a Wrapper, directly extract its fields from the input object. transparent: bool, /// Change the name of an enum variant in the generated error message. annotation: Option, + /// Change the path for the pyo3 crate + krate: Option, } /// Attributes for deriving FromPyObject scoped on containers. -#[derive(Clone, Debug, PartialEq)] +#[derive(Debug)] enum ContainerPyO3Attribute { /// Treat the Container as a Wrapper, directly extract its fields from the input object. Transparent(attributes::kw::transparent), /// Change the name of an enum variant in the generated error message. ErrorAnnotation(LitStr), + /// Change the path for the pyo3 crate + Crate(CrateAttribute), } impl Parse for ContainerPyO3Attribute { @@ -326,6 +334,8 @@ impl Parse for ContainerPyO3Attribute { let _: attributes::kw::annotation = input.parse()?; let _: Token![=] = input.parse()?; input.parse().map(ContainerPyO3Attribute::ErrorAnnotation) + } else if lookahead.peek(Token![crate]) { + input.parse().map(ContainerPyO3Attribute::Crate) } else { Err(lookahead.error()) } @@ -334,10 +344,8 @@ impl Parse for ContainerPyO3Attribute { impl ContainerOptions { fn from_attrs(attrs: &[Attribute]) -> Result { - let mut options = ContainerOptions { - transparent: false, - annotation: None, - }; + let mut options = ContainerOptions::default(); + for attr in attrs { if let Some(pyo3_attrs) = get_pyo3_options(attr)? { for pyo3_attr in pyo3_attrs { @@ -356,6 +364,13 @@ impl ContainerOptions { ); options.annotation = Some(lit_str); } + ContainerPyO3Attribute::Crate(path) => { + ensure_spanned!( + options.krate.is_none(), + path.0.span() => "`crate` may only be provided once" + ); + options.krate = Some(path); + } } } } @@ -499,13 +514,18 @@ pub fn build_derive_from_pyobject(tokens: &DeriveInput) -> Result { .predicates .push(parse_quote!(#gen_ident: FromPyObject<#lt_param>)) } + let options = ContainerOptions::from_attrs(&tokens.attrs)?; + let krate = get_pyo3_crate(&options.krate); let derives = match &tokens.data { syn::Data::Enum(en) => { + if options.transparent || options.annotation.is_some() { + bail_spanned!(tokens.span() => "`transparent` or `annotation` is not supported \ + at top level for enums"); + } let en = Enum::new(en, &tokens.ident)?; en.build() } syn::Data::Struct(st) => { - let options = ContainerOptions::from_attrs(&tokens.attrs)?; if let Some(lit_str) = &options.annotation { bail_spanned!(lit_str.span() => "`annotation` is unsupported for structs"); } @@ -520,11 +540,15 @@ pub fn build_derive_from_pyobject(tokens: &DeriveInput) -> Result { let ident = &tokens.ident; Ok(quote!( - #[automatically_derived] - impl#trait_generics ::pyo3::FromPyObject<#lt_param> for #ident#generics #where_clause { - fn extract(obj: &#lt_param ::pyo3::PyAny) -> ::pyo3::PyResult { - #derives + const _: () = { + use #krate as _pyo3; + + #[automatically_derived] + impl#trait_generics _pyo3::FromPyObject<#lt_param> for #ident#generics #where_clause { + fn extract(obj: &#lt_param _pyo3::PyAny) -> _pyo3::PyResult { + #derives + } } - } + }; )) } diff --git a/pyo3-macros-backend/src/konst.rs b/pyo3-macros-backend/src/konst.rs index d0bc458d..c11c828d 100644 --- a/pyo3-macros-backend/src/konst.rs +++ b/pyo3-macros-backend/src/konst.rs @@ -1,10 +1,7 @@ use std::borrow::Cow; use crate::{ - attributes::{ - self, get_deprecated_name_attribute, get_pyo3_options, is_attribute_ident, take_attributes, - NameAttribute, - }, + attributes::{self, get_pyo3_options, is_attribute_ident, take_attributes, NameAttribute}, deprecations::Deprecations, }; use proc_macro2::{Ident, TokenStream}; @@ -81,11 +78,6 @@ impl ConstAttributes { } } Ok(true) - } else if let Some(name) = - get_deprecated_name_attribute(attr, &mut attributes.deprecations)? - { - attributes.set_name(name)?; - Ok(true) } else { Ok(false) } diff --git a/pyo3-macros-backend/src/lib.rs b/pyo3-macros-backend/src/lib.rs index f42bd3b9..69fc24d2 100644 --- a/pyo3-macros-backend/src/lib.rs +++ b/pyo3-macros-backend/src/lib.rs @@ -25,7 +25,7 @@ mod pyproto; pub use from_pyobject::build_derive_from_pyobject; pub use module::{process_functions_in_module, py_init, PyModuleOptions}; -pub use pyclass::{build_py_class, PyClassArgs}; +pub use pyclass::{build_py_class, build_py_enum, PyClassArgs}; pub use pyfunction::{build_py_function, PyFunctionOptions}; pub use pyimpl::{build_py_methods, PyClassMethodsType}; pub use pyproto::build_py_proto; diff --git a/pyo3-macros-backend/src/method.rs b/pyo3-macros-backend/src/method.rs index e57ef337..dda829be 100644 --- a/pyo3-macros-backend/src/method.rs +++ b/pyo3-macros-backend/src/method.rs @@ -5,7 +5,7 @@ use crate::deprecations::Deprecation; use crate::params::{accept_args_kwargs, impl_arg_params}; use crate::pyfunction::PyFunctionOptions; use crate::pyfunction::{PyFunctionArgPyO3Attributes, PyFunctionSignature}; -use crate::utils::{self, PythonDoc}; +use crate::utils::{self, get_pyo3_crate, PythonDoc}; use crate::{deprecations::Deprecations, pyfunction::Argument}; use proc_macro2::{Span, TokenStream}; use quote::ToTokens; @@ -106,12 +106,12 @@ impl FnType { } FnType::FnClass => { quote! { - let _slf = ::pyo3::types::PyType::from_type_ptr(_py, _slf as *mut ::pyo3::ffi::PyTypeObject); + let _slf = _pyo3::types::PyType::from_type_ptr(_py, _slf as *mut _pyo3::ffi::PyTypeObject); } } FnType::FnModule => { quote! { - let _slf = _py.from_borrowed_ptr::<::pyo3::types::PyModule>(_slf); + let _slf = _py.from_borrowed_ptr::<_pyo3::types::PyModule>(_slf); } } } @@ -141,13 +141,13 @@ impl SelfType { pub fn receiver(&self, cls: &syn::Type, error_mode: ExtractErrorMode) -> TokenStream { let cell = match error_mode { ExtractErrorMode::Raise => { - quote! { _py.from_borrowed_ptr::<::pyo3::PyAny>(_slf).downcast::<::pyo3::PyCell<#cls>>()? } + quote! { _py.from_borrowed_ptr::<_pyo3::PyAny>(_slf).downcast::<_pyo3::PyCell<#cls>>()? } } ExtractErrorMode::NotImplemented => { quote! { - match _py.from_borrowed_ptr::<::pyo3::PyAny>(_slf).downcast::<::pyo3::PyCell<#cls>>() { + match _py.from_borrowed_ptr::<_pyo3::PyAny>(_slf).downcast::<_pyo3::PyCell<#cls>>() { ::std::result::Result::Ok(cell) => cell, - ::std::result::Result::Err(_) => return ::pyo3::callback::convert(_py, _py.NotImplemented()), + ::std::result::Result::Err(_) => return _pyo3::callback::convert(_py, _py.NotImplemented()), } } } @@ -228,6 +228,7 @@ pub struct FnSpec<'a> { pub deprecations: Deprecations, pub convention: CallingConvention, pub text_signature: Option, + pub krate: syn::Path, } pub fn get_return_info(output: &syn::ReturnType) -> syn::Type { @@ -254,12 +255,14 @@ pub fn parse_method_receiver(arg: &syn::FnArg) -> Result { impl<'a> FnSpec<'a> { /// Parser function signature and function attributes pub fn parse( + // Signature is mutable to remove the `Python` argument. sig: &'a mut syn::Signature, meth_attrs: &mut Vec, options: PyFunctionOptions, ) -> Result> { let PyFunctionOptions { text_signature, + krate, name, mut deprecations, .. @@ -278,6 +281,7 @@ impl<'a> FnSpec<'a> { let name = &sig.ident; let ty = get_return_info(&sig.output); let python_name = python_name.as_ref().unwrap_or(name).unraw(); + let krate = get_pyo3_crate(&krate); let doc = utils::get_doc( meth_attrs, @@ -311,6 +315,7 @@ impl<'a> FnSpec<'a> { doc, deprecations, text_signature, + krate, }) } @@ -352,15 +357,12 @@ impl<'a> FnSpec<'a> { parse_method_receiver(first_arg) }; - #[allow(clippy::manual_strip)] // for strip_prefix replacement supporting rust < 1.45 // strip get_ or set_ let strip_fn_name = |prefix: &'static str| { - let ident = name.unraw().to_string(); - if ident.starts_with(prefix) { - Some(syn::Ident::new(&ident[prefix.len()..], ident.span())) - } else { - None - } + name.unraw() + .to_string() + .strip_prefix(prefix) + .map(|stripped| syn::Ident::new(stripped, name.span())) }; let (fn_type, skip_first_arg, fixed_convention) = match fn_type_attr { @@ -474,17 +476,19 @@ impl<'a> FnSpec<'a> { quote!(#func_name) }; let rust_call = - quote! { ::pyo3::callback::convert(#py, #rust_name(#self_arg #(#arg_names),*)) }; + quote! { _pyo3::callback::convert(#py, #rust_name(#self_arg #(#arg_names),*)) }; + let krate = &self.krate; Ok(match self.convention { CallingConvention::Noargs => { quote! { unsafe extern "C" fn #ident ( - _slf: *mut ::pyo3::ffi::PyObject, - _args: *mut ::pyo3::ffi::PyObject, - ) -> *mut ::pyo3::ffi::PyObject + _slf: *mut #krate::ffi::PyObject, + _args: *mut #krate::ffi::PyObject, + ) -> *mut #krate::ffi::PyObject { + use #krate as _pyo3; #deprecations - ::pyo3::callback::handle_panic(|#py| { + _pyo3::callback::handle_panic(|#py| { #self_conversion #rust_call }) @@ -495,17 +499,18 @@ impl<'a> FnSpec<'a> { let arg_convert_and_rust_call = impl_arg_params(self, cls, rust_call, &py, true)?; quote! { unsafe extern "C" fn #ident ( - _slf: *mut ::pyo3::ffi::PyObject, - _args: *const *mut ::pyo3::ffi::PyObject, - _nargs: ::pyo3::ffi::Py_ssize_t, - _kwnames: *mut ::pyo3::ffi::PyObject) -> *mut ::pyo3::ffi::PyObject + _slf: *mut #krate::ffi::PyObject, + _args: *const *mut #krate::ffi::PyObject, + _nargs: #krate::ffi::Py_ssize_t, + _kwnames: *mut #krate::ffi::PyObject) -> *mut #krate::ffi::PyObject { + use #krate as _pyo3; #deprecations - ::pyo3::callback::handle_panic(|#py| { + _pyo3::callback::handle_panic(|#py| { #self_conversion - let _kwnames: ::std::option::Option<&::pyo3::types::PyTuple> = #py.from_borrowed_ptr_or_opt(_kwnames); + let _kwnames: ::std::option::Option<&_pyo3::types::PyTuple> = #py.from_borrowed_ptr_or_opt(_kwnames); // Safety: &PyAny has the same memory layout as `*mut ffi::PyObject` - let _args = _args as *const &::pyo3::PyAny; + let _args = _args as *const &_pyo3::PyAny; let _kwargs = if let ::std::option::Option::Some(kwnames) = _kwnames { ::std::slice::from_raw_parts(_args.offset(_nargs), kwnames.len()) } else { @@ -522,15 +527,16 @@ impl<'a> FnSpec<'a> { let arg_convert_and_rust_call = impl_arg_params(self, cls, rust_call, &py, false)?; quote! { unsafe extern "C" fn #ident ( - _slf: *mut ::pyo3::ffi::PyObject, - _args: *mut ::pyo3::ffi::PyObject, - _kwargs: *mut ::pyo3::ffi::PyObject) -> *mut ::pyo3::ffi::PyObject + _slf: *mut #krate::ffi::PyObject, + _args: *mut #krate::ffi::PyObject, + _kwargs: *mut #krate::ffi::PyObject) -> *mut #krate::ffi::PyObject { + use #krate as _pyo3; #deprecations - ::pyo3::callback::handle_panic(|#py| { + _pyo3::callback::handle_panic(|#py| { #self_conversion - let _args = #py.from_borrowed_ptr::<::pyo3::types::PyTuple>(_args); - let _kwargs: ::std::option::Option<&::pyo3::types::PyDict> = #py.from_borrowed_ptr_or_opt(_kwargs); + let _args = #py.from_borrowed_ptr::<_pyo3::types::PyTuple>(_args); + let _kwargs: ::std::option::Option<&_pyo3::types::PyDict> = #py.from_borrowed_ptr_or_opt(_kwargs); #arg_convert_and_rust_call }) @@ -542,20 +548,21 @@ impl<'a> FnSpec<'a> { let arg_convert_and_rust_call = impl_arg_params(self, cls, rust_call, &py, false)?; quote! { unsafe extern "C" fn #ident ( - subtype: *mut ::pyo3::ffi::PyTypeObject, - _args: *mut ::pyo3::ffi::PyObject, - _kwargs: *mut ::pyo3::ffi::PyObject) -> *mut ::pyo3::ffi::PyObject + subtype: *mut #krate::ffi::PyTypeObject, + _args: *mut #krate::ffi::PyObject, + _kwargs: *mut #krate::ffi::PyObject) -> *mut #krate::ffi::PyObject { + use #krate as _pyo3; #deprecations - use ::pyo3::callback::IntoPyCallbackOutput; - ::pyo3::callback::handle_panic(|#py| { - let _args = #py.from_borrowed_ptr::<::pyo3::types::PyTuple>(_args); - let _kwargs: ::std::option::Option<&::pyo3::types::PyDict> = #py.from_borrowed_ptr_or_opt(_kwargs); + use _pyo3::callback::IntoPyCallbackOutput; + _pyo3::callback::handle_panic(|#py| { + let _args = #py.from_borrowed_ptr::<_pyo3::types::PyTuple>(_args); + let _kwargs: ::std::option::Option<&_pyo3::types::PyDict> = #py.from_borrowed_ptr_or_opt(_kwargs); let result = #arg_convert_and_rust_call; - let initializer: ::pyo3::PyClassInitializer::<#cls> = result.convert(#py)?; + 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) + ::std::result::Result::Ok(cell as *mut _pyo3::ffi::PyObject) }) } } @@ -570,23 +577,23 @@ impl<'a> FnSpec<'a> { let doc = &self.doc; match self.convention { CallingConvention::Noargs => quote! { - ::pyo3::class::methods::PyMethodDef::noargs( + _pyo3::class::methods::PyMethodDef::noargs( #python_name, - ::pyo3::class::methods::PyCFunction(#wrapper), + _pyo3::class::methods::PyCFunction(#wrapper), #doc, ) }, CallingConvention::Fastcall => quote! { - ::pyo3::class::methods::PyMethodDef::fastcall_cfunction_with_keywords( + _pyo3::class::methods::PyMethodDef::fastcall_cfunction_with_keywords( #python_name, - ::pyo3::class::methods::PyCFunctionFastWithKeywords(#wrapper), + _pyo3::class::methods::PyCFunctionFastWithKeywords(#wrapper), #doc, ) }, CallingConvention::Varargs => quote! { - ::pyo3::class::methods::PyMethodDef::cfunction_with_keywords( + _pyo3::class::methods::PyMethodDef::cfunction_with_keywords( #python_name, - ::pyo3::class::methods::PyCFunctionWithKeywords(#wrapper), + _pyo3::class::methods::PyCFunctionWithKeywords(#wrapper), #doc, ) }, diff --git a/pyo3-macros-backend/src/module.rs b/pyo3-macros-backend/src/module.rs index 43c42d3f..1e6fe24a 100644 --- a/pyo3-macros-backend/src/module.rs +++ b/pyo3-macros-backend/src/module.rs @@ -2,14 +2,11 @@ //! Code generation for the function that initializes a python module and adds classes and function. use crate::{ - attributes::{self, take_pyo3_options}, - deprecations::Deprecations, + attributes::{ + self, is_attribute_ident, take_attributes, take_pyo3_options, CrateAttribute, NameAttribute, + }, pyfunction::{impl_wrap_pyfunction, PyFunctionOptions}, - utils::PythonDoc, -}; -use crate::{ - attributes::{is_attribute_ident, take_attributes, NameAttribute}, - deprecations::Deprecation, + utils::{get_pyo3_crate, PythonDoc}, }; use proc_macro2::{Span, TokenStream}; use quote::quote; @@ -21,29 +18,20 @@ use syn::{ Ident, Path, Result, }; +#[derive(Default)] pub struct PyModuleOptions { + krate: Option, name: Option, - deprecations: Deprecations, } impl PyModuleOptions { - pub fn from_pymodule_arg_and_attrs( - deprecated_pymodule_name_arg: Option, - attrs: &mut Vec, - ) -> Result { - let mut deprecations = Deprecations::new(); - if let Some(name) = &deprecated_pymodule_name_arg { - deprecations.push(Deprecation::PyModuleNameArgument, name.span()); - } - - let mut options: PyModuleOptions = PyModuleOptions { - name: deprecated_pymodule_name_arg, - deprecations, - }; + pub fn from_attrs(attrs: &mut Vec) -> Result { + let mut options: PyModuleOptions = Default::default(); for option in take_pyo3_options(attrs)? { match option { PyModulePyO3Option::Name(name) => options.set_name(name.0)?, + PyModulePyO3Option::Crate(path) => options.set_crate(path)?, } } @@ -59,13 +47,23 @@ impl PyModuleOptions { self.name = Some(name); Ok(()) } + + fn set_crate(&mut self, path: CrateAttribute) -> Result<()> { + ensure_spanned!( + self.krate.is_none(), + path.0.span() => "`crate` may only be specified once" + ); + + self.krate = Some(path); + Ok(()) + } } /// Generates the function that is called by the python interpreter to initialize the native /// module pub fn py_init(fnname: &Ident, options: PyModuleOptions, doc: PythonDoc) -> TokenStream { let name = options.name.unwrap_or_else(|| fnname.unraw()); - let deprecations = options.deprecations; + let krate = get_pyo3_crate(&options.krate); let cb_name = Ident::new(&format!("PyInit_{}", name), Span::call_site()); quote! { @@ -73,15 +71,14 @@ pub fn py_init(fnname: &Ident, options: PyModuleOptions, doc: PythonDoc) -> Toke #[allow(non_snake_case)] /// This autogenerated function is called by the python interpreter when importing /// the module. - pub unsafe extern "C" fn #cb_name() -> *mut ::pyo3::ffi::PyObject { - use ::pyo3::derive_utils::ModuleDef; + pub unsafe extern "C" fn #cb_name() -> *mut #krate::ffi::PyObject { + use #krate as _pyo3; + use _pyo3::derive_utils::ModuleDef; static NAME: &str = concat!(stringify!(#name), "\0"); static DOC: &str = #doc; static MODULE_DEF: ModuleDef = unsafe { ModuleDef::new(NAME, DOC) }; - #deprecations - - ::pyo3::callback::handle_panic(|_py| { MODULE_DEF.make_module(_py, #fnname) }) + _pyo3::callback::handle_panic(|_py| { MODULE_DEF.make_module(_py, #fnname) }) } } } @@ -129,23 +126,10 @@ impl Parse for PyFnArgs { let _: Comma = input.parse()?; - let mut deprecated_name_argument = None; - if let Ok(lit_str) = input.parse::() { - deprecated_name_argument = Some(lit_str); - if !input.is_empty() { - let _: Comma = input.parse()?; - } - } - - let mut options: PyFunctionOptions = input.parse()?; - if let Some(lit_str) = deprecated_name_argument { - options.set_name(NameAttribute(lit_str.parse()?))?; - options - .deprecations - .push(Deprecation::PyfnNameArgument, lit_str.span()); - } - - Ok(Self { modname, options }) + Ok(Self { + modname, + options: input.parse()?, + }) } } @@ -167,13 +151,16 @@ fn get_pyfn_attr(attrs: &mut Vec) -> syn::Result "Required positional parameters cannot come after optional parameters" + ); required_positional_parameters += 1; + } else { + all_positional_required = false; } if posonly { positional_only_parameters += 1; @@ -128,7 +136,7 @@ pub fn impl_arg_params( let (accept_args, accept_kwargs) = accept_args_kwargs(&spec.attrs); let cls_name = if let Some(cls) = self_ { - quote! { ::std::option::Option::Some(<#cls as ::pyo3::type_object::PyTypeInfo>::NAME) } + quote! { ::std::option::Option::Some(<#cls as _pyo3::type_object::PyTypeInfo>::NAME) } } else { quote! { ::std::option::Option::None } }; @@ -155,7 +163,7 @@ pub fn impl_arg_params( // create array of arguments, and then parse Ok(quote! {{ - const DESCRIPTION: ::pyo3::derive_utils::FunctionDescription = ::pyo3::derive_utils::FunctionDescription { + const DESCRIPTION: _pyo3::derive_utils::FunctionDescription = _pyo3::derive_utils::FunctionDescription { cls_name: #cls_name, func_name: stringify!(#python_name), positional_parameter_names: &[#(#positional_parameter_names),*], @@ -206,7 +214,7 @@ fn impl_arg_param( let ty = arg.ty; let name = arg.name; let transform_error = quote! { - |e| ::pyo3::derive_utils::argument_extraction_error(#py, stringify!(#name), e) + |e| _pyo3::derive_utils::argument_extraction_error(#py, stringify!(#name), e) }; if is_args(&spec.attrs, name) { @@ -275,7 +283,7 @@ fn impl_arg_param( let (target_ty, borrow_tmp) = if arg.optional.is_some() { // Get Option<&T> from Option> ( - quote_arg_span! { ::std::option::Option<<#tref as ::pyo3::derive_utils::ExtractExt<'_>>::Target> }, + quote_arg_span! { ::std::option::Option<<#tref as _pyo3::derive_utils::ExtractExt<'_>>::Target> }, if mut_.is_some() { quote_arg_span! { _tmp.as_deref_mut() } } else { @@ -285,13 +293,14 @@ fn impl_arg_param( } else { // Get &T from PyRef ( - quote_arg_span! { <#tref as ::pyo3::derive_utils::ExtractExt<'_>>::Target }, + quote_arg_span! { <#tref as _pyo3::derive_utils::ExtractExt<'_>>::Target }, quote_arg_span! { &#mut_ *_tmp }, ) }; Ok(quote_arg_span! { let #mut_ _tmp: #target_ty = #arg_value_or_default; + #[allow(clippy::needless_option_as_deref)] let #arg_name = #borrow_tmp; }) } else { diff --git a/pyo3-macros-backend/src/proto_method.rs b/pyo3-macros-backend/src/proto_method.rs index 3c14843f..648a9734 100644 --- a/pyo3-macros-backend/src/proto_method.rs +++ b/pyo3-macros-backend/src/proto_method.rs @@ -15,13 +15,11 @@ pub struct MethodProto { } impl MethodProto { - // TODO: workaround for no unsized casts in const fn on Rust 1.45 (stable in 1.46) - const EMPTY_ARGS: &'static [&'static str] = &[]; pub const fn new(name: &'static str, proto: &'static str) -> Self { MethodProto { name, proto, - args: MethodProto::EMPTY_ARGS, + args: &[], with_self: false, with_result: true, } diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index a75b2584..2760571e 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -1,19 +1,26 @@ // Copyright (c) 2017-present PyO3 Project and Contributors use crate::attributes::{ - self, take_deprecated_text_signature_attribute, take_pyo3_options, NameAttribute, - TextSignatureAttribute, + self, take_pyo3_options, CrateAttribute, NameAttribute, TextSignatureAttribute, }; use crate::deprecations::Deprecations; -use crate::pyimpl::PyClassMethodsType; +use crate::konst::{ConstAttributes, ConstSpec}; +use crate::pyimpl::{gen_default_slot_impls, gen_py_const, PyClassMethodsType}; use crate::pymethod::{impl_py_getter_def, impl_py_setter_def, PropertyType}; -use crate::utils::{self, unwrap_group, PythonDoc}; +use crate::utils::{self, get_pyo3_crate, unwrap_group, 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}; +use syn::{parse_quote, spanned::Spanned, Expr, Result, Token}; //unraw + +/// If the class is derived from a Rust `struct` or `enum`. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum PyClassKind { + Struct, + Enum, +} /// The parsed arguments of the pyclass macro pub struct PyClassArgs { @@ -27,38 +34,43 @@ pub struct PyClassArgs { pub has_extends: bool, pub has_unsendable: bool, pub module: Option, + pub class_kind: PyClassKind, } -impl Parse for PyClassArgs { - fn parse(input: ParseStream) -> Result { - let mut slf = PyClassArgs::default(); - +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) } -} -impl Default for PyClassArgs { - fn default() -> Self { + pub fn parse_stuct_args(input: ParseStream) -> syn::Result { + Self::parse(input, PyClassKind::Struct) + } + + 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 }, + base: parse_quote! { _pyo3::PyAny }, has_dict: false, has_weaklist: false, is_gc: false, is_basetype: false, has_extends: false, has_unsendable: false, + class_kind, } } -} -impl PyClassArgs { /// 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<()> { @@ -116,6 +128,9 @@ impl PyClassArgs { }, "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, @@ -150,6 +165,9 @@ impl PyClassArgs { 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" => { @@ -170,10 +188,12 @@ impl PyClassArgs { pub struct PyClassPyO3Options { pub text_signature: Option, pub deprecations: Deprecations, + pub krate: Option, } enum PyClassPyO3Option { TextSignature(TextSignatureAttribute), + Crate(CrateAttribute), } impl Parse for PyClassPyO3Option { @@ -181,6 +201,8 @@ impl Parse for PyClassPyO3Option { let lookahead = input.lookahead1(); if lookahead.peek(attributes::kw::text_signature) { input.parse().map(PyClassPyO3Option::TextSignature) + } else if lookahead.peek(Token![crate]) { + input.parse().map(PyClassPyO3Option::Crate) } else { Err(lookahead.error()) } @@ -195,6 +217,9 @@ impl PyClassPyO3Options { PyClassPyO3Option::TextSignature(text_signature) => { options.set_text_signature(text_signature)?; } + PyClassPyO3Option::Crate(path) => { + options.set_crate(path)?; + } } } Ok(options) @@ -211,6 +236,15 @@ impl PyClassPyO3Options { self.text_signature = Some(text_signature); Ok(()) } + + 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); + Ok(()) + } } pub fn build_py_class( @@ -218,12 +252,7 @@ pub fn build_py_class( args: &PyClassArgs, methods_type: PyClassMethodsType, ) -> syn::Result { - let mut options = PyClassPyO3Options::take_pyo3_options(&mut class.attrs)?; - if let Some(text_signature) = - take_deprecated_text_signature_attribute(&mut class.attrs, &mut options.deprecations)? - { - options.set_text_signature(text_signature)?; - } + let options = PyClassPyO3Options::take_pyo3_options(&mut class.attrs)?; let doc = utils::get_doc( &class.attrs, options @@ -231,6 +260,7 @@ pub fn build_py_class( .as_ref() .map(|attr| (get_class_python_name(&class.ident, args), attr)), ); + let krate = get_pyo3_crate(&options.krate); ensure_spanned!( class.generics.params.is_empty(), @@ -267,6 +297,7 @@ pub fn build_py_class( field_options, methods_type, options.deprecations, + krate, ) } @@ -336,41 +367,6 @@ impl FieldPyO3Options { } } -/// To allow multiple #[pymethods] block, we define inventory types. -fn impl_methods_inventory(cls: &syn::Ident) -> TokenStream { - // Try to build a unique type for better error messages - let name = format!("Pyo3MethodsInventoryFor{}", cls.unraw()); - let inventory_cls = syn::Ident::new(&name, Span::call_site()); - - quote! { - #[doc(hidden)] - pub struct #inventory_cls { - methods: ::std::vec::Vec<::pyo3::class::PyMethodDefType>, - slots: ::std::vec::Vec<::pyo3::ffi::PyType_Slot>, - } - impl ::pyo3::class::impl_::PyMethodsInventory for #inventory_cls { - fn new( - methods: ::std::vec::Vec<::pyo3::class::PyMethodDefType>, - slots: ::std::vec::Vec<::pyo3::ffi::PyType_Slot>, - ) -> Self { - Self { methods, slots } - } - fn methods(&'static self) -> &'static [::pyo3::class::PyMethodDefType] { - &self.methods - } - fn slots(&'static self) -> &'static [::pyo3::ffi::PyType_Slot] { - &self.slots - } - } - - impl ::pyo3::class::impl_::HasMethodsInventory for #cls { - type Methods = #inventory_cls; - } - - ::pyo3::inventory::collect!(#inventory_cls); - } -} - fn get_class_python_name<'a>(cls: &'a syn::Ident, attr: &'a PyClassArgs) -> &'a syn::Ident { attr.name.as_ref().unwrap_or(cls) } @@ -382,245 +378,162 @@ fn impl_class( field_options: Vec<(&syn::Field, FieldPyO3Options)>, methods_type: PyClassMethodsType, deprecations: Deprecations, + krate: syn::Path, ) -> syn::Result { - let cls_name = get_class_python_name(cls, attr).to_string(); + let pytypeinfo_impl = impl_pytypeinfo(cls, attr, Some(&deprecations)); - let alloc = attr.freelist.as_ref().map(|freelist| { - quote! { - impl ::pyo3::class::impl_::PyClassWithFreeList for #cls { - #[inline] - fn get_free_list(_py: ::pyo3::Python<'_>) -> &mut ::pyo3::impl_::freelist::FreeList<*mut ::pyo3::ffi::PyObject> { - static mut FREELIST: *mut ::pyo3::impl_::freelist::FreeList<*mut ::pyo3::ffi::PyObject> = 0 as *mut _; - unsafe { - if FREELIST.is_null() { - FREELIST = ::std::boxed::Box::into_raw(::std::boxed::Box::new( - ::pyo3::impl_::freelist::FreeList::with_capacity(#freelist))); - } - &mut *FREELIST - } - } - } - - impl ::pyo3::class::impl_::PyClassAllocImpl<#cls> for ::pyo3::class::impl_::PyClassImplCollector<#cls> { - #[inline] - fn alloc_impl(self) -> ::std::option::Option<::pyo3::ffi::allocfunc> { - ::std::option::Option::Some(::pyo3::class::impl_::alloc_with_freelist::<#cls>) - } - } - - impl ::pyo3::class::impl_::PyClassFreeImpl<#cls> for ::pyo3::class::impl_::PyClassImplCollector<#cls> { - #[inline] - fn free_impl(self) -> ::std::option::Option<::pyo3::ffi::freefunc> { - ::std::option::Option::Some(::pyo3::class::impl_::free_with_freelist::<#cls>) - } - } - } - }); + let py_class_impl = PyClassImplsBuilder::new(cls, attr, methods_type) + .doc(doc) + .impl_all(); let descriptors = impl_descriptors(cls, field_options)?; - // insert space for weak ref - let weakref = if attr.has_weaklist { - quote! { ::pyo3::pyclass_slots::PyClassWeakRefSlot } - } else if attr.has_extends { - quote! { ::WeakRef } - } else { - quote! { ::pyo3::pyclass_slots::PyClassDummySlot } - }; - let dict = if attr.has_dict { - quote! { ::pyo3::pyclass_slots::PyClassDictSlot } - } else if attr.has_extends { - quote! { ::Dict } - } else { - quote! { ::pyo3::pyclass_slots::PyClassDummySlot } - }; - let module = if let Some(m) = &attr.module { - quote! { ::std::option::Option::Some(#m) } - } else { - quote! { ::std::option::Option::None } - }; - - // Enforce at compile time that PyGCProtocol is implemented - let gc_impl = 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>(); - } - } - } else { - quote! {} - }; - - let (impl_inventory, for_each_py_method) = match methods_type { - PyClassMethodsType::Specialization => (None, quote! { visitor(collector.py_methods()); }), - PyClassMethodsType::Inventory => ( - Some(impl_methods_inventory(cls)), - quote! { - for inventory in ::pyo3::inventory::iter::<::Methods>() { - visitor(::pyo3::class::impl_::PyMethodsInventory::methods(inventory)); - } - }, - ), - }; - - let methods_protos = match methods_type { - PyClassMethodsType::Specialization => { - quote! { visitor(collector.methods_protocol_slots()); } - } - PyClassMethodsType::Inventory => { - quote! { - for inventory in ::pyo3::inventory::iter::<::Methods>() { - visitor(::pyo3::class::impl_::PyMethodsInventory::slots(inventory)); - } - } - } - }; - - let base = &attr.base; - let base_nativetype = if attr.has_extends { - quote! { ::BaseNativeType } - } else { - quote! { ::pyo3::PyAny } - }; - - // If #cls is not extended type, we allow Self->PyObject conversion - let into_pyobject = if !attr.has_extends { - quote! { - impl ::pyo3::IntoPy<::pyo3::PyObject> for #cls { - fn into_py(self, py: ::pyo3::Python) -> ::pyo3::PyObject { - ::pyo3::IntoPy::into_py(::pyo3::Py::new(py, self).unwrap(), py) - } - } - } - } else { - quote! {} - }; - - let thread_checker = if attr.has_unsendable { - quote! { ::pyo3::class::impl_::ThreadCheckerImpl<#cls> } - } else if attr.has_extends { - quote! { - ::pyo3::class::impl_::ThreadCheckerInherited<#cls, <#cls as ::pyo3::class::impl_::PyClassImpl>::BaseType> - } - } else { - quote! { ::pyo3::class::impl_::ThreadCheckerStub<#cls> } - }; - - let is_gc = attr.is_gc; - let is_basetype = attr.is_basetype; - let is_subclass = attr.has_extends; - Ok(quote! { - unsafe impl ::pyo3::type_object::PyTypeInfo for #cls { - type AsRefTarget = ::pyo3::PyCell; + const _: () = { + use #krate as _pyo3; - const NAME: &'static str = #cls_name; - const MODULE: ::std::option::Option<&'static str> = #module; + #pytypeinfo_impl - #[inline] - fn type_object_raw(py: ::pyo3::Python<'_>) -> *mut ::pyo3::ffi::PyTypeObject { - #deprecations + #py_class_impl - use ::pyo3::type_object::LazyStaticType; - static TYPE_OBJECT: LazyStaticType = LazyStaticType::new(); - TYPE_OBJECT.get_or_init::(py) - } - } - - impl ::pyo3::PyClass for #cls { - type Dict = #dict; - type WeakRef = #weakref; - type BaseNativeType = #base_nativetype; - } - - impl<'a> ::pyo3::derive_utils::ExtractExt<'a> for &'a #cls - { - type Target = ::pyo3::PyRef<'a, #cls>; - } - - impl<'a> ::pyo3::derive_utils::ExtractExt<'a> for &'a mut #cls - { - type Target = ::pyo3::PyRefMut<'a, #cls>; - } - - #into_pyobject - - #impl_inventory - - impl ::pyo3::class::impl_::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; - - type Layout = ::pyo3::PyCell; - type BaseType = #base; - type ThreadChecker = #thread_checker; - - fn for_each_method_def(visitor: &mut dyn ::std::ops::FnMut(&[::pyo3::class::PyMethodDefType])) { - use ::pyo3::class::impl_::*; - let collector = PyClassImplCollector::::new(); - #for_each_py_method; - visitor(collector.py_class_descriptors()); - visitor(collector.object_protocol_methods()); - visitor(collector.async_protocol_methods()); - visitor(collector.context_protocol_methods()); - visitor(collector.descr_protocol_methods()); - visitor(collector.mapping_protocol_methods()); - visitor(collector.number_protocol_methods()); - } - fn get_new() -> ::std::option::Option<::pyo3::ffi::newfunc> { - use ::pyo3::class::impl_::*; - let collector = PyClassImplCollector::::new(); - collector.new_impl() - } - fn get_alloc() -> ::std::option::Option<::pyo3::ffi::allocfunc> { - use ::pyo3::class::impl_::*; - let collector = PyClassImplCollector::::new(); - collector.alloc_impl() - } - fn get_free() -> ::std::option::Option<::pyo3::ffi::freefunc> { - use ::pyo3::class::impl_::*; - let collector = PyClassImplCollector::::new(); - collector.free_impl() - } - - fn for_each_proto_slot(visitor: &mut dyn ::std::ops::FnMut(&[::pyo3::ffi::PyType_Slot])) { - // Implementation which uses dtolnay specialization to load all slots. - use ::pyo3::class::impl_::*; - let collector = PyClassImplCollector::::new(); - visitor(collector.object_protocol_slots()); - visitor(collector.number_protocol_slots()); - visitor(collector.iter_protocol_slots()); - visitor(collector.gc_protocol_slots()); - visitor(collector.descr_protocol_slots()); - visitor(collector.mapping_protocol_slots()); - visitor(collector.sequence_protocol_slots()); - visitor(collector.async_protocol_slots()); - visitor(collector.buffer_protocol_slots()); - #methods_protos - } - - fn get_buffer() -> ::std::option::Option<&'static ::pyo3::class::impl_::PyBufferProcs> { - use ::pyo3::class::impl_::*; - let collector = PyClassImplCollector::::new(); - collector.buffer_procs() - } - } - - #alloc - - #descriptors - - #gc_impl + #descriptors + }; }) } +struct PyClassEnumVariant<'a> { + ident: &'a syn::Ident, + /* currently have no more options */ +} + +pub fn build_py_enum( + enum_: &mut syn::ItemEnum, + args: &PyClassArgs, + method_type: PyClassMethodsType, +) -> syn::Result { + let options = PyClassPyO3Options::take_pyo3_options(&mut enum_.attrs)?; + + if enum_.variants.is_empty() { + bail_spanned!(enum_.brace_token.span => "Empty enums can't be #[pyclass]."); + } + let variants: Vec = enum_ + .variants + .iter() + .map(extract_variant_data) + .collect::>()?; + impl_enum(enum_, args, variants, method_type, options) +} + +fn impl_enum( + enum_: &syn::ItemEnum, + args: &PyClassArgs, + variants: Vec, + methods_type: PyClassMethodsType, + options: PyClassPyO3Options, +) -> syn::Result { + let enum_name = &enum_.ident; + let doc = utils::get_doc( + &enum_.attrs, + options + .text_signature + .as_ref() + .map(|attr| (get_class_python_name(&enum_.ident, args), attr)), + ); + let krate = get_pyo3_crate(&options.krate); + impl_enum_class(enum_name, args, variants, doc, methods_type, krate) +} + +fn impl_enum_class( + cls: &syn::Ident, + args: &PyClassArgs, + variants: Vec, + doc: PythonDoc, + methods_type: PyClassMethodsType, + krate: syn::Path, +) -> syn::Result { + let pytypeinfo = impl_pytypeinfo(cls, args, None); + let pyclass_impls = PyClassImplsBuilder::new(cls, args, methods_type) + .doc(doc) + .impl_all(); + let descriptors = unit_variants_as_descriptors(cls, variants.iter().map(|v| v.ident)); + + let default_repr_impl = { + 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! { + #[doc(hidden)] + #[allow(non_snake_case)] + #[pyo3(name = "__repr__")] + fn __pyo3__repr__(&self) -> &'static str { + match self { + #(#variants_repr)* + _ => unreachable!("Unsupported variant type."), + } + } + } + }; + + let default_impls = gen_default_slot_impls(cls, vec![default_repr_impl]); + Ok(quote! { + const _: () = { + use #krate as _pyo3; + + #pytypeinfo + + #pyclass_impls + + #default_impls + + #descriptors + }; + }) +} + +fn unit_variants_as_descriptors<'a>( + cls: &'a syn::Ident, + variant_names: impl IntoIterator, +) -> TokenStream { + 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())), + deprecations: Default::default(), + }, + }; + let py_methods = variant_names + .into_iter() + .map(|var| gen_py_const(&cls_type, &variant_to_attribute(var))); + + quote! { + impl _pyo3::class::impl_::PyClassDescriptors<#cls> + for _pyo3::class::impl_::PyClassImplCollector<#cls> + { + fn py_class_descriptors(self) -> &'static [_pyo3::class::methods::PyMethodDefType] { + static METHODS: &[_pyo3::class::methods::PyMethodDefType] = &[#(#py_methods),*]; + METHODS + } + } + } +} + +fn extract_variant_data(variant: &syn::Variant) -> syn::Result { + use syn::Fields; + let ident = match variant.fields { + Fields::Unit => &variant.ident, + _ => bail_spanned!(variant.span() => "Currently only support unit variants."), + }; + if let Some(discriminant) = variant.discriminant.as_ref() { + bail_spanned!(discriminant.0.span() => "Currently does not support discriminats.") + }; + Ok(PyClassEnumVariant { ident }) +} + fn impl_descriptors( cls: &syn::Ident, field_options: Vec<(&syn::Field, FieldPyO3Options)>, @@ -661,13 +574,358 @@ fn impl_descriptors( .collect::>()?; Ok(quote! { - impl ::pyo3::class::impl_::PyClassDescriptors<#cls> - for ::pyo3::class::impl_::PyClassImplCollector<#cls> + impl _pyo3::class::impl_::PyClassDescriptors<#cls> + for _pyo3::class::impl_::PyClassImplCollector<#cls> { - fn py_class_descriptors(self) -> &'static [::pyo3::class::methods::PyMethodDefType] { - static METHODS: &[::pyo3::class::methods::PyMethodDefType] = &[#(#py_methods),*]; + fn py_class_descriptors(self) -> &'static [_pyo3::class::methods::PyMethodDefType] { + static METHODS: &[_pyo3::class::methods::PyMethodDefType] = &[#(#py_methods),*]; METHODS } } }) } + +fn impl_pytypeinfo( + cls: &syn::Ident, + attr: &PyClassArgs, + deprecations: Option<&Deprecations>, +) -> 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) } + } else { + quote! { ::core::option::Option::None } + }; + + quote! { + unsafe impl _pyo3::type_object::PyTypeInfo for #cls { + type AsRefTarget = _pyo3::PyCell; + + const NAME: &'static str = #cls_name; + const MODULE: ::std::option::Option<&'static str> = #module; + + #[inline] + fn type_object_raw(py: _pyo3::Python<'_>) -> *mut _pyo3::ffi::PyTypeObject { + #deprecations + + use _pyo3::type_object::LazyStaticType; + static TYPE_OBJECT: LazyStaticType = LazyStaticType::new(); + TYPE_OBJECT.get_or_init::(py) + } + } + } +} + +/// Implements most traits used by `#[pyclass]`. +/// +/// Specifically, it implements traits that only depend on class name, +/// and attributes of `#[pyclass]`, and docstrings. +/// Therefore it doesn't implement traits that depends on struct fields and enum variants. +struct PyClassImplsBuilder<'a> { + cls: &'a syn::Ident, + attr: &'a PyClassArgs, + methods_type: PyClassMethodsType, + doc: Option, +} + +impl<'a> PyClassImplsBuilder<'a> { + fn new(cls: &'a syn::Ident, attr: &'a PyClassArgs, methods_type: PyClassMethodsType) -> Self { + Self { + cls, + attr, + methods_type, + doc: None, + } + } + + fn doc(self, doc: PythonDoc) -> Self { + Self { + doc: Some(doc), + ..self + } + } + + fn impl_all(&self) -> TokenStream { + vec![ + self.impl_pyclass(), + self.impl_extractext(), + self.impl_into_py(), + self.impl_pyclassimpl(), + self.impl_freelist(), + self.impl_gc(), + ] + .into_iter() + .collect() + } + + fn impl_pyclass(&self) -> TokenStream { + let cls = self.cls; + let attr = self.attr; + let dict = if attr.has_dict { + quote! { _pyo3::pyclass_slots::PyClassDictSlot } + } else if attr.has_extends { + quote! { ::Dict } + } else { + quote! { _pyo3::pyclass_slots::PyClassDummySlot } + }; + + // insert space for weak ref + let weakref = if attr.has_weaklist { + quote! { _pyo3::pyclass_slots::PyClassWeakRefSlot } + } else if attr.has_extends { + quote! { ::WeakRef } + } else { + quote! { _pyo3::pyclass_slots::PyClassDummySlot } + }; + + let base_nativetype = if attr.has_extends { + quote! { ::BaseNativeType } + } else { + quote! { _pyo3::PyAny } + }; + quote! { + impl _pyo3::PyClass for #cls { + type Dict = #dict; + type WeakRef = #weakref; + type BaseNativeType = #base_nativetype; + } + } + } + fn impl_extractext(&self) -> TokenStream { + let cls = self.cls; + quote! { + impl<'a> _pyo3::derive_utils::ExtractExt<'a> for &'a #cls + { + type Target = _pyo3::PyRef<'a, #cls>; + } + + impl<'a> _pyo3::derive_utils::ExtractExt<'a> for &'a mut #cls + { + type Target = _pyo3::PyRefMut<'a, #cls>; + } + } + } + + fn impl_into_py(&self) -> TokenStream { + let cls = self.cls; + let attr = self.attr; + // If #cls is not extended type, we allow Self->PyObject conversion + if !attr.has_extends { + quote! { + impl _pyo3::IntoPy<_pyo3::PyObject> for #cls { + fn into_py(self, py: _pyo3::Python) -> _pyo3::PyObject { + _pyo3::IntoPy::into_py(_pyo3::Py::new(py, self).unwrap(), py) + } + } + } + } else { + quote! {} + } + } + 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 thread_checker = if self.attr.has_unsendable { + quote! { _pyo3::class::impl_::ThreadCheckerImpl<#cls> } + } else if self.attr.has_extends { + quote! { + _pyo3::class::impl_::ThreadCheckerInherited<#cls, <#cls as _pyo3::class::impl_::PyClassImpl>::BaseType> + } + } else { + quote! { _pyo3::class::impl_::ThreadCheckerStub<#cls> } + }; + + let (for_each_py_method, methods_protos, inventory, inventory_class) = match self + .methods_type + { + PyClassMethodsType::Specialization => ( + quote! { visitor(collector.py_methods()); }, + quote! { visitor(collector.methods_protocol_slots()); }, + None, + None, + ), + PyClassMethodsType::Inventory => { + // To allow multiple #[pymethods] block, we define inventory types. + let inventory_class_name = syn::Ident::new( + &format!("Pyo3MethodsInventoryFor{}", cls.unraw()), + Span::call_site(), + ); + ( + quote! { + for inventory in _pyo3::inventory::iter::<::Inventory>() { + visitor(_pyo3::class::impl_::PyClassInventory::methods(inventory)); + } + }, + quote! { + for inventory in _pyo3::inventory::iter::<::Inventory>() { + visitor(_pyo3::class::impl_::PyClassInventory::slots(inventory)); + } + }, + Some(quote! { type Inventory = #inventory_class_name; }), + Some(define_inventory_class(&inventory_class_name)), + ) + } + }; + quote! { + impl _pyo3::class::impl_::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; + + type Layout = _pyo3::PyCell; + type BaseType = #base; + type ThreadChecker = #thread_checker; + #inventory + + fn for_each_method_def(visitor: &mut dyn ::std::ops::FnMut(&[_pyo3::class::PyMethodDefType])) { + use _pyo3::class::impl_::*; + let collector = PyClassImplCollector::::new(); + #for_each_py_method; + visitor(collector.py_class_descriptors()); + visitor(collector.object_protocol_methods()); + visitor(collector.async_protocol_methods()); + visitor(collector.descr_protocol_methods()); + visitor(collector.mapping_protocol_methods()); + visitor(collector.number_protocol_methods()); + } + fn get_new() -> ::std::option::Option<_pyo3::ffi::newfunc> { + use _pyo3::class::impl_::*; + let collector = PyClassImplCollector::::new(); + collector.new_impl() + } + fn get_alloc() -> ::std::option::Option<_pyo3::ffi::allocfunc> { + use _pyo3::class::impl_::*; + let collector = PyClassImplCollector::::new(); + collector.alloc_impl() + } + fn get_free() -> ::std::option::Option<_pyo3::ffi::freefunc> { + use _pyo3::class::impl_::*; + let collector = PyClassImplCollector::::new(); + collector.free_impl() + } + + fn for_each_proto_slot(visitor: &mut dyn ::std::ops::FnMut(&[_pyo3::ffi::PyType_Slot])) { + // Implementation which uses dtolnay specialization to load all slots. + use _pyo3::class::impl_::*; + let collector = PyClassImplCollector::::new(); + // This depends on Python implementation detail; + // an old slot entry will be overriden by newer ones. + visitor(collector.py_class_default_slots()); + visitor(collector.object_protocol_slots()); + visitor(collector.number_protocol_slots()); + visitor(collector.iter_protocol_slots()); + visitor(collector.gc_protocol_slots()); + visitor(collector.descr_protocol_slots()); + visitor(collector.mapping_protocol_slots()); + visitor(collector.sequence_protocol_slots()); + visitor(collector.async_protocol_slots()); + visitor(collector.buffer_protocol_slots()); + #methods_protos + } + + fn get_buffer() -> ::std::option::Option<&'static _pyo3::class::impl_::PyBufferProcs> { + use _pyo3::class::impl_::*; + let collector = PyClassImplCollector::::new(); + collector.buffer_procs() + } + } + + #inventory_class + } + } + + fn impl_freelist(&self) -> TokenStream { + let cls = self.cls; + + self.attr.freelist.as_ref().map_or(quote!{}, |freelist| { + quote! { + impl _pyo3::class::impl_::PyClassWithFreeList for #cls { + #[inline] + fn get_free_list(_py: _pyo3::Python<'_>) -> &mut _pyo3::impl_::freelist::FreeList<*mut _pyo3::ffi::PyObject> { + static mut FREELIST: *mut _pyo3::impl_::freelist::FreeList<*mut _pyo3::ffi::PyObject> = 0 as *mut _; + unsafe { + if FREELIST.is_null() { + FREELIST = ::std::boxed::Box::into_raw(::std::boxed::Box::new( + _pyo3::impl_::freelist::FreeList::with_capacity(#freelist))); + } + &mut *FREELIST + } + } + } + + impl _pyo3::class::impl_::PyClassAllocImpl<#cls> for _pyo3::class::impl_::PyClassImplCollector<#cls> { + #[inline] + fn alloc_impl(self) -> ::std::option::Option<_pyo3::ffi::allocfunc> { + ::std::option::Option::Some(_pyo3::class::impl_::alloc_with_freelist::<#cls>) + } + } + + impl _pyo3::class::impl_::PyClassFreeImpl<#cls> for _pyo3::class::impl_::PyClassImplCollector<#cls> { + #[inline] + fn free_impl(self) -> ::std::option::Option<_pyo3::ffi::freefunc> { + ::std::option::Option::Some(_pyo3::class::impl_::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>(); + } + } + } else { + quote! {} + } + } +} + +fn define_inventory_class(inventory_class_name: &syn::Ident) -> TokenStream { + quote! { + #[doc(hidden)] + pub struct #inventory_class_name { + methods: &'static [_pyo3::class::PyMethodDefType], + slots: &'static [_pyo3::ffi::PyType_Slot], + } + impl #inventory_class_name { + const fn new( + methods: &'static [_pyo3::class::PyMethodDefType], + slots: &'static [_pyo3::ffi::PyType_Slot], + ) -> Self { + Self { methods, slots } + } + } + + impl _pyo3::class::impl_::PyClassInventory for #inventory_class_name { + fn methods(&'static self) -> &'static [_pyo3::class::PyMethodDefType] { + self.methods + } + fn slots(&'static self) -> &'static [_pyo3::ffi::PyType_Slot] { + self.slots + } + } + + // inventory requires these bounds + unsafe impl ::std::marker::Send for #inventory_class_name {} + unsafe impl ::std::marker::Sync for #inventory_class_name {} + + _pyo3::inventory::collect!(#inventory_class_name); + } +} diff --git a/pyo3-macros-backend/src/pyfunction.rs b/pyo3-macros-backend/src/pyfunction.rs index a5017e1b..28577bfb 100644 --- a/pyo3-macros-backend/src/pyfunction.rs +++ b/pyo3-macros-backend/src/pyfunction.rs @@ -2,14 +2,13 @@ use crate::{ attributes::{ - self, get_deprecated_name_attribute, get_deprecated_text_signature_attribute, - get_pyo3_options, take_attributes, FromPyWithAttribute, NameAttribute, - TextSignatureAttribute, + self, get_pyo3_options, take_attributes, take_pyo3_options, CrateAttribute, + FromPyWithAttribute, NameAttribute, TextSignatureAttribute, }, deprecations::Deprecations, method::{self, CallingConvention, FnArg}, pymethod::check_generic, - utils::{self, ensure_not_async_fn}, + utils::{self, ensure_not_async_fn, get_pyo3_crate}, }; use proc_macro2::{Span, TokenStream}; use quote::{format_ident, quote}; @@ -240,17 +239,12 @@ pub struct PyFunctionOptions { pub signature: Option, pub text_signature: Option, pub deprecations: Deprecations, + pub krate: Option, } impl Parse for PyFunctionOptions { fn parse(input: ParseStream) -> Result { - let mut options = PyFunctionOptions { - pass_module: None, - name: None, - signature: None, - text_signature: None, - deprecations: Deprecations::new(), - }; + let mut options = PyFunctionOptions::default(); while !input.is_empty() { let lookahead = input.lookahead1(); @@ -263,6 +257,9 @@ impl Parse for PyFunctionOptions { if !input.is_empty() { let _: Comma = input.parse()?; } + } else if lookahead.peek(syn::Token![crate]) { + // TODO needs duplicate check? + options.krate = Some(input.parse()?); } else { // If not recognised attribute, this is "legacy" pyfunction syntax #[pyfunction(a, b)] // @@ -281,6 +278,7 @@ pub enum PyFunctionOption { PassModule(attributes::kw::pass_module), Signature(PyFunctionSignature), TextSignature(TextSignatureAttribute), + Crate(CrateAttribute), } impl Parse for PyFunctionOption { @@ -294,6 +292,8 @@ impl Parse for PyFunctionOption { input.parse().map(PyFunctionOption::Signature) } else if lookahead.peek(attributes::kw::text_signature) { input.parse().map(PyFunctionOption::TextSignature) + } else if lookahead.peek(syn::Token![crate]) { + input.parse().map(PyFunctionOption::Crate) } else { Err(lookahead.error()) } @@ -303,34 +303,10 @@ impl Parse for PyFunctionOption { impl PyFunctionOptions { pub fn from_attrs(attrs: &mut Vec) -> syn::Result { let mut options = PyFunctionOptions::default(); - options.take_pyo3_options(attrs)?; + options.add_attributes(take_pyo3_options(attrs)?)?; Ok(options) } - pub fn take_pyo3_options(&mut self, attrs: &mut Vec) -> syn::Result<()> { - take_attributes(attrs, |attr| { - if let Some(pyo3_attributes) = get_pyo3_options(attr)? { - self.add_attributes(pyo3_attributes)?; - Ok(true) - } else if let Some(name) = get_deprecated_name_attribute(attr, &mut self.deprecations)? - { - self.set_name(name)?; - Ok(true) - } else if let Some(text_signature) = - get_deprecated_text_signature_attribute(attr, &mut self.deprecations)? - { - self.add_attributes(std::iter::once(PyFunctionOption::TextSignature( - text_signature, - )))?; - Ok(true) - } else { - Ok(false) - } - })?; - - Ok(()) - } - pub fn add_attributes( &mut self, attrs: impl IntoIterator, @@ -360,6 +336,13 @@ impl PyFunctionOptions { ); self.text_signature = Some(text_signature); } + PyFunctionOption::Crate(path) => { + ensure_spanned!( + self.krate.is_none(), + path.0.span() => "`crate` may only be specified once" + ); + self.krate = Some(path); + } } } Ok(()) @@ -379,7 +362,7 @@ pub fn build_py_function( ast: &mut syn::ItemFn, mut options: PyFunctionOptions, ) -> syn::Result { - options.take_pyo3_options(&mut ast.attrs)?; + options.add_attributes(take_pyo3_options(&mut ast.attrs)?)?; Ok(impl_wrap_pyfunction(ast, options)?.1) } @@ -435,6 +418,7 @@ pub fn impl_wrap_pyfunction( ); let function_wrapper_ident = function_wrapper_ident(&func.sig.ident); + let krate = get_pyo3_crate(&options.krate); let spec = method::FnSpec { tp: if options.pass_module.is_some() { @@ -451,6 +435,7 @@ pub fn impl_wrap_pyfunction( doc, deprecations: options.deprecations, text_signature: options.text_signature, + krate: krate.clone(), }; let wrapper_ident = format_ident!("__pyo3_raw_{}", spec.name); @@ -459,10 +444,12 @@ pub fn impl_wrap_pyfunction( let wrapped_pyfunction = quote! { #wrapper + pub(crate) fn #function_wrapper_ident<'a>( - args: impl ::std::convert::Into<::pyo3::derive_utils::PyFunctionArguments<'a>> - ) -> ::pyo3::PyResult<&'a ::pyo3::types::PyCFunction> { - ::pyo3::types::PyCFunction::internal_new(#methoddef, args.into()) + args: impl ::std::convert::Into<#krate::derive_utils::PyFunctionArguments<'a>> + ) -> #krate::PyResult<&'a #krate::types::PyCFunction> { + use #krate as _pyo3; + _pyo3::types::PyCFunction::internal_new(#methoddef, args.into()) } }; Ok((function_wrapper_ident, wrapped_pyfunction)) diff --git a/pyo3-macros-backend/src/pyimpl.rs b/pyo3-macros-backend/src/pyimpl.rs index 54e248c9..46fe05b9 100644 --- a/pyo3-macros-backend/src/pyimpl.rs +++ b/pyo3-macros-backend/src/pyimpl.rs @@ -3,21 +3,72 @@ use std::collections::HashSet; use crate::{ + attributes::{take_pyo3_options, CrateAttribute}, konst::{ConstAttributes, ConstSpec}, pyfunction::PyFunctionOptions, pymethod::{self, is_proto_method}, + utils::get_pyo3_crate, }; use proc_macro2::TokenStream; use pymethod::GeneratedPyMethod; use quote::quote; -use syn::spanned::Spanned; +use syn::{ + parse::{Parse, ParseStream}, + spanned::Spanned, + Result, +}; /// The mechanism used to collect `#[pymethods]` into the type object +#[derive(Copy, Clone)] pub enum PyClassMethodsType { Specialization, Inventory, } +enum PyImplPyO3Option { + Crate(CrateAttribute), +} + +impl Parse for PyImplPyO3Option { + fn parse(input: ParseStream) -> Result { + let lookahead = input.lookahead1(); + if lookahead.peek(syn::Token![crate]) { + input.parse().map(PyImplPyO3Option::Crate) + } else { + Err(lookahead.error()) + } + } +} + +#[derive(Default)] +pub struct PyImplOptions { + krate: Option, +} + +impl PyImplOptions { + pub fn from_attrs(attrs: &mut Vec) -> Result { + let mut options: PyImplOptions = Default::default(); + + for option in take_pyo3_options(attrs)? { + match option { + PyImplPyO3Option::Crate(path) => options.set_crate(path)?, + } + } + + Ok(options) + } + + fn set_crate(&mut self, path: CrateAttribute) -> Result<()> { + ensure_spanned!( + self.krate.is_none(), + path.0.span() => "`crate` may only be specified once" + ); + + self.krate = Some(path); + Ok(()) + } +} + pub fn build_py_methods( ast: &mut syn::ItemImpl, methods_type: PyClassMethodsType, @@ -30,7 +81,8 @@ pub fn build_py_methods( "#[pymethods] cannot be used with lifetime parameters or generics" ); } else { - impl_methods(&ast.self_ty, &mut ast.items, methods_type) + let options = PyImplOptions::from_attrs(&mut ast.attrs)?; + impl_methods(&ast.self_ty, &mut ast.items, methods_type, options) } } @@ -38,6 +90,7 @@ pub fn impl_methods( ty: &syn::Type, impls: &mut Vec, methods_type: PyClassMethodsType, + options: PyImplOptions, ) -> syn::Result { let mut trait_impls = Vec::new(); let mut proto_impls = Vec::new(); @@ -48,8 +101,9 @@ pub fn impl_methods( for iimpl in impls.iter_mut() { match iimpl { syn::ImplItem::Method(meth) => { - let options = PyFunctionOptions::from_attrs(&mut meth.attrs)?; - match pymethod::gen_py_method(ty, &mut meth.sig, &mut meth.attrs, options)? { + let mut fun_options = PyFunctionOptions::from_attrs(&mut meth.attrs)?; + fun_options.krate = fun_options.krate.or_else(|| options.krate.clone()); + match pymethod::gen_py_method(ty, &mut meth.sig, &mut meth.attrs, fun_options)? { GeneratedPyMethod::Method(token_stream) => { let attrs = get_cfg_attributes(&meth.attrs); methods.push(quote!(#(#attrs)* #token_stream)); @@ -94,42 +148,52 @@ pub fn impl_methods( add_shared_proto_slots(ty, &mut proto_impls, implemented_proto_fragments); + let krate = get_pyo3_crate(&options.krate); + Ok(match methods_type { PyClassMethodsType::Specialization => { let methods_registration = impl_py_methods(ty, methods); let protos_registration = impl_protos(ty, proto_impls); quote! { - #(#trait_impls)* + const _: () = { + use #krate as _pyo3; - #protos_registration + #(#trait_impls)* - #methods_registration + #protos_registration + + #methods_registration + }; } } PyClassMethodsType::Inventory => { let inventory = submit_methods_inventory(ty, methods, proto_impls); quote! { - #(#trait_impls)* + const _: () = { + use #krate as _pyo3; - #inventory + #(#trait_impls)* + + #inventory + }; } } }) } -fn gen_py_const(cls: &syn::Type, spec: &ConstSpec) -> TokenStream { +pub fn gen_py_const(cls: &syn::Type, spec: &ConstSpec) -> TokenStream { let member = &spec.rust_ident; let deprecations = &spec.attributes.deprecations; let python_name = &spec.null_terminated_python_name(); quote! { - ::pyo3::class::PyMethodDefType::ClassAttribute({ - ::pyo3::class::PyClassAttributeDef::new( + _pyo3::class::PyMethodDefType::ClassAttribute({ + _pyo3::class::PyClassAttributeDef::new( #python_name, - ::pyo3::class::methods::PyClassAttributeFactory({ - fn __wrap(py: ::pyo3::Python<'_>) -> ::pyo3::PyObject { + _pyo3::class::methods::PyClassAttributeFactory({ + fn __wrap(py: _pyo3::Python<'_>) -> _pyo3::PyObject { #deprecations - ::pyo3::IntoPy::into_py(#cls::#member, py) + _pyo3::IntoPy::into_py(#cls::#member, py) } __wrap }) @@ -138,13 +202,54 @@ fn gen_py_const(cls: &syn::Type, spec: &ConstSpec) -> TokenStream { } } +pub fn gen_default_slot_impls(cls: &syn::Ident, method_defs: Vec) -> TokenStream { + // 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 { + 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)) + } + GeneratedPyMethod::SlotTraitImpl(..) => { + panic!("SlotFragment methods cannot have default implementation!") + } + GeneratedPyMethod::Method(_) | GeneratedPyMethod::TraitImpl(_) => { + panic!("Only protocol methods can have default implementation!") + } + } + } + + quote! { + impl #cls { + #(#method_defs)* + } + impl ::pyo3::class::impl_::PyClassDefaultSlots<#cls> + for ::pyo3::class::impl_::PyClassImplCollector<#cls> { + fn py_class_default_slots(self) -> &'static [::pyo3::ffi::PyType_Slot] { + &[#(#proto_impls),*] + } + } + } +} + fn impl_py_methods(ty: &syn::Type, methods: Vec) -> TokenStream { quote! { - impl ::pyo3::class::impl_::PyMethods<#ty> - for ::pyo3::class::impl_::PyClassImplCollector<#ty> + impl _pyo3::class::impl_::PyMethods<#ty> + for _pyo3::class::impl_::PyClassImplCollector<#ty> { - fn py_methods(self) -> &'static [::pyo3::class::methods::PyMethodDefType] { - static METHODS: &[::pyo3::class::methods::PyMethodDefType] = &[#(#methods),*]; + fn py_methods(self) -> &'static [_pyo3::class::methods::PyMethodDefType] { + static METHODS: &[_pyo3::class::methods::PyMethodDefType] = &[#(#methods),*]; METHODS } } @@ -161,7 +266,7 @@ fn add_shared_proto_slots( let first_implemented = implemented_proto_fragments.remove($first); let second_implemented = implemented_proto_fragments.remove($second); if first_implemented || second_implemented { - proto_impls.push(quote! { ::pyo3::$slot!(#ty) }) + proto_impls.push(quote! { _pyo3::class::impl_::$slot!(#ty) }) } }}; } @@ -193,10 +298,10 @@ fn add_shared_proto_slots( fn impl_protos(ty: &syn::Type, proto_impls: Vec) -> TokenStream { quote! { - impl ::pyo3::class::impl_::PyMethodsProtocolSlots<#ty> - for ::pyo3::class::impl_::PyClassImplCollector<#ty> + impl _pyo3::class::impl_::PyMethodsProtocolSlots<#ty> + for _pyo3::class::impl_::PyClassImplCollector<#ty> { - fn methods_protocol_slots(self) -> &'static [::pyo3::ffi::PyType_Slot] { + fn methods_protocol_slots(self) -> &'static [_pyo3::ffi::PyType_Slot] { &[#(#proto_impls),*] } } @@ -209,11 +314,9 @@ fn submit_methods_inventory( proto_impls: Vec, ) -> TokenStream { quote! { - ::pyo3::inventory::submit! { - #![crate = ::pyo3] { - type Inventory = <#ty as ::pyo3::class::impl_::HasMethodsInventory>::Methods; - ::new(::std::vec![#(#methods),*], ::std::vec![#(#proto_impls),*]) - } + _pyo3::inventory::submit! { + type Inventory = <#ty as _pyo3::class::impl_::PyClassImpl>::Inventory; + Inventory::new(&[#(#methods),*], &[#(#proto_impls),*]) } } } diff --git a/pyo3-macros-backend/src/pymethod.rs b/pyo3-macros-backend/src/pymethod.rs index eb8611dc..c8b4d607 100644 --- a/pyo3-macros-backend/src/pymethod.rs +++ b/pyo3-macros-backend/src/pymethod.rs @@ -120,12 +120,12 @@ pub fn gen_py_method( (_, FnType::FnClass) => GeneratedPyMethod::Method(impl_py_method_def( cls, spec, - Some(quote!(::pyo3::ffi::METH_CLASS)), + Some(quote!(_pyo3::ffi::METH_CLASS)), )?), (_, FnType::FnStatic) => GeneratedPyMethod::Method(impl_py_method_def( cls, spec, - Some(quote!(::pyo3::ffi::METH_STATIC)), + Some(quote!(_pyo3::ffi::METH_STATIC)), )?), // special prototypes (_, FnType::FnNew) => GeneratedPyMethod::TraitImpl(impl_py_method_def_new(cls, spec)?), @@ -186,7 +186,7 @@ pub fn impl_py_method_def( }; let methoddef = spec.get_methoddef(quote! {{ #wrapper_def #wrapper_ident }}); Ok(quote! { - ::pyo3::class::PyMethodDefType::#methoddef_type(#methoddef #add_flags) + _pyo3::class::PyMethodDefType::#methoddef_type(#methoddef #add_flags) }) } @@ -194,8 +194,8 @@ fn impl_py_method_def_new(cls: &syn::Type, spec: &FnSpec) -> Result let wrapper_ident = syn::Ident::new("__wrap", Span::call_site()); let wrapper = spec.get_wrapper_function(&wrapper_ident, Some(cls))?; Ok(quote! { - impl ::pyo3::class::impl_::PyClassNewImpl<#cls> for ::pyo3::class::impl_::PyClassImplCollector<#cls> { - fn new_impl(self) -> ::std::option::Option<::pyo3::ffi::newfunc> { + impl _pyo3::class::impl_::PyClassNewImpl<#cls> for _pyo3::class::impl_::PyClassImplCollector<#cls> { + fn new_impl(self) -> ::std::option::Option<_pyo3::ffi::newfunc> { ::std::option::Option::Some({ #wrapper #wrapper_ident @@ -214,9 +214,9 @@ fn impl_call_slot(cls: &syn::Type, mut spec: FnSpec) -> Result { let wrapper = spec.get_wrapper_function(&wrapper_ident, Some(cls))?; Ok(quote! {{ #wrapper - ::pyo3::ffi::PyType_Slot { - slot: ::pyo3::ffi::Py_tp_call, - pfunc: __wrap as ::pyo3::ffi::ternaryfunc as _ + _pyo3::ffi::PyType_Slot { + slot: _pyo3::ffi::Py_tp_call, + pfunc: __wrap as _pyo3::ffi::ternaryfunc as _ } }}) } @@ -226,13 +226,13 @@ fn impl_py_class_attribute(cls: &syn::Type, spec: &FnSpec) -> TokenStream { let deprecations = &spec.deprecations; let python_name = spec.null_terminated_python_name(); quote! { - ::pyo3::class::PyMethodDefType::ClassAttribute({ - ::pyo3::class::PyClassAttributeDef::new( + _pyo3::class::PyMethodDefType::ClassAttribute({ + _pyo3::class::PyClassAttributeDef::new( #python_name, - ::pyo3::class::methods::PyClassAttributeFactory({ - fn __wrap(py: ::pyo3::Python<'_>) -> ::pyo3::PyObject { + _pyo3::class::methods::PyClassAttributeFactory({ + fn __wrap(py: _pyo3::Python<'_>) -> _pyo3::PyObject { #deprecations - ::pyo3::IntoPy::into_py(#cls::#name(), py) + _pyo3::IntoPy::into_py(#cls::#name(), py) } __wrap }) @@ -295,26 +295,26 @@ pub fn impl_py_setter_def(cls: &syn::Type, property_type: PropertyType) -> Resul } }; Ok(quote! { - ::pyo3::class::PyMethodDefType::Setter({ + _pyo3::class::PyMethodDefType::Setter({ #deprecations - ::pyo3::class::PySetterDef::new( + _pyo3::class::PySetterDef::new( #python_name, - ::pyo3::class::methods::PySetter({ + _pyo3::class::methods::PySetter({ unsafe extern "C" fn __wrap( - _slf: *mut ::pyo3::ffi::PyObject, - _value: *mut ::pyo3::ffi::PyObject, + _slf: *mut _pyo3::ffi::PyObject, + _value: *mut _pyo3::ffi::PyObject, _: *mut ::std::os::raw::c_void ) -> ::std::os::raw::c_int { - ::pyo3::callback::handle_panic(|_py| { + _pyo3::callback::handle_panic(|_py| { #slf let _value = _py .from_borrowed_ptr_or_opt(_value) .ok_or_else(|| { - ::pyo3::exceptions::PyAttributeError::new_err("can't delete attribute") + _pyo3::exceptions::PyAttributeError::new_err("can't delete attribute") })?; - let _val = ::pyo3::FromPyObject::extract(_value)?; + let _val = _pyo3::FromPyObject::extract(_value)?; - ::pyo3::callback::convert(_py, #setter_impl) + _pyo3::callback::convert(_py, #setter_impl) }) } __wrap @@ -375,18 +375,18 @@ pub fn impl_py_getter_def(cls: &syn::Type, property_type: PropertyType) -> Resul } }; Ok(quote! { - ::pyo3::class::PyMethodDefType::Getter({ + _pyo3::class::PyMethodDefType::Getter({ #deprecations - ::pyo3::class::PyGetterDef::new( + _pyo3::class::PyGetterDef::new( #python_name, - ::pyo3::class::methods::PyGetter({ + _pyo3::class::methods::PyGetter({ unsafe extern "C" fn __wrap( - _slf: *mut ::pyo3::ffi::PyObject, + _slf: *mut _pyo3::ffi::PyObject, _: *mut ::std::os::raw::c_void - ) -> *mut ::pyo3::ffi::PyObject { - ::pyo3::callback::handle_panic(|_py| { + ) -> *mut _pyo3::ffi::PyObject { + _pyo3::callback::handle_panic(|_py| { #slf - ::pyo3::callback::convert(_py, #getter_impl) + _pyo3::callback::convert(_py, #getter_impl) }) } __wrap @@ -399,14 +399,9 @@ 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]) { - if args - .get(0) - .map(|py| utils::is_python(py.ty)) - .unwrap_or(false) - { - (Some(&args[0]), &args[1..]) - } else { - (None, args) + match args { + [py, args @ ..] if utils::is_python(py.ty) => (Some(py), args), + args => (None, args), } } @@ -464,10 +459,10 @@ const __GETATTR__: SlotDef = SlotDef::new("Py_tp_getattro", "getattrofunc") 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); + 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(); + _pyo3::ffi::PyErr_Clear(); } else { return existing; } @@ -478,21 +473,21 @@ const __REPR__: SlotDef = SlotDef::new("Py_tp_repr", "reprfunc"); const __HASH__: SlotDef = SlotDef::new("Py_tp_hash", "hashfunc") .ret_ty(Ty::PyHashT) .return_conversion(TokenGenerator( - || quote! { ::pyo3::callback::HashCallbackOutput }, + || quote! { _pyo3::callback::HashCallbackOutput }, )); const __RICHCMP__: SlotDef = SlotDef::new("Py_tp_richcompare", "richcmpfunc") .extract_error_mode(ExtractErrorMode::NotImplemented) .arguments(&[Ty::Object, Ty::CompareOp]); -const __GET__: SlotDef = - SlotDef::new("Py_tp_descr_get", "descrgetfunc").arguments(&[Ty::Object, Ty::Object]); +const __GET__: SlotDef = SlotDef::new("Py_tp_descr_get", "descrgetfunc") + .arguments(&[Ty::MaybeNullObject, Ty::MaybeNullObject]); const __ITER__: SlotDef = SlotDef::new("Py_tp_iter", "getiterfunc"); const __NEXT__: SlotDef = SlotDef::new("Py_tp_iternext", "iternextfunc").return_conversion( - TokenGenerator(|| quote! { ::pyo3::class::iter::IterNextOutput::<_, _> }), + TokenGenerator(|| quote! { _pyo3::class::iter::IterNextOutput::<_, _> }), ); const __AWAIT__: SlotDef = SlotDef::new("Py_am_await", "unaryfunc"); const __AITER__: SlotDef = SlotDef::new("Py_am_aiter", "unaryfunc"); const __ANEXT__: SlotDef = SlotDef::new("Py_am_anext", "unaryfunc").return_conversion( - TokenGenerator(|| quote! { ::pyo3::class::pyasync::IterANextOutput::<_, _> }), + TokenGenerator(|| quote! { _pyo3::class::pyasync::IterANextOutput::<_, _> }), ); const __LEN__: SlotDef = SlotDef::new("Py_mp_length", "lenfunc").ret_ty(Ty::PySsizeT); const __CONTAINS__: SlotDef = SlotDef::new("Py_sq_contains", "objobjproc") @@ -606,6 +601,7 @@ fn pyproto(method_name: &str) -> Option<&'static SlotDef> { #[derive(Clone, Copy)] enum Ty { Object, + MaybeNullObject, NonNullObject, CompareOp, Int, @@ -617,11 +613,11 @@ enum Ty { impl Ty { fn ffi_type(self) -> TokenStream { match self { - Ty::Object => quote! { *mut ::pyo3::ffi::PyObject }, - Ty::NonNullObject => quote! { ::std::ptr::NonNull<::pyo3::ffi::PyObject> }, + Ty::Object | Ty::MaybeNullObject => quote! { *mut _pyo3::ffi::PyObject }, + Ty::NonNullObject => quote! { ::std::ptr::NonNull<_pyo3::ffi::PyObject> }, Ty::Int | Ty::CompareOp => quote! { ::std::os::raw::c_int }, - Ty::PyHashT => quote! { ::pyo3::ffi::Py_hash_t }, - Ty::PySsizeT => quote! { ::pyo3::ffi::Py_ssize_t }, + Ty::PyHashT => quote! { _pyo3::ffi::Py_hash_t }, + Ty::PySsizeT => quote! { _pyo3::ffi::Py_ssize_t }, Ty::Void => quote! { () }, } } @@ -640,7 +636,23 @@ impl Ty { extract_error_mode, py, quote! { - #py.from_borrowed_ptr::<::pyo3::PyAny>(#ident).extract() + #py.from_borrowed_ptr::<_pyo3::PyAny>(#ident).extract() + }, + ); + extract_object(cls, arg.ty, ident, extract) + } + Ty::MaybeNullObject => { + let extract = handle_error( + extract_error_mode, + py, + quote! { + #py.from_borrowed_ptr::<_pyo3::PyAny>( + if #ident.is_null() { + _pyo3::ffi::Py_None() + } else { + #ident + } + ).extract() }, ); extract_object(cls, arg.ty, ident, extract) @@ -650,7 +662,7 @@ impl Ty { extract_error_mode, py, quote! { - #py.from_borrowed_ptr::<::pyo3::PyAny>(#ident.as_ptr()).extract() + #py.from_borrowed_ptr::<_pyo3::PyAny>(#ident.as_ptr()).extract() }, ); extract_object(cls, arg.ty, ident, extract) @@ -660,8 +672,8 @@ impl Ty { extract_error_mode, py, quote! { - ::pyo3::class::basic::CompareOp::from_raw(#ident) - .ok_or_else(|| ::pyo3::exceptions::PyValueError::new_err("invalid comparison operator")) + _pyo3::class::basic::CompareOp::from_raw(#ident) + .ok_or_else(|| _pyo3::exceptions::PyValueError::new_err("invalid comparison operator")) }, ); quote! { @@ -683,7 +695,7 @@ fn handle_error( ExtractErrorMode::NotImplemented => quote! { match #extract { ::std::result::Result::Ok(value) => value, - ::std::result::Result::Err(_) => { return ::pyo3::callback::convert(#py, #py.NotImplemented()); }, + ::std::result::Result::Err(_) => { return _pyo3::callback::convert(#py, #py.NotImplemented()); }, } }, } @@ -700,7 +712,7 @@ fn extract_object( replace_self(&mut tref.elem, cls); let mut_ = tref.mutability; quote! { - let #mut_ #ident: <#tref as ::pyo3::derive_utils::ExtractExt<'_>>::Target = #extract; + let #mut_ #ident: <#tref as _pyo3::derive_utils::ExtractExt<'_>>::Target = #extract; let #ident = &#mut_ *#ident; } } else { @@ -719,13 +731,13 @@ impl ReturnMode { fn return_call_output(&self, py: &syn::Ident, call: TokenStream) -> TokenStream { match self { ReturnMode::Conversion(conversion) => quote! { - let _result: ::pyo3::PyResult<#conversion> = #call; - ::pyo3::callback::convert(#py, _result) + let _result: _pyo3::PyResult<#conversion> = #call; + _pyo3::callback::convert(#py, _result) }, ReturnMode::ReturnSelf => quote! { - let _result: ::pyo3::PyResult<()> = #call; + let _result: _pyo3::PyResult<()> = #call; _result?; - ::pyo3::ffi::Py_XINCREF(_raw_slf); + _pyo3::ffi::Py_XINCREF(_raw_slf); ::std::result::Result::Ok(_raw_slf) }, } @@ -809,16 +821,16 @@ impl SlotDef { return_mode.as_ref(), )?; Ok(quote!({ - unsafe extern "C" fn __wrap(_raw_slf: *mut ::pyo3::ffi::PyObject, #(#method_arguments),*) -> #ret_ty { + 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| { + _pyo3::callback::handle_panic(|#py| { #body }) } - ::pyo3::ffi::PyType_Slot { - slot: ::pyo3::ffi::#slot, - pfunc: __wrap as ::pyo3::ffi::#func_ty as _ + _pyo3::ffi::PyType_Slot { + slot: _pyo3::ffi::#slot, + pfunc: __wrap as _pyo3::ffi::#func_ty as _ } })) } @@ -846,7 +858,7 @@ fn generate_method_body( let rust_name = spec.name; let (arg_idents, conversions) = extract_proto_arguments(cls, py, &spec.args, arguments, extract_error_mode)?; - let call = quote! { ::pyo3::callback::convert(#py, #cls::#rust_name(_slf, #(#arg_idents),*)) }; + let call = quote! { _pyo3::callback::convert(#py, #cls::#rust_name(_slf, #(#arg_idents),*)) }; let body = if let Some(return_mode) = return_mode { return_mode.return_call_output(py, call) } else { @@ -900,15 +912,15 @@ impl SlotFragmentDef { let body = generate_method_body(cls, spec, &py, arguments, *extract_error_mode, None)?; let ret_ty = ret_ty.ffi_type(); Ok(quote! { - impl ::pyo3::class::impl_::#fragment_trait<#cls> for ::pyo3::class::impl_::PyClassImplCollector<#cls> { + impl _pyo3::class::impl_::#fragment_trait<#cls> for _pyo3::class::impl_::PyClassImplCollector<#cls> { #[inline] unsafe fn #method( self, - #py: ::pyo3::Python, - _raw_slf: *mut ::pyo3::ffi::PyObject, + #py: _pyo3::Python, + _raw_slf: *mut _pyo3::ffi::PyObject, #(#method_arguments),* - ) -> ::pyo3::PyResult<#ret_ty> { + ) -> _pyo3::PyResult<#ret_ty> { let _slf = _raw_slf; #body } diff --git a/pyo3-macros-backend/src/pyproto.rs b/pyo3-macros-backend/src/pyproto.rs index 23e5d8a5..08b8527c 100644 --- a/pyo3-macros-backend/src/pyproto.rs +++ b/pyo3-macros-backend/src/pyproto.rs @@ -18,7 +18,6 @@ pub fn build_py_proto(ast: &mut syn::ItemImpl) -> syn::Result { Some(segment) if segment.ident == "PyAsyncProtocol" => &defs::ASYNC, Some(segment) if segment.ident == "PyMappingProtocol" => &defs::MAPPING, Some(segment) if segment.ident == "PyIterProtocol" => &defs::ITER, - Some(segment) if segment.ident == "PyContextProtocol" => &defs::CONTEXT, Some(segment) if segment.ident == "PySequenceProtocol" => &defs::SEQ, Some(segment) if segment.ident == "PyNumberProtocol" => &defs::NUM, Some(segment) if segment.ident == "PyDescrProtocol" => &defs::DESCR, @@ -68,7 +67,7 @@ fn impl_proto_impl( let flags = if m.can_coexist { // We need METH_COEXIST here to prevent __add__ from overriding __radd__ - Some(quote!(::pyo3::ffi::METH_COEXIST)) + Some(quote!(_pyo3::ffi::METH_COEXIST)) } else { None }; @@ -87,10 +86,14 @@ fn impl_proto_impl( } let normal_methods = impl_normal_methods(py_methods, ty, proto); let protocol_methods = impl_proto_methods(method_names, ty, proto); + Ok(quote! { - #trait_impls - #normal_methods - #protocol_methods + const _: () = { + use ::pyo3 as _pyo3; // pyproto doesn't support specifying #[pyo3(crate)] + #trait_impls + #normal_methods + #protocol_methods + }; }) } @@ -106,11 +109,11 @@ fn impl_normal_methods( let methods_trait = proto.methods_trait(); let methods_trait_methods = proto.methods_trait_methods(); quote! { - impl ::pyo3::class::impl_::#methods_trait<#ty> - for ::pyo3::class::impl_::PyClassImplCollector<#ty> + impl _pyo3::class::impl_::#methods_trait<#ty> + for _pyo3::class::impl_::PyClassImplCollector<#ty> { - fn #methods_trait_methods(self) -> &'static [::pyo3::class::methods::PyMethodDefType] { - static METHODS: &[::pyo3::class::methods::PyMethodDefType] = + fn #methods_trait_methods(self) -> &'static [_pyo3::class::methods::PyMethodDefType] { + static METHODS: &[_pyo3::class::methods::PyMethodDefType] = &[#(#py_methods),*]; METHODS } @@ -139,16 +142,16 @@ fn impl_proto_methods( if build_config.version <= PY39 && proto.name == "Buffer" { maybe_buffer_methods = Some(quote! { - impl ::pyo3::class::impl_::PyBufferProtocolProcs<#ty> - for ::pyo3::class::impl_::PyClassImplCollector<#ty> + impl _pyo3::class::impl_::PyBufferProtocolProcs<#ty> + for _pyo3::class::impl_::PyClassImplCollector<#ty> { fn buffer_procs( self - ) -> ::std::option::Option<&'static ::pyo3::class::impl_::PyBufferProcs> { - static PROCS: ::pyo3::class::impl_::PyBufferProcs - = ::pyo3::class::impl_::PyBufferProcs { - bf_getbuffer: ::std::option::Option::Some(::pyo3::class::buffer::getbuffer::<#ty>), - bf_releasebuffer: ::std::option::Option::Some(::pyo3::class::buffer::releasebuffer::<#ty>), + ) -> ::std::option::Option<&'static _pyo3::class::impl_::PyBufferProcs> { + static PROCS: _pyo3::class::impl_::PyBufferProcs + = _pyo3::class::impl_::PyBufferProcs { + bf_getbuffer: ::std::option::Option::Some(_pyo3::class::buffer::getbuffer::<#ty>), + bf_releasebuffer: ::std::option::Option::Some(_pyo3::class::buffer::releasebuffer::<#ty>), }; ::std::option::Option::Some(&PROCS) } @@ -162,8 +165,8 @@ fn impl_proto_methods( let slot = syn::Ident::new(def.slot, Span::call_site()); let slot_impl = syn::Ident::new(def.slot_impl, Span::call_site()); quote! {{ - ::pyo3::ffi::PyType_Slot { - slot: ::pyo3::ffi::#slot, + _pyo3::ffi::PyType_Slot { + slot: _pyo3::ffi::#slot, pfunc: #module::#slot_impl::<#ty> as _ } }} @@ -177,10 +180,10 @@ fn impl_proto_methods( quote! { #maybe_buffer_methods - impl ::pyo3::class::impl_::#slots_trait<#ty> - for ::pyo3::class::impl_::PyClassImplCollector<#ty> + impl _pyo3::class::impl_::#slots_trait<#ty> + for _pyo3::class::impl_::PyClassImplCollector<#ty> { - fn #slots_trait_slots(self) -> &'static [::pyo3::ffi::PyType_Slot] { + fn #slots_trait_slots(self) -> &'static [_pyo3::ffi::PyType_Slot] { &[#(#tokens),*] } } diff --git a/pyo3-macros-backend/src/utils.rs b/pyo3-macros-backend/src/utils.rs index b0353cdc..9c75239e 100644 --- a/pyo3-macros-backend/src/utils.rs +++ b/pyo3-macros-backend/src/utils.rs @@ -3,7 +3,7 @@ use proc_macro2::{Span, TokenStream}; use quote::ToTokens; use syn::spanned::Spanned; -use crate::attributes::TextSignatureAttribute; +use crate::attributes::{CrateAttribute, TextSignatureAttribute}; /// Macro inspired by `anyhow::anyhow!` to create a compiler error with the given span. macro_rules! err_spanned { @@ -62,8 +62,6 @@ pub fn option_type_argument(ty: &syn::Type) -> Option<&syn::Type> { #[derive(Clone)] pub struct PythonDoc(TokenStream); -// TODO(#1782) use strip_prefix on Rust 1.45 or greater -#[allow(clippy::manual_strip)] /// Collects all #[doc = "..."] attributes into a TokenStream evaluating to a null-terminated string /// e.g. concat!("...", "\n", "\0") pub fn get_doc( @@ -79,11 +77,7 @@ 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.to_string(), - text_signature.lit.value() - ); + let signature_lines = format!("{}{}\n--\n\n", python_name, text_signature.lit.value()); signature_lines.to_tokens(tokens); comma.to_tokens(tokens); } @@ -107,11 +101,11 @@ pub fn get_doc( // Strip single left space from literal strings, if needed. // e.g. `/// Hello world` expands to #[doc = " Hello world"] let doc_line = lit_str.value(); - if doc_line.starts_with(' ') { - syn::LitStr::new(&doc_line[1..], lit_str.span()).to_tokens(tokens) - } else { - lit_str.to_tokens(tokens) - } + doc_line + .strip_prefix(' ') + .map(|stripped| syn::LitStr::new(stripped, lit_str.span())) + .unwrap_or(lit_str) + .to_tokens(tokens); } else { // This is probably a macro doc from Rust 1.54, e.g. #[doc = include_str!(...)] token_stream.to_tokens(tokens) @@ -195,3 +189,10 @@ 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()) + .unwrap_or_else(|| syn::parse_str("::pyo3").unwrap()) +} diff --git a/pyo3-macros/Cargo.toml b/pyo3-macros/Cargo.toml index 1cd663d6..a7e4550c 100644 --- a/pyo3-macros/Cargo.toml +++ b/pyo3-macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3-macros" -version = "0.15.0" +version = "0.15.1" description = "Proc macros for PyO3 package" authors = ["PyO3 Project and Contributors "] keywords = ["pyo3", "python", "cpython", "ffi"] @@ -19,4 +19,4 @@ multiple-pymethods = [] [dependencies] quote = "1" syn = { version = "1", features = ["full", "extra-traits"] } -pyo3-macros-backend = { path = "../pyo3-macros-backend", version = "=0.15.0" } +pyo3-macros-backend = { path = "../pyo3-macros-backend", version = "=0.15.1" } diff --git a/pyo3-macros/src/lib.rs b/pyo3-macros/src/lib.rs index b8ab3f6a..a6f6725c 100644 --- a/pyo3-macros/src/lib.rs +++ b/pyo3-macros/src/lib.rs @@ -7,12 +7,12 @@ extern crate proc_macro; use proc_macro::TokenStream; use pyo3_macros_backend::{ - build_derive_from_pyobject, build_py_class, build_py_function, build_py_methods, + build_derive_from_pyobject, build_py_class, build_py_enum, build_py_function, build_py_methods, build_py_proto, get_doc, process_functions_in_module, py_init, PyClassArgs, PyClassMethodsType, PyFunctionOptions, PyModuleOptions, }; use quote::quote; -use syn::parse_macro_input; +use syn::{parse::Nothing, parse_macro_input}; /// A proc macro used to implement Python modules. /// @@ -31,19 +31,11 @@ use syn::parse_macro_input; /// /// [1]: https://pyo3.rs/latest/module.html #[proc_macro_attribute] -pub fn pymodule(attr: TokenStream, input: TokenStream) -> TokenStream { +pub fn pymodule(args: TokenStream, input: TokenStream) -> TokenStream { + parse_macro_input!(args as Nothing); + let mut ast = parse_macro_input!(input as syn::ItemFn); - - let deprecated_pymodule_name_arg = if attr.is_empty() { - None - } else { - Some(parse_macro_input!(attr as syn::Ident)) - }; - - let options = match PyModuleOptions::from_pymodule_arg_and_attrs( - deprecated_pymodule_name_arg, - &mut ast.attrs, - ) { + let options = match PyModuleOptions::from_attrs(&mut ast.attrs) { Ok(options) => options, Err(e) => return e.to_compile_error().into(), }; @@ -115,12 +107,17 @@ pub fn pyproto(_: TokenStream, input: TokenStream) -> TokenStream { /// [10]: https://en.wikipedia.org/wiki/Free_list #[proc_macro_attribute] pub fn pyclass(attr: TokenStream, input: TokenStream) -> TokenStream { - let methods_type = if cfg!(feature = "multiple-pymethods") { - PyClassMethodsType::Inventory - } else { - PyClassMethodsType::Specialization - }; - pyclass_impl(attr, input, methods_type) + use syn::Item; + let item = parse_macro_input!(input as Item); + match item { + Item::Struct(struct_) => pyclass_impl(attr, struct_, methods_type()), + 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() + } + } } /// A proc macro used to expose methods to Python. @@ -203,12 +200,11 @@ pub fn derive_from_py_object(item: TokenStream) -> TokenStream { } fn pyclass_impl( - attr: TokenStream, - input: TokenStream, + attrs: TokenStream, + mut ast: syn::ItemStruct, methods_type: PyClassMethodsType, ) -> TokenStream { - let mut ast = parse_macro_input!(input as syn::ItemStruct); - let args = parse_macro_input!(attr as PyClassArgs); + let args = parse_macro_input!(attrs with PyClassArgs::parse_stuct_args); let expanded = build_py_class(&mut ast, &args, methods_type).unwrap_or_else(|e| e.to_compile_error()); @@ -219,6 +215,22 @@ fn pyclass_impl( .into() } +fn pyclass_enum_impl( + attrs: TokenStream, + mut ast: syn::ItemEnum, + 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_else(|e| e.into_compile_error()); + + quote!( + #ast + #expanded + ) + .into() +} + fn pymethods_impl(input: TokenStream, methods_type: PyClassMethodsType) -> TokenStream { let mut ast = parse_macro_input!(input as syn::ItemImpl); let expanded = @@ -230,3 +242,11 @@ fn pymethods_impl(input: TokenStream, methods_type: PyClassMethodsType) -> Token ) .into() } + +fn methods_type() -> PyClassMethodsType { + if cfg!(feature = "multiple-pymethods") { + PyClassMethodsType::Inventory + } else { + PyClassMethodsType::Specialization + } +} diff --git a/src/buffer.rs b/src/buffer.rs index 07f6321a..1f4f57fb 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -75,12 +75,7 @@ impl ElementType { pub fn from_format(format: &CStr) -> ElementType { match format.to_bytes() { [char] | [b'@', char] => native_element_type_from_type_char(*char), - [modifier, char] - if (*modifier == b'=' - || *modifier == b'<' - || *modifier == b'>' - || *modifier == b'!') => - { + [modifier, char] if matches!(modifier, b'=' | b'<' | b'>' | b'!') => { standard_element_type_from_type_char(*char) } _ => ElementType::Unknown, @@ -168,6 +163,10 @@ fn is_matching_endian(c: u8) -> bool { } /// Trait implemented for possible element types of `PyBuffer`. +/// +/// # Safety +/// +/// This trait must only be implemented for types which represent valid elements of Python buffers. pub unsafe trait Element: Copy { /// Gets whether the element specified in the format string is potentially compatible. /// Alignment and size are checked separately from this function. diff --git a/src/class/basic.rs b/src/class/basic.rs index 1e34d493..9d4d569b 100644 --- a/src/class/basic.rs +++ b/src/class/basic.rs @@ -81,17 +81,6 @@ pub trait PyObjectProtocol<'p>: PyClass { unimplemented!() } - #[deprecated( - since = "0.14.0", - note = "prefer implementing `__format__` in `#[pymethods]` instead of in a protocol" - )] - fn __format__(&'p self, format_spec: Self::Format) -> Self::Result - where - Self: PyObjectFormatProtocol<'p>, - { - unimplemented!() - } - fn __hash__(&'p self) -> Self::Result where Self: PyObjectHashProtocol<'p>, @@ -99,17 +88,6 @@ pub trait PyObjectProtocol<'p>: PyClass { unimplemented!() } - #[deprecated( - since = "0.14.0", - note = "prefer implementing `__bytes__` in `#[pymethods]` instead of in a protocol" - )] - fn __bytes__(&'p self) -> Self::Result - where - Self: PyObjectBytesProtocol<'p>, - { - unimplemented!() - } - fn __richcmp__(&'p self, other: Self::Other, op: CompareOp) -> Self::Result where Self: PyObjectRichcmpProtocol<'p>, @@ -143,19 +121,12 @@ pub trait PyObjectStrProtocol<'p>: PyObjectProtocol<'p> { pub trait PyObjectReprProtocol<'p>: PyObjectProtocol<'p> { type Result: IntoPyCallbackOutput; } -pub trait PyObjectFormatProtocol<'p>: PyObjectProtocol<'p> { - type Format: FromPyObject<'p>; - type Result: IntoPyCallbackOutput; -} pub trait PyObjectHashProtocol<'p>: PyObjectProtocol<'p> { type Result: IntoPyCallbackOutput; } pub trait PyObjectBoolProtocol<'p>: PyObjectProtocol<'p> { type Result: IntoPyCallbackOutput; } -pub trait PyObjectBytesProtocol<'p>: PyObjectProtocol<'p> { - type Result: IntoPyCallbackOutput; -} pub trait PyObjectRichcmpProtocol<'p>: PyObjectProtocol<'p> { type Other: FromPyObject<'p>; type Result: IntoPyCallbackOutput; diff --git a/src/class/context.rs b/src/class/context.rs deleted file mode 100644 index 4fc8d6c5..00000000 --- a/src/class/context.rs +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright (c) 2017-present PyO3 Project and Contributors - -//! Context manager api -//! Trait and support implementation for context manager api - -use crate::callback::IntoPyCallbackOutput; -use crate::{PyClass, PyObject}; - -/// Context manager interface -#[allow(unused_variables)] -pub trait PyContextProtocol<'p>: PyClass { - #[deprecated( - since = "0.14.0", - note = "prefer implementing `__enter__` in `#[pymethods]` instead of in a protocol" - )] - fn __enter__(&'p mut self) -> Self::Result - where - Self: PyContextEnterProtocol<'p>, - { - unimplemented!() - } - - #[deprecated( - since = "0.14.0", - note = "prefer implementing `__exit__` in `#[pymethods]` instead of in a protocol" - )] - fn __exit__( - &'p mut self, - exc_type: Option, - exc_value: Option, - traceback: Option, - ) -> Self::Result - where - Self: PyContextExitProtocol<'p>, - { - unimplemented!() - } -} - -pub trait PyContextEnterProtocol<'p>: PyContextProtocol<'p> { - type Result: IntoPyCallbackOutput; -} - -pub trait PyContextExitProtocol<'p>: PyContextProtocol<'p> { - type ExcType: crate::FromPyObject<'p>; - type ExcValue: crate::FromPyObject<'p>; - type Traceback: crate::FromPyObject<'p>; - type Result: IntoPyCallbackOutput; -} diff --git a/src/class/descr.rs b/src/class/descr.rs index 9cc775a9..1c693d44 100644 --- a/src/class/descr.rs +++ b/src/class/descr.rs @@ -6,7 +6,6 @@ //! https://docs.python.org/3/reference/datamodel.html#implementing-descriptors) use crate::callback::IntoPyCallbackOutput; -use crate::types::PyAny; use crate::{FromPyObject, PyClass, PyObject}; use std::os::raw::c_int; @@ -30,28 +29,6 @@ pub trait PyDescrProtocol<'p>: PyClass { { unimplemented!() } - - #[deprecated( - since = "0.14.0", - note = "prefer implementing `__delete__` in `#[pymethods]` instead of in a protocol" - )] - fn __delete__(&'p self, instance: &'p PyAny) -> Self::Result - where - Self: PyDescrDeleteProtocol<'p>, - { - unimplemented!() - } - - #[deprecated( - since = "0.14.0", - note = "prefer implementing `__set_name__` in `#[pymethods]` instead of in a protocol" - )] - fn __set_name__(&'p self, instance: &'p PyAny) -> Self::Result - where - Self: PyDescrSetNameProtocol<'p>, - { - unimplemented!() - } } pub trait PyDescrGetProtocol<'p>: PyDescrProtocol<'p> { @@ -68,15 +45,5 @@ pub trait PyDescrSetProtocol<'p>: PyDescrProtocol<'p> { type Result: IntoPyCallbackOutput<()>; } -pub trait PyDescrDeleteProtocol<'p>: PyDescrProtocol<'p> { - type Inst: FromPyObject<'p>; - type Result: IntoPyCallbackOutput<()>; -} - -pub trait PyDescrSetNameProtocol<'p>: PyDescrProtocol<'p> { - type Inst: FromPyObject<'p>; - type Result: IntoPyCallbackOutput<()>; -} - py_ternarys_func!(descr_get, PyDescrGetProtocol, Self::__get__); py_ternarys_func!(descr_set, PyDescrSetProtocol, Self::__set__, c_int); diff --git a/src/class/impl_.rs b/src/class/impl_.rs index e9fb6492..7f1488a6 100644 --- a/src/class/impl_.rs +++ b/src/class/impl_.rs @@ -67,6 +67,9 @@ pub trait PyClassImpl: Sized { /// can be accessed by multiple threads by `threading` module. type ThreadChecker: PyClassThreadChecker; + #[cfg(feature = "multiple-pymethods")] + type Inventory: PyClassInventory; + fn for_each_method_def(_visitor: &mut dyn FnMut(&[PyMethodDefType])) {} fn get_new() -> Option { None @@ -180,6 +183,7 @@ macro_rules! define_pyclass_setattr_slot { } }}; } + pub use $generate_macro; }; } @@ -272,7 +276,7 @@ macro_rules! define_pyclass_binary_operator_slot { _other: *mut $crate::ffi::PyObject, ) -> *mut $crate::ffi::PyObject { $crate::callback::handle_panic(|py| { - use ::pyo3::class::impl_::*; + use $crate::class::impl_::*; let collector = PyClassImplCollector::<$cls>::new(); let lhs_result = collector.$lhs(py, _slf, _other)?; if lhs_result == $crate::ffi::Py_NotImplemented() { @@ -289,6 +293,7 @@ macro_rules! define_pyclass_binary_operator_slot { } }}; } + pub use $generate_macro; }; } @@ -464,7 +469,7 @@ macro_rules! generate_pyclass_pow_slot { _mod: *mut $crate::ffi::PyObject, ) -> *mut $crate::ffi::PyObject { $crate::callback::handle_panic(|py| { - use ::pyo3::class::impl_::*; + use $crate::class::impl_::*; let collector = PyClassImplCollector::<$cls>::new(); let lhs_result = collector.__pow__(py, _slf, _other, _mod)?; if lhs_result == $crate::ffi::Py_NotImplemented() { @@ -481,6 +486,7 @@ macro_rules! generate_pyclass_pow_slot { } }}; } +pub use generate_pyclass_pow_slot; pub trait PyClassAllocImpl { fn alloc_impl(self) -> Option; @@ -542,7 +548,6 @@ pub unsafe extern "C" fn alloc_with_freelist( /// # Safety /// - `obj` must be a valid pointer to an instance of T (not a subclass). /// - The GIL must be held. -#[allow(clippy::collapsible_if)] // for if cfg! pub unsafe extern "C" fn free_with_freelist(obj: *mut c_void) { let obj = obj as *mut ffi::PyObject; debug_assert_eq!( @@ -560,10 +565,9 @@ pub unsafe extern "C" fn free_with_freelist(obj: *mut c_ }; free(obj as *mut c_void); - if cfg!(Py_3_8) { - if ffi::PyType_HasFeature(ty, ffi::Py_TPFLAGS_HEAPTYPE) != 0 { - ffi::Py_DECREF(ty as *mut ffi::PyObject); - } + #[cfg(Py_3_8)] + if ffi::PyType_HasFeature(ty, ffi::Py_TPFLAGS_HEAPTYPE) != 0 { + ffi::Py_DECREF(ty as *mut ffi::PyObject); } } } @@ -609,11 +613,8 @@ macro_rules! methods_trait { /// Method storage for `#[pyclass]`. /// Allows arbitrary `#[pymethod]` blocks to submit their methods, /// which are eventually collected by `#[pyclass]`. -#[cfg(all(feature = "macros", feature = "multiple-pymethods"))] -pub trait PyMethodsInventory: inventory::Collect { - /// Create a new instance - fn new(methods: Vec, slots: Vec) -> Self; - +#[cfg(feature = "multiple-pymethods")] +pub trait PyClassInventory: inventory::Collect { /// Returns the methods for a single `#[pymethods] impl` block fn methods(&'static self) -> &'static [PyMethodDefType]; @@ -621,13 +622,6 @@ pub trait PyMethodsInventory: inventory::Collect { fn slots(&'static self) -> &'static [ffi::PyType_Slot]; } -/// Implemented for `#[pyclass]` in our proc macro code. -/// Indicates that the pyclass has its own method storage. -#[cfg(all(feature = "macros", feature = "multiple-pymethods"))] -pub trait HasMethodsInventory { - type Methods: PyMethodsInventory; -} - // Methods from #[pyo3(get, set)] on struct fields. methods_trait!(PyClassDescriptors, py_class_descriptors); @@ -666,13 +660,15 @@ slots_trait!(PyAsyncProtocolSlots, async_protocol_slots); slots_trait!(PySequenceProtocolSlots, sequence_protocol_slots); slots_trait!(PyBufferProtocolSlots, buffer_protocol_slots); +// slots that PyO3 implements by default, but can be overidden by the users. +slots_trait!(PyClassDefaultSlots, py_class_default_slots); + // Protocol slots from #[pymethods] if not using inventory. #[cfg(not(feature = "multiple-pymethods"))] slots_trait!(PyMethodsProtocolSlots, methods_protocol_slots); methods_trait!(PyObjectProtocolMethods, object_protocol_methods); methods_trait!(PyAsyncProtocolMethods, async_protocol_methods); -methods_trait!(PyContextProtocolMethods, context_protocol_methods); methods_trait!(PyDescrProtocolMethods, descr_protocol_methods); methods_trait!(PyMappingProtocolMethods, mapping_protocol_methods); methods_trait!(PyNumberProtocolMethods, number_protocol_methods); diff --git a/src/class/mapping.rs b/src/class/mapping.rs index 9961c5d1..b6868521 100644 --- a/src/class/mapping.rs +++ b/src/class/mapping.rs @@ -36,17 +36,6 @@ pub trait PyMappingProtocol<'p>: PyClass { { unimplemented!() } - - #[deprecated( - since = "0.14.0", - note = "prefer implementing `__reversed__` in `#[pymethods]` instead of in a protocol" - )] - fn __reversed__(&'p self) -> Self::Result - where - Self: PyMappingReversedProtocol<'p>, - { - unimplemented!() - } } // The following are a bunch of marker traits used to detect @@ -72,10 +61,6 @@ pub trait PyMappingDelItemProtocol<'p>: PyMappingProtocol<'p> { type Result: IntoPyCallbackOutput<()>; } -pub trait PyMappingReversedProtocol<'p>: PyMappingProtocol<'p> { - type Result: IntoPyCallbackOutput; -} - py_len_func!(len, PyMappingLenProtocol, Self::__len__); py_binary_func!(getitem, PyMappingGetItemProtocol, Self::__getitem__); py_func_set!(setitem, PyMappingSetItemProtocol, Self::__setitem__); diff --git a/src/class/methods.rs b/src/class/methods.rs index 2441cd79..5f537e3a 100644 --- a/src/class/methods.rs +++ b/src/class/methods.rs @@ -28,7 +28,7 @@ pub enum PyMethodDefType { pub enum PyMethodType { PyCFunction(PyCFunction), PyCFunctionWithKeywords(PyCFunctionWithKeywords), - #[cfg(all(Py_3_7, not(Py_LIMITED_API)))] + #[cfg(not(Py_LIMITED_API))] PyCFunctionFastWithKeywords(PyCFunctionFastWithKeywords), } @@ -38,7 +38,7 @@ pub enum PyMethodType { pub struct PyCFunction(pub ffi::PyCFunction); #[derive(Clone, Copy, Debug)] pub struct PyCFunctionWithKeywords(pub ffi::PyCFunctionWithKeywords); -#[cfg(all(Py_3_7, not(Py_LIMITED_API)))] +#[cfg(not(Py_LIMITED_API))] #[derive(Clone, Copy, Debug)] pub struct PyCFunctionFastWithKeywords(pub ffi::_PyCFunctionFastWithKeywords); #[derive(Clone, Copy, Debug)] @@ -111,7 +111,7 @@ impl PyMethodDef { } /// Define a function that can take `*args` and `**kwargs`. - #[cfg(all(Py_3_7, not(Py_LIMITED_API)))] + #[cfg(not(Py_LIMITED_API))] pub const fn fastcall_cfunction_with_keywords( name: &'static str, cfunction: PyCFunctionFastWithKeywords, @@ -135,7 +135,7 @@ impl PyMethodDef { let meth = match self.ml_meth { PyMethodType::PyCFunction(meth) => meth.0, PyMethodType::PyCFunctionWithKeywords(meth) => unsafe { std::mem::transmute(meth.0) }, - #[cfg(all(Py_3_7, not(Py_LIMITED_API)))] + #[cfg(not(Py_LIMITED_API))] PyMethodType::PyCFunctionFastWithKeywords(meth) => unsafe { std::mem::transmute(meth.0) }, diff --git a/src/class/mod.rs b/src/class/mod.rs index 4b7543aa..0e889573 100644 --- a/src/class/mod.rs +++ b/src/class/mod.rs @@ -8,7 +8,6 @@ mod macros; pub mod basic; #[cfg(not(Py_LIMITED_API))] pub mod buffer; -pub mod context; pub mod descr; pub mod gc; #[doc(hidden)] @@ -24,7 +23,6 @@ pub mod sequence; pub use self::basic::PyObjectProtocol; #[cfg(not(Py_LIMITED_API))] pub use self::buffer::PyBufferProtocol; -pub use self::context::PyContextProtocol; pub use self::descr::PyDescrProtocol; pub use self::gc::{PyGCProtocol, PyTraverseError, PyVisit}; pub use self::iter::PyIterProtocol; diff --git a/src/class/number.rs b/src/class/number.rs index 903744eb..2b0c2ade 100644 --- a/src/class/number.rs +++ b/src/class/number.rs @@ -283,16 +283,6 @@ pub trait PyNumberProtocol<'p>: PyClass { { unimplemented!() } - #[deprecated( - since = "0.14.0", - note = "prefer implementing `__complex__` in `#[pymethods]` instead of in a protocol" - )] - fn __complex__(&'p self) -> Self::Result - where - Self: PyNumberComplexProtocol<'p>, - { - unimplemented!() - } fn __int__(&'p self) -> Self::Result where Self: PyNumberIntProtocol<'p>, @@ -311,16 +301,6 @@ pub trait PyNumberProtocol<'p>: PyClass { { unimplemented!() } - #[deprecated( - since = "0.14.0", - note = "prefer implementing `__round__` in `#[pymethods]` instead of in a protocol" - )] - fn __round__(&'p self, ndigits: Option) -> Self::Result - where - Self: PyNumberRoundProtocol<'p>, - { - unimplemented!() - } } pub trait PyNumberAddProtocol<'p>: PyNumberProtocol<'p> { @@ -569,10 +549,6 @@ pub trait PyNumberInvertProtocol<'p>: PyNumberProtocol<'p> { type Result: IntoPyCallbackOutput; } -pub trait PyNumberComplexProtocol<'p>: PyNumberProtocol<'p> { - type Result: IntoPyCallbackOutput; -} - pub trait PyNumberIntProtocol<'p>: PyNumberProtocol<'p> { type Result: IntoPyCallbackOutput; } @@ -581,11 +557,6 @@ pub trait PyNumberFloatProtocol<'p>: PyNumberProtocol<'p> { type Result: IntoPyCallbackOutput; } -pub trait PyNumberRoundProtocol<'p>: PyNumberProtocol<'p> { - type NDigits: FromPyObject<'p>; - type Result: IntoPyCallbackOutput; -} - pub trait PyNumberIndexProtocol<'p>: PyNumberProtocol<'p> { type Result: IntoPyCallbackOutput; } diff --git a/src/class/pyasync.rs b/src/class/pyasync.rs index ebfde9fa..f79cd7ac 100644 --- a/src/class/pyasync.rs +++ b/src/class/pyasync.rs @@ -37,33 +37,6 @@ pub trait PyAsyncProtocol<'p>: PyClass { { unimplemented!() } - - #[deprecated( - since = "0.14.0", - note = "prefer implementing `__aenter__` in `#[pymethods]` instead of in a protocol" - )] - fn __aenter__(&'p mut self) -> Self::Result - where - Self: PyAsyncAenterProtocol<'p>, - { - unimplemented!() - } - - #[deprecated( - since = "0.14.0", - note = "prefer implementing `__aexit__` in `#[pymethods]` instead of in a protocol" - )] - fn __aexit__( - &'p mut self, - exc_type: Option, - exc_value: Option, - traceback: Option, - ) -> Self::Result - where - Self: PyAsyncAexitProtocol<'p>, - { - unimplemented!() - } } pub trait PyAsyncAwaitProtocol<'p>: PyAsyncProtocol<'p> { @@ -81,17 +54,6 @@ pub trait PyAsyncAnextProtocol<'p>: PyAsyncProtocol<'p> { type Result: IntoPyCallbackOutput; } -pub trait PyAsyncAenterProtocol<'p>: PyAsyncProtocol<'p> { - type Result: IntoPyCallbackOutput; -} - -pub trait PyAsyncAexitProtocol<'p>: PyAsyncProtocol<'p> { - type ExcType: crate::FromPyObject<'p>; - type ExcValue: crate::FromPyObject<'p>; - type Traceback: crate::FromPyObject<'p>; - type Result: IntoPyCallbackOutput; -} - py_unarys_func!(await_, PyAsyncAwaitProtocol, Self::__await__); py_unarys_func!(aiter, PyAsyncAiterProtocol, Self::__aiter__); py_unarys_func!(anext, PyAsyncAnextProtocol, Self::__anext__); diff --git a/src/conversion.rs b/src/conversion.rs index e1238edc..ccd690be 100644 --- a/src/conversion.rs +++ b/src/conversion.rs @@ -463,6 +463,10 @@ impl IntoPy> for () { } /// 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`. /// diff --git a/src/conversions/anyhow.rs b/src/conversions/anyhow.rs index 7dd733d7..e9d1e339 100644 --- a/src/conversions/anyhow.rs +++ b/src/conversions/anyhow.rs @@ -117,8 +117,8 @@ impl From for PyErr { #[cfg(test)] mod test_anyhow { - use pyo3::prelude::*; - use pyo3::types::IntoPyDict; + use crate::prelude::*; + use crate::types::IntoPyDict; use anyhow::{anyhow, bail, Context, Result}; @@ -144,7 +144,7 @@ mod test_anyhow { Python::with_gil(|py| { let locals = [("err", pyerr)].into_py_dict(py); let pyerr = py.run("raise err", None, Some(locals)).unwrap_err(); - assert_eq!(pyerr.pvalue(py).to_string(), expected_contents); + assert_eq!(pyerr.value(py).to_string(), expected_contents); }) } @@ -161,7 +161,7 @@ mod test_anyhow { Python::with_gil(|py| { let locals = [("err", pyerr)].into_py_dict(py); let pyerr = py.run("raise err", None, Some(locals)).unwrap_err(); - assert_eq!(pyerr.pvalue(py).to_string(), expected_contents); + assert_eq!(pyerr.value(py).to_string(), expected_contents); }) } } diff --git a/src/conversions/eyre.rs b/src/conversions/eyre.rs index 172f18c5..4d4014ff 100644 --- a/src/conversions/eyre.rs +++ b/src/conversions/eyre.rs @@ -118,8 +118,8 @@ impl From for PyErr { #[cfg(test)] mod tests { - use pyo3::prelude::*; - use pyo3::types::IntoPyDict; + use crate::prelude::*; + use crate::types::IntoPyDict; use eyre::{bail, Result, WrapErr}; @@ -145,7 +145,7 @@ mod tests { Python::with_gil(|py| { let locals = [("err", pyerr)].into_py_dict(py); let pyerr = py.run("raise err", None, Some(locals)).unwrap_err(); - assert_eq!(pyerr.pvalue(py).to_string(), expected_contents); + assert_eq!(pyerr.value(py).to_string(), expected_contents); }) } } diff --git a/src/conversions/path.rs b/src/conversions/path.rs index 91a41666..bd0eca69 100644 --- a/src/conversions/path.rs +++ b/src/conversions/path.rs @@ -20,7 +20,7 @@ impl FromPyObject<'_> for PathBuf { let py = ob.py(); let pathlib = py.import("pathlib")?; let pathlib_path: &PyType = pathlib.getattr("Path")?.downcast()?; - if pathlib_path.is_instance(ob)? { + if ob.is_instance(pathlib_path)? { let path_str = ob.call_method0("__str__")?; OsString::extract(path_str)? } else { diff --git a/src/derive_utils.rs b/src/derive_utils.rs index 6d79b2c4..0fc18ae2 100644 --- a/src/derive_utils.rs +++ b/src/derive_utils.rs @@ -273,9 +273,9 @@ impl FunctionDescription { /// Add the argument name to the error message of an error which occurred during argument extraction pub fn argument_extraction_error(py: Python, arg_name: &str, error: PyErr) -> PyErr { - if error.ptype(py) == py.get_type::() { + if error.is_instance_of::(py) { let reason = error - .instance(py) + .value(py) .str() .unwrap_or_else(|_| PyString::new(py, "")); PyTypeError::new_err(format!("argument '{}': {}", arg_name, reason)) diff --git a/src/err/err_state.rs b/src/err/err_state.rs index b1390dc1..97a55ec5 100644 --- a/src/err/err_state.rs +++ b/src/err/err_state.rs @@ -2,7 +2,7 @@ use crate::{ exceptions::{PyBaseException, PyTypeError}, ffi, type_object::PyTypeObject, - types::PyType, + types::{PyTraceback, PyType}, AsPyPointer, IntoPy, IntoPyPointer, Py, PyObject, Python, }; @@ -10,7 +10,7 @@ use crate::{ pub(crate) struct PyErrStateNormalized { pub ptype: Py, pub pvalue: Py, - pub ptraceback: Option, + pub ptraceback: Option>, } pub(crate) enum PyErrState { diff --git a/src/err/mod.rs b/src/err/mod.rs index cc446119..5e08bb0e 100644 --- a/src/err/mod.rs +++ b/src/err/mod.rs @@ -2,12 +2,14 @@ use crate::panic::PanicException; use crate::type_object::PyTypeObject; -use crate::types::PyType; +use crate::types::{PyTraceback, PyType}; use crate::{ exceptions::{self, PyBaseException}, ffi, }; -use crate::{AsPyPointer, IntoPy, Py, PyAny, PyObject, Python, ToBorrowedObject, ToPyObject}; +use crate::{ + AsPyPointer, IntoPy, IntoPyPointer, Py, PyAny, PyObject, Python, ToBorrowedObject, ToPyObject, +}; use std::borrow::Cow; use std::cell::UnsafeCell; use std::ffi::CString; @@ -19,7 +21,14 @@ mod impls; pub use err_state::PyErrArguments; use err_state::{boxed_args, PyErrState, PyErrStateNormalized}; -/// Represents a Python exception that was raised. +/// Represents a Python exception. +/// +/// Python exceptions can be raised in a "lazy" fashion, where the full Python object for the +/// exception is not created until needed. The process of creating the full object is known +/// as "normalization". An exception which has not yet been created is known as "unnormalized". +/// +/// This struct builds upon that design, supporting all lazily-created Python exceptions and also +/// supporting exceptions lazily-created from Rust. pub struct PyErr { // Safety: can only hand out references when in the "normalized" state. Will never change // after normalization. @@ -54,15 +63,21 @@ impl<'a> PyDowncastError<'a> { impl PyErr { /// Creates a new PyErr of type `T`. /// - /// `value` can be: - /// * a tuple: the exception instance will be created using Python `T(*tuple)` - /// * any other value: the exception instance will be created using Python `T(value)` + /// `args` can be: + /// * a tuple: the exception instance will be created using the equivalent to the Python + /// expression `T(*tuple)` + /// * any other value: the exception instance will be created using the equivalent to the Python + /// expression `T(value)` /// - /// Note: if `value` is not `Send` or `Sync`, consider using `PyErr::from_instance` instead. + /// This error will be stored in an unnormalized state. This avoids the need for the Python GIL + /// to be held, but requires `args` to be `Send` and `Sync`. If `args` is not `Send` or `Sync`, + /// consider using [`PyErr::from_value`] instead. /// - /// Panics if `T` is not a Python class derived from `BaseException`. + /// If an error occurs during normalization (for example if `T` is not a Python type which + /// extends from `BaseException`), then a different error may be produced during normalization. + /// + /// # Example /// - /// Example: /// ```ignore /// return Err(PyErr::new::("Error message")); /// ``` @@ -71,6 +86,7 @@ impl PyErr { /// ```ignore /// return Err(exceptions::PyTypeError::new_err("Error message")); /// ``` + #[inline] pub fn new(args: A) -> PyErr where T: PyTypeObject, @@ -82,11 +98,17 @@ impl PyErr { }) } - /// Constructs a new error, with the usual lazy initialization of Python exceptions. + /// Constructs a new PyErr from the given Python type and arguments. /// - /// `exc` is the exception type; usually one of the standard exceptions + /// `ty` is the exception type; usually one of the standard exceptions /// like `exceptions::PyRuntimeError`. - /// `args` is the a tuple of arguments to pass to the exception constructor. + /// + /// `args` is either a tuple or a single value, with the same meaning as in [`PyErr::new`]. + /// + /// If an error occurs during normalization (for example if `T` is not a Python type which + /// extends from `BaseException`), then a different error may be produced during normalization. + /// + /// This error will be stored in an unnormalized state. pub fn from_type(ty: &PyType, args: A) -> PyErr where A: PyErrArguments + Send + Sync + 'static, @@ -103,32 +125,34 @@ impl PyErr { /// Creates a new PyErr. /// - /// `obj` must be an Python exception instance, the PyErr will use that instance. - /// If `obj` is a Python exception type object, the PyErr will (lazily) create a new - /// instance of that type. - /// Otherwise, a `TypeError` is created instead. + /// If `obj` is a Python exception object, the PyErr will contain that object. The error will be + /// in a normalized state. + /// + /// If `obj` is a Python exception type object, this is equivalent to `PyErr::from_type(obj, ())`. + /// + /// Otherwise, a `TypeError` is created. /// /// # Examples /// ```rust /// use pyo3::{exceptions::PyTypeError, types::PyType, IntoPy, PyErr, Python}; /// Python::with_gil(|py| { - /// // Case #1: Exception instance - /// let err = PyErr::from_instance(PyTypeError::new_err("some type error").instance(py)); + /// // Case #1: Exception object + /// let err = PyErr::from_value(PyTypeError::new_err("some type error").value(py)); /// assert_eq!(err.to_string(), "TypeError: some type error"); /// /// // Case #2: Exception type - /// let err = PyErr::from_instance(PyType::new::(py)); + /// let err = PyErr::from_value(PyType::new::(py)); /// assert_eq!(err.to_string(), "TypeError: "); /// /// // Case #3: Invalid exception value - /// let err = PyErr::from_instance("foo".into_py(py).as_ref(py)); + /// let err = PyErr::from_value("foo".into_py(py).as_ref(py)); /// assert_eq!( /// err.to_string(), /// "TypeError: exceptions must derive from BaseException" /// ); /// }); /// ``` - pub fn from_instance(obj: &PyAny) -> PyErr { + pub fn from_value(obj: &PyAny) -> PyErr { let ptr = obj.as_ptr(); let state = if unsafe { ffi::PyExceptionInstance_Check(ptr) } != 0 { @@ -150,7 +174,7 @@ impl PyErr { PyErr::from_state(state) } - /// Get the type of this exception object. + /// Returns the type of this exception. /// /// The object will be normalized first if needed. /// @@ -160,14 +184,14 @@ impl PyErr { /// /// Python::with_gil(|py| { /// let err: PyErr = PyTypeError::new_err(("some type error",)); - /// assert_eq!(err.ptype(py), PyType::new::(py)); + /// assert_eq!(err.get_type(py), PyType::new::(py)); /// }); /// ``` - pub fn ptype<'py>(&'py self, py: Python<'py>) -> &'py PyType { + pub fn get_type<'py>(&'py self, py: Python<'py>) -> &'py PyType { self.normalized(py).ptype.as_ref(py) } - /// Get the value of this exception object. + /// Returns the value of this exception. /// /// The object will be normalized first if needed. /// @@ -178,15 +202,23 @@ impl PyErr { /// /// Python::with_gil(|py| { /// let err: PyErr = PyTypeError::new_err(("some type error",)); - /// assert!(err.is_instance::(py)); - /// assert_eq!(err.pvalue(py).to_string(), "some type error"); + /// assert!(err.is_instance_of::(py)); + /// assert_eq!(err.value(py).to_string(), "some type error"); /// }); /// ``` - pub fn pvalue<'py>(&'py self, py: Python<'py>) -> &'py PyBaseException { + pub fn value<'py>(&'py self, py: Python<'py>) -> &'py PyBaseException { self.normalized(py).pvalue.as_ref(py) } - /// Get the value of this exception object. + /// Consumes self to take ownership of the exception value contained in this error. + 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. + self.normalized(py).pvalue.clone_ref(py) + } + + /// Returns the traceback of this exception object. /// /// The object will be normalized first if needed. /// @@ -196,10 +228,10 @@ impl PyErr { /// /// Python::with_gil(|py| { /// let err = PyTypeError::new_err(("some type error",)); - /// assert_eq!(err.ptraceback(py), None); + /// assert_eq!(err.traceback(py), None); /// }); /// ``` - pub fn ptraceback<'py>(&'py self, py: Python<'py>) -> Option<&'py PyAny> { + pub fn traceback<'py>(&'py self, py: Python<'py>) -> Option<&'py PyTraceback> { self.normalized(py) .ptraceback .as_ref() @@ -219,8 +251,8 @@ impl PyErr { /// callback) then this function will resume the panic. /// /// 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]. + /// 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 { let (ptype, pvalue, ptraceback) = unsafe { let mut ptype: *mut ffi::PyObject = std::ptr::null_mut(); @@ -265,7 +297,6 @@ impl PyErr { eprintln!("Python stack trace below:"); unsafe { - use crate::conversion::IntoPyPointer; ffi::PyErr_Restore(ptype.into_ptr(), pvalue.into_ptr(), ptraceback.into_ptr()); ffi::PyErr_PrintEx(0); } @@ -380,30 +411,23 @@ impl PyErr { T: ToBorrowedObject, { exc.with_borrowed_ptr(py, |exc| unsafe { - ffi::PyErr_GivenExceptionMatches(self.ptype_ptr(py), exc) != 0 + ffi::PyErr_GivenExceptionMatches(self.type_ptr(py), exc) != 0 }) } /// Returns true if the current exception is instance of `T`. - pub fn is_instance(&self, py: Python) -> bool + #[inline] + 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 where T: PyTypeObject, { - unsafe { - ffi::PyErr_GivenExceptionMatches(self.ptype_ptr(py), T::type_object(py).as_ptr()) != 0 - } - } - - /// Retrieves the exception instance for this error. - pub fn instance<'py>(&'py self, py: Python<'py>) -> &'py PyBaseException { - self.normalized(py).pvalue.as_ref(py) - } - - /// Consumes self to take ownership of the exception instance for this error. - pub fn into_instance(self, py: Python) -> Py { - let out = self.normalized(py).pvalue.as_ref(py).into(); - std::mem::forget(self); - out + self.is_instance(py, T::type_object(py)) } /// Writes the error back to the Python interpreter's global state. @@ -442,11 +466,12 @@ 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.ptype(py), err_clone.ptype(py)); - /// assert_eq!(err.pvalue(py), err_clone.pvalue(py)); - /// assert_eq!(err.ptraceback(py), err_clone.ptraceback(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)); /// }); /// ``` + #[inline] pub fn clone_ref(&self, py: Python) -> PyErr { PyErr::from_state(PyErrState::Normalized(self.normalized(py).clone())) } @@ -454,25 +479,23 @@ impl PyErr { /// Return the cause (either an exception instance, or None, set by `raise ... from ...`) /// associated with the exception, as accessible from Python through `__cause__`. pub fn cause(&self, py: Python) -> Option { - let ptr = unsafe { ffi::PyException_GetCause(self.pvalue(py).as_ptr()) }; + let ptr = unsafe { ffi::PyException_GetCause(self.value(py).as_ptr()) }; let obj = unsafe { py.from_owned_ptr_or_opt::(ptr) }; - obj.map(|x| Self::from_instance(x)) + 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) { - if let Some(cause) = cause { - let cause = cause.into_instance(py); - unsafe { - ffi::PyException_SetCause(self.pvalue(py).as_ptr(), cause.as_ptr()); - } - } else { - unsafe { - ffi::PyException_SetCause(self.pvalue(py).as_ptr(), std::ptr::null_mut()); - } + unsafe { + // PyException_SetCause _steals_ a reference to cause, so must use .into_ptr() + ffi::PyException_SetCause( + self.value(py).as_ptr(), + cause.map_or(std::ptr::null_mut(), |err| err.into_value(py).into_ptr()), + ); } } + #[inline] fn from_state(state: PyErrState) -> PyErr { PyErr { state: UnsafeCell::new(Some(state)), @@ -480,7 +503,7 @@ impl PyErr { } /// Returns borrowed reference to this Err's type - fn ptype_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. @@ -492,17 +515,26 @@ impl PyErr { } } + #[inline] 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() + } { + return n; + } + + self.make_normalized(py) + } + + #[cold] + 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. // - State is set to None during the normalization process, so that a second // concurrent normalization attempt will panic before changing anything. - if let Some(PyErrState::Normalized(n)) = unsafe { &*self.state.get() } { - return n; - } - let state = unsafe { (*self.state.get()) .take() @@ -516,7 +548,7 @@ impl PyErr { *self_state = Some(PyErrState::Normalized(PyErrStateNormalized { ptype: Py::from_owned_ptr_or_opt(py, ptype).expect("Exception type missing"), pvalue: Py::from_owned_ptr_or_opt(py, pvalue).expect("Exception value missing"), - ptraceback: PyObject::from_owned_ptr_or_opt(py, ptraceback), + ptraceback: Py::from_owned_ptr_or_opt(py, ptraceback), })); match self_state { @@ -525,15 +557,66 @@ impl PyErr { } } } + + /// Deprecated name for [`PyErr::get_type`]. + #[deprecated( + since = "0.16.0", + note = "Use err.get_type(py) instead of err.ptype(py)" + )] + pub fn ptype<'py>(&'py self, py: Python<'py>) -> &'py PyType { + self.get_type(py) + } + + /// Deprecated name for [`PyErr::value`]. + #[deprecated(since = "0.16.0", note = "Use err.value(py) instead of err.pvalue(py)")] + pub fn pvalue<'py>(&'py self, py: Python<'py>) -> &'py PyBaseException { + self.value(py) + } + + /// Deprecated name for [`PyErr::traceback`]. + #[deprecated( + since = "0.16.0", + note = "Use err.traceback(py) instead of err.ptraceback(py)" + )] + pub fn ptraceback<'py>(&'py self, py: Python<'py>) -> Option<&'py PyTraceback> { + self.traceback(py) + } + + /// Deprecated name for [`PyErr::value`]. + #[deprecated( + since = "0.16.0", + note = "Use err.value(py) instead of err.instance(py)" + )] + pub fn instance<'py>(&'py self, py: Python<'py>) -> &'py PyBaseException { + self.value(py) + } + + /// Deprecated name for [`PyErr::from_value`]. + #[deprecated( + since = "0.16.0", + note = "Use err.from_value(py, obj) instead of err.from_instance(py, obj)" + )] + pub fn from_instance(value: &PyAny) -> PyErr { + PyErr::from_value(value) + } + + /// Deprecated name for [`PyErr::into_value`]. + #[deprecated( + since = "0.16.0", + note = "Use err.into_value(py) instead of err.into_instance(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> { Python::with_gil(|py| { f.debug_struct("PyErr") - .field("type", self.ptype(py)) - .field("value", self.pvalue(py)) - .field("traceback", &self.ptraceback(py)) + .field("type", self.get_type(py)) + .field("value", self.value(py)) + .field("traceback", &self.traceback(py)) .finish() }) } @@ -542,10 +625,10 @@ impl std::fmt::Debug for PyErr { impl std::fmt::Display for PyErr { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { Python::with_gil(|py| { - let instance = self.instance(py); - let type_name = instance.get_type().name().map_err(|_| std::fmt::Error)?; + let value = self.value(py); + let type_name = value.get_type().name().map_err(|_| std::fmt::Error)?; write!(f, "{}", type_name)?; - if let Ok(s) = instance.str() { + if let Ok(s) = value.str() { write!(f, ": {}", &s.to_string_lossy()) } else { write!(f, ": ") @@ -558,7 +641,7 @@ impl std::error::Error for PyErr {} impl IntoPy for PyErr { fn into_py(self, py: Python) -> PyObject { - self.into_instance(py).into() + self.into_value(py).into() } } @@ -631,11 +714,11 @@ mod tests { fn set_valueerror() { Python::with_gil(|py| { let err: PyErr = exceptions::PyValueError::new_err("some exception message"); - assert!(err.is_instance::(py)); + assert!(err.is_instance_of::(py)); err.restore(py); assert!(PyErr::occurred(py)); let err = PyErr::fetch(py); - assert!(err.is_instance::(py)); + assert!(err.is_instance_of::(py)); assert_eq!(err.to_string(), "ValueError: some exception message"); }) } @@ -644,10 +727,10 @@ mod tests { fn invalid_error_type() { Python::with_gil(|py| { let err: PyErr = PyErr::new::(()); - assert!(err.is_instance::(py)); + assert!(err.is_instance_of::(py)); err.restore(py); let err = PyErr::fetch(py); - assert!(err.is_instance::(py)); + assert!(err.is_instance_of::(py)); assert_eq!( err.to_string(), "TypeError: exceptions must derive from BaseException" @@ -702,12 +785,7 @@ mod tests { let mut fields = debug_str["PyErr { ".len()..debug_str.len() - 2].split(", "); assert_eq!(fields.next().unwrap(), "type: "); - if py.version_info() >= (3, 7) { - assert_eq!(fields.next().unwrap(), "value: Exception('banana')"); - } else { - // Python 3.6 and below formats the repr differently - assert_eq!(fields.next().unwrap(), ("value: Exception('banana',)")); - } + assert_eq!(fields.next().unwrap(), "value: Exception('banana')"); let traceback = fields.next().unwrap(); assert!(traceback.starts_with("traceback: Some( for $crate::PyErr { #[inline] fn from(err: &$name) -> $crate::PyErr { - $crate::PyErr::from_instance(err) + $crate::PyErr::from_value(err) } } @@ -312,7 +312,7 @@ fn always_throws() -> PyResult<()> { # Python::with_gil(|py| { # let fun = pyo3::wrap_pyfunction!(always_throws, py).unwrap(); # let err = fun.call0().expect_err(\"called a function that should always return an error but the return value was Ok\"); -# assert!(err.is_instance::(py)) +# assert!(err.is_instance_of::(py)) # }); ``` @@ -337,7 +337,7 @@ Python::with_gil(|py| { let error_type = match result { Ok(_) => \"Not an error\", - Err(error) if error.is_instance::(py) => \"" , $name, "\", + Err(error) if error.is_instance_of::(py) => \"" , $name, "\", Err(_) => \"Some other error\", }; @@ -656,15 +656,15 @@ macro_rules! test_exception { .unwrap_or($exc_ty::new_err("a test exception")) }; - assert!(err.is_instance::<$exc_ty>(py)); + assert!(err.is_instance_of::<$exc_ty>(py)); - let value: &$exc_ty = err.instance(py).downcast().unwrap(); + let value: &$exc_ty = err.value(py).downcast().unwrap(); assert!(value.source().is_none()); err.set_cause(py, Some($crate::exceptions::PyValueError::new_err("a cause"))); assert!(value.source().is_some()); - assert!($crate::PyErr::from(value).is_instance::<$exc_ty>(py)); + assert!($crate::PyErr::from(value).is_instance_of::<$exc_ty>(py)); }) } }; @@ -859,7 +859,7 @@ mod tests { let exc = py .run("raise Exception('banana')", None, None) .expect_err("raising should have given us an error") - .into_instance(py) + .into_value(py) .into_ref(py); assert_eq!( format!("{:?}", exc), @@ -874,7 +874,7 @@ mod tests { let exc = py .run("raise Exception('banana')", None, None) .expect_err("raising should have given us an error") - .into_instance(py) + .into_value(py) .into_ref(py); assert_eq!( exc.to_string(), @@ -895,22 +895,14 @@ mod tests { None, ) .expect_err("raising should have given us an error") - .into_instance(py) + .into_value(py) .into_ref(py); - if py.version_info() >= (3, 7) { - assert_eq!(format!("{:?}", exc), "Exception('banana')"); - } else { - assert_eq!(format!("{:?}", exc), "Exception('banana',)"); - } + assert_eq!(format!("{:?}", exc), "Exception('banana')"); let source = exc.source().expect("cause should exist"); - if py.version_info() >= (3, 7) { - assert_eq!(format!("{:?}", source), "TypeError('peach')"); - } else { - assert_eq!(format!("{:?}", source), "TypeError('peach',)"); - } + assert_eq!(format!("{:?}", source), "TypeError('peach')"); let source_source = source.source(); assert!(source_source.is_none(), "source_source should be None"); @@ -973,7 +965,7 @@ mod tests { test_exception!(PyUnicodeDecodeError, |py| { let invalid_utf8 = b"fo\xd8o"; let err = std::str::from_utf8(invalid_utf8).expect_err("should be invalid utf8"); - PyErr::from_instance(PyUnicodeDecodeError::new_utf8(py, invalid_utf8, err).unwrap()) + PyErr::from_value(PyUnicodeDecodeError::new_utf8(py, invalid_utf8, err).unwrap()) }); test_exception!(PyUnicodeEncodeError, |py: Python<'_>| { py.eval("chr(40960).encode('ascii')", None, None) diff --git a/src/ffi/abstract_.rs b/src/ffi/abstract_.rs index 3fa035b3..07e02da4 100644 --- a/src/ffi/abstract_.rs +++ b/src/ffi/abstract_.rs @@ -91,7 +91,7 @@ extern "C" { pub fn PyObject_GetIter(arg1: *mut PyObject) -> *mut PyObject; } -// Defined as this macro in Python 3.6, 3.7 limited API, but relies on +// Defined as this macro in Python limited API, but relies on // non-limited PyTypeObject. Don't expose this since it cannot be used. #[cfg(not(any(Py_LIMITED_API, PyPy)))] #[inline] @@ -156,7 +156,7 @@ extern "C" { pub fn PyNumber_Or(o1: *mut PyObject, o2: *mut PyObject) -> *mut PyObject; } -// Defined as this macro in Python 3.6, 3.7 limited API, but relies on +// Defined as this macro in Python limited API, but relies on // non-limited PyTypeObject. Don't expose this since it cannot be used. #[cfg(not(any(Py_LIMITED_API, PyPy)))] #[inline] diff --git a/src/ffi/ceval.rs b/src/ffi/ceval.rs index 202c2b1e..4031daa7 100644 --- a/src/ffi/ceval.rs +++ b/src/ffi/ceval.rs @@ -52,7 +52,6 @@ extern "C" { fn _Py_CheckRecursiveCall(_where: *mut c_char) -> c_int; } -// TODO // skipped Py_EnterRecursiveCall // skipped Py_LeaveRecursiveCall @@ -68,7 +67,6 @@ extern "C" { pub fn PyEval_RestoreThread(arg1: *mut PyThreadState); } -#[cfg(py_sys_config = "WITH_THREAD")] extern "C" { #[cfg_attr(PyPy, link_name = "PyPyEval_ThreadsInitialized")] pub fn PyEval_ThreadsInitialized() -> c_int; diff --git a/src/ffi/cpython/import.rs b/src/ffi/cpython/import.rs index 00fe424b..f342c0e3 100644 --- a/src/ffi/cpython/import.rs +++ b/src/ffi/cpython/import.rs @@ -6,33 +6,19 @@ use std::os::raw::{c_char, c_int, c_uchar}; extern "C" { pub fn _PyImport_IsInitialized(state: *mut PyInterpreterState) -> c_int; // skipped _PyImport_GetModuleId - #[cfg(Py_3_7)] pub fn _PyImport_SetModule(name: *mut PyObject, module: *mut PyObject) -> c_int; - #[cfg(Py_3_7)] pub fn _PyImport_SetModuleString(name: *const c_char, module: *mut PyObject) -> c_int; pub fn _PyImport_AcquireLock(); pub fn _PyImport_ReleaseLock() -> c_int; - #[cfg(not(Py_3_7))] - pub fn _PyImport_FindBuiltin(name: *const c_char) -> *mut PyObject; - #[cfg(all(Py_3_7, not(Py_3_9)))] + #[cfg(not(Py_3_9))] pub fn _PyImport_FindBuiltin(name: *const c_char, modules: *mut PyObject) -> *mut PyObject; #[cfg(not(Py_3_11))] pub fn _PyImport_FindExtensionObject(a: *mut PyObject, b: *mut PyObject) -> *mut PyObject; - #[cfg(not(Py_3_7))] - pub fn _PyImport_FixupBuiltin(module: *mut PyObject, name: *const c_char) -> c_int; - #[cfg(Py_3_7)] pub fn _PyImport_FixupBuiltin( module: *mut PyObject, name: *const c_char, modules: *mut PyObject, ) -> c_int; - #[cfg(not(Py_3_7))] - pub fn _PyImport_FixupExtensionObject( - a: *mut PyObject, - b: *mut PyObject, - c: *mut PyObject, - ) -> c_int; - #[cfg(Py_3_7)] pub fn _PyImport_FixupExtensionObject( a: *mut PyObject, b: *mut PyObject, diff --git a/src/ffi/cpython/unicodeobject.rs b/src/ffi/cpython/unicodeobject.rs index f357d91d..00b832fc 100644 --- a/src/ffi/cpython/unicodeobject.rs +++ b/src/ffi/cpython/unicodeobject.rs @@ -323,14 +323,9 @@ extern "C" { extern "C" { // skipped _PyUnicode_AsStringAndSize - #[cfg(Py_3_7)] #[cfg_attr(PyPy, link_name = "PyPyUnicode_AsUTF8")] pub fn PyUnicode_AsUTF8(unicode: *mut PyObject) -> *const c_char; - #[cfg(not(Py_3_7))] - #[cfg_attr(PyPy, link_name = "PyPyUnicode_AsUTF8")] - pub fn PyUnicode_AsUTF8(unicode: *mut PyObject) -> *mut c_char; - // skipped _PyUnicode_AsString pub fn PyUnicode_Encode( diff --git a/src/ffi/datetime.rs b/src/ffi/datetime.rs index 8510b96e..c7d7b787 100644 --- a/src/ffi/datetime.rs +++ b/src/ffi/datetime.rs @@ -361,7 +361,7 @@ pub struct PyDateTime_CAPI { pub TimeType: *mut PyTypeObject, pub DeltaType: *mut PyTypeObject, pub TZInfoType: *mut PyTypeObject, - #[cfg(all(Py_3_7, any(not(PyPy), Py_3_8)))] + #[cfg(not(all(PyPy, not(Py_3_8))))] pub TimeZone_UTC: *mut PyObject, pub Date_FromDate: unsafe extern "C" fn( year: c_int, @@ -395,7 +395,7 @@ pub struct PyDateTime_CAPI { normalize: c_int, cls: *mut PyTypeObject, ) -> *mut PyObject, - #[cfg(all(Py_3_7, any(not(PyPy), Py_3_8)))] + #[cfg(not(all(PyPy, not(Py_3_8))))] pub TimeZone_FromTimeZone: unsafe extern "C" fn(offset: *mut PyObject, name: *mut PyObject) -> *mut PyObject, @@ -451,7 +451,7 @@ pub static PyDateTimeAPI: _PyDateTimeAPI_impl = _PyDateTimeAPI_impl { /// /// The type obtained by dereferencing this object is `&'static PyObject`. This may change in the /// future to be a more specific type representing that this is a `datetime.timezone` object. -#[cfg(all(Py_3_7, any(not(PyPy), Py_3_8)))] +#[cfg(not(all(PyPy, not(Py_3_8))))] pub static PyDateTime_TimeZone_UTC: _PyDateTime_TimeZone_UTC_impl = _PyDateTime_TimeZone_UTC_impl { inner: &PyDateTimeAPI, }; @@ -609,12 +609,12 @@ impl Deref for _PyDateTimeAPI_impl { } #[doc(hidden)] -#[cfg(all(Py_3_7, any(not(PyPy), Py_3_8)))] +#[cfg(not(all(PyPy, not(Py_3_8))))] pub struct _PyDateTime_TimeZone_UTC_impl { inner: &'static _PyDateTimeAPI_impl, } -#[cfg(all(Py_3_7, any(not(PyPy), Py_3_8)))] +#[cfg(not(all(PyPy, not(Py_3_8))))] impl Deref for _PyDateTime_TimeZone_UTC_impl { type Target = crate::PyObject; @@ -630,7 +630,7 @@ impl Deref for _PyDateTime_TimeZone_UTC_impl { #[cfg(test)] mod tests { use super::*; - use crate::{py_run, AsPyPointer, IntoPy, Py, PyAny, Python}; + use crate::{types::PyDict, AsPyPointer, IntoPy, Py, PyAny, Python}; #[test] fn test_datetime_fromtimestamp() { @@ -638,11 +638,14 @@ mod tests { let args: Py = (100,).into_py(py); unsafe { PyDateTime_IMPORT() }; let dt: &PyAny = unsafe { py.from_owned_ptr(PyDateTime_FromTimestamp(args.as_ptr())) }; - py_run!( - py, - dt, - "import datetime; assert dt == datetime.datetime.fromtimestamp(100)" - ); + let locals = PyDict::new(py); + locals.set_item("dt", dt).unwrap(); + py.run( + "import datetime; assert dt == datetime.datetime.fromtimestamp(100)", + None, + Some(locals), + ) + .unwrap(); }) } @@ -652,24 +655,30 @@ mod tests { let args: Py = (100,).into_py(py); unsafe { PyDateTime_IMPORT() }; let dt: &PyAny = unsafe { py.from_owned_ptr(PyDate_FromTimestamp(args.as_ptr())) }; - py_run!( - py, - dt, - "import datetime; assert dt == datetime.date.fromtimestamp(100)" - ); + let locals = PyDict::new(py); + locals.set_item("dt", dt).unwrap(); + py.run( + "import datetime; assert dt == datetime.date.fromtimestamp(100)", + None, + Some(locals), + ) + .unwrap(); }) } #[test] - #[cfg(all(Py_3_7, any(not(PyPy), Py_3_8)))] + #[cfg(not(all(PyPy, not(Py_3_8))))] fn test_utc_timezone() { Python::with_gil(|py| { let utc_timezone = PyDateTime_TimeZone_UTC.as_ref(py); - py_run!( - py, - utc_timezone, - "import datetime; assert utc_timezone is datetime.timezone.utc" - ); + let locals = PyDict::new(py); + locals.set_item("utc_timezone", utc_timezone).unwrap(); + py.run( + "import datetime; assert utc_timezone is datetime.timezone.utc", + None, + Some(locals), + ) + .unwrap(); }) } } diff --git a/src/ffi/import.rs b/src/ffi/import.rs index 81d85c3b..0edb58cc 100644 --- a/src/ffi/import.rs +++ b/src/ffi/import.rs @@ -69,10 +69,7 @@ extern "C" { #[cfg_attr(PyPy, link_name = "PyPyImport_ReloadModule")] pub fn PyImport_ReloadModule(m: *mut PyObject) -> *mut PyObject; #[cfg(not(Py_3_9))] - #[deprecated( - since = "0.14.0", - note = "Removed in Python 3.9 as it was \"For internal use only\"." - )] + #[deprecated(note = "Removed in Python 3.9 as it was \"For internal use only\".")] pub fn PyImport_Cleanup(); pub fn PyImport_ImportFrozenModuleObject(name: *mut PyObject) -> c_int; pub fn PyImport_ImportFrozenModule(name: *const c_char) -> c_int; diff --git a/src/ffi/intrcheck.rs b/src/ffi/intrcheck.rs index ffd6c79c..3d8a58f7 100644 --- a/src/ffi/intrcheck.rs +++ b/src/ffi/intrcheck.rs @@ -4,19 +4,13 @@ extern "C" { #[cfg_attr(PyPy, link_name = "PyPyOS_InterruptOccurred")] pub fn PyOS_InterruptOccurred() -> c_int; #[cfg(not(Py_3_10))] - #[deprecated( - since = "0.14.0", - note = "Not documented in Python API; see Python 3.10 release notes" - )] + #[deprecated(note = "Not documented in Python API; see Python 3.10 release notes")] pub fn PyOS_InitInterrupts(); - #[cfg(any(not(Py_LIMITED_API), Py_3_7))] pub fn PyOS_BeforeFork(); - #[cfg(any(not(Py_LIMITED_API), Py_3_7))] pub fn PyOS_AfterFork_Parent(); - #[cfg(any(not(Py_LIMITED_API), Py_3_7))] pub fn PyOS_AfterFork_Child(); - #[cfg_attr(Py_3_7, deprecated(note = "use PyOS_AfterFork_Child instead"))] + #[deprecated(note = "use PyOS_AfterFork_Child instead")] #[cfg_attr(PyPy, link_name = "PyPyOS_AfterFork")] pub fn PyOS_AfterFork(); diff --git a/src/ffi/methodobject.rs b/src/ffi/methodobject.rs index b1b435d7..24dac732 100644 --- a/src/ffi/methodobject.rs +++ b/src/ffi/methodobject.rs @@ -31,7 +31,7 @@ pub unsafe fn PyCFunction_Check(op: *mut PyObject) -> c_int { pub type PyCFunction = unsafe extern "C" fn(slf: *mut PyObject, args: *mut PyObject) -> *mut PyObject; -#[cfg(all(Py_3_7, not(Py_LIMITED_API)))] +#[cfg(not(Py_LIMITED_API))] pub type _PyCFunctionFast = unsafe extern "C" fn( slf: *mut PyObject, args: *mut *mut PyObject, @@ -45,7 +45,7 @@ pub type PyCFunctionWithKeywords = unsafe extern "C" fn( kwds: *mut PyObject, ) -> *mut PyObject; -#[cfg(all(Py_3_7, not(Py_LIMITED_API)))] +#[cfg(not(Py_LIMITED_API))] pub type _PyCFunctionFastWithKeywords = unsafe extern "C" fn( slf: *mut PyObject, args: *const *mut PyObject, @@ -119,7 +119,7 @@ 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(all(Py_3_7, not(Py_LIMITED_API)))] +#[cfg(not(Py_LIMITED_API))] pub const METH_FASTCALL: c_int = 0x0080; // skipped METH_STACKLESS diff --git a/src/ffi/object.rs b/src/ffi/object.rs index 05898a06..e113d1ee 100644 --- a/src/ffi/object.rs +++ b/src/ffi/object.rs @@ -280,6 +280,7 @@ extern "C" { #[cfg_attr(PyPy, link_name = "PyPyObject_SetAttr")] pub fn PyObject_SetAttr(arg1: *mut PyObject, arg2: *mut PyObject, arg3: *mut PyObject) -> c_int; + #[cfg_attr(PyPy, link_name = "PyPyObject_HasAttr")] pub fn PyObject_HasAttr(arg1: *mut PyObject, arg2: *mut PyObject) -> c_int; #[cfg_attr(PyPy, link_name = "PyPyObject_SelfIter")] pub fn PyObject_SelfIter(arg1: *mut PyObject) -> *mut PyObject; diff --git a/src/ffi/pystate.rs b/src/ffi/pystate.rs index a00af79d..4eb2049f 100644 --- a/src/ffi/pystate.rs +++ b/src/ffi/pystate.rs @@ -25,7 +25,7 @@ extern "C" { #[cfg(all(Py_3_8, not(PyPy)))] pub fn PyInterpreterState_GetDict(arg1: *mut PyInterpreterState) -> *mut PyObject; - #[cfg(all(Py_3_7, not(PyPy)))] + #[cfg(not(PyPy))] pub fn PyInterpreterState_GetID(arg1: *mut PyInterpreterState) -> i64; #[cfg(not(PyPy))] diff --git a/src/ffi/sliceobject.rs b/src/ffi/sliceobject.rs index fa73102d..31488bb4 100644 --- a/src/ffi/sliceobject.rs +++ b/src/ffi/sliceobject.rs @@ -52,20 +52,8 @@ extern "C" { stop: *mut Py_ssize_t, step: *mut Py_ssize_t, ) -> c_int; - - #[cfg(not(Py_3_7))] - #[cfg_attr(PyPy, link_name = "PyPySlice_GetIndicesEx")] - pub fn PySlice_GetIndicesEx( - r: *mut PyObject, - length: Py_ssize_t, - start: *mut Py_ssize_t, - stop: *mut Py_ssize_t, - step: *mut Py_ssize_t, - slicelength: *mut Py_ssize_t, - ) -> c_int; } -#[cfg(Py_3_7)] #[inline] pub unsafe fn PySlice_GetIndicesEx( slice: *mut PyObject, diff --git a/src/ffi/unicodeobject.rs b/src/ffi/unicodeobject.rs index 61a60f8c..2b02b37e 100644 --- a/src/ffi/unicodeobject.rs +++ b/src/ffi/unicodeobject.rs @@ -165,12 +165,9 @@ extern "C" { ) -> *mut PyObject; #[cfg_attr(PyPy, link_name = "PyPyUnicode_AsUTF8String")] pub fn PyUnicode_AsUTF8String(unicode: *mut PyObject) -> *mut PyObject; - #[cfg(any(Py_3_10, all(Py_3_7, not(Py_LIMITED_API))))] + #[cfg(any(Py_3_10, not(Py_LIMITED_API)))] #[cfg_attr(PyPy, link_name = "PyPyUnicode_AsUTF8AndSize")] pub fn PyUnicode_AsUTF8AndSize(unicode: *mut PyObject, size: *mut Py_ssize_t) -> *const c_char; - #[cfg(not(any(Py_3_7, Py_LIMITED_API)))] - #[cfg_attr(PyPy, link_name = "PyPyUnicode_AsUTF8AndSize")] - pub fn PyUnicode_AsUTF8AndSize(unicode: *mut PyObject, size: *mut Py_ssize_t) -> *mut c_char; #[cfg_attr(PyPy, link_name = "PyPyUnicode_DecodeUTF32")] pub fn PyUnicode_DecodeUTF32( string: *const c_char, diff --git a/src/gil.rs b/src/gil.rs index 588d45f1..7e580b2e 100644 --- a/src/gil.rs +++ b/src/gil.rs @@ -45,20 +45,12 @@ pub(crate) fn gil_is_acquired() -> bool { /// signal handling depends on the notion of a 'main thread', which must be the thread that /// initializes the Python interpreter. /// -/// If both the Python interpreter and Python threading are already initialized, this function has -/// no effect. +/// If the Python interpreter is already initialized, this function has no effect. /// /// This function is unavailable under PyPy because PyPy cannot be embedded in Rust (or any other /// software). Support for this is tracked on the /// [PyPy issue tracker](https://foss.heptapod.net/pypy/pypy/-/issues/3286). /// -/// Python 3.6 only: If the Python interpreter is initialized but Python threading is not, this -/// function will initialize Python threading. -/// -/// # Panics -/// - Python 3.6 only: If this function needs to initialize Python threading but the calling thread -/// is not the thread which initialized Python, this function will panic. -/// /// # Examples /// ```rust /// use pyo3::prelude::*; @@ -69,33 +61,13 @@ pub(crate) fn gil_is_acquired() -> bool { /// # } /// ``` #[cfg(not(PyPy))] -#[allow(clippy::collapsible_if)] // for if cfg! pub fn prepare_freethreaded_python() { // Protect against race conditions when Python is not yet initialized and multiple threads // concurrently call 'prepare_freethreaded_python()'. Note that we do not protect against // concurrent initialization of the Python runtime by other users of the Python C API. START.call_once_force(|_| unsafe { - if cfg!(not(Py_3_7)) { - // Use call_once_force because if initialization panics, it's okay to try again. - if ffi::Py_IsInitialized() != 0 { - if ffi::PyEval_ThreadsInitialized() == 0 { - // We can only safely initialize threads if this thread holds the GIL. - assert!( - !ffi::PyGILState_GetThisThreadState().is_null(), - "Python threading is not initialized and cannot be initialized by this \ - thread, because it is not the thread which initialized Python." - ); - ffi::PyEval_InitThreads(); - } - } else { - ffi::Py_InitializeEx(0); - ffi::PyEval_InitThreads(); - - // Release the GIL. - ffi::PyEval_SaveThread(); - } - } else if ffi::Py_IsInitialized() == 0 { - // In Python 3.7 and up PyEval_InitThreads is irrelevant. + // Use call_once_force because if initialization panics, it's okay to try again. + if ffi::Py_IsInitialized() == 0 { ffi::Py_InitializeEx(0); // Release the GIL. @@ -134,7 +106,6 @@ pub fn prepare_freethreaded_python() { /// # } /// ``` #[cfg(not(PyPy))] -#[allow(clippy::collapsible_if)] // for if cfg! pub unsafe fn with_embedded_python_interpreter(f: F) -> R where F: for<'p> FnOnce(Python<'p>) -> R, @@ -147,15 +118,7 @@ where ffi::Py_InitializeEx(0); - // Changed in version 3.7: This function is now called by Py_Initialize(), so you don’t have to - // call it yourself anymore. - if cfg!(not(Py_3_7)) { - if ffi::PyEval_ThreadsInitialized() == 0 { - ffi::PyEval_InitThreads(); - } - } - - // Safe: the GIL is already held because of the Py_IntializeEx call. + // Safety: the GIL is already held because of the Py_IntializeEx call. let pool = GILPool::new(); // Import the threading module - this ensures that it will associate this thread as the "main" @@ -218,6 +181,15 @@ impl GILGuard { if #[cfg(all(feature = "auto-initialize", not(PyPy)))] { prepare_freethreaded_python(); } else { + // This is a "hack" to make running `cargo test` for PyO3 convenient (i.e. no need + // to specify `--features auto-initialize` manually. Tests within the crate itself + // all depend on the auto-initialize feature for conciseness but Cargo does not + // provide a mechanism to specify required features for tests. + if option_env!("CARGO_PRIMARY_PACKAGE").is_some() { + #[cfg(not(PyPy))] + prepare_freethreaded_python(); + } + START.call_once_force(|_| unsafe { // Use call_once_force because if there is a panic because the interpreter is // not initialized, it's fine for the user to initialize the interpreter and @@ -230,14 +202,6 @@ impl GILGuard { Consider calling `pyo3::prepare_freethreaded_python()` before attempting \ to use Python APIs." ); - assert_ne!( - ffi::PyEval_ThreadsInitialized(), - 0, - "Python threading is not initalized and the `auto-initialize` feature is \ - not enabled.\n\n\ - Consider calling `pyo3::prepare_freethreaded_python()` before attempting \ - to use Python APIs." - ); }); } } diff --git a/src/impl_/deprecations.rs b/src/impl_/deprecations.rs index 1fbabf9e..c0faa0cf 100644 --- a/src/impl_/deprecations.rs +++ b/src/impl_/deprecations.rs @@ -1,28 +1,4 @@ //! Symbols used to denote deprecated usages of PyO3's proc macros. -#[deprecated( - since = "0.14.0", - note = "use `#[pyo3(name = \"...\")]` instead of `#[name = \"...\"]`" -)] -pub const NAME_ATTRIBUTE: () = (); - -#[deprecated( - since = "0.14.0", - note = "use `#[pyfn(m)] #[pyo3(name = \"...\")]` instead of `#[pyfn(m, \"...\")]`" -)] -pub const PYFN_NAME_ARGUMENT: () = (); - -#[deprecated( - since = "0.14.0", - note = "use `#[pymodule] #[pyo3(name = \"...\")]` instead of `#[pymodule(...)]`" -)] -pub const PYMODULE_NAME_ARGUMENT: () = (); - -#[deprecated( - since = "0.14.0", - note = "use `#[pyo3(text_signature = \"...\")]` instead of `#[text_signature = \"...\"]`" -)] -pub const TEXT_SIGNATURE_ATTRIBUTE: () = (); - #[deprecated(since = "0.15.0", note = "use `fn __call__` instead of `#[call]`")] pub const CALL_ATTRIBUTE: () = (); diff --git a/src/instance.rs b/src/instance.rs index 269d22f3..0037abb8 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -17,6 +17,10 @@ use std::ptr::NonNull; /// PyO3 is designed in a way that all references to those types are bound /// to the GIL, which is why you can get a token from all references of those /// types. +/// +/// # Safety +/// +/// This trait must only be implemented for types which cannot be accessed without the GIL. pub unsafe trait PyNativeType: Sized { /// Returns a GIL marker constrained to the lifetime of this type. #[inline] @@ -528,6 +532,21 @@ 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<()> + where + N: ToPyObject, + V: ToPyObject, + { + attr_name.with_borrowed_ptr(py, move |attr_name| { + value.with_borrowed_ptr(py, |value| unsafe { + err::error_on_minusone(py, ffi::PyObject_SetAttr(self.as_ptr(), attr_name, value)) + }) + }) + } + /// Calls the object. /// /// This is equivalent to the Python expression `self(*args, **kwargs)`. diff --git a/src/lib.rs b/src/lib.rs index 14bc7466..e8ebffcb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -102,7 +102,7 @@ //! PyO3 uses `rustc`'s `--cfg` flags to enable or disable code used for different Python versions. //! If you want to do this for your own crate, you can do so with the [`pyo3-build-config`] crate. //! -//! - `Py_3_6`, `Py_3_7`, `Py_3_8`, `Py_3_9`, `Py_3_10`: Marks code that is only enabled when +//! - `Py_3_7`, `Py_3_8`, `Py_3_9`, `Py_3_10`: Marks code that is only enabled when //! compiling for a given minimum Python version. //! - `Py_LIMITED_API`: Marks code enabled when the `abi3` feature flag is enabled. //! - `PyPy` - Marks code enabled when compiling for PyPy. @@ -110,8 +110,8 @@ //! # Minimum supported Rust and Python versions //! //! PyO3 supports the following software versions: -//! - Python 3.6 and up (CPython and PyPy) -//! - Rust 1.41 and up +//! - Python 3.7 and up (CPython and PyPy) +//! - Rust 1.48 and up //! //! # Example: Building a native Python module //! @@ -360,6 +360,11 @@ pub use pyo3_macros::{pyclass, pyfunction, pymethods, pymodule, pyproto, FromPyO #[macro_use] mod macros; +/// Test macro hygiene - this is in the crate since we won't have +/// `pyo3` available in the crate root. +#[cfg(all(test, feature = "macros"))] +mod test_hygiene; + /// Test readme and user guide #[cfg(doctest)] pub mod doc_test { diff --git a/src/pyclass.rs b/src/pyclass.rs index fc8a4efa..132fe599 100644 --- a/src/pyclass.rs +++ b/src/pyclass.rs @@ -83,7 +83,8 @@ where slots.push(ffi::Py_tp_free, free as _); } - if cfg!(Py_3_9) { + #[cfg(Py_3_9)] + { let members = py_class_members::(); if !members.is_empty() { slots.push(ffi::Py_tp_members, into_raw(members)) @@ -155,7 +156,8 @@ fn tp_init_additional(type_object: *mut ffi::PyTypeObject) { // Setting buffer protocols via slots doesn't work until Python 3.9, so on older versions we // must manually fixup the type object. - if cfg!(not(Py_3_9)) { + #[cfg(not(Py_3_9))] + { if let Some(buffer) = T::get_buffer() { unsafe { (*(*type_object).tp_as_buffer).bf_getbuffer = buffer.bf_getbuffer; @@ -166,7 +168,8 @@ fn tp_init_additional(type_object: *mut ffi::PyTypeObject) { // Setting tp_dictoffset and tp_weaklistoffset via slots doesn't work until Python 3.9, so on // older versions again we must fixup the type object. - if cfg!(not(Py_3_9)) { + #[cfg(not(Py_3_9))] + { // __dict__ support if let Some(dict_offset) = PyCell::::dict_offset() { unsafe { @@ -258,12 +261,6 @@ fn py_class_members() -> Vec { members } -// Stub needed since the `if cfg!()` above still compiles contained code. -#[cfg(not(Py_3_9))] -fn py_class_members() -> Vec { - vec![] -} - const PY_GET_SET_DEF_INIT: ffi::PyGetSetDef = ffi::PyGetSetDef { name: ptr::null_mut(), get: None, @@ -272,7 +269,6 @@ const PY_GET_SET_DEF_INIT: ffi::PyGetSetDef = ffi::PyGetSetDef { closure: ptr::null_mut(), }; -#[allow(clippy::collapsible_if)] // for if cfg! fn py_class_properties( is_dummy: bool, for_each_method_def: &dyn Fn(&mut dyn FnMut(&[PyMethodDefType])), diff --git a/src/python.rs b/src/python.rs index 4f2fdceb..52de8c25 100644 --- a/src/python.rs +++ b/src/python.rs @@ -326,7 +326,7 @@ impl<'py> Python<'py> { /// py.allow_threads(move || { /// // An example of an "expensive" Rust calculation /// let sum = numbers.iter().sum(); - /// + /// /// Ok(sum) /// }) /// } @@ -367,26 +367,30 @@ impl<'py> Python<'py> { F: Send + FnOnce() -> T, T: Send, { + // Use a guard pattern to handle reacquiring the GIL, so that the GIL will be reacquired + // even if `f` panics. + + struct RestoreGuard { + count: usize, + tstate: *mut ffi::PyThreadState, + } + + impl Drop for RestoreGuard { + fn drop(&mut self) { + gil::GIL_COUNT.with(|c| c.set(self.count)); + unsafe { + ffi::PyEval_RestoreThread(self.tstate); + } + } + } + // The `Send` bound on the closure prevents the user from // transferring the `Python` token into the closure. let count = gil::GIL_COUNT.with(|c| c.replace(0)); let tstate = unsafe { ffi::PyEval_SaveThread() }; - // Unwinding right here corrupts the Python interpreter state and leads to weird - // crashes such as stack overflows. We will catch the unwind and resume as soon as - // we've restored the GIL state. - // - // Because we will resume unwinding as soon as the GIL state is fixed, we can assert - // that the closure is unwind safe. - let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)); - // Restore GIL state - gil::GIL_COUNT.with(|c| c.set(count)); - unsafe { - ffi::PyEval_RestoreThread(tstate); - } - - // Now that the GIL state has been safely reset, we can unwind if a panic was caught. - result.unwrap_or_else(|payload| std::panic::resume_unwind(payload)) + let _guard = RestoreGuard { count, tstate }; + f() } /// Evaluates a Python expression in the given context and returns the result. @@ -546,9 +550,9 @@ impl<'py> Python<'py> { /// ```rust /// # use pyo3::Python; /// Python::with_gil(|py| { - /// // PyO3 supports Python 3.6 and up. - /// assert!(py.version_info() >= (3, 6)); - /// assert!(py.version_info() >= (3, 6, 0)); + /// // PyO3 supports Python 3.7 and up. + /// assert!(py.version_info() >= (3, 7)); + /// assert!(py.version_info() >= (3, 7, 0)); /// }); /// ``` pub fn version_info(self) -> PythonVersionInfo<'py> { @@ -840,6 +844,29 @@ mod tests { }); } + #[test] + fn test_allow_threads_releases_and_acquires_gil() { + Python::with_gil(|py| { + let b = std::sync::Arc::new(std::sync::Barrier::new(2)); + + let b2 = b.clone(); + std::thread::spawn(move || Python::with_gil(|_| b2.wait())); + + py.allow_threads(|| { + // If allow_threads does not release the GIL, this will deadlock because + // the thread spawned above will never be able to acquire the GIL. + b.wait(); + }); + + unsafe { + // If the GIL is not reacquired at the end of allow_threads, this call + // will crash the Python interpreter. + let tstate = ffi::PyEval_SaveThread(); + ffi::PyEval_RestoreThread(tstate); + } + }); + } + #[test] fn test_allow_threads_panics_safely() { Python::with_gil(|py| { @@ -864,10 +891,6 @@ mod tests { fn test_python_version_info() { Python::with_gil(|py| { let version = py.version_info(); - #[cfg(Py_3_6)] - assert!(version >= (3, 6)); - #[cfg(Py_3_6)] - assert!(version >= (3, 6, 0)); #[cfg(Py_3_7)] assert!(version >= (3, 7)); #[cfg(Py_3_7)] diff --git a/src/test_hygiene/misc.rs b/src/test_hygiene/misc.rs new file mode 100644 index 00000000..eb49718e --- /dev/null +++ b/src/test_hygiene/misc.rs @@ -0,0 +1,29 @@ +#![no_implicit_prelude] + +#[derive(crate::FromPyObject)] +#[pyo3(crate = "crate")] +struct Derive1(i32); // newtype case + +#[derive(crate::FromPyObject)] +#[pyo3(crate = "crate")] +#[allow(dead_code)] +struct Derive2(i32, i32); // tuple case + +#[derive(crate::FromPyObject)] +#[pyo3(crate = "crate")] +#[allow(dead_code)] +struct Derive3 { + f: i32, + g: i32, +} // struct case + +#[derive(crate::FromPyObject)] +#[pyo3(crate = "crate")] +#[allow(dead_code)] +enum Derive4 { + A(i32), + B { f: i32 }, +} // enum case + +crate::create_exception!(mymodule, CustomError, crate::exceptions::PyException); +crate::import_exception!(socket, gaierror); diff --git a/src/test_hygiene/mod.rs b/src/test_hygiene/mod.rs new file mode 100644 index 00000000..d907d2e8 --- /dev/null +++ b/src/test_hygiene/mod.rs @@ -0,0 +1,5 @@ +mod misc; +mod pyclass; +mod pyfunction; +mod pymethods; +mod pymodule; diff --git a/tests/hygiene/pyclass.rs b/src/test_hygiene/pyclass.rs similarity index 61% rename from tests/hygiene/pyclass.rs rename to src/test_hygiene/pyclass.rs index 985d05dc..adadc08c 100644 --- a/tests/hygiene/pyclass.rs +++ b/src/test_hygiene/pyclass.rs @@ -1,27 +1,30 @@ #![no_implicit_prelude] #![allow(unused_variables)] -#[::pyo3::pyclass] +#[crate::pyclass] +#[pyo3(crate = "crate")] #[derive(::std::clone::Clone)] pub struct Foo; -#[::pyo3::pyclass] +#[crate::pyclass] +#[pyo3(crate = "crate")] pub struct Foo2; -#[::pyo3::pyclass( +#[crate::pyclass( name = "ActuallyBar", freelist = 8, weakref, unsendable, subclass, - extends = ::pyo3::types::PyAny, + extends = crate::types::PyAny, module = "Spam" )] +#[pyo3(crate = "crate")] pub struct Bar { #[pyo3(get, set)] a: u8, #[pyo3(get, set)] b: Foo, #[pyo3(get, set)] - c: ::std::option::Option<::pyo3::Py>, + c: ::std::option::Option>, } diff --git a/src/test_hygiene/pyfunction.rs b/src/test_hygiene/pyfunction.rs new file mode 100644 index 00000000..9a7d515e --- /dev/null +++ b/src/test_hygiene/pyfunction.rs @@ -0,0 +1,16 @@ +#![no_implicit_prelude] +#![allow(unused_variables)] + +#[crate::pyfunction] +#[pyo3(crate = "crate")] +fn do_something(x: i32) -> crate::PyResult { + ::std::result::Result::Ok(x) +} + +#[test] +fn invoke_wrap_pyfunction() { + crate::Python::with_gil(|py| { + let func = crate::wrap_pyfunction!(do_something)(py).unwrap(); + crate::py_run!(py, func, r#"func(5)"#); + }); +} diff --git a/tests/hygiene/pymethods.rs b/src/test_hygiene/pymethods.rs similarity index 67% rename from tests/hygiene/pymethods.rs rename to src/test_hygiene/pymethods.rs index 37a916f9..832e7239 100644 --- a/tests/hygiene/pymethods.rs +++ b/src/test_hygiene/pymethods.rs @@ -1,13 +1,16 @@ #![no_implicit_prelude] #![allow(unused_variables)] -#[::pyo3::pyclass] +#[crate::pyclass] +#[pyo3(crate = "crate")] pub struct Dummy; -#[::pyo3::pyclass] +#[crate::pyclass] +#[pyo3(crate = "crate")] pub struct DummyIter; -#[::pyo3::pymethods] +#[crate::pymethods] +#[pyo3(crate = "crate")] impl Dummy { ////////////////////// // Basic customization @@ -20,8 +23,8 @@ impl Dummy { "Dummy" } - fn __bytes__<'py>(&self, py: ::pyo3::Python<'py>) -> &'py ::pyo3::types::PyBytes { - ::pyo3::types::PyBytes::new(py, &[0]) + fn __bytes__<'py>(&self, py: crate::Python<'py>) -> &'py crate::types::PyBytes { + crate::types::PyBytes::new(py, &[0]) } fn __format__(&self, format_spec: ::std::string::String) -> ::std::string::String { @@ -60,11 +63,11 @@ impl Dummy { // Customizing attribute access ////////////////////// - fn __getattr__(&self, name: ::std::string::String) -> &::pyo3::PyAny { + fn __getattr__(&self, name: ::std::string::String) -> &crate::PyAny { ::std::panic!("unimplemented isn't hygienic before 1.50") } - fn __getattribute__(&self, name: ::std::string::String) -> &::pyo3::PyAny { + fn __getattribute__(&self, name: ::std::string::String) -> &crate::PyAny { ::std::panic!("unimplemented isn't hygienic before 1.50") } @@ -72,8 +75,8 @@ impl Dummy { fn __delattr__(&mut self, name: ::std::string::String) {} - fn __dir__<'py>(&self, py: ::pyo3::Python<'py>) -> &'py ::pyo3::types::PyList { - ::pyo3::types::PyList::new(py, ::std::vec![0_u8]) + fn __dir__<'py>(&self, py: crate::Python<'py>) -> &'py crate::types::PyList { + crate::types::PyList::new(py, ::std::vec![0_u8]) } ////////////////////// @@ -82,17 +85,17 @@ impl Dummy { fn __get__( &self, - instance: &::pyo3::PyAny, - owner: &::pyo3::PyAny, - ) -> ::pyo3::PyResult<&::pyo3::PyAny> { + instance: &crate::PyAny, + owner: &crate::PyAny, + ) -> crate::PyResult<&crate::PyAny> { ::std::panic!("unimplemented isn't hygienic before 1.50") } - fn __set__(&self, instance: &::pyo3::PyAny, owner: &::pyo3::PyAny) {} + fn __set__(&self, instance: &crate::PyAny, owner: &crate::PyAny) {} - fn __delete__(&self, instance: &::pyo3::PyAny) {} + fn __delete__(&self, instance: &crate::PyAny) {} - fn __set_name__(&self, owner: &::pyo3::PyAny, name: &::pyo3::PyAny) {} + fn __set_name__(&self, owner: &crate::PyAny, name: &crate::PyAny) {} ////////////////////// // Implementing Descriptors @@ -102,24 +105,24 @@ impl Dummy { 0 } - fn __getitem__(&self, key: u32) -> ::pyo3::PyResult { - ::std::result::Result::Err(::pyo3::exceptions::PyKeyError::new_err("boo")) + fn __getitem__(&self, key: u32) -> crate::PyResult { + ::std::result::Result::Err(crate::exceptions::PyKeyError::new_err("boo")) } fn __setitem__(&self, key: u32, value: u32) {} fn __delitem__(&self, key: u32) {} - fn __iter__(_: ::pyo3::pycell::PyRef, py: ::pyo3::Python) -> ::pyo3::Py { - ::pyo3::Py::new(py, DummyIter {}).unwrap() + fn __iter__(_: crate::pycell::PyRef, py: crate::Python) -> crate::Py { + crate::Py::new(py, DummyIter {}).unwrap() } fn __next__(&mut self) -> ::std::option::Option<()> { ::std::option::Option::None } - fn __reversed__(slf: ::pyo3::pycell::PyRef, py: ::pyo3::Python) -> ::pyo3::Py { - ::pyo3::Py::new(py, DummyIter {}).unwrap() + fn __reversed__(slf: crate::pycell::PyRef, py: crate::Python) -> crate::Py { + crate::Py::new(py, DummyIter {}).unwrap() } fn __contains__(&self, item: u32) -> bool { @@ -142,12 +145,12 @@ impl Dummy { Dummy {} } - fn __truediv__(&self, _other: &Self) -> ::pyo3::PyResult<()> { - ::std::result::Result::Err(::pyo3::exceptions::PyZeroDivisionError::new_err("boo")) + fn __truediv__(&self, _other: &Self) -> crate::PyResult<()> { + ::std::result::Result::Err(crate::exceptions::PyZeroDivisionError::new_err("boo")) } - fn __floordiv__(&self, _other: &Self) -> ::pyo3::PyResult<()> { - ::std::result::Result::Err(::pyo3::exceptions::PyZeroDivisionError::new_err("boo")) + fn __floordiv__(&self, _other: &Self) -> crate::PyResult<()> { + ::std::result::Result::Err(crate::exceptions::PyZeroDivisionError::new_err("boo")) } fn __mod__(&self, _other: &Self) -> u32 { @@ -194,12 +197,12 @@ impl Dummy { Dummy {} } - fn __rtruediv__(&self, _other: &Self) -> ::pyo3::PyResult<()> { - ::std::result::Result::Err(::pyo3::exceptions::PyZeroDivisionError::new_err("boo")) + fn __rtruediv__(&self, _other: &Self) -> crate::PyResult<()> { + ::std::result::Result::Err(crate::exceptions::PyZeroDivisionError::new_err("boo")) } - fn __rfloordiv__(&self, _other: &Self) -> ::pyo3::PyResult<()> { - ::std::result::Result::Err(::pyo3::exceptions::PyZeroDivisionError::new_err("boo")) + fn __rfloordiv__(&self, _other: &Self) -> crate::PyResult<()> { + ::std::result::Result::Err(crate::exceptions::PyZeroDivisionError::new_err("boo")) } fn __rmod__(&self, _other: &Self) -> u32 { @@ -258,24 +261,24 @@ impl Dummy { fn __ior__(&mut self, other: &Self) {} - fn __neg__(slf: ::pyo3::pycell::PyRef) -> ::pyo3::pycell::PyRef { + fn __neg__(slf: crate::pycell::PyRef) -> crate::pycell::PyRef { slf } - fn __pos__(slf: ::pyo3::pycell::PyRef) -> ::pyo3::pycell::PyRef { + fn __pos__(slf: crate::pycell::PyRef) -> crate::pycell::PyRef { slf } - fn __abs__(slf: ::pyo3::pycell::PyRef) -> ::pyo3::pycell::PyRef { + fn __abs__(slf: crate::pycell::PyRef) -> crate::pycell::PyRef { slf } - fn __invert__(slf: ::pyo3::pycell::PyRef) -> ::pyo3::pycell::PyRef { + fn __invert__(slf: crate::pycell::PyRef) -> crate::pycell::PyRef { slf } - fn __complex__<'py>(&self, py: ::pyo3::Python<'py>) -> &'py ::pyo3::types::PyComplex { - ::pyo3::types::PyComplex::from_doubles(py, 0.0, 0.0) + fn __complex__<'py>(&self, py: crate::Python<'py>) -> &'py crate::types::PyComplex { + crate::types::PyComplex::from_doubles(py, 0.0, 0.0) } fn __int__(&self) -> u32 { @@ -314,9 +317,9 @@ impl Dummy { fn __exit__( &mut self, - exc_type: &::pyo3::PyAny, - exc_value: &::pyo3::PyAny, - traceback: &::pyo3::PyAny, + exc_type: &crate::PyAny, + exc_value: &crate::PyAny, + traceback: &crate::PyAny, ) { } @@ -324,7 +327,7 @@ impl Dummy { // Awaitable Objects ////////////////////// - fn __await__(slf: ::pyo3::pycell::PyRef) -> ::pyo3::pycell::PyRef { + fn __await__(slf: crate::pycell::PyRef) -> crate::pycell::PyRef { slf } @@ -333,8 +336,8 @@ impl Dummy { // Asynchronous Iterators ////////////////////// - fn __aiter__(slf: ::pyo3::pycell::PyRef, py: ::pyo3::Python) -> ::pyo3::Py { - ::pyo3::Py::new(py, DummyIter {}).unwrap() + fn __aiter__(slf: crate::pycell::PyRef, py: crate::Python) -> crate::Py { + crate::Py::new(py, DummyIter {}).unwrap() } fn __anext__(&mut self) -> ::std::option::Option<()> { @@ -349,9 +352,9 @@ impl Dummy { fn __aexit__( &mut self, - exc_type: &::pyo3::PyAny, - exc_value: &::pyo3::PyAny, - traceback: &::pyo3::PyAny, + exc_type: &crate::PyAny, + exc_value: &crate::PyAny, + traceback: &crate::PyAny, ) { } @@ -362,13 +365,13 @@ impl Dummy { #[staticmethod] fn staticmethod() {} #[classmethod] - fn clsmethod(_: &::pyo3::types::PyType) {} + fn clsmethod(_: &crate::types::PyType) {} #[args(args = "*", kwds = "**")] fn __call__( &self, - _args: &::pyo3::types::PyTuple, - _kwds: ::std::option::Option<&::pyo3::types::PyDict>, - ) -> ::pyo3::PyResult { + _args: &crate::types::PyTuple, + _kwds: ::std::option::Option<&crate::types::PyDict>, + ) -> crate::PyResult { ::std::panic!("unimplemented isn't hygienic before 1.50") } #[new] @@ -391,8 +394,8 @@ impl Dummy { fn __richcmp__( &self, other: &Self, - op: ::pyo3::class::basic::CompareOp, - ) -> ::pyo3::PyResult { + op: crate::class::basic::CompareOp, + ) -> crate::PyResult { ::std::result::Result::Ok(false) } // PyGcProtocol diff --git a/src/test_hygiene/pymodule.rs b/src/test_hygiene/pymodule.rs new file mode 100644 index 00000000..478cea3a --- /dev/null +++ b/src/test_hygiene/pymodule.rs @@ -0,0 +1,23 @@ +#![no_implicit_prelude] +#![allow(unused_variables)] + +#[crate::pyfunction] +#[pyo3(crate = "crate")] +fn do_something(x: i32) -> crate::PyResult { + ::std::result::Result::Ok(x) +} + +#[crate::pymodule] +#[pyo3(crate = "crate")] +fn foo(_py: crate::Python, _m: &crate::types::PyModule) -> crate::PyResult<()> { + ::std::result::Result::Ok(()) +} + +#[crate::pymodule] +#[pyo3(crate = "crate")] +fn 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))?; + + ::std::result::Result::Ok(()) +} diff --git a/src/type_object.rs b/src/type_object.rs index 0f16f566..bf60d18f 100644 --- a/src/type_object.rs +++ b/src/type_object.rs @@ -16,6 +16,10 @@ use std::thread::{self, ThreadId}; /// is of `PyAny`. /// /// This trait is intended to be used internally. +/// +/// # Safety +/// +/// This trait must only be implemented for types which represent valid layouts of Python objects. pub unsafe trait PyLayout {} /// `T: PySizedLayout` represents that `T` is not a instance of @@ -31,6 +35,11 @@ pub trait PySizedLayout: PyLayout + Sized {} /// - the return value of type_object must always point to the same PyTypeObject instance /// /// It is safely implemented by the `pyclass` macro. +/// +/// # Safety +/// +/// Implementations must provide an implementation for `type_object_raw` which infallibly produces a +/// non-null pointer to the corresponding Python type object. pub unsafe trait PyTypeInfo: Sized { /// Class name. const NAME: &'static str; @@ -57,6 +66,8 @@ pub unsafe trait PyTypeInfo: Sized { /// Python object types that have a corresponding type object. /// +/// # Safety +/// /// This trait is marked unsafe because not fulfilling the contract for type_object /// leads to UB. /// diff --git a/src/types/any.rs b/src/types/any.rs index 8fc8640d..66eb4fca 100644 --- a/src/types/any.rs +++ b/src/types/any.rs @@ -74,7 +74,7 @@ impl PyAny { /// /// Python::with_gil(|py| { /// let dict = PyDict::new(py); - /// assert!(dict.is_instance::().unwrap()); + /// assert!(dict.is_instance_of::().unwrap()); /// let any: &PyAny = dict.as_ref(); /// assert!(any.downcast::().is_ok()); /// assert!(any.downcast::().is_err()); @@ -665,11 +665,21 @@ impl PyAny { unsafe { self.py().from_owned_ptr(ffi::PyObject_Dir(self.as_ptr())) } } + /// Checks whether this object is an instance of type `typ`. + /// + /// This is equivalent to the Python expression `isinstance(self, typ)`. + pub fn is_instance(&self, typ: &PyType) -> PyResult { + let result = unsafe { ffi::PyObject_IsInstance(self.as_ptr(), typ.as_ptr()) }; + err::error_on_minusone(self.py(), result)?; + Ok(result == 1) + } + /// Checks whether this object is an instance of type `T`. /// - /// This is equivalent to the Python expression `isinstance(self, T)`. - pub fn is_instance(&self) -> PyResult { - T::type_object(self.py()).is_instance(self) + /// This is equivalent to the Python expression `isinstance(self, T)`, + /// if the type `T` is known at compile time. + pub fn is_instance_of(&self) -> PyResult { + self.is_instance(T::type_object(self.py())) } /// Returns a GIL marker constrained to the lifetime of this type. @@ -682,17 +692,11 @@ impl PyAny { #[cfg(test)] mod tests { use crate::{ + type_object::PyTypeObject, types::{IntoPyDict, PyList, PyLong, PyModule}, Python, ToPyObject, }; - macro_rules! test_module { - ($py:ident, $code:literal) => { - PyModule::from_code($py, indoc::indoc!($code), file!(), "test_module") - .expect("module creation failed") - }; - } - #[test] fn test_call_for_non_existing_method() { Python::with_gil(|py| { @@ -717,14 +721,17 @@ mod tests { #[test] fn test_call_method0() { Python::with_gil(|py| { - let module = test_module!( + let module = PyModule::from_code( py, r#" - class SimpleClass: - def foo(self): - return 42 - "# - ); +class SimpleClass: + def foo(self): + return 42 +"#, + file!(), + "test_module", + ) + .expect("module creation failed"); let simple_class = module.getattr("SimpleClass").unwrap().call0().unwrap(); assert_eq!( @@ -776,10 +783,18 @@ mod tests { fn test_any_isinstance() { Python::with_gil(|py| { let x = 5.to_object(py).into_ref(py); - assert!(x.is_instance::().unwrap()); + assert!(x.is_instance_of::().unwrap()); let l = vec![x, x].to_object(py).into_ref(py); - assert!(l.is_instance::().unwrap()); + assert!(l.is_instance_of::().unwrap()); + }); + } + + #[test] + fn test_any_isinstance_of() { + Python::with_gil(|py| { + let l = vec![1u8, 2].to_object(py).into_ref(py); + assert!(l.is_instance(PyList::type_object(py)).unwrap()); }); } } diff --git a/src/types/bytearray.rs b/src/types/bytearray.rs index 29750a5c..a63dd4db 100644 --- a/src/types/bytearray.rs +++ b/src/types/bytearray.rs @@ -241,7 +241,7 @@ mod tests { fn test_from_err() { Python::with_gil(|py| { if let Err(err) = PyByteArray::from(py, &py.None()) { - assert!(err.is_instance::(py)); + assert!(err.is_instance_of::(py)); } else { panic!("error"); } @@ -293,7 +293,7 @@ mod tests { assert!(py_bytearray_result .err() .unwrap() - .is_instance::(py)); + .is_instance_of::(py)); }) } } diff --git a/src/types/bytes.rs b/src/types/bytes.rs index af61a6fd..e733582b 100644 --- a/src/types/bytes.rs +++ b/src/types/bytes.rs @@ -174,7 +174,7 @@ mod tests { assert!(py_bytes_result .err() .unwrap() - .is_instance::(py)); + .is_instance_of::(py)); }); } } diff --git a/src/types/capsule.rs b/src/types/capsule.rs new file mode 100644 index 00000000..931a5355 --- /dev/null +++ b/src/types/capsule.rs @@ -0,0 +1,412 @@ +// Copyright (c) 2017-present PyO3 Project and Contributors +use crate::Python; +use crate::{ffi, AsPyPointer, PyAny}; +use crate::{pyobject_native_type_core, PyErr, PyResult}; +use std::ffi::{c_void, CStr}; +use std::os::raw::c_int; + +/// Represents a Python Capsule +/// as described in [Capsules](https://docs.python.org/3/c-api/capsule.html#capsules): +/// > This subtype of PyObject represents an opaque value, useful for C extension +/// > modules who need to pass an opaque value (as a void* pointer) through Python +/// > code to other C code. It is often used to make a C function pointer defined +/// > in one module available to other modules, so the regular import mechanism can +/// > be used to access C APIs defined in dynamically loaded modules. +/// +/// +/// # Example +/// ``` +/// use std::ffi::CString; +/// use pyo3::{prelude::*, types::PyCapsule}; +/// +/// #[repr(C)] +/// struct Foo { +/// pub val: u32, +/// } +/// +/// let r = Python::with_gil(|py| -> PyResult<()> { +/// let foo = Foo { val: 123 }; +/// let name = CString::new("builtins.capsule").unwrap(); +/// +/// let capsule = PyCapsule::new(py, foo, name.as_ref())?; +/// +/// let module = PyModule::import(py, "builtins")?; +/// module.add("capsule", capsule)?; +/// +/// let cap: &Foo = unsafe { PyCapsule::import(py, name.as_ref())? }; +/// assert_eq!(cap.val, 123); +/// Ok(()) +/// }); +/// assert!(r.is_ok()); +/// ``` +#[repr(transparent)] +pub struct PyCapsule(PyAny); + +pyobject_native_type_core!(PyCapsule, ffi::PyCapsule_Type, #checkfunction=ffi::PyCapsule_CheckExact); + +impl PyCapsule { + /// Constructs a new capsule whose contents are `value`, associated with `name`. + /// `name` is the identifier for the capsule; if it is stored as an attribute of a module, + /// the name should be in the format `"modulename.attribute"`. + /// + /// It is checked at compile time that the type T is not zero-sized. Rust function items + /// need to be cast to a function pointer (`fn(args) -> result`) to be put into a capsule. + /// + /// # Example + /// + /// ``` + /// use pyo3::{prelude::*, types::PyCapsule}; + /// use std::ffi::CString; + /// + /// Python::with_gil(|py| { + /// let name = CString::new("foo").unwrap(); + /// let capsule = PyCapsule::new(py, 123_u32, &name).unwrap(); + /// let val = unsafe { capsule.reference::() }; + /// assert_eq!(*val, 123); + /// }); + /// ``` + /// + /// However, attempting to construct a `PyCapsule` with a zero-sized type will not compile: + /// + /// ```compile_fail + /// use pyo3::{prelude::*, types::PyCapsule}; + /// use std::ffi::CString; + /// + /// Python::with_gil(|py| { + /// let name = CString::new("foo").unwrap(); + /// let capsule = PyCapsule::new(py, (), &name).unwrap(); // Oops! `()` is zero sized! + /// }); + /// ``` + pub fn new<'py, T: 'static + Send + AssertNotZeroSized>( + py: Python<'py>, + value: T, + name: &CStr, + ) -> PyResult<&'py Self> { + Self::new_with_destructor(py, value, name, |_, _| {}) + } + + /// Constructs a new capsule whose contents are `value`, associated with `name`. + /// + /// Also provides a destructor: when the `PyCapsule` is destroyed, it will be passed the original object, + /// as well as a `*mut c_void` which will point to the capsule's context, if any. + pub fn new_with_destructor< + 'py, + T: 'static + Send + AssertNotZeroSized, + F: FnOnce(T, *mut c_void), + >( + py: Python<'py>, + value: T, + name: &CStr, + destructor: F, + ) -> PyResult<&'py Self> { + AssertNotZeroSized::assert_not_zero_sized(&value); + let val = Box::new(CapsuleContents { value, destructor }); + + let cap_ptr = unsafe { + ffi::PyCapsule_New( + Box::into_raw(val) as *mut c_void, + name.as_ptr(), + Some(capsule_destructor::), + ) + }; + unsafe { py.from_owned_ptr_or_err(cap_ptr) } + } + + /// Imports an existing capsule. + /// + /// The `name` should match the path to the module attribute exactly in the form + /// of `"module.attribute"`, which should be the same as the name within the capsule. + /// + /// # Safety + /// + /// It must be known that this capsule's value pointer is to an item of type `T`. + pub unsafe fn import<'py, T>(py: Python<'py>, name: &CStr) -> PyResult<&'py T> { + let ptr = ffi::PyCapsule_Import(name.as_ptr(), false as c_int); + if ptr.is_null() { + Err(PyErr::fetch(py)) + } else { + Ok(&*(ptr as *const T)) + } + } + + /// Sets the context pointer in the capsule. + /// + /// # Notes + /// + /// The context is treated much like the value of the capsule, but should likely act as + /// a place to store any state management when using the capsule. + /// + /// If you want to store a Rust value as the context, and drop it from the destructor, use + /// `Box::into_raw` to convert it into a pointer, see the example. + /// + /// # Example + /// + /// ``` + /// use std::ffi::CString; + /// use std::sync::mpsc::{channel, Sender}; + /// use libc::c_void; + /// use pyo3::{prelude::*, types::PyCapsule}; + /// + /// let (tx, rx) = channel::(); + /// + /// fn destructor(val: u32, context: *mut c_void) { + /// let ctx = unsafe { *Box::from_raw(context as *mut Sender) }; + /// ctx.send("Destructor called!".to_string()).unwrap(); + /// } + /// + /// Python::with_gil(|py| { + /// let name = CString::new("foo").unwrap(); + /// let capsule = + /// PyCapsule::new_with_destructor(py, 123, &name, destructor as fn(u32, *mut c_void)) + /// .unwrap(); + /// let context = Box::new(tx); // `Sender` is our context, box it up and ship it! + /// capsule.set_context(py, Box::into_raw(context) as *mut c_void).unwrap(); + /// // This scope will end, causing our destructor to be called... + /// }); + /// + /// assert_eq!(rx.recv(), Ok("Destructor called!".to_string())); + /// ``` + #[allow(clippy::not_unsafe_ptr_arg_deref)] + pub fn set_context(&self, 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)) + } else { + Ok(()) + } + } + + /// 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> { + let ctx = unsafe { ffi::PyCapsule_GetContext(self.as_ptr()) }; + if ctx.is_null() && self.is_valid() && PyErr::occurred(py) { + Err(PyErr::fetch(py)) + } else { + Ok(ctx) + } + } + + /// Obtains a reference to the value of this capsule. + /// + /// # Safety + /// + /// It must be known that this capsule's pointer is to an item of type `T`. + pub unsafe fn reference(&self) -> &T { + &*(self.pointer() as *const T) + } + + /// Gets the raw `c_void` pointer to the value in this capsule. + pub fn pointer(&self) -> *mut c_void { + unsafe { ffi::PyCapsule_GetPointer(self.0.as_ptr(), self.name().as_ptr()) } + } + + /// Checks if this is a valid capsule. + pub fn is_valid(&self) -> bool { + let r = unsafe { ffi::PyCapsule_IsValid(self.as_ptr(), self.name().as_ptr()) } as u8; + r != 0 + } + + /// Retrieves the name of this capsule. + pub fn name(&self) -> &CStr { + unsafe { + let ptr = ffi::PyCapsule_GetName(self.as_ptr()); + CStr::from_ptr(ptr) + } + } +} + +// C layout, as PyCapsule::get_reference depends on `T` being first. +#[repr(C)] +struct CapsuleContents { + value: T, + destructor: D, +} + +// Wrapping ffi::PyCapsule_Destructor for a user supplied FnOnce(T) for capsule destructor +unsafe extern "C" fn capsule_destructor( + capsule: *mut ffi::PyObject, +) { + let ptr = ffi::PyCapsule_GetPointer(capsule, ffi::PyCapsule_GetName(capsule)); + let ctx = ffi::PyCapsule_GetContext(capsule); + let CapsuleContents { value, destructor } = *Box::from_raw(ptr as *mut CapsuleContents); + destructor(value, ctx) +} + +/// Guarantee `T` is not zero sized at compile time. +// credit: `` +#[doc(hidden)] +pub trait AssertNotZeroSized: Sized { + const _CONDITION: usize = (std::mem::size_of::() == 0) as usize; + const _CHECK: &'static str = + ["PyCapsule value type T must not be zero-sized!"][Self::_CONDITION]; + #[allow(path_statements, clippy::no_effect)] + fn assert_not_zero_sized(&self) { + ::_CHECK; + } +} + +impl AssertNotZeroSized for T {} + +#[cfg(test)] +mod tests { + use libc::c_void; + + use crate::prelude::PyModule; + use crate::{types::PyCapsule, Py, PyResult, Python}; + use std::ffi::CString; + use std::sync::mpsc::{channel, Sender}; + + #[test] + fn test_pycapsule_struct() -> PyResult<()> { + #[repr(C)] + struct Foo { + pub val: u32, + } + + impl Foo { + fn get_val(&self) -> u32 { + self.val + } + } + + Python::with_gil(|py| -> PyResult<()> { + let foo = Foo { val: 123 }; + let name = CString::new("foo").unwrap(); + + let cap = PyCapsule::new(py, foo, &name)?; + assert!(cap.is_valid()); + + let foo_capi = unsafe { cap.reference::() }; + assert_eq!(foo_capi.val, 123); + assert_eq!(foo_capi.get_val(), 123); + assert_eq!(cap.name(), name.as_ref()); + Ok(()) + }) + } + + #[test] + fn test_pycapsule_func() { + fn foo(x: u32) -> u32 { + x + } + + let cap: Py = Python::with_gil(|py| { + let name = CString::new("foo").unwrap(); + let cap = PyCapsule::new(py, foo as fn(u32) -> u32, &name).unwrap(); + cap.into() + }); + + Python::with_gil(|py| { + let f = unsafe { cap.as_ref(py).reference:: u32>() }; + assert_eq!(f(123), 123); + }); + } + + #[test] + fn test_pycapsule_context() -> PyResult<()> { + Python::with_gil(|py| { + let name = CString::new("foo").unwrap(); + let cap = PyCapsule::new(py, 0, &name)?; + + let c = cap.get_context(py)?; + assert!(c.is_null()); + + let ctx = Box::new(123_u32); + cap.set_context(py, Box::into_raw(ctx) as _)?; + + let ctx_ptr: *mut c_void = cap.get_context(py)?; + let ctx = unsafe { *Box::from_raw(ctx_ptr as *mut u32) }; + assert_eq!(ctx, 123); + Ok(()) + }) + } + + #[test] + fn test_pycapsule_import() -> PyResult<()> { + #[repr(C)] + struct Foo { + pub val: u32, + } + + Python::with_gil(|py| -> PyResult<()> { + let foo = Foo { val: 123 }; + let name = CString::new("builtins.capsule").unwrap(); + + let capsule = PyCapsule::new(py, foo, &name)?; + + let module = PyModule::import(py, "builtins")?; + module.add("capsule", capsule)?; + + // check error when wrong named passed for capsule. + let wrong_name = CString::new("builtins.non_existant").unwrap(); + let result: PyResult<&Foo> = unsafe { PyCapsule::import(py, wrong_name.as_ref()) }; + assert!(result.is_err()); + + // corret name is okay. + let cap: &Foo = unsafe { PyCapsule::import(py, name.as_ref())? }; + assert_eq!(cap.val, 123); + Ok(()) + }) + } + + #[test] + fn test_vec_storage() { + let cap: Py = Python::with_gil(|py| { + let name = CString::new("foo").unwrap(); + + let stuff: Vec = vec![1, 2, 3, 4]; + let cap = PyCapsule::new(py, stuff, &name).unwrap(); + + cap.into() + }); + + Python::with_gil(|py| { + let ctx: &Vec = unsafe { cap.as_ref(py).reference() }; + assert_eq!(ctx, &[1, 2, 3, 4]); + }) + } + + #[test] + fn test_vec_context() { + let context: Vec = vec![1, 2, 3, 4]; + + let cap: Py = Python::with_gil(|py| { + let name = CString::new("foo").unwrap(); + let cap = PyCapsule::new(py, 0, &name).unwrap(); + cap.set_context(py, Box::into_raw(Box::new(&context)) as _) + .unwrap(); + + cap.into() + }); + + Python::with_gil(|py| { + let ctx_ptr: *mut c_void = cap.as_ref(py).get_context(py).unwrap(); + let ctx = unsafe { *Box::from_raw(ctx_ptr as *mut &Vec) }; + assert_eq!(ctx, &vec![1_u8, 2, 3, 4]); + }) + } + + #[test] + fn test_pycapsule_destructor() { + let (tx, rx) = channel::(); + + fn destructor(_val: u32, ctx: *mut c_void) { + assert!(!ctx.is_null()); + let context = unsafe { *Box::from_raw(ctx as *mut Sender) }; + context.send(true).unwrap(); + } + + Python::with_gil(|py| { + let name = CString::new("foo").unwrap(); + let cap = + PyCapsule::new_with_destructor(py, 0, &name, destructor as fn(u32, *mut c_void)) + .unwrap(); + cap.set_context(py, Box::into_raw(Box::new(tx)) as _) + .unwrap(); + }); + + // the destructor was called. + assert_eq!(rx.recv(), Ok(true)); + } +} diff --git a/src/types/iterator.rs b/src/types/iterator.rs index a2518732..02d437c8 100644 --- a/src/types/iterator.rs +++ b/src/types/iterator.rs @@ -113,7 +113,6 @@ mod tests { #[cfg(any(not(Py_LIMITED_API), Py_3_8))] use crate::PyTryFrom; use crate::{Py, PyAny, Python, ToPyObject}; - use indoc::indoc; #[test] fn vec_iter() { @@ -177,16 +176,14 @@ mod tests { #[test] fn fibonacci_generator() { - let fibonacci_generator = indoc!( - r#" - def fibonacci(target): - a = 1 - b = 1 - for _ in range(target): - yield a - a, b = b, a + b - "# - ); + let fibonacci_generator = r#" +def fibonacci(target): + a = 1 + b = 1 + for _ in range(target): + yield a + a, b = b, a + b +"#; Python::with_gil(|py| { let context = PyDict::new(py); @@ -206,7 +203,7 @@ mod tests { let x = 5.to_object(py); let err = PyIterator::from_object(py, &x).unwrap_err(); - assert!(err.is_instance::(py)); + assert!(err.is_instance_of::(py)); }); } diff --git a/src/types/mapping.rs b/src/types/mapping.rs index 651bcc56..0ff430a0 100644 --- a/src/types/mapping.rs +++ b/src/types/mapping.rs @@ -180,7 +180,7 @@ mod tests { assert!(mapping .get_item(8i32) .unwrap_err() - .is_instance::(py)); + .is_instance_of::(py)); }); } @@ -216,7 +216,7 @@ mod tests { assert!(mapping .get_item(7i32) .unwrap_err() - .is_instance::(py)); + .is_instance_of::(py)); }); } diff --git a/src/types/mod.rs b/src/types/mod.rs index db09044a..cbbeafb5 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -6,6 +6,7 @@ pub use self::any::PyAny; pub use self::boolobject::PyBool; pub use self::bytearray::PyByteArray; pub use self::bytes::PyBytes; +pub use self::capsule::PyCapsule; pub use self::complex::PyComplex; #[cfg(not(Py_LIMITED_API))] pub use self::datetime::{ @@ -26,6 +27,7 @@ pub use self::slice::{PySlice, PySliceIndices}; #[cfg(all(not(Py_LIMITED_API), target_endian = "little"))] pub use self::string::PyStringData; pub use self::string::{PyString, PyString as PyUnicode}; +pub use self::traceback::PyTraceback; pub use self::tuple::PyTuple; pub use self::typeobject::PyType; @@ -222,6 +224,7 @@ mod any; mod boolobject; mod bytearray; mod bytes; +mod capsule; mod complex; #[cfg(not(Py_LIMITED_API))] mod datetime; @@ -237,5 +240,6 @@ mod sequence; mod set; mod slice; mod string; +mod traceback; mod tuple; mod typeobject; diff --git a/src/types/module.rs b/src/types/module.rs index e3cbcf16..8330f29d 100644 --- a/src/types/module.rs +++ b/src/types/module.rs @@ -8,9 +8,9 @@ use crate::exceptions; use crate::ffi; use crate::pyclass::PyClass; use crate::type_object::PyTypeObject; +use crate::types::PyCFunction; use crate::types::{PyAny, PyDict, PyList}; -use crate::types::{PyCFunction, PyTuple}; -use crate::{AsPyPointer, IntoPy, Py, PyObject, Python}; +use crate::{AsPyPointer, IntoPy, PyObject, Python}; use std::ffi::{CStr, CString}; use std::str; @@ -146,7 +146,7 @@ impl PyModule { match self.getattr("__all__") { Ok(idx) => idx.downcast().map_err(PyErr::from), Err(err) => { - if err.is_instance::(self.py()) { + if err.is_instance_of::(self.py()) { let l = PyList::empty(self.py()); self.setattr("__all__", l).map_err(PyErr::from)?; Ok(l) @@ -367,46 +367,6 @@ impl PyModule { let name = fun.getattr("__name__")?.extract()?; self.add(name, fun) } - - /// Calls a function in the module. - /// - /// This is equivalent to the Python expression `module.name(*args, **kwargs)`. - #[deprecated( - since = "0.14.0", - note = "use getattr(name)?.call(args, kwargs) instead" - )] - pub fn call( - &self, - name: &str, - args: impl IntoPy>, - kwargs: Option<&PyDict>, - ) -> PyResult<&PyAny> { - self.getattr(name)?.call(args, kwargs) - } - - /// Calls a function in the module with only positional arguments. - /// - /// This is equivalent to the Python expression `module.name(*args)`. - #[deprecated(since = "0.14.0", note = "use getattr(name)?.call1(args) instead")] - pub fn call1(&self, name: &str, args: impl IntoPy>) -> PyResult<&PyAny> { - self.getattr(name)?.call1(args) - } - - /// Calls a function in the module without arguments. - /// - /// This is equivalent to the Python expression `module.name()`. - #[deprecated(since = "0.14.0", note = "use getattr(name)?.call0() instead")] - pub fn call0(&self, name: &str) -> PyResult<&PyAny> { - self.getattr(name)?.call0() - } - - /// Gets a member from the module. - /// - /// This is equivalent to the Python expression `module.name`. - #[deprecated(since = "0.14.0", note = "use getattr(name) instead")] - pub fn get(&self, name: &str) -> PyResult<&PyAny> { - self.getattr(name) - } } #[cfg(test)] diff --git a/src/types/num.rs b/src/types/num.rs index c9e05eb0..60e4c695 100644 --- a/src/types/num.rs +++ b/src/types/num.rs @@ -278,6 +278,7 @@ fn err_if_invalid_value( #[cfg(test)] mod test_128bit_intergers { use super::*; + use crate::types::PyDict; #[cfg(not(target_arch = "wasm32"))] use proptest::prelude::*; @@ -288,7 +289,9 @@ mod test_128bit_intergers { fn test_i128_roundtrip(x: i128) { Python::with_gil(|py| { let x_py = x.into_py(py); - crate::py_run!(py, x_py, &format!("assert x_py == {}", x)); + let locals = PyDict::new(py); + locals.set_item("x_py", x_py.clone_ref(py)).unwrap(); + py.run(&format!("assert x_py == {}", x), None, Some(locals)).unwrap(); let roundtripped: i128 = x_py.extract(py).unwrap(); assert_eq!(x, roundtripped); }) @@ -301,7 +304,9 @@ mod test_128bit_intergers { fn test_u128_roundtrip(x: u128) { Python::with_gil(|py| { let x_py = x.into_py(py); - crate::py_run!(py, x_py, &format!("assert x_py == {}", x)); + let locals = PyDict::new(py); + locals.set_item("x_py", x_py.clone_ref(py)).unwrap(); + py.run(&format!("assert x_py == {}", x), None, Some(locals)).unwrap(); let roundtripped: u128 = x_py.extract(py).unwrap(); assert_eq!(x, roundtripped); }) @@ -345,7 +350,7 @@ mod test_128bit_intergers { Python::with_gil(|py| { let obj = py.eval("(1 << 130) * -1", None, None).unwrap(); let err = obj.extract::().unwrap_err(); - assert!(err.is_instance::(py)); + assert!(err.is_instance_of::(py)); }) } @@ -354,7 +359,7 @@ mod test_128bit_intergers { Python::with_gil(|py| { let obj = py.eval("1 << 130", None, None).unwrap(); let err = obj.extract::().unwrap_err(); - assert!(err.is_instance::(py)); + assert!(err.is_instance_of::(py)); }) } } @@ -421,7 +426,7 @@ mod tests { let obj = ("123").to_object(py); let err = obj.extract::<$t>(py).unwrap_err(); - assert!(err.is_instance::(py)); + assert!(err.is_instance_of::(py)); }); } @@ -431,7 +436,7 @@ mod tests { let obj = (12.3).to_object(py); let err = obj.extract::<$t>(py).unwrap_err(); - assert!(err.is_instance::(py));}); + assert!(err.is_instance_of::(py));}); } #[test] diff --git a/src/types/string.rs b/src/types/string.rs index 4c5ff52d..22496e26 100644 --- a/src/types/string.rs +++ b/src/types/string.rs @@ -73,7 +73,7 @@ impl<'a> PyStringData<'a> { match self { Self::Ucs1(data) => match str::from_utf8(data) { Ok(s) => Ok(Cow::Borrowed(s)), - Err(e) => Err(crate::PyErr::from_instance(PyUnicodeDecodeError::new_utf8( + Err(e) => Err(crate::PyErr::from_value(PyUnicodeDecodeError::new_utf8( py, data, e, )?)), }, @@ -83,7 +83,7 @@ impl<'a> PyStringData<'a> { let mut message = e.to_string().as_bytes().to_vec(); message.push(0); - Err(crate::PyErr::from_instance(PyUnicodeDecodeError::new( + Err(crate::PyErr::from_value(PyUnicodeDecodeError::new( py, CStr::from_bytes_with_nul(b"utf-16\0").unwrap(), self.as_bytes(), @@ -94,7 +94,7 @@ impl<'a> PyStringData<'a> { }, Self::Ucs4(data) => match data.iter().map(|&c| std::char::from_u32(c)).collect() { Some(s) => Ok(Cow::Owned(s)), - None => Err(crate::PyErr::from_instance(PyUnicodeDecodeError::new( + None => Err(crate::PyErr::from_value(PyUnicodeDecodeError::new( py, CStr::from_bytes_with_nul(b"utf-32\0").unwrap(), self.as_bytes(), @@ -227,8 +227,9 @@ impl PyString { pub unsafe fn data(&self) -> PyResult> { let ptr = self.as_ptr(); - if cfg!(not(Py_3_12)) { - #[allow(deprecated)] + #[cfg(not(Py_3_12))] + #[allow(deprecated)] + { let ready = ffi::PyUnicode_READY(ptr); if ready != 0 { // Exception was created on failure. @@ -503,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.ptype(py), PyUnicodeDecodeError::type_object(py)); + assert_eq!(err.get_type(py), PyUnicodeDecodeError::type_object(py)); assert!(err .to_string() .contains("'utf-8' codec can't decode byte 0xfe in position 1")); @@ -545,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.ptype(py), PyUnicodeDecodeError::type_object(py)); + assert_eq!(err.get_type(py), PyUnicodeDecodeError::type_object(py)); assert!(err .to_string() .contains("'utf-16' codec can't decode bytes in position 0-3")); @@ -584,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.ptype(py), PyUnicodeDecodeError::type_object(py)); + assert_eq!(err.get_type(py), 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/traceback.rs b/src/types/traceback.rs new file mode 100644 index 00000000..6d799a70 --- /dev/null +++ b/src/types/traceback.rs @@ -0,0 +1,82 @@ +// Copyright (c) 2017-present PyO3 Project and Contributors + +use crate::err::{error_on_minusone, PyResult}; +use crate::ffi; +use crate::types::PyString; +use crate::{AsPyPointer, PyAny}; + +/// Represents a Python traceback. +#[repr(transparent)] +pub struct PyTraceback(PyAny); + +pyobject_native_type_core!( + PyTraceback, + ffi::PyTraceBack_Type, + #checkfunction=ffi::PyTraceBack_Check +); + +impl PyTraceback { + /// Formats the traceback as a string. + /// + /// This does not include the exception type and value. The exception type and value can be + /// formatted using the `Display` implementation for `PyErr`. + /// + /// # Example + /// + /// The following code formats a Python traceback and exception pair from Rust: + /// + /// ```rust + /// # use pyo3::{Python, PyResult}; + /// # let result: PyResult<()> = + /// Python::with_gil(|py| { + /// let err = py + /// .run("raise Exception('banana')", None, None) + /// .expect_err("raise will create a Python error"); + /// + /// let traceback = err.traceback(py).expect("raised exception will have a traceback"); + /// assert_eq!( + /// format!("{}{}", traceback.format()?, err), + /// "\ + /// Traceback (most recent call last): + /// File \"\", line 1, in + /// Exception: banana\ + /// " + /// ); + /// Ok(()) + /// }) + /// # ; + /// # result.expect("example failed"); + /// ``` + pub fn format(&self) -> PyResult { + let py = self.py(); + let string_io = py.import("io")?.getattr("StringIO")?.call0()?; + let result = unsafe { ffi::PyTraceBack_Print(self.as_ptr(), string_io.as_ptr()) }; + error_on_minusone(py, result)?; + let formatted = string_io + .getattr("getvalue")? + .call0()? + .downcast::()? + .to_str()? + .to_owned(); + Ok(formatted) + } +} + +#[cfg(test)] +mod tests { + use crate::Python; + + #[test] + fn format_traceback() { + Python::with_gil(|py| { + let err = py + .run("raise Exception('banana')", None, None) + .expect_err("raising should have given us an error"); + + assert_eq!( + err.traceback(py).unwrap().format().unwrap(), + "Traceback (most recent call last):\n File \"\", line 1, in \n" + ); + }) + } +} diff --git a/src/types/typeobject.rs b/src/types/typeobject.rs index 09a1f8f2..8156a4f3 100644 --- a/src/types/typeobject.rs +++ b/src/types/typeobject.rs @@ -40,25 +40,72 @@ impl PyType { self.getattr("__qualname__")?.extract() } - /// Checks whether `self` is subclass of type `T`. + /// Checks whether `self` is a subclass of `other`. /// - /// Equivalent to Python's `issubclass` function. - pub fn is_subclass(&self) -> PyResult - where - T: PyTypeObject, - { - let result = - unsafe { ffi::PyObject_IsSubclass(self.as_ptr(), T::type_object(self.py()).as_ptr()) }; + /// Equivalent to the Python expression `issubclass(self, other)`. + pub fn is_subclass(&self, other: &PyType) -> PyResult { + let result = unsafe { ffi::PyObject_IsSubclass(self.as_ptr(), other.as_ptr()) }; err::error_on_minusone(self.py(), result)?; Ok(result == 1) } - /// Check whether `obj` is an instance of `self`. + /// Checks whether `self` is a subclass of type `T`. /// - /// Equivalent to Python's `isinstance` function. + /// Equivalent to the Python expression `issubclass(self, T)`, if the type + /// `T` is known at compile time. + pub fn is_subclass_of(&self) -> PyResult + where + T: PyTypeObject, + { + self.is_subclass(T::type_object(self.py())) + } + + #[deprecated( + since = "0.16.0", + note = "prefer obj.is_instance(type) to typ.is_instance(obj)" + )] + /// Equivalent to Python's `isinstance(obj, self)`. + /// + /// This function has been deprecated because it has inverted argument ordering compared to + /// other `is_instance` functions in PyO3 such as [`PyAny::is_instance`]. pub fn is_instance(&self, obj: &T) -> PyResult { - let result = unsafe { ffi::PyObject_IsInstance(obj.as_ptr(), self.as_ptr()) }; - err::error_on_minusone(self.py(), result)?; - Ok(result == 1) + let any: &PyAny = unsafe { self.py().from_borrowed_ptr(obj.as_ptr()) }; + any.is_instance(self) + } +} + +#[cfg(test)] +mod tests { + use crate::{ + type_object::PyTypeObject, + types::{PyBool, PyLong}, + Python, + }; + + #[test] + fn test_type_is_subclass() { + Python::with_gil(|py| { + let bool_type = PyBool::type_object(py); + let long_type = PyLong::type_object(py); + assert!(bool_type.is_subclass(long_type).unwrap()); + }); + } + + #[test] + fn test_type_is_subclass_of() { + Python::with_gil(|py| { + assert!(PyBool::type_object(py).is_subclass_of::().unwrap()); + }); + } + + #[test] + #[allow(deprecated)] + fn type_is_instance() { + Python::with_gil(|py| { + let bool_object = PyBool::new(py, false); + let bool_type = bool_object.get_type(); + assert!(bool_type.is_instance(bool_object).unwrap()); + assert!(bool_object.is_instance(bool_type).unwrap()); + }) } } diff --git a/tests/hygiene/misc.rs b/tests/hygiene/misc.rs deleted file mode 100644 index abd0cc6a..00000000 --- a/tests/hygiene/misc.rs +++ /dev/null @@ -1,25 +0,0 @@ -#![no_implicit_prelude] - -#[derive(::pyo3::FromPyObject)] -struct Derive1(i32); // newtype case - -#[derive(::pyo3::FromPyObject)] -#[allow(dead_code)] -struct Derive2(i32, i32); // tuple case - -#[derive(::pyo3::FromPyObject)] -#[allow(dead_code)] -struct Derive3 { - f: i32, - g: i32, -} // struct case - -#[derive(::pyo3::FromPyObject)] -#[allow(dead_code)] -enum Derive4 { - A(i32), - B { f: i32 }, -} // enum case - -::pyo3::create_exception!(mymodule, CustomError, ::pyo3::exceptions::PyException); -::pyo3::import_exception!(socket, gaierror); diff --git a/tests/hygiene/pyfunction.rs b/tests/hygiene/pyfunction.rs deleted file mode 100644 index ba1402cd..00000000 --- a/tests/hygiene/pyfunction.rs +++ /dev/null @@ -1,15 +0,0 @@ -#![no_implicit_prelude] -#![allow(unused_variables)] - -#[::pyo3::pyfunction] -fn do_something(x: i32) -> ::pyo3::PyResult { - ::std::result::Result::Ok(x) -} - -#[test] -fn invoke_wrap_pyfunction() { - ::pyo3::Python::with_gil(|py| { - let func = ::pyo3::wrap_pyfunction!(do_something)(py).unwrap(); - ::pyo3::py_run!(py, func, r#"func(5)"#); - }); -} diff --git a/tests/hygiene/pymodule.rs b/tests/hygiene/pymodule.rs deleted file mode 100644 index 8bb5e19e..00000000 --- a/tests/hygiene/pymodule.rs +++ /dev/null @@ -1,20 +0,0 @@ -#![no_implicit_prelude] -#![allow(unused_variables)] - -#[::pyo3::pyfunction] -fn do_something(x: i32) -> ::pyo3::PyResult { - ::std::result::Result::Ok(x) -} - -#[::pyo3::pymodule] -fn foo(_py: ::pyo3::Python, _m: &::pyo3::types::PyModule) -> ::pyo3::PyResult<()> { - ::std::result::Result::Ok(()) -} - -#[::pyo3::pymodule] -fn my_module(_py: ::pyo3::Python, m: &::pyo3::types::PyModule) -> ::pyo3::PyResult<()> { - m.add_function(::pyo3::wrap_pyfunction!(do_something, m)?)?; - m.add_wrapped(::pyo3::wrap_pymodule!(foo))?; - - ::std::result::Result::Ok(()) -} diff --git a/tests/hygiene/pyproto.rs b/tests/hygiene/pyproto.rs deleted file mode 100644 index 9f6b0af8..00000000 --- a/tests/hygiene/pyproto.rs +++ /dev/null @@ -1,58 +0,0 @@ -#![no_implicit_prelude] -#![allow(unused_variables)] - -#[::pyo3::pyclass] -#[derive(::std::clone::Clone)] -pub struct Foo; - -#[::pyo3::pyclass] -pub struct Foo2; - -#[::pyo3::pyclass( - name = "ActuallyBar", - freelist = 8, - weakref, - unsendable, - gc, - subclass, - extends = ::pyo3::types::PyAny, - module = "Spam" -)] -pub struct Bar { - #[pyo3(get, set)] - a: u8, - #[pyo3(get, set)] - b: Foo, - #[pyo3(get, set)] - c: ::std::option::Option<::pyo3::Py>, -} - -#[::pyo3::pyproto] -impl ::pyo3::class::gc::PyGCProtocol for Bar { - fn __traverse__( - &self, - visit: ::pyo3::class::gc::PyVisit, - ) -> ::std::result::Result<(), ::pyo3::class::gc::PyTraverseError> { - if let ::std::option::Option::Some(obj) = &self.c { - visit.call(obj)? - } - ::std::result::Result::Ok(()) - } - - fn __clear__(&mut self) { - self.c = ::std::option::Option::None; - } -} - -#[cfg(not(Py_LIMITED_API))] -#[::pyo3::pyproto] -impl ::pyo3::class::PyBufferProtocol for Bar { - fn bf_getbuffer( - _s: ::pyo3::PyRefMut, - _v: *mut ::pyo3::ffi::Py_buffer, - _f: ::std::os::raw::c_int, - ) -> ::pyo3::PyResult<()> { - ::std::panic!("unimplemented isn't hygienic before 1.50") - } - fn bf_releasebuffer(_s: ::pyo3::PyRefMut, _v: *mut ::pyo3::ffi::Py_buffer) {} -} diff --git a/tests/test_arithmetics.rs b/tests/test_arithmetics.rs index 1faf8492..5d9e2b1a 100644 --- a/tests/test_arithmetics.rs +++ b/tests/test_arithmetics.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "macros")] + use pyo3::class::basic::CompareOp; use pyo3::prelude::*; use pyo3::py_run; diff --git a/tests/test_arithmetics_protos.rs b/tests/test_arithmetics_protos.rs index 9036d737..b57c5f49 100644 --- a/tests/test_arithmetics_protos.rs +++ b/tests/test_arithmetics_protos.rs @@ -1,4 +1,5 @@ #![allow(deprecated)] // for deprecated protocol methods +#![cfg(feature = "macros")] use pyo3::class::basic::CompareOp; use pyo3::class::*; @@ -38,10 +39,6 @@ impl PyNumberProtocol for UnaryArithmetic { fn __abs__(&self) -> Self { Self::new(self.inner.abs()) } - - fn __round__(&self, _ndigits: Option) -> Self { - Self::new(self.inner.round()) - } } #[test] @@ -53,8 +50,6 @@ fn unary_arithmetic() { py_run!(py, c, "assert repr(-c) == 'UA(-2.7)'"); py_run!(py, c, "assert repr(+c) == 'UA(2.7)'"); py_run!(py, c, "assert repr(abs(c)) == 'UA(2.7)'"); - py_run!(py, c, "assert repr(round(c)) == 'UA(3)'"); - py_run!(py, c, "assert repr(round(c, 1)) == 'UA(3)'"); } #[pyclass] diff --git a/tests/test_buffer.rs b/tests/test_buffer.rs index bcdfda1c..a5ffdbee 100644 --- a/tests/test_buffer.rs +++ b/tests/test_buffer.rs @@ -1,3 +1,4 @@ +#![cfg(feature = "macros")] #![cfg(not(Py_LIMITED_API))] use pyo3::{ diff --git a/tests/test_buffer_protocol.rs b/tests/test_buffer_protocol.rs index f7174449..76c9b608 100644 --- a/tests/test_buffer_protocol.rs +++ b/tests/test_buffer_protocol.rs @@ -1,3 +1,4 @@ +#![cfg(feature = "macros")] #![cfg(not(Py_LIMITED_API))] use pyo3::buffer::PyBuffer; diff --git a/tests/test_bytes.rs b/tests/test_bytes.rs index a9300cec..710677dd 100644 --- a/tests/test_bytes.rs +++ b/tests/test_bytes.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "macros")] + use pyo3::prelude::*; use pyo3::types::PyBytes; diff --git a/tests/test_class_attributes.rs b/tests/test_class_attributes.rs index ca6b77d9..55f0d882 100644 --- a/tests/test_class_attributes.rs +++ b/tests/test_class_attributes.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "macros")] + use pyo3::prelude::*; mod common; diff --git a/tests/test_class_basics.rs b/tests/test_class_basics.rs index 193a216b..a4906192 100644 --- a/tests/test_class_basics.rs +++ b/tests/test_class_basics.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "macros")] + use pyo3::prelude::*; use pyo3::types::PyType; use pyo3::{py_run, PyClass}; diff --git a/tests/test_class_conversion.rs b/tests/test_class_conversion.rs index d379f774..ef7b403e 100644 --- a/tests/test_class_conversion.rs +++ b/tests/test_class_conversion.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "macros")] + use pyo3::prelude::*; use pyo3::ToPyObject; diff --git a/tests/test_class_new.rs b/tests/test_class_new.rs index 65e22bdd..777a67b3 100644 --- a/tests/test_class_new.rs +++ b/tests/test_class_new.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "macros")] + use pyo3::exceptions::PyValueError; use pyo3::prelude::*; diff --git a/tests/test_compile_error.rs b/tests/test_compile_error.rs index 4751f774..ce9288fa 100644 --- a/tests/test_compile_error.rs +++ b/tests/test_compile_error.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "macros")] + #[rustversion::stable] #[test] fn test_compile_errors() { @@ -19,25 +21,19 @@ fn _test_compile_errors() { t.compile_fail("tests/ui/invalid_need_module_arg_position.rs"); t.compile_fail("tests/ui/invalid_property_args.rs"); t.compile_fail("tests/ui/invalid_pyclass_args.rs"); + t.compile_fail("tests/ui/invalid_pyclass_enum.rs"); + t.compile_fail("tests/ui/invalid_pyclass_item.rs"); t.compile_fail("tests/ui/invalid_pyfunctions.rs"); t.compile_fail("tests/ui/invalid_pymethods.rs"); t.compile_fail("tests/ui/invalid_pymethod_names.rs"); - t.compile_fail("tests/ui/invalid_argument_attributes.rs"); + t.compile_fail("tests/ui/invalid_pymodule_args.rs"); + t.compile_fail("tests/ui/missing_clone.rs"); t.compile_fail("tests/ui/reject_generics.rs"); - tests_rust_1_48(&t); tests_rust_1_49(&t); - tests_rust_1_54(&t); tests_rust_1_55(&t); tests_rust_1_56(&t); - - #[rustversion::since(1.48)] - fn tests_rust_1_48(t: &trybuild::TestCases) { - t.compile_fail("tests/ui/missing_clone.rs"); - t.compile_fail("tests/ui/wrong_aspyref_lifetimes.rs"); - } - #[rustversion::before(1.48)] - fn tests_rust_1_48(_t: &trybuild::TestCases) {} + tests_rust_1_57(&t); #[rustversion::since(1.49)] fn tests_rust_1_49(t: &trybuild::TestCases) { @@ -46,14 +42,6 @@ fn _test_compile_errors() { #[rustversion::before(1.49)] fn tests_rust_1_49(_t: &trybuild::TestCases) {} - #[rustversion::since(1.54)] - fn tests_rust_1_54(t: &trybuild::TestCases) { - t.compile_fail("tests/ui/invalid_frompy_derive.rs"); - t.compile_fail("tests/ui/static_ref.rs"); - } - #[rustversion::before(1.54)] - fn tests_rust_1_54(_t: &trybuild::TestCases) {} - #[rustversion::since(1.55)] fn tests_rust_1_55(t: &trybuild::TestCases) { t.compile_fail("tests/ui/invalid_pymethod_receiver.rs"); @@ -74,4 +62,15 @@ fn _test_compile_errors() { #[rustversion::before(1.56)] fn tests_rust_1_56(_t: &trybuild::TestCases) {} + + #[rustversion::since(1.57)] + fn tests_rust_1_57(t: &trybuild::TestCases) { + t.compile_fail("tests/ui/invalid_argument_attributes.rs"); + t.compile_fail("tests/ui/invalid_frompy_derive.rs"); + t.compile_fail("tests/ui/static_ref.rs"); + t.compile_fail("tests/ui/wrong_aspyref_lifetimes.rs"); + } + + #[rustversion::before(1.57)] + fn tests_rust_1_57(_t: &trybuild::TestCases) {} } diff --git a/tests/test_datetime.rs b/tests/test_datetime.rs index c6d62cd1..0cb5eddf 100644 --- a/tests/test_datetime.rs +++ b/tests/test_datetime.rs @@ -33,21 +33,21 @@ fn _get_subclasses<'p>( } macro_rules! assert_check_exact { - ($check_func:ident, $obj: expr) => { + ($check_func:ident, $check_func_exact:ident, $obj: expr) => { unsafe { - use pyo3::{AsPyPointer, ffi::*}; + use pyo3::{ffi::*, AsPyPointer}; assert!($check_func(($obj).as_ptr()) != 0); - assert!(pyo3::paste::expr!([<$check_func Exact>])(($obj).as_ptr()) != 0); + assert!($check_func_exact(($obj).as_ptr()) != 0); } }; } macro_rules! assert_check_only { - ($check_func:ident, $obj: expr) => { + ($check_func:ident, $check_func_exact:ident, $obj: expr) => { unsafe { - use pyo3::{AsPyPointer, ffi::*}; + use pyo3::{ffi::*, AsPyPointer}; assert!($check_func(($obj).as_ptr()) != 0); - assert!(pyo3::paste::expr!([<$check_func Exact>])(($obj).as_ptr()) == 0); + assert!($check_func_exact(($obj).as_ptr()) == 0); } }; } @@ -58,9 +58,9 @@ fn test_date_check() { let py = gil.python(); let (obj, sub_obj, sub_sub_obj) = _get_subclasses(&py, "date", "2018, 1, 1").unwrap(); - assert_check_exact!(PyDate_Check, obj); - assert_check_only!(PyDate_Check, sub_obj); - assert_check_only!(PyDate_Check, sub_sub_obj); + assert_check_exact!(PyDate_Check, PyDate_CheckExact, obj); + assert_check_only!(PyDate_Check, PyDate_CheckExact, sub_obj); + assert_check_only!(PyDate_Check, PyDate_CheckExact, sub_sub_obj); } #[test] @@ -69,9 +69,9 @@ fn test_time_check() { let py = gil.python(); let (obj, sub_obj, sub_sub_obj) = _get_subclasses(&py, "time", "12, 30, 15").unwrap(); - assert_check_exact!(PyTime_Check, obj); - assert_check_only!(PyTime_Check, sub_obj); - assert_check_only!(PyTime_Check, sub_sub_obj); + assert_check_exact!(PyTime_Check, PyTime_CheckExact, obj); + assert_check_only!(PyTime_Check, PyTime_CheckExact, sub_obj); + assert_check_only!(PyTime_Check, PyTime_CheckExact, sub_sub_obj); } #[test] @@ -82,10 +82,10 @@ fn test_datetime_check() { .map_err(|e| e.print(py)) .unwrap(); - assert_check_only!(PyDate_Check, obj); - assert_check_exact!(PyDateTime_Check, obj); - assert_check_only!(PyDateTime_Check, sub_obj); - assert_check_only!(PyDateTime_Check, sub_sub_obj); + assert_check_only!(PyDate_Check, PyDate_CheckExact, obj); + assert_check_exact!(PyDateTime_Check, PyDateTime_CheckExact, obj); + assert_check_only!(PyDateTime_Check, PyDateTime_CheckExact, sub_obj); + assert_check_only!(PyDateTime_Check, PyDateTime_CheckExact, sub_sub_obj); } #[test] @@ -94,9 +94,9 @@ fn test_delta_check() { let py = gil.python(); let (obj, sub_obj, sub_sub_obj) = _get_subclasses(&py, "timedelta", "1, -3").unwrap(); - assert_check_exact!(PyDelta_Check, obj); - assert_check_only!(PyDelta_Check, sub_obj); - assert_check_only!(PyDelta_Check, sub_sub_obj); + assert_check_exact!(PyDelta_Check, PyDelta_CheckExact, obj); + assert_check_only!(PyDelta_Check, PyDelta_CheckExact, sub_obj); + assert_check_only!(PyDelta_Check, PyDelta_CheckExact, sub_sub_obj); } #[test] diff --git a/tests/test_default_impls.rs b/tests/test_default_impls.rs new file mode 100644 index 00000000..a764c1f4 --- /dev/null +++ b/tests/test_default_impls.rs @@ -0,0 +1,43 @@ +#![cfg(feature = "macros")] + +use pyo3::prelude::*; + +mod common; + +// Test default generated __repr__. +#[pyclass] +enum TestDefaultRepr { + Var, +} + +#[test] +fn test_default_slot_exists() { + Python::with_gil(|py| { + let test_object = Py::new(py, TestDefaultRepr::Var).unwrap(); + py_assert!( + py, + test_object, + "repr(test_object) == 'TestDefaultRepr.Var'" + ); + }) +} + +#[pyclass] +enum OverrideSlot { + Var, +} + +#[pymethods] +impl OverrideSlot { + fn __repr__(&self) -> &str { + "overriden" + } +} + +#[test] +fn test_override_slot() { + Python::with_gil(|py| { + let test_object = Py::new(py, OverrideSlot::Var).unwrap(); + py_assert!(py, test_object, "repr(test_object) == 'overriden'"); + }) +} diff --git a/tests/test_enum.rs b/tests/test_enum.rs new file mode 100644 index 00000000..405e7f01 --- /dev/null +++ b/tests/test_enum.rs @@ -0,0 +1,65 @@ +#![cfg(feature = "macros")] + +use pyo3::prelude::*; +use pyo3::{py_run, wrap_pyfunction}; + +mod common; + +#[pyclass] +#[derive(Debug, PartialEq, Clone)] +pub enum MyEnum { + Variant, + OtherVariant, +} + +#[test] +fn test_enum_class_attr() { + let gil = Python::acquire_gil(); + let py = gil.python(); + let my_enum = py.get_type::(); + py_assert!(py, my_enum, "getattr(my_enum, 'Variant', None) is not None"); + py_assert!(py, my_enum, "getattr(my_enum, 'foobar', None) is None"); + py_run!(py, my_enum, "my_enum.Variant = None"); +} + +#[pyfunction] +fn return_enum() -> MyEnum { + MyEnum::Variant +} + +#[test] +#[ignore] // need to implement __eq__ +fn test_return_enum() { + let gil = Python::acquire_gil(); + let py = gil.python(); + let f = wrap_pyfunction!(return_enum)(py).unwrap(); + let mynum = py.get_type::(); + + py_run!(py, f mynum, "assert f() == mynum.Variant") +} + +#[pyfunction] +fn enum_arg(e: MyEnum) { + assert_eq!(MyEnum::OtherVariant, e) +} + +#[test] +#[ignore] // need to implement __eq__ +fn test_enum_arg() { + let gil = Python::acquire_gil(); + let py = gil.python(); + let f = wrap_pyfunction!(enum_arg)(py).unwrap(); + let mynum = py.get_type::(); + + py_run!(py, f mynum, "f(mynum.Variant)") +} + +#[test] +fn test_default_repr_correct() { + Python::with_gil(|py| { + let var1 = Py::new(py, MyEnum::Variant).unwrap(); + let var2 = Py::new(py, MyEnum::OtherVariant).unwrap(); + py_assert!(py, var1, "repr(var1) == 'MyEnum.Variant'"); + py_assert!(py, var2, "repr(var2) == 'MyEnum.OtherVariant'"); + }) +} diff --git a/tests/test_exceptions.rs b/tests/test_exceptions.rs index ad7885c2..50d0f0c7 100644 --- a/tests/test_exceptions.rs +++ b/tests/test_exceptions.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "macros")] + use pyo3::prelude::*; use pyo3::{exceptions, py_run, PyErr, PyResult}; use std::error::Error; diff --git a/tests/test_frompyobject.rs b/tests/test_frompyobject.rs index 54f6e322..18c73eba 100644 --- a/tests/test_frompyobject.rs +++ b/tests/test_frompyobject.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "macros")] + use pyo3::exceptions::PyValueError; use pyo3::prelude::*; use pyo3::types::{PyDict, PyString, PyTuple}; @@ -194,6 +196,7 @@ struct PyBaz { } #[derive(Debug, FromPyObject)] +#[allow(dead_code)] struct Baz { e: E, tup: Tuple, diff --git a/tests/test_gc.rs b/tests/test_gc.rs index 3d8f64bc..752568c7 100644 --- a/tests/test_gc.rs +++ b/tests/test_gc.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "macros")] + use pyo3::class::PyGCProtocol; use pyo3::class::PyTraverseError; use pyo3::class::PyVisit; diff --git a/tests/test_getter_setter.rs b/tests/test_getter_setter.rs index 38b9761a..685c0c79 100644 --- a/tests/test_getter_setter.rs +++ b/tests/test_getter_setter.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "macros")] + use pyo3::prelude::*; use pyo3::py_run; use pyo3::types::{IntoPyDict, PyList}; diff --git a/tests/test_hygiene.rs b/tests/test_hygiene.rs deleted file mode 100644 index 47ffa621..00000000 --- a/tests/test_hygiene.rs +++ /dev/null @@ -1,8 +0,0 @@ -mod hygiene { - mod misc; - mod pyclass; - mod pyfunction; - mod pymethods; - mod pymodule; - mod pyproto; -} diff --git a/tests/test_inheritance.rs b/tests/test_inheritance.rs index 01b124b8..b36a68b1 100644 --- a/tests/test_inheritance.rs +++ b/tests/test_inheritance.rs @@ -1,5 +1,8 @@ +#![cfg(feature = "macros")] + use pyo3::prelude::*; use pyo3::py_run; +use pyo3::type_object::PyTypeObject; use pyo3::types::IntoPyDict; @@ -102,6 +105,23 @@ fn mutation_fails() { assert_eq!(&e.to_string(), "RuntimeError: Already borrowed") } +#[test] +fn is_subclass_and_is_instance() { + let gil = Python::acquire_gil(); + let py = gil.python(); + + let sub_ty = SubClass::type_object(py); + let base_ty = BaseClass::type_object(py); + assert!(sub_ty.is_subclass_of::().unwrap()); + assert!(sub_ty.is_subclass(base_ty).unwrap()); + + let obj = PyCell::new(py, SubClass::new()).unwrap(); + assert!(obj.is_instance_of::().unwrap()); + assert!(obj.is_instance_of::().unwrap()); + assert!(obj.is_instance(sub_ty).unwrap()); + assert!(obj.is_instance(base_ty).unwrap()); +} + #[pyclass(subclass)] struct BaseClassWithResult { _val: usize, diff --git a/tests/test_macros.rs b/tests/test_macros.rs index 78d68ab5..87dcc628 100644 --- a/tests/test_macros.rs +++ b/tests/test_macros.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "macros")] + //! Ensure that pyo3 macros can be used inside macro_rules! use pyo3::prelude::*; diff --git a/tests/test_mapping.rs b/tests/test_mapping.rs index 42710094..221496b5 100644 --- a/tests/test_mapping.rs +++ b/tests/test_mapping.rs @@ -1,4 +1,4 @@ -#![allow(deprecated)] // for deprecated protocol methods +#![cfg(feature = "macros")] use std::collections::HashMap; @@ -59,16 +59,6 @@ impl PyMappingProtocol for Mapping { Ok(()) } } - - /// not an actual reversed implementation, just to demonstrate that the method is callable. - fn __reversed__(&self) -> PyObject { - let gil = Python::acquire_gil(); - self.index - .keys() - .cloned() - .collect::>() - .into_py(gil.python()) - } } /// Return a dict with `m = Mapping(['1', '2', '3'])`. @@ -117,12 +107,3 @@ fn test_delitem() { py_expect_exception!(py, *d, "del m[-1]", PyTypeError); py_expect_exception!(py, *d, "del m['4']", PyKeyError); } - -#[test] -fn test_reversed() { - let gil = Python::acquire_gil(); - let py = gil.python(); - - let d = map_dict(py); - py_assert!(py, *d, "set(reversed(m)) == {'1', '2', '3'}"); -} diff --git a/tests/test_methods.rs b/tests/test_methods.rs index 1032acf9..ac401f03 100644 --- a/tests/test_methods.rs +++ b/tests/test_methods.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "macros")] + use pyo3::prelude::*; use pyo3::py_run; use pyo3::types::{IntoPyDict, PyDict, PyList, PySet, PyString, PyTuple, PyType}; diff --git a/tests/test_module.rs b/tests/test_module.rs index 18efb5bf..e959fc13 100644 --- a/tests/test_module.rs +++ b/tests/test_module.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "macros")] + use pyo3::prelude::*; use pyo3::py_run; @@ -23,10 +25,6 @@ impl ValueClass { #[pyclass(module = "module")] struct LocatedClass {} -fn sum_as_string(a: i64, b: i64) -> String { - format!("{}", a + b) -} - #[pyfunction] /// Doubles the given value fn double(x: usize) -> usize { @@ -36,12 +34,6 @@ fn double(x: usize) -> usize { /// This module is implemented in Rust. #[pymodule] fn module_with_functions(_py: Python, m: &PyModule) -> PyResult<()> { - #![allow(deprecated)] - #[pyfn(m, "sum_as_string")] - fn function_with_deprecated_name(_py: Python, a: i64, b: i64) -> String { - sum_as_string(a, b) - } - #[pyfn(m)] #[pyo3(name = "no_parameters")] fn function_with_name() -> usize { @@ -89,7 +81,6 @@ fn test_module_with_functions() { *d, "module_with_functions.__doc__ == 'This module is implemented in Rust.'" ); - py_assert!(py, *d, "module_with_functions.sum_as_string(1, 2) == '3'"); py_assert!(py, *d, "module_with_functions.no_parameters() == 42"); py_assert!(py, *d, "module_with_functions.foo == 'bar'"); py_assert!(py, *d, "module_with_functions.AnonClass != None"); @@ -438,20 +429,6 @@ fn test_module_functions_with_module() { ); } -#[test] -#[allow(deprecated)] -fn test_module_with_deprecated_name() { - #[pymodule(custom_name)] - fn my_module(_py: Python, _m: &PyModule) -> PyResult<()> { - Ok(()) - } - - Python::with_gil(|py| { - let m = pyo3::wrap_pymodule!(custom_name)(py); - py_assert!(py, m, "m.__name__ == 'custom_name'"); - }) -} - #[test] fn test_module_doc_hidden() { #[doc(hidden)] diff --git a/tests/test_not_msrv.rs b/tests/test_not_msrv.rs index d432c980..b0d1eba8 100644 --- a/tests/test_not_msrv.rs +++ b/tests/test_not_msrv.rs @@ -1,10 +1,11 @@ +#![cfg(feature = "macros")] + //! Functionality which is not only not supported on MSRV, //! but can't even be cfg-ed out on MSRV because the compiler doesn't support //! the syntax. -// TODO(#1782) rustversion attribute can't go on modules until Rust 1.42, so this -// funky dance has to happen... +#[rustversion::since(1.54)] mod requires_1_54 { - #[rustversion::since(1.54)] + include!("not_msrv/requires_1_54.rs"); } diff --git a/tests/test_proto_methods.rs b/tests/test_proto_methods.rs index 0b6c30d4..6dd1a752 100644 --- a/tests/test_proto_methods.rs +++ b/tests/test_proto_methods.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "macros")] + use pyo3::exceptions::PyValueError; use pyo3::types::{PyList, PySlice, PyType}; use pyo3::{exceptions::PyAttributeError, prelude::*}; @@ -97,7 +99,7 @@ fn test_getattr() { assert!(example_py .getattr("other_attr") .unwrap_err() - .is_instance::(py)); + .is_instance_of::(py)); }) } @@ -548,11 +550,23 @@ fn descr_getset() { r#" class Class: counter = Counter() + +# access via type +counter = Class.counter +assert counter.count == 1 + +# access with instance directly +assert Counter.__get__(counter, Class()).count == 2 + +# access via instance c = Class() -c.counter # count += 1 -assert c.counter.count == 2 -c.counter = Counter() assert c.counter.count == 3 + +# __set__ +c.counter = Counter() +assert c.counter.count == 4 + +# __delete__ del c.counter assert c.counter.count == 1 "# diff --git a/tests/test_py36_init.rs b/tests/test_py36_init.rs deleted file mode 100644 index d4f1c8e9..00000000 --- a/tests/test_py36_init.rs +++ /dev/null @@ -1,9 +0,0 @@ -// This test checks Python initialization on python 3.6, so needs to be standalone in its own process. - -#[cfg(not(PyPy))] -#[test] -fn test_py36_init_threads() { - unsafe { pyo3::ffi::Py_InitializeEx(0) }; - pyo3::prepare_freethreaded_python(); - assert_eq!(unsafe { pyo3::ffi::PyEval_ThreadsInitialized() }, 1); -} diff --git a/tests/test_pyfunction.rs b/tests/test_pyfunction.rs index 2fe675b4..3f0113c4 100644 --- a/tests/test_pyfunction.rs +++ b/tests/test_pyfunction.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "macros")] + #[cfg(not(Py_LIMITED_API))] use pyo3::buffer::PyBuffer; use pyo3::prelude::*; diff --git a/tests/test_pyproto.rs b/tests/test_pyproto.rs index da2dc0de..85d783e2 100644 --- a/tests/test_pyproto.rs +++ b/tests/test_pyproto.rs @@ -1,8 +1,8 @@ -#![allow(deprecated)] // for deprecated protocol methods +#![cfg(feature = "macros")] use pyo3::class::{ - PyAsyncProtocol, PyContextProtocol, PyDescrProtocol, PyIterProtocol, PyMappingProtocol, - PyObjectProtocol, PySequenceProtocol, + PyAsyncProtocol, PyDescrProtocol, PyIterProtocol, PyMappingProtocol, PyObjectProtocol, + PySequenceProtocol, }; use pyo3::exceptions::{PyIndexError, PyValueError}; use pyo3::prelude::*; @@ -91,14 +91,6 @@ impl PyObjectProtocol for StringMethods { fn __repr__(&self) -> &'static str { "repr" } - - fn __format__(&self, format_spec: String) -> String { - format!("format({})", format_spec) - } - - fn __bytes__(&self) -> &'static [u8] { - b"bytes" - } } #[test] @@ -109,12 +101,6 @@ fn string_methods() { let obj = Py::new(py, StringMethods {}).unwrap(); py_assert!(py, obj, "str(obj) == 'str'"); py_assert!(py, obj, "repr(obj) == 'repr'"); - py_assert!(py, obj, "'{0:x}'.format(obj) == 'format(x)'"); - py_assert!(py, obj, "bytes(obj) == b'bytes'"); - - // Test that `__bytes__` takes no arguments (should be METH_NOARGS) - py_assert!(py, obj, "obj.__bytes__() == b'bytes'"); - py_expect_exception!(py, obj, "obj.__bytes__('unexpected argument')", PyTypeError); } #[pyclass] @@ -298,25 +284,6 @@ fn setdelitem() { assert_eq!(c.val, None); } -#[pyclass] -struct Reversed {} - -#[pyproto] -impl PyMappingProtocol for Reversed { - fn __reversed__(&self) -> &'static str { - "I am reversed" - } -} - -#[test] -fn reversed() { - let gil = Python::acquire_gil(); - let py = gil.python(); - - let c = Py::new(py, Reversed {}).unwrap(); - py_run!(py, c, "assert reversed(c) == 'I am reversed'"); -} - #[pyclass] struct Contains {} @@ -338,57 +305,6 @@ fn contains() { py_expect_exception!(py, c, "assert 'wrong type' not in c", PyTypeError); } -#[pyclass] -struct ContextManager { - exit_called: bool, -} - -#[pyproto] -impl PyContextProtocol for ContextManager { - fn __enter__(&mut self) -> i32 { - 42 - } - - fn __exit__( - &mut self, - ty: Option<&PyType>, - _value: Option<&PyAny>, - _traceback: Option<&PyAny>, - ) -> bool { - let gil = Python::acquire_gil(); - self.exit_called = true; - ty == Some(gil.python().get_type::()) - } -} - -#[test] -fn context_manager() { - let gil = Python::acquire_gil(); - let py = gil.python(); - - let c = PyCell::new(py, ContextManager { exit_called: false }).unwrap(); - py_run!(py, c, "with c as x: assert x == 42"); - { - let mut c = c.borrow_mut(); - assert!(c.exit_called); - c.exit_called = false; - } - py_run!(py, c, "with c as x: raise ValueError"); - { - let mut c = c.borrow_mut(); - assert!(c.exit_called); - c.exit_called = false; - } - py_expect_exception!( - py, - c, - "with c as x: raise NotImplementedError", - PyNotImplementedError - ); - let c = c.borrow(); - assert!(c.exit_called); -} - #[test] fn test_basics() { let gil = Python::acquire_gil(); diff --git a/tests/test_pyself.rs b/tests/test_pyself.rs index 31dec01a..7e2eb50f 100644 --- a/tests/test_pyself.rs +++ b/tests/test_pyself.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "macros")] + //! Test slf: PyRef/PyMutRef(especially, slf.into::) works use pyo3::prelude::*; use pyo3::types::{PyBytes, PyString}; diff --git a/tests/test_sequence.rs b/tests/test_sequence.rs index 0cb467ee..eebd8650 100644 --- a/tests/test_sequence.rs +++ b/tests/test_sequence.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "macros")] + use pyo3::class::PySequenceProtocol; use pyo3::exceptions::{PyIndexError, PyValueError}; use pyo3::prelude::*; diff --git a/tests/test_string.rs b/tests/test_string.rs index f1ee07e9..18bb9564 100644 --- a/tests/test_string.rs +++ b/tests/test_string.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "macros")] + use pyo3::prelude::*; mod common; diff --git a/tests/test_text_signature.rs b/tests/test_text_signature.rs index 5d537eed..47ea328f 100644 --- a/tests/test_text_signature.rs +++ b/tests/test_text_signature.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "macros")] + use pyo3::prelude::*; use pyo3::{types::PyType, wrap_pymodule, PyCell}; diff --git a/tests/test_unsendable_dict.rs b/tests/test_unsendable_dict.rs index 4222bb2c..75486185 100644 --- a/tests/test_unsendable_dict.rs +++ b/tests/test_unsendable_dict.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "macros")] + use pyo3::prelude::*; use pyo3::py_run; diff --git a/tests/test_variable_arguments.rs b/tests/test_variable_arguments.rs index 10670614..dfdbc89a 100644 --- a/tests/test_variable_arguments.rs +++ b/tests/test_variable_arguments.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "macros")] + use pyo3::prelude::*; use pyo3::types::{PyDict, PyTuple}; diff --git a/tests/test_various.rs b/tests/test_various.rs index 6e570fa2..e830d8ca 100644 --- a/tests/test_various.rs +++ b/tests/test_various.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "macros")] + use pyo3::prelude::*; use pyo3::types::{PyDict, PyTuple}; use pyo3::{py_run, PyCell}; diff --git a/tests/test_wrap_pyfunction_deduction.rs b/tests/test_wrap_pyfunction_deduction.rs index 010a76e1..4adb3975 100644 --- a/tests/test_wrap_pyfunction_deduction.rs +++ b/tests/test_wrap_pyfunction_deduction.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "macros")] + use pyo3::{prelude::*, types::PyCFunction}; #[pyfunction] diff --git a/tests/ui/abi3_nativetype_inheritance.stderr b/tests/ui/abi3_nativetype_inheritance.stderr index a90d4044..6faab088 100644 --- a/tests/ui/abi3_nativetype_inheritance.stderr +++ b/tests/ui/abi3_nativetype_inheritance.stderr @@ -6,15 +6,15 @@ error[E0277]: the trait bound `PyDict: PyClass` is not satisfied | = note: required because of the requirements on the impl of `PyClassBaseType` for `PyDict` note: required by a bound in `PyClassBaseType` - --> src/class/impl_.rs:766:1 + --> src/class/impl_.rs | -766 | / pub trait PyClassBaseType: Sized { -767 | | type Dict; -768 | | type WeakRef; -769 | | type LayoutAsBase: PyCellLayout; + | / pub trait PyClassBaseType: Sized { + | | type Dict; + | | type WeakRef; + | | type LayoutAsBase: PyCellLayout; ... | -772 | | type Initializer: PyObjectInit; -773 | | } + | | type Initializer: PyObjectInit; + | | } | |_^ required by this bound in `PyClassBaseType` = note: this error originates in the attribute macro `pyclass` (in Nightly builds, run with -Z macro-backtrace for more info) @@ -26,8 +26,8 @@ error[E0277]: the trait bound `PyDict: PyClass` is not satisfied | = note: required because of the requirements on the impl of `PyClassBaseType` for `PyDict` note: required by a bound in `ThreadCheckerInherited` - --> src/class/impl_.rs:753:47 + --> src/class/impl_.rs | -753 | pub struct ThreadCheckerInherited(PhantomData, U::ThreadChecker); + | pub struct ThreadCheckerInherited(PhantomData, U::ThreadChecker); | ^^^^^^^^^^^^^^^ required by this bound in `ThreadCheckerInherited` = note: this error originates in the attribute macro `pyclass` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/tests/ui/deprecations.rs b/tests/ui/deprecations.rs index 50236b39..5ad70f9c 100644 --- a/tests/ui/deprecations.rs +++ b/tests/ui/deprecations.rs @@ -2,28 +2,6 @@ use pyo3::prelude::*; -#[pyclass] -#[text_signature = "()"] -struct TestClass { - num: u32, -} - -#[pymethods] -impl TestClass { - #[classattr] - #[name = "num"] - const DEPRECATED_NAME_CONSTANT: i32 = 0; - - #[name = "num"] - #[text_signature = "()"] - fn deprecated_name_pymethod(&self) { } - - #[staticmethod] - #[name = "custom_static"] - #[text_signature = "()"] - fn deprecated_name_staticmethod() {} -} - #[pyclass] struct DeprecatedCall; @@ -33,23 +11,6 @@ impl DeprecatedCall { fn deprecated_call(&self) {} } -#[pyfunction] -#[name = "foo"] -#[text_signature = "()"] -fn deprecated_name_pyfunction() { } - -#[pymodule(deprecated_module_name)] -fn my_module(_py: Python, m: &PyModule) -> PyResult<()> { - #[pyfn(m, "some_name")] - #[text_signature = "()"] - fn deprecated_name_pyfn() { } - - Ok(()) -} - fn main() { } - - -// TODO: ensure name deprecated on #[pyfunction] and #[pymodule] diff --git a/tests/ui/deprecations.stderr b/tests/ui/deprecations.stderr index d0131a9c..d71c1927 100644 --- a/tests/ui/deprecations.stderr +++ b/tests/ui/deprecations.stderr @@ -1,77 +1,11 @@ -error: use of deprecated constant `pyo3::impl_::deprecations::NAME_ATTRIBUTE`: use `#[pyo3(name = "...")]` instead of `#[name = "..."]` - --> tests/ui/deprecations.rs:14:5 +error: use of deprecated constant `pyo3::impl_::deprecations::CALL_ATTRIBUTE`: use `fn __call__` instead of `#[call]` + --> tests/ui/deprecations.rs:10:7 | -14 | #[name = "num"] - | ^ +10 | #[call] + | ^^^^ | note: the lint level is defined here --> tests/ui/deprecations.rs:1:9 | 1 | #![deny(deprecated)] | ^^^^^^^^^^ - -error: use of deprecated constant `pyo3::impl_::deprecations::NAME_ATTRIBUTE`: use `#[pyo3(name = "...")]` instead of `#[name = "..."]` - --> tests/ui/deprecations.rs:17:5 - | -17 | #[name = "num"] - | ^ - -error: use of deprecated constant `pyo3::impl_::deprecations::TEXT_SIGNATURE_ATTRIBUTE`: use `#[pyo3(text_signature = "...")]` instead of `#[text_signature = "..."]` - --> tests/ui/deprecations.rs:18:5 - | -18 | #[text_signature = "()"] - | ^ - -error: use of deprecated constant `pyo3::impl_::deprecations::NAME_ATTRIBUTE`: use `#[pyo3(name = "...")]` instead of `#[name = "..."]` - --> tests/ui/deprecations.rs:22:5 - | -22 | #[name = "custom_static"] - | ^ - -error: use of deprecated constant `pyo3::impl_::deprecations::TEXT_SIGNATURE_ATTRIBUTE`: use `#[pyo3(text_signature = "...")]` instead of `#[text_signature = "..."]` - --> tests/ui/deprecations.rs:23:5 - | -23 | #[text_signature = "()"] - | ^ - -error: use of deprecated constant `pyo3::impl_::deprecations::CALL_ATTRIBUTE`: use `fn __call__` instead of `#[call]` - --> tests/ui/deprecations.rs:32:7 - | -32 | #[call] - | ^^^^ - -error: use of deprecated constant `pyo3::impl_::deprecations::NAME_ATTRIBUTE`: use `#[pyo3(name = "...")]` instead of `#[name = "..."]` - --> tests/ui/deprecations.rs:37:1 - | -37 | #[name = "foo"] - | ^ - -error: use of deprecated constant `pyo3::impl_::deprecations::TEXT_SIGNATURE_ATTRIBUTE`: use `#[pyo3(text_signature = "...")]` instead of `#[text_signature = "..."]` - --> tests/ui/deprecations.rs:38:1 - | -38 | #[text_signature = "()"] - | ^ - -error: use of deprecated constant `pyo3::impl_::deprecations::PYFN_NAME_ARGUMENT`: use `#[pyfn(m)] #[pyo3(name = "...")]` instead of `#[pyfn(m, "...")]` - --> tests/ui/deprecations.rs:43:15 - | -43 | #[pyfn(m, "some_name")] - | ^^^^^^^^^^^ - -error: use of deprecated constant `pyo3::impl_::deprecations::TEXT_SIGNATURE_ATTRIBUTE`: use `#[pyo3(text_signature = "...")]` instead of `#[text_signature = "..."]` - --> tests/ui/deprecations.rs:44:5 - | -44 | #[text_signature = "()"] - | ^ - -error: use of deprecated constant `pyo3::impl_::deprecations::PYMODULE_NAME_ARGUMENT`: use `#[pymodule] #[pyo3(name = "...")]` instead of `#[pymodule(...)]` - --> tests/ui/deprecations.rs:41:12 - | -41 | #[pymodule(deprecated_module_name)] - | ^^^^^^^^^^^^^^^^^^^^^^ - -error: use of deprecated constant `pyo3::impl_::deprecations::TEXT_SIGNATURE_ATTRIBUTE`: use `#[pyo3(text_signature = "...")]` instead of `#[text_signature = "..."]` - --> tests/ui/deprecations.rs:6:1 - | -6 | #[text_signature = "()"] - | ^ diff --git a/tests/ui/invalid_argument_attributes.stderr b/tests/ui/invalid_argument_attributes.stderr index dd830a3e..ed6479d5 100644 --- a/tests/ui/invalid_argument_attributes.stderr +++ b/tests/ui/invalid_argument_attributes.stderr @@ -5,10 +5,10 @@ error: expected `from_py_with` | ^^^ error: expected `=` - --> tests/ui/invalid_argument_attributes.rs:7:32 + --> tests/ui/invalid_argument_attributes.rs:7:45 | 7 | fn from_py_with_no_value(#[pyo3(from_py_with)] param: String) {} - | ^^^^^^^^^^^^^^ + | ^ error: expected `from_py_with` --> tests/ui/invalid_argument_attributes.rs:10:31 diff --git a/tests/ui/invalid_frompy_derive.stderr b/tests/ui/invalid_frompy_derive.stderr index b5cb180d..04280d73 100644 --- a/tests/ui/invalid_frompy_derive.stderr +++ b/tests/ui/invalid_frompy_derive.stderr @@ -109,10 +109,10 @@ error: attribute name cannot be empty | ^^ error: unexpected end of input, expected string literal - --> tests/ui/invalid_frompy_derive.rs:100:21 + --> tests/ui/invalid_frompy_derive.rs:100:22 | 100 | #[pyo3(attribute())] - | ^^ + | ^ error: expected at most one argument: `item` or `item(key)` --> tests/ui/invalid_frompy_derive.rs:106:20 @@ -121,10 +121,10 @@ error: expected at most one argument: `item` or `item(key)` | ^ error: unexpected end of input, expected literal - --> tests/ui/invalid_frompy_derive.rs:112:16 + --> tests/ui/invalid_frompy_derive.rs:112:17 | 112 | #[pyo3(item())] - | ^^ + | ^ error: only one of `attribute` or `item` can be provided --> tests/ui/invalid_frompy_derive.rs:118:5 @@ -132,7 +132,7 @@ error: only one of `attribute` or `item` can be provided 118 | #[pyo3(item, attribute)] | ^ -error: expected `transparent` or `annotation` +error: expected one of: `transparent`, `annotation`, `crate` --> tests/ui/invalid_frompy_derive.rs:123:8 | 123 | #[pyo3(unknown = "should not work")] @@ -171,10 +171,10 @@ error: cannot derive FromPyObject for empty structs and variants = note: this error originates in the derive macro `FromPyObject` (in Nightly builds, run with -Z macro-backtrace for more info) error: expected `=` - --> tests/ui/invalid_frompy_derive.rs:158:11 + --> tests/ui/invalid_frompy_derive.rs:158:24 | 158 | #[pyo3(from_py_with)] - | ^^^^^^^^^^^^^^ + | ^ error: expected string literal --> tests/ui/invalid_frompy_derive.rs:164:27 diff --git a/tests/ui/invalid_pyclass_enum.rs b/tests/ui/invalid_pyclass_enum.rs new file mode 100644 index 00000000..a12accbf --- /dev/null +++ b/tests/ui/invalid_pyclass_enum.rs @@ -0,0 +1,18 @@ +use pyo3::prelude::*; + +#[pyclass(subclass)] +enum NotBaseClass { + x, + y, +} + +#[pyclass(extends = PyList)] +enum NotDrivedClass { + x, + y, +} + +#[pyclass] +enum NoEmptyEnum {} + +fn main() {} diff --git a/tests/ui/invalid_pyclass_enum.stderr b/tests/ui/invalid_pyclass_enum.stderr new file mode 100644 index 00000000..eea36e5c --- /dev/null +++ b/tests/ui/invalid_pyclass_enum.stderr @@ -0,0 +1,17 @@ +error: enums can't be inherited by other classes + --> tests/ui/invalid_pyclass_enum.rs:3:11 + | +3 | #[pyclass(subclass)] + | ^^^^^^^^ + +error: enums cannot extend from other classes + --> tests/ui/invalid_pyclass_enum.rs:9:11 + | +9 | #[pyclass(extends = PyList)] + | ^^^^^^^ + +error: Empty enums can't be #[pyclass]. + --> tests/ui/invalid_pyclass_enum.rs:16:18 + | +16 | enum NoEmptyEnum {} + | ^^ diff --git a/tests/ui/invalid_pyclass_item.rs b/tests/ui/invalid_pyclass_item.rs new file mode 100644 index 00000000..4b348540 --- /dev/null +++ b/tests/ui/invalid_pyclass_item.rs @@ -0,0 +1,6 @@ +use pyo3::prelude::*; + +#[pyclass] +fn foo() {} + +fn main() {} diff --git a/tests/ui/invalid_pyclass_item.stderr b/tests/ui/invalid_pyclass_item.stderr new file mode 100644 index 00000000..f29756df --- /dev/null +++ b/tests/ui/invalid_pyclass_item.stderr @@ -0,0 +1,5 @@ +error: #[pyclass] only supports structs and enums. + --> tests/ui/invalid_pyclass_item.rs:4:1 + | +4 | fn foo() {} + | ^^^^^^^^^^^ diff --git a/tests/ui/invalid_pyfunctions.rs b/tests/ui/invalid_pyfunctions.rs index 680ca56e..ab5abd2b 100644 --- a/tests/ui/invalid_pyfunctions.rs +++ b/tests/ui/invalid_pyfunctions.rs @@ -9,4 +9,7 @@ fn impl_trait_function(impl_trait: impl AsRef) {} #[pyfunction] async fn async_function() {} +#[pyfunction] +fn required_arg_after_optional(optional: Option, required: isize) {} + fn main() {} diff --git a/tests/ui/invalid_pyfunctions.stderr b/tests/ui/invalid_pyfunctions.stderr index 00bf963f..fdec5afd 100644 --- a/tests/ui/invalid_pyfunctions.stderr +++ b/tests/ui/invalid_pyfunctions.stderr @@ -17,3 +17,9 @@ Additional crates such as `pyo3-asyncio` can be used to integrate async Rust and | 10 | async fn async_function() {} | ^^^^^ + +error: Required positional parameters cannot come after optional parameters + --> tests/ui/invalid_pyfunctions.rs:13:57 + | +13 | fn required_arg_after_optional(optional: Option, required: isize) {} + | ^^^^^^^^ diff --git a/tests/ui/invalid_pymethods.rs b/tests/ui/invalid_pymethods.rs index a0829635..a46a190c 100644 --- a/tests/ui/invalid_pymethods.rs +++ b/tests/ui/invalid_pymethods.rs @@ -113,4 +113,15 @@ impl MyClass { fn method_cannot_pass_module(&self, m: &PyModule) {} } +#[pymethods] +impl MyClass { + fn required_arg_after_optional(&self, optional: Option, required: isize) {} +} + +#[pymethods] +impl MyClass { + #[args(has_default = "1")] + fn default_arg_before_required(&self, has_default: isize, required: isize) {} +} + fn main() {} diff --git a/tests/ui/invalid_pymethods.stderr b/tests/ui/invalid_pymethods.stderr index 3556c272..2a25508c 100644 --- a/tests/ui/invalid_pymethods.stderr +++ b/tests/ui/invalid_pymethods.stderr @@ -101,3 +101,15 @@ error: `pass_module` cannot be used on Python methods | 112 | #[pyo3(pass_module)] | ^^^^^^^^^^^ + +error: Required positional parameters cannot come after optional parameters + --> tests/ui/invalid_pymethods.rs:118:68 + | +118 | fn required_arg_after_optional(&self, optional: Option, required: isize) {} + | ^^^^^^^^ + +error: Required positional parameters cannot come after optional parameters + --> tests/ui/invalid_pymethods.rs:124:63 + | +124 | fn default_arg_before_required(&self, has_default: isize, required: isize) {} + | ^^^^^^^^ diff --git a/tests/ui/invalid_pymodule_args.rs b/tests/ui/invalid_pymodule_args.rs new file mode 100644 index 00000000..a11ec937 --- /dev/null +++ b/tests/ui/invalid_pymodule_args.rs @@ -0,0 +1,8 @@ +use pyo3::prelude::*; + +#[pymodule(some_arg)] +fn module(_py: Python, m: &PyModule) -> PyResult<()> { + Ok(()) +} + +fn main(){} diff --git a/tests/ui/invalid_pymodule_args.stderr b/tests/ui/invalid_pymodule_args.stderr new file mode 100644 index 00000000..933b6d60 --- /dev/null +++ b/tests/ui/invalid_pymodule_args.stderr @@ -0,0 +1,5 @@ +error: unexpected token + --> tests/ui/invalid_pymodule_args.rs:3:12 + | +3 | #[pymodule(some_arg)] + | ^^^^^^^^ diff --git a/tests/ui/pyclass_send.stderr b/tests/ui/pyclass_send.stderr index 56217f1e..531a4e9d 100644 --- a/tests/ui/pyclass_send.stderr +++ b/tests/ui/pyclass_send.stderr @@ -11,8 +11,8 @@ note: required because it appears within the type `NotThreadSafe` 5 | struct NotThreadSafe { | ^^^^^^^^^^^^^ note: required by a bound in `ThreadCheckerStub` - --> src/class/impl_.rs:710:33 + --> src/class/impl_.rs | -710 | pub struct ThreadCheckerStub(PhantomData); + | pub struct ThreadCheckerStub(PhantomData); | ^^^^ required by this bound in `ThreadCheckerStub` = note: this error originates in the attribute macro `pyclass` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/tests/ui/static_ref.stderr b/tests/ui/static_ref.stderr index 58fc6a53..9165f63a 100644 --- a/tests/ui/static_ref.stderr +++ b/tests/ui/static_ref.stderr @@ -4,7 +4,7 @@ error[E0495]: cannot infer an appropriate lifetime for lifetime parameter `'py` 4 | #[pyfunction] | ^^^^^^^^^^^^^ | -note: first, the lifetime cannot outlive the anonymous lifetime #1 defined on the body at 4:1... +note: first, the lifetime cannot outlive the anonymous lifetime #1 defined here... --> tests/ui/static_ref.rs:4:1 | 4 | #[pyfunction] diff --git a/tests/ui/wrong_aspyref_lifetimes.stderr b/tests/ui/wrong_aspyref_lifetimes.stderr index 319f4ee5..0042bc6b 100644 --- a/tests/ui/wrong_aspyref_lifetimes.stderr +++ b/tests/ui/wrong_aspyref_lifetimes.stderr @@ -2,9 +2,9 @@ error[E0505]: cannot move out of `gil` because it is borrowed --> tests/ui/wrong_aspyref_lifetimes.rs:7:10 | 6 | let dict: &PyDict = dict.as_ref(gil.python()); - | --- borrow of `gil` occurs here + | ------------ borrow of `gil` occurs here 7 | drop(gil); | ^^^ move out of `gil` occurs here 8 | 9 | let _py: Python = dict.py(); // Obtain a Python<'p> without GIL. - | ---- borrow later used here + | --------- borrow later used here diff --git a/tox.ini b/tox.ini index 6bbebd29..a1cfba86 100644 --- a/tox.ini +++ b/tox.ini @@ -1,9 +1,9 @@ [tox] -envlist = py35, - py36, - py37, +envlist = py37, py38, -minversion = 3.4.0 + py39, + py310, +minversion = 3.7.0 skip_missing_interpreters = true [testenv]