From 427b2e93869f747c2e5013cc426cf3176edf6791 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Orhun=20Parmaks=C4=B1z?= Date: Wed, 11 Oct 2023 13:44:43 +0300 Subject: [PATCH 01/52] Add support for `SmallVec` in conversion traits (#3440) --- Cargo.toml | 2 + newsfragments/3507.added.md | 1 + src/conversions/mod.rs | 1 + src/conversions/smallvec.rs | 140 ++++++++++++++++++++++++++++++++++++ 4 files changed, 144 insertions(+) create mode 100644 newsfragments/3507.added.md create mode 100644 src/conversions/smallvec.rs diff --git a/Cargo.toml b/Cargo.toml index 690072b1a6b..16a1ba3a647 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,6 +41,7 @@ num-bigint = { version = "0.4", optional = true } num-complex = { version = ">= 0.2, < 0.5", optional = true } rust_decimal = { version = "1.0.0", default-features = false, optional = true } serde = { version = "1.0", optional = true } +smallvec = { version = "1.11.1", optional = true } [dev-dependencies] assert_approx_eq = "1.1.0" @@ -104,6 +105,7 @@ full = [ "num-bigint", "num-complex", "hashbrown", + "smallvec", "serde", "indexmap", "eyre", diff --git a/newsfragments/3507.added.md b/newsfragments/3507.added.md new file mode 100644 index 00000000000..2068ab4c3f7 --- /dev/null +++ b/newsfragments/3507.added.md @@ -0,0 +1 @@ +Add `smallvec` feature to add `ToPyObject`, `IntoPy` and `FromPyObject` implementations for `smallvec::SmallVec`. diff --git a/src/conversions/mod.rs b/src/conversions/mod.rs index 5544dc23532..a9c2b0cd2a6 100644 --- a/src/conversions/mod.rs +++ b/src/conversions/mod.rs @@ -9,4 +9,5 @@ pub mod num_bigint; pub mod num_complex; pub mod rust_decimal; pub mod serde; +pub mod smallvec; mod std; diff --git a/src/conversions/smallvec.rs b/src/conversions/smallvec.rs new file mode 100644 index 00000000000..d2e84421da3 --- /dev/null +++ b/src/conversions/smallvec.rs @@ -0,0 +1,140 @@ +#![cfg(feature = "smallvec")] + +//! Conversions to and from [smallvec](https://docs.rs/smallvec/). +//! +//! # Setup +//! +//! To use this feature, add this to your **`Cargo.toml`**: +//! +//! ```toml +//! [dependencies] +//! # change * to the latest versions +//! smallvec = "*" +#![doc = concat!("pyo3 = { version = \"", env!("CARGO_PKG_VERSION"), "\", features = [\"smallvec\"] }")] +//! ``` +//! +//! Note that you must use compatible versions of smallvec and PyO3. +//! The required smallvec version may vary based on the version of PyO3. +use crate::exceptions::PyTypeError; +#[cfg(feature = "experimental-inspect")] +use crate::inspect::types::TypeInfo; +use crate::types::list::new_from_iter; +use crate::types::{PySequence, PyString}; +use crate::{ + ffi, FromPyObject, IntoPy, PyAny, PyDowncastError, PyObject, PyResult, Python, ToPyObject, +}; +use smallvec::{Array, SmallVec}; + +impl ToPyObject for SmallVec +where + A: Array, + A::Item: ToPyObject, +{ + fn to_object(&self, py: Python<'_>) -> PyObject { + self.as_slice().to_object(py) + } +} + +impl IntoPy for SmallVec +where + A: Array, + A::Item: IntoPy, +{ + fn into_py(self, py: Python<'_>) -> PyObject { + let mut iter = self.into_iter().map(|e| e.into_py(py)); + let list = new_from_iter(py, &mut iter); + list.into() + } + + #[cfg(feature = "experimental-inspect")] + fn type_output() -> TypeInfo { + TypeInfo::list_of(A::Item::type_output()) + } +} + +impl<'a, A> FromPyObject<'a> for SmallVec +where + A: Array, + A::Item: FromPyObject<'a>, +{ + fn extract(obj: &'a PyAny) -> PyResult { + if obj.is_instance_of::() { + return Err(PyTypeError::new_err("Can't extract `str` to `SmallVec`")); + } + extract_sequence(obj) + } + + #[cfg(feature = "experimental-inspect")] + fn type_input() -> TypeInfo { + TypeInfo::sequence_of(A::Item::type_input()) + } +} + +fn extract_sequence<'s, A>(obj: &'s PyAny) -> PyResult> +where + A: Array, + A::Item: FromPyObject<'s>, +{ + // Types that pass `PySequence_Check` usually implement enough of the sequence protocol + // to support this function and if not, we will only fail extraction safely. + let seq: &PySequence = unsafe { + if ffi::PySequence_Check(obj.as_ptr()) != 0 { + obj.downcast_unchecked() + } else { + return Err(PyDowncastError::new(obj, "Sequence").into()); + } + }; + + let mut sv = SmallVec::with_capacity(seq.len().unwrap_or(0)); + for item in seq.iter()? { + sv.push(item?.extract::()?); + } + Ok(sv) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::types::{PyDict, PyList}; + + #[test] + fn test_smallvec_into_py() { + Python::with_gil(|py| { + let sv: SmallVec<[u64; 8]> = [1, 2, 3, 4, 5].iter().cloned().collect(); + let hso: PyObject = sv.clone().into_py(py); + let l = PyList::new(py, [1, 2, 3, 4, 5]); + assert!(l.eq(hso).unwrap()); + }); + } + + #[test] + fn test_smallvec_from_py_object() { + Python::with_gil(|py| { + let l = PyList::new(py, [1, 2, 3, 4, 5]); + let sv: SmallVec<[u64; 8]> = l.extract().unwrap(); + assert_eq!(sv.as_slice(), [1, 2, 3, 4, 5]); + }); + } + + #[test] + fn test_smallvec_from_py_object_fails() { + Python::with_gil(|py| { + let dict = PyDict::new(py); + let sv: PyResult> = dict.extract(); + assert_eq!( + sv.unwrap_err().to_string(), + "TypeError: 'dict' object cannot be converted to 'Sequence'" + ); + }); + } + + #[test] + fn test_smallvec_to_object() { + Python::with_gil(|py| { + let sv: SmallVec<[u64; 8]> = [1, 2, 3, 4, 5].iter().cloned().collect(); + let hso: PyObject = sv.to_object(py); + let l = PyList::new(py, [1, 2, 3, 4, 5]); + assert!(l.eq(hso).unwrap()); + }); + } +} From 8392ed2a94786093e5d79e9c796fd4b3c1ad16a2 Mon Sep 17 00:00:00 2001 From: David Hewitt <1939362+davidhewitt@users.noreply.github.com> Date: Mon, 2 Oct 2023 21:12:33 +0100 Subject: [PATCH 02/52] bump "latest" CI jobs to 3.12 --- .github/workflows/ci.yml | 8 ++++---- .gitignore | 1 + .python-version | 2 +- emscripten/.gitignore | 1 + emscripten/Makefile | 13 +++++++------ emscripten/pybuilddir.txt | 1 - noxfile.py | 2 ++ pyo3-runtime/pyproject.toml | 1 + 8 files changed, 17 insertions(+), 12 deletions(-) create mode 100644 emscripten/.gitignore delete mode 100644 emscripten/pybuilddir.txt diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cc87bca59f3..d4f71af172f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -144,7 +144,7 @@ jobs: matrix: extra-features: ["multiple-pymethods"] rust: [stable] - python-version: ["3.11"] + python-version: ["3.12"] platform: [ { @@ -221,7 +221,7 @@ jobs: include: # Test minimal supported Rust version - rust: 1.56.0 - python-version: "3.11" + python-version: "3.12" platform: { os: "ubuntu-latest", @@ -233,7 +233,7 @@ jobs: # Test the `nightly` feature - rust: nightly - python-version: "3.11" + python-version: "3.12" platform: { os: "ubuntu-latest", @@ -244,7 +244,7 @@ jobs: # Test 32-bit Windows only with the latest Python version - rust: stable - python-version: "3.11" + python-version: "3.12" platform: { os: "windows-latest", diff --git a/.gitignore b/.gitignore index f67547aaf97..4240d326f71 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,4 @@ valgrind-python.supp *.pyd lcov.info netlify_build/ +.nox/ diff --git a/.python-version b/.python-version index 2c0733315e4..e4fba218358 100644 --- a/.python-version +++ b/.python-version @@ -1 +1 @@ -3.11 +3.12 diff --git a/emscripten/.gitignore b/emscripten/.gitignore new file mode 100644 index 00000000000..a8ee9284392 --- /dev/null +++ b/emscripten/.gitignore @@ -0,0 +1 @@ +pybuilddir.txt diff --git a/emscripten/Makefile b/emscripten/Makefile index af224854c26..5420603eda2 100644 --- a/emscripten/Makefile +++ b/emscripten/Makefile @@ -2,9 +2,10 @@ CURDIR=$(abspath .) # These three are passed in from nox. BUILDROOT ?= $(CURDIR)/builddir -PYMAJORMINORMICRO ?= 3.11.0 +PYMAJORMINORMICRO ?= 3.12.0 +PYTHON ?= python3.12 -EMSCRIPTEN_VERSION=3.1.13 +EMSCRIPTEN_VERSION=3.1.46 export EMSDKDIR = $(BUILDROOT)/emsdk @@ -34,7 +35,7 @@ PYTHONLIBDIR=$(BUILDROOT)/install/Python-$(PYVERSION)/lib all: $(PYTHONLIBDIR)/libpython$(PYMAJORMINOR).a -$(BUILDROOT)/.exists: +$(BUILDROOT)/.exists: mkdir -p $(BUILDROOT) touch $@ @@ -66,18 +67,18 @@ $(PYTHONBUILD)/Makefile: $(PYTHONBUILD)/.patched $(BUILDROOT)/emsdk --build=$(shell $(PYTHONBUILD)/config.guess) \ --with-emscripten-target=browser \ --enable-wasm-dynamic-linking \ - --with-build-python=python3.11 + --with-build-python=$(PYTHON) $(PYTHONLIBDIR)/libpython$(PYMAJORMINOR).a : $(PYTHONBUILD)/Makefile cd $(PYTHONBUILD) && \ emmake make -j3 libpython$(PYMAJORMINOR).a # Generate sysconfigdata - _PYTHON_SYSCONFIGDATA_NAME=$(SYSCONFIGDATA_NAME) _PYTHON_PROJECT_BASE=$(PYTHONBUILD) python3.11 -m sysconfig --generate-posix-vars + _PYTHON_SYSCONFIGDATA_NAME=$(SYSCONFIGDATA_NAME) _PYTHON_PROJECT_BASE=$(PYTHONBUILD) $(PYTHON) -m sysconfig --generate-posix-vars cp `cat pybuilddir.txt`/$(SYSCONFIGDATA_NAME).py $(PYTHONBUILD)/Lib mkdir -p $(PYTHONLIBDIR) - # Copy libexpat.a, libmpdec.a, and libpython3.11.a + # Copy libexpat.a, libmpdec.a, and libpython3.12.a # In noxfile, we explicitly link libexpat and libmpdec via RUSTFLAGS find $(PYTHONBUILD) -name '*.a' -exec cp {} $(PYTHONLIBDIR) \; # Install Python stdlib diff --git a/emscripten/pybuilddir.txt b/emscripten/pybuilddir.txt deleted file mode 100644 index 59f2a4a7546..00000000000 --- a/emscripten/pybuilddir.txt +++ /dev/null @@ -1 +0,0 @@ -build/lib.linux-x86_64-3.11 \ No newline at end of file diff --git a/noxfile.py b/noxfile.py index eb1a4d4d83c..0fec8f81f71 100644 --- a/noxfile.py +++ b/noxfile.py @@ -261,6 +261,7 @@ def build_emscripten(session: nox.Session): "make", "-C", str(info.emscripten_dir), + f"PYTHON={sys.executable}", f"BUILDROOT={info.builddir}", f"PYMAJORMINORMICRO={info.pymajorminormicro}", f"PYPRERELEASE={info.pydev}", @@ -288,6 +289,7 @@ def test_emscripten(session: nox.Session): f"-C link-arg=-lpython{info.pymajorminor}", "-C link-arg=-lexpat", "-C link-arg=-lmpdec", + "-C link-arg=-lHacl_Hash_SHA2", "-C link-arg=-lz", "-C link-arg=-lbz2", "-C link-arg=-sALLOW_MEMORY_GROWTH=1", diff --git a/pyo3-runtime/pyproject.toml b/pyo3-runtime/pyproject.toml index 3aa7dbd187d..b02c64dd881 100644 --- a/pyo3-runtime/pyproject.toml +++ b/pyo3-runtime/pyproject.toml @@ -21,6 +21,7 @@ classifiers = [ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", ] From 9e07203afb58f535a7b299aa018e4700655b27eb Mon Sep 17 00:00:00 2001 From: David Hewitt <1939362+davidhewitt@users.noreply.github.com> Date: Sun, 8 Oct 2023 22:54:00 +0100 Subject: [PATCH 03/52] also test emscripten with CI-build-full --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d4f71af172f..279cf27702e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -334,7 +334,7 @@ jobs: emscripten: name: emscripten - if: ${{ github.event_name != 'pull_request' && github.ref != 'refs/heads/main' }} + if: ${{ contains(github.event.pull_request.labels.*.name, 'CI-build-full') || (github.event_name != 'pull_request' && github.ref != 'refs/heads/main') }} runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 From 84264b358e33995c8d4ecef0a6409910bbfe0ee8 Mon Sep 17 00:00:00 2001 From: David Hewitt <1939362+davidhewitt@users.noreply.github.com> Date: Fri, 13 Oct 2023 08:25:36 +0200 Subject: [PATCH 04/52] keep emscripten back on 3.11 for now --- .github/workflows/ci.yml | 3 +++ emscripten/Makefile | 13 ++++++------- emscripten/pybuilddir.txt | 1 + noxfile.py | 1 - 4 files changed, 10 insertions(+), 8 deletions(-) create mode 100644 emscripten/pybuilddir.txt diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 279cf27702e..7b7a2e6b5c1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -339,6 +339,9 @@ jobs: steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v4 + with: + # TODO bump emscripten builds to test on 3.12 + python-version: 3.11 id: setup-python - name: Install Rust toolchain uses: dtolnay/rust-toolchain@stable diff --git a/emscripten/Makefile b/emscripten/Makefile index 5420603eda2..af224854c26 100644 --- a/emscripten/Makefile +++ b/emscripten/Makefile @@ -2,10 +2,9 @@ CURDIR=$(abspath .) # These three are passed in from nox. BUILDROOT ?= $(CURDIR)/builddir -PYMAJORMINORMICRO ?= 3.12.0 -PYTHON ?= python3.12 +PYMAJORMINORMICRO ?= 3.11.0 -EMSCRIPTEN_VERSION=3.1.46 +EMSCRIPTEN_VERSION=3.1.13 export EMSDKDIR = $(BUILDROOT)/emsdk @@ -35,7 +34,7 @@ PYTHONLIBDIR=$(BUILDROOT)/install/Python-$(PYVERSION)/lib all: $(PYTHONLIBDIR)/libpython$(PYMAJORMINOR).a -$(BUILDROOT)/.exists: +$(BUILDROOT)/.exists: mkdir -p $(BUILDROOT) touch $@ @@ -67,18 +66,18 @@ $(PYTHONBUILD)/Makefile: $(PYTHONBUILD)/.patched $(BUILDROOT)/emsdk --build=$(shell $(PYTHONBUILD)/config.guess) \ --with-emscripten-target=browser \ --enable-wasm-dynamic-linking \ - --with-build-python=$(PYTHON) + --with-build-python=python3.11 $(PYTHONLIBDIR)/libpython$(PYMAJORMINOR).a : $(PYTHONBUILD)/Makefile cd $(PYTHONBUILD) && \ emmake make -j3 libpython$(PYMAJORMINOR).a # Generate sysconfigdata - _PYTHON_SYSCONFIGDATA_NAME=$(SYSCONFIGDATA_NAME) _PYTHON_PROJECT_BASE=$(PYTHONBUILD) $(PYTHON) -m sysconfig --generate-posix-vars + _PYTHON_SYSCONFIGDATA_NAME=$(SYSCONFIGDATA_NAME) _PYTHON_PROJECT_BASE=$(PYTHONBUILD) python3.11 -m sysconfig --generate-posix-vars cp `cat pybuilddir.txt`/$(SYSCONFIGDATA_NAME).py $(PYTHONBUILD)/Lib mkdir -p $(PYTHONLIBDIR) - # Copy libexpat.a, libmpdec.a, and libpython3.12.a + # Copy libexpat.a, libmpdec.a, and libpython3.11.a # In noxfile, we explicitly link libexpat and libmpdec via RUSTFLAGS find $(PYTHONBUILD) -name '*.a' -exec cp {} $(PYTHONLIBDIR) \; # Install Python stdlib diff --git a/emscripten/pybuilddir.txt b/emscripten/pybuilddir.txt new file mode 100644 index 00000000000..59f2a4a7546 --- /dev/null +++ b/emscripten/pybuilddir.txt @@ -0,0 +1 @@ +build/lib.linux-x86_64-3.11 \ No newline at end of file diff --git a/noxfile.py b/noxfile.py index 0fec8f81f71..ee24a26f3bb 100644 --- a/noxfile.py +++ b/noxfile.py @@ -289,7 +289,6 @@ def test_emscripten(session: nox.Session): f"-C link-arg=-lpython{info.pymajorminor}", "-C link-arg=-lexpat", "-C link-arg=-lmpdec", - "-C link-arg=-lHacl_Hash_SHA2", "-C link-arg=-lz", "-C link-arg=-lbz2", "-C link-arg=-sALLOW_MEMORY_GROWTH=1", From 779eb2412ccb3f275eddad89fcadbfdce2df3590 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Orhun=20Parmaks=C4=B1z?= Date: Fri, 13 Oct 2023 13:35:30 +0300 Subject: [PATCH 05/52] Add an entry to features table in lib --- src/lib.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index e39caabac4a..4ebe90e0051 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -94,6 +94,7 @@ //! - [`eyre`]: Enables a conversion from [eyre]’s [`Report`] type to [`PyErr`]. //! - [`hashbrown`]: Enables conversions between Python objects and [hashbrown]'s [`HashMap`] and //! [`HashSet`] types. +//! - [`smallvec`][smallvec]: Enables conversions between Python list and [smallvec]'s [`SmallVec`]. //! - [`indexmap`][indexmap_feature]: Enables conversions between Python dictionary and [indexmap]'s [`IndexMap`]. //! - [`num-bigint`]: Enables conversions between Python objects and [num-bigint]'s [`BigInt`] and //! [`BigUint`] types. @@ -256,6 +257,7 @@ //! [inventory]: https://docs.rs/inventory //! [`HashMap`]: https://docs.rs/hashbrown/latest/hashbrown/struct.HashMap.html //! [`HashSet`]: https://docs.rs/hashbrown/latest/hashbrown/struct.HashSet.html +//! [`SmallVec`]: https://docs.rs/smallvec/latest/smallvec/struct.SmallVec.html //! [`IndexMap`]: https://docs.rs/indexmap/latest/indexmap/map/struct.IndexMap.html //! [`BigInt`]: https://docs.rs/num-bigint/latest/num_bigint/struct.BigInt.html //! [`BigUint`]: https://docs.rs/num-bigint/latest/num_bigint/struct.BigUint.html @@ -282,6 +284,7 @@ //! [feature flags]: https://doc.rust-lang.org/cargo/reference/features.html "Features - The Cargo Book" //! [global interpreter lock]: https://docs.python.org/3/glossary.html#term-global-interpreter-lock //! [hashbrown]: https://docs.rs/hashbrown +//! [smallvec]: https://docs.rs/smallvec //! [indexmap]: https://docs.rs/indexmap //! [manual_builds]: https://pyo3.rs/latest/building_and_distribution.html#manual-builds "Manual builds - Building and Distribution - PyO3 user guide" //! [num-bigint]: https://docs.rs/num-bigint From 826fa973b63c160fab406bea026f2fc350dd2397 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Orhun=20Parmaks=C4=B1z?= Date: Fri, 13 Oct 2023 15:37:25 +0300 Subject: [PATCH 06/52] Set version of smallvec to 1.0 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 16a1ba3a647..1024e8827c3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,7 +41,7 @@ num-bigint = { version = "0.4", optional = true } num-complex = { version = ">= 0.2, < 0.5", optional = true } rust_decimal = { version = "1.0.0", default-features = false, optional = true } serde = { version = "1.0", optional = true } -smallvec = { version = "1.11.1", optional = true } +smallvec = { version = "1.0", optional = true } [dev-dependencies] assert_approx_eq = "1.1.0" From d468f570aec37a91dcfe1dd3b234675a919254fc Mon Sep 17 00:00:00 2001 From: Adam Reichold Date: Sat, 14 Oct 2023 09:05:37 +0200 Subject: [PATCH 07/52] Align chrono dev and runtime dependency version specifications. --- Cargo.toml | 2 +- newsfragments/3512.fixed.md | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 newsfragments/3512.fixed.md diff --git a/Cargo.toml b/Cargo.toml index 1024e8827c3..677c4bb6b88 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,7 +33,7 @@ inventory = { version = "0.3.0", optional = true } # crate integrations that can be added using the eponymous features anyhow = { version = "1.0", optional = true } -chrono = { version = "0.4", default-features = false, optional = true } +chrono = { version = "0.4.25", default-features = false, optional = true } eyre = { version = ">= 0.4, < 0.7", optional = true } hashbrown = { version = ">= 0.9, < 0.15", optional = true } indexmap = { version = ">= 1.6, < 3", optional = true } diff --git a/newsfragments/3512.fixed.md b/newsfragments/3512.fixed.md new file mode 100644 index 00000000000..39b8087669e --- /dev/null +++ b/newsfragments/3512.fixed.md @@ -0,0 +1 @@ +Fix minimum version specification for optional `chrono` dependency From f745299b7b4f486186c3891f9ea0e896f488a128 Mon Sep 17 00:00:00 2001 From: David Hewitt <1939362+davidhewitt@users.noreply.github.com> Date: Sun, 15 Oct 2023 14:15:38 +0100 Subject: [PATCH 08/52] ci: drop psutil dependency --- pytests/requirements-dev.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/pytests/requirements-dev.txt b/pytests/requirements-dev.txt index 1b92cbfef5a..aa0c703a8cd 100644 --- a/pytests/requirements-dev.txt +++ b/pytests/requirements-dev.txt @@ -2,5 +2,4 @@ hypothesis>=3.55 pytest>=6.0 pytest-asyncio>=0.21 pytest-benchmark>=3.4 -psutil>=5.6 typing_extensions>=4.0.0 From 30463b67207c71af880b434b402ef451282ebb38 Mon Sep 17 00:00:00 2001 From: David Hewitt <1939362+davidhewitt@users.noreply.github.com> Date: Sun, 15 Oct 2023 21:55:36 +0100 Subject: [PATCH 09/52] remove comparison to rust-cpython --- guide/src/SUMMARY.md | 5 +-- guide/src/rust_cpython.md | 82 --------------------------------------- src/lib.rs | 1 - 3 files changed, 2 insertions(+), 86 deletions(-) delete mode 100644 guide/src/rust_cpython.md diff --git a/guide/src/SUMMARY.md b/guide/src/SUMMARY.md index e75095f4bef..730f998530d 100644 --- a/guide/src/SUMMARY.md +++ b/guide/src/SUMMARY.md @@ -36,9 +36,8 @@ --- [Appendix A: Migration guide](migration.md) -[Appendix B: PyO3 and rust-cpython](rust_cpython.md) -[Appendix C: Trait bounds](trait_bounds.md) -[Appendix D: Python typing hints](python_typing_hints.md) +[Appendix B: Trait bounds](trait_bounds.md) +[Appendix C: Python typing hints](python_typing_hints.md) [CHANGELOG](changelog.md) --- diff --git a/guide/src/rust_cpython.md b/guide/src/rust_cpython.md deleted file mode 100644 index 6c68d2cfe73..00000000000 --- a/guide/src/rust_cpython.md +++ /dev/null @@ -1,82 +0,0 @@ -# PyO3 and rust-cpython - -PyO3 began as fork of [rust-cpython](https://github.com/dgrunwald/rust-cpython) when rust-cpython wasn't maintained. Over time PyO3 has become fundamentally different from rust-cpython. - -## Macros - -While rust-cpython has a `macro_rules!` based DSL for declaring modules and classes, PyO3 uses proc macros. PyO3 also doesn't change your struct and functions so you can still use them as normal Rust functions. - -**rust-cpython** - -```rust,ignore -py_class!(class MyClass |py| { - data number: i32; - def __new__(_cls, arg: i32) -> PyResult { - MyClass::create_instance(py, arg) - } - def half(&self) -> PyResult { - Ok(self.number(py) / 2) - } -}); -``` - -**PyO3** - -```rust -use pyo3::prelude::*; - -#[pyclass] -struct MyClass { - num: u32, -} - -#[pymethods] -impl MyClass { - #[new] - fn new(num: u32) -> Self { - MyClass { num } - } - - fn half(&self) -> PyResult { - Ok(self.num / 2) - } -} -``` - -## Ownership and lifetimes - -While in rust-cpython you always own Python objects, PyO3 allows efficient *borrowed objects* -and most APIs work with references. - -Here is an example of the PyList API: - -**rust-cpython** - -```rust,ignore -impl PyList { - - fn new(py: Python<'_>) -> PyList {...} - - fn get_item(&self, py: Python<'_>, index: isize) -> PyObject {...} -} -``` - -**PyO3** - -```rust,ignore -impl PyList { - - fn new(py: Python<'_>) -> &PyList {...} - - fn get_item(&self, index: isize) -> &PyAny {...} -} -``` - -In PyO3, all object references are bounded by the GIL lifetime. -So owned Python objects are not required, and it is safe to have functions like `fn py<'p>(&'p self) -> Python<'p> {}`. - -## Error handling - -rust-cpython requires a `Python` parameter for constructing a `PyErr`, so error handling ergonomics is pretty bad. It is not possible to use `?` with Rust errors. - -PyO3 on other hand does not require `Python` for constructing a `PyErr`, it is only required if you want to raise an exception in Python with the `PyErr::restore()` method. Due to various `std::convert::From for PyErr` implementations for Rust standard error types `E`, propagating `?` is supported automatically. diff --git a/src/lib.rs b/src/lib.rs index 4ebe90e0051..9e1a5407411 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -502,7 +502,6 @@ pub mod doc_test { "guide/src/performance.md" => guide_performance_md, "guide/src/python_from_rust.md" => guide_python_from_rust_md, "guide/src/python_typing_hints.md" => guide_python_typing_hints_md, - "guide/src/rust_cpython.md" => guide_rust_cpython_md, "guide/src/trait_bounds.md" => guide_trait_bounds_md, "guide/src/types.md" => guide_types_md, } From 422f8665c989186b4436ff05e972cbd3bfa711f5 Mon Sep 17 00:00:00 2001 From: David Hewitt <1939362+davidhewitt@users.noreply.github.com> Date: Mon, 16 Oct 2023 20:16:01 +0100 Subject: [PATCH 10/52] ci: install prebuilt cargo-careful --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7b7a2e6b5c1..ed085866511 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -287,7 +287,7 @@ jobs: - uses: dtolnay/rust-toolchain@nightly with: components: rust-src - - run: cargo install cargo-careful + - uses: taiki-e/install-action@cargo-careful - run: python -m pip install --upgrade pip && pip install nox - run: nox -s test-rust -- careful skip-full env: From 97bf1941527ecddc4197c318924e1aa304985c54 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Oct 2023 20:32:21 +0000 Subject: [PATCH 11/52] Bump actions/setup-node from 3 to 4 Bumps [actions/setup-node](https://github.com/actions/setup-node) from 3 to 4. - [Release notes](https://github.com/actions/setup-node/releases) - [Commits](https://github.com/actions/setup-node/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/setup-node dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ed085866511..55bacb77ca8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -347,7 +347,7 @@ jobs: uses: dtolnay/rust-toolchain@stable with: targets: wasm32-unknown-emscripten - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 with: node-version: 14 - run: python -m pip install --upgrade pip && pip install nox From e900df02f0ea3c4e95db09913c274338abad6bc4 Mon Sep 17 00:00:00 2001 From: David Hewitt <1939362+davidhewitt@users.noreply.github.com> Date: Tue, 24 Oct 2023 21:50:10 +0100 Subject: [PATCH 12/52] ci: use older hashbrown and indexmap for MSRV --- noxfile.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/noxfile.py b/noxfile.py index ee24a26f3bb..583b6051292 100644 --- a/noxfile.py +++ b/noxfile.py @@ -485,8 +485,8 @@ def set_minimal_package_versions(session: nox.Session): min_pkg_versions = { "rust_decimal": "1.26.1", "csv": "1.1.6", - "indexmap": "1.9.3", - "hashbrown": "0.12.3", + "indexmap": "1.6.2", + "hashbrown": "0.9.1", "log": "0.4.17", "once_cell": "1.17.2", "rayon": "1.6.1", From 8c272a6ef28d2434d647b1b7e87f20ff21e6b8ee Mon Sep 17 00:00:00 2001 From: David Hewitt <1939362+davidhewitt@users.noreply.github.com> Date: Tue, 24 Oct 2023 07:45:24 +0100 Subject: [PATCH 13/52] ci: tidy up some dev deps --- Cargo.toml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 677c4bb6b88..8b4584a88ba 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,8 +52,7 @@ proptest = { version = "1.0", default-features = false, features = ["std"] } send_wrapper = "0.6" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0.61" -rayon = "1.0.2" -rust_decimal = { version = "1.8.0", features = ["std"] } +rayon = "1.6.1" widestring = "0.5.1" [build-dependencies] From 92cde096b5513d034279006dccd917fefeb62b66 Mon Sep 17 00:00:00 2001 From: David Hewitt <1939362+davidhewitt@users.noreply.github.com> Date: Sun, 22 Oct 2023 23:15:08 +0100 Subject: [PATCH 14/52] add coverage for `emit_pyo3_cfgs` --- build.rs | 6 +- pyo3-build-config/src/errors.rs | 16 ++-- pyo3-build-config/src/impl_.rs | 132 +++++++++++++++++++++++++++++--- pyo3-build-config/src/lib.rs | 4 +- pyo3-ffi/build.rs | 6 +- 5 files changed, 145 insertions(+), 19 deletions(-) diff --git a/build.rs b/build.rs index aff0365358e..e20206310db 100644 --- a/build.rs +++ b/build.rs @@ -33,10 +33,12 @@ fn ensure_auto_initialize_ok(interpreter_config: &InterpreterConfig) -> Result<( fn configure_pyo3() -> Result<()> { let interpreter_config = pyo3_build_config::get(); - interpreter_config.emit_pyo3_cfgs(); - ensure_auto_initialize_ok(interpreter_config)?; + for cfg in interpreter_config.build_script_outputs() { + println!("{}", cfg) + } + // Emit cfgs like `thread_local_const_init` print_feature_cfgs(); diff --git a/pyo3-build-config/src/errors.rs b/pyo3-build-config/src/errors.rs index 8670b355bac..87c59a998b4 100644 --- a/pyo3-build-config/src/errors.rs +++ b/pyo3-build-config/src/errors.rs @@ -12,15 +12,21 @@ macro_rules! ensure { ($condition:expr, $($args: tt)+) => { if !($condition) { bail!($($args)+) } }; } -/// Show warning. If needed, please extend this macro to support arguments. +/// Show warning. #[macro_export] #[doc(hidden)] macro_rules! warn { - ($msg: literal) => { - println!(concat!("cargo:warning=", $msg)) + ($($args: tt)+) => { + println!("{}", $crate::format_warn!($($args)+)) }; - ($fmt: expr, $($args: tt)+) => { - println!("cargo:warning={}", format_args!($fmt, $($args)+)) +} + +/// Format warning into string. +#[macro_export] +#[doc(hidden)] +macro_rules! format_warn { + ($($args: tt)+) => { + format!("cargo:warning={}", format_args!($($args)+)) }; } diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index e9c9f5a8019..8ef14d15b9d 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -27,7 +27,7 @@ use target_lexicon::{Environment, OperatingSystem}; use crate::{ bail, ensure, errors::{Context, Error, Result}, - warn, + format_warn, warn, }; /// Minimum Python version PyO3 supports. @@ -154,31 +154,35 @@ pub struct InterpreterConfig { impl InterpreterConfig { #[doc(hidden)] - pub fn emit_pyo3_cfgs(&self) { + pub fn build_script_outputs(&self) -> Vec { // This should have been checked during pyo3-build-config build time. assert!(self.version >= MINIMUM_SUPPORTED_VERSION); + let mut out = vec![]; + // 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); + out.push(format!("cargo:rustc-cfg=Py_3_{}", i)); } if self.implementation.is_pypy() { - println!("cargo:rustc-cfg=PyPy"); + out.push("cargo:rustc-cfg=PyPy".to_owned()); if self.abi3 { - warn!( + out.push(format_warn!( "PyPy does not yet support abi3 so the build artifacts will be version-specific. \ See https://foss.heptapod.net/pypy/pypy/-/issues/3397 for more information." - ); + )); } } else if self.abi3 { - println!("cargo:rustc-cfg=Py_LIMITED_API"); + out.push("cargo:rustc-cfg=Py_LIMITED_API".to_owned()); } for flag in &self.build_flags.0 { - println!("cargo:rustc-cfg=py_sys_config=\"{}\"", flag); + out.push(format!("cargo:rustc-cfg=py_sys_config=\"{}\"", flag)); } + + out } #[doc(hidden)] @@ -1011,12 +1015,12 @@ impl BuildFlags { Self( BuildFlags::ALL .iter() - .cloned() .filter(|flag| { config_map .get_value(&flag.to_string()) .map_or(false, |value| value == "1") }) + .cloned() .collect(), ) .fixup() @@ -2581,4 +2585,114 @@ mod tests { .expect("failed to run Python script"); assert_eq!(out.trim_end(), "42"); } + + #[test] + fn test_build_script_outputs_base() { + let interpreter_config = InterpreterConfig { + implementation: PythonImplementation::CPython, + version: PythonVersion { major: 3, minor: 8 }, + shared: true, + abi3: false, + lib_name: Some("python3".into()), + lib_dir: None, + executable: None, + pointer_width: None, + build_flags: BuildFlags::default(), + suppress_build_script_link_lines: false, + extra_build_script_lines: vec![], + }; + assert_eq!( + interpreter_config.build_script_outputs(), + [ + "cargo:rustc-cfg=Py_3_6".to_owned(), + "cargo:rustc-cfg=Py_3_7".to_owned(), + "cargo:rustc-cfg=Py_3_8".to_owned(), + ] + ); + + let interpreter_config = InterpreterConfig { + implementation: PythonImplementation::PyPy, + ..interpreter_config + }; + assert_eq!( + interpreter_config.build_script_outputs(), + [ + "cargo:rustc-cfg=Py_3_6".to_owned(), + "cargo:rustc-cfg=Py_3_7".to_owned(), + "cargo:rustc-cfg=Py_3_8".to_owned(), + "cargo:rustc-cfg=PyPy".to_owned(), + ] + ); + } + + #[test] + fn test_build_script_outputs_abi3() { + let interpreter_config = InterpreterConfig { + implementation: PythonImplementation::CPython, + version: PythonVersion { major: 3, minor: 7 }, + shared: true, + abi3: true, + lib_name: Some("python3".into()), + lib_dir: None, + executable: None, + pointer_width: None, + build_flags: BuildFlags::default(), + suppress_build_script_link_lines: false, + extra_build_script_lines: vec![], + }; + + assert_eq!( + interpreter_config.build_script_outputs(), + [ + "cargo:rustc-cfg=Py_3_6".to_owned(), + "cargo:rustc-cfg=Py_3_7".to_owned(), + "cargo:rustc-cfg=Py_LIMITED_API".to_owned(), + ] + ); + + let interpreter_config = InterpreterConfig { + implementation: PythonImplementation::PyPy, + ..interpreter_config + }; + assert_eq!( + interpreter_config.build_script_outputs(), + [ + "cargo:rustc-cfg=Py_3_6".to_owned(), + "cargo:rustc-cfg=Py_3_7".to_owned(), + "cargo:rustc-cfg=PyPy".to_owned(), + "cargo:warning=PyPy does not yet support abi3 so the build artifacts \ + will be version-specific. See https://foss.heptapod.net/pypy/pypy/-/issues/3397 \ + for more information." + .to_owned(), + ] + ); + } + + #[test] + fn test_build_script_outputs_debug() { + let mut build_flags = BuildFlags::default(); + build_flags.0.insert(BuildFlag::Py_DEBUG); + let interpreter_config = InterpreterConfig { + implementation: PythonImplementation::CPython, + version: PythonVersion { major: 3, minor: 7 }, + shared: true, + abi3: false, + lib_name: Some("python3".into()), + lib_dir: None, + executable: None, + pointer_width: None, + build_flags, + suppress_build_script_link_lines: false, + extra_build_script_lines: vec![], + }; + + assert_eq!( + interpreter_config.build_script_outputs(), + [ + "cargo:rustc-cfg=Py_3_6".to_owned(), + "cargo:rustc-cfg=Py_3_7".to_owned(), + "cargo:rustc-cfg=py_sys_config=\"Py_DEBUG\"".to_owned(), + ] + ); + } } diff --git a/pyo3-build-config/src/lib.rs b/pyo3-build-config/src/lib.rs index e4655cd7b23..576dd37024b 100644 --- a/pyo3-build-config/src/lib.rs +++ b/pyo3-build-config/src/lib.rs @@ -42,7 +42,9 @@ use target_lexicon::OperatingSystem; /// For examples of how to use these attributes, [see PyO3's guide](https://pyo3.rs/latest/building_and_distribution/multiple_python_versions.html). #[cfg(feature = "resolve-config")] pub fn use_pyo3_cfgs() { - get().emit_pyo3_cfgs(); + for cargo_command in get().build_script_outputs() { + println!("{}", cargo_command) + } } /// Adds linker arguments suitable for PyO3's `extension-module` feature. diff --git a/pyo3-ffi/build.rs b/pyo3-ffi/build.rs index c472003df91..940d9808648 100644 --- a/pyo3-ffi/build.rs +++ b/pyo3-ffi/build.rs @@ -93,7 +93,9 @@ fn configure_pyo3() -> Result<()> { emit_link_config(&interpreter_config)?; } - interpreter_config.emit_pyo3_cfgs(); + for cfg in interpreter_config.build_script_outputs() { + println!("{}", cfg) + } // Extra lines come last, to support last write wins. for line in &interpreter_config.extra_build_script_lines { @@ -109,7 +111,7 @@ fn configure_pyo3() -> Result<()> { fn print_config_and_exit(config: &InterpreterConfig) { println!("\n-- PYO3_PRINT_CONFIG=1 is set, printing configuration and halting compile --"); config - .to_writer(&mut std::io::stdout()) + .to_writer(std::io::stdout()) .expect("failed to print config to stdout"); println!("\nnote: unset the PYO3_PRINT_CONFIG environment variable and retry to compile with the above config"); std::process::exit(101); From 410ef89456d79e0ac185fb0465262e9e56219126 Mon Sep 17 00:00:00 2001 From: David Hewitt <1939362+davidhewitt@users.noreply.github.com> Date: Wed, 25 Oct 2023 23:27:12 +0100 Subject: [PATCH 15/52] docs: improve detail around pyenv install --- guide/src/getting_started.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/guide/src/getting_started.md b/guide/src/getting_started.md index 682632cfa70..e3f277718cc 100644 --- a/guide/src/getting_started.md +++ b/guide/src/getting_started.md @@ -14,13 +14,21 @@ To use PyO3, you need at least Python 3.7. While you can simply use the default ## Virtualenvs -While you can use any virtualenv manager you like, we recommend the use of `pyenv` in particular if you want to develop or test for multiple different Python versions, so that is what the examples in this book will use. The installation instructions for `pyenv` can be found [here](https://github.com/pyenv/pyenv#getting-pyenv). +While you can use any virtualenv manager you like, we recommend the use of `pyenv` in particular if you want to develop or test for multiple different Python versions, so that is what the examples in this book will use. The installation instructions for `pyenv` can be found [here](https://github.com/pyenv/pyenv#getting-pyenv). You will need to install both `pyenv` and the [`pyenv-virtualenv`](https://github.com/pyenv/pyenv-virtualenv) plugin. The [pyenv installer](https://github.com/pyenv/pyenv-installer#installation--update--uninstallation) will install both together. -Note that when using `pyenv`, you should also set the following environment variable: +If you intend to run Python from Rust (for example in unit tests) you should set the following environment variable when installing a new Python version using `pyenv`: ```bash PYTHON_CONFIGURE_OPTS="--enable-shared" ``` +For example: + +```bash +env PYTHON_CONFIGURE_OPTS="--enable-shared" pyenv install 3.12 +``` + +You can read more about `pyenv`'s configuration options [here](https://github.com/pyenv/pyenv/blob/master/plugins/python-build/README.md#building-with---enable-shared). + ### Building There are a number of build and Python package management systems such as [`setuptools-rust`](https://github.com/PyO3/setuptools-rust) or [manually](https://pyo3.rs/latest/building_and_distribution.html#manual-builds). We recommend the use of `maturin`, which you can install [here](https://maturin.rs/installation.html). It is developed to work with PyO3 and provides the most "batteries included" experience, especially if you are aiming to publish to PyPI. `maturin` is just a Python package, so you can add it in the same you already install Python packages. From ba5a1da4a88778252ec5941c107834a4770b4a66 Mon Sep 17 00:00:00 2001 From: David Hewitt <1939362+davidhewitt@users.noreply.github.com> Date: Wed, 25 Oct 2023 21:31:12 +0100 Subject: [PATCH 16/52] ci: fix nightly unused import warnings --- pyo3-ffi/src/code.rs | 4 +++- pyo3-ffi/src/lib.rs | 2 ++ src/lib.rs | 1 + src/tests/common.rs | 1 + 4 files changed, 7 insertions(+), 1 deletion(-) diff --git a/pyo3-ffi/src/code.rs b/pyo3-ffi/src/code.rs index 769801021c5..d28f68cded7 100644 --- a/pyo3-ffi/src/code.rs +++ b/pyo3-ffi/src/code.rs @@ -1,2 +1,4 @@ -#[cfg(Py_LIMITED_API)] +// This header doesn't exist in CPython, but Include/cpython/code.h does. We add +// this here so that PyCodeObject has a definition under the limited API. + opaque_struct!(PyCodeObject); diff --git a/pyo3-ffi/src/lib.rs b/pyo3-ffi/src/lib.rs index cddf53e0333..5e241fbb2c7 100644 --- a/pyo3-ffi/src/lib.rs +++ b/pyo3-ffi/src/lib.rs @@ -262,6 +262,7 @@ pub use self::boolobject::*; pub use self::bytearrayobject::*; pub use self::bytesobject::*; pub use self::ceval::*; +#[cfg(Py_LIMITED_API)] pub use self::code::*; pub use self::codecs::*; pub use self::compile::*; @@ -326,6 +327,7 @@ mod bytesobject; // skipped cellobject.h mod ceval; // skipped classobject.h +#[cfg(Py_LIMITED_API)] mod code; mod codecs; mod compile; diff --git a/src/lib.rs b/src/lib.rs index 9e1a5407411..f852f6e8daa 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -436,6 +436,7 @@ pub mod once_cell { pub use crate::sync::{GILOnceCell, Interned}; } +#[allow(unused_imports)] // with no features enabled this module has no public exports pub use crate::conversions::*; #[cfg(feature = "macros")] diff --git a/src/tests/common.rs b/src/tests/common.rs index e74b09a7170..bc5a33d5cdd 100644 --- a/src/tests/common.rs +++ b/src/tests/common.rs @@ -149,4 +149,5 @@ mod inner { } } +#[allow(unused_imports)] // some tests use just the macros and none of the other functionality pub use inner::*; From 7f328767a3968b197d317deb020757809625a37c Mon Sep 17 00:00:00 2001 From: David Hewitt <1939362+davidhewitt@users.noreply.github.com> Date: Thu, 26 Oct 2023 08:21:31 +0100 Subject: [PATCH 17/52] Note about `pyenv activate` and `pyenv virtualenv` commands Co-authored-by: Niko Matsakis --- guide/src/getting_started.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guide/src/getting_started.md b/guide/src/getting_started.md index e3f277718cc..22bce336d80 100644 --- a/guide/src/getting_started.md +++ b/guide/src/getting_started.md @@ -14,7 +14,7 @@ To use PyO3, you need at least Python 3.7. While you can simply use the default ## Virtualenvs -While you can use any virtualenv manager you like, we recommend the use of `pyenv` in particular if you want to develop or test for multiple different Python versions, so that is what the examples in this book will use. The installation instructions for `pyenv` can be found [here](https://github.com/pyenv/pyenv#getting-pyenv). You will need to install both `pyenv` and the [`pyenv-virtualenv`](https://github.com/pyenv/pyenv-virtualenv) plugin. The [pyenv installer](https://github.com/pyenv/pyenv-installer#installation--update--uninstallation) will install both together. +While you can use any virtualenv manager you like, we recommend the use of `pyenv` in particular if you want to develop or test for multiple different Python versions, so that is what the examples in this book will use. The installation instructions for `pyenv` can be found [here](https://github.com/pyenv/pyenv#getting-pyenv). (Note: To get the `pyenv activate` and `pyenv virtualenv` commands, you will also need to install the [`pyenv-virtualenv`](https://github.com/pyenv/pyenv-virtualenv) plugin. The [pyenv installer](https://github.com/pyenv/pyenv-installer#installation--update--uninstallation) will install both together.) If you intend to run Python from Rust (for example in unit tests) you should set the following environment variable when installing a new Python version using `pyenv`: ```bash From 8e08e4ad1b07ed3936654a2fa6d3bb6b18d04c9e Mon Sep 17 00:00:00 2001 From: David Hewitt <1939362+davidhewitt@users.noreply.github.com> Date: Fri, 20 Oct 2023 22:37:04 +0100 Subject: [PATCH 18/52] examples: remove requirements-dev.txt files --- examples/decorator/.template/pyproject.toml | 3 +++ examples/decorator/noxfile.py | 7 +++---- examples/decorator/pyproject.toml | 3 +++ examples/decorator/requirements-dev.txt | 3 --- examples/getitem/.template/pyproject.toml | 3 +++ examples/getitem/noxfile.py | 7 +++---- examples/getitem/pyproject.toml | 3 +++ examples/getitem/requirements-dev.txt | 3 --- examples/maturin-starter/.template/pyproject.toml | 3 +++ examples/maturin-starter/noxfile.py | 5 ++--- examples/maturin-starter/pyproject.toml | 3 +++ examples/maturin-starter/requirements-dev.txt | 3 --- examples/plugin/plugin_api/noxfile.py | 5 ++--- examples/plugin/plugin_api/pyproject.toml | 3 +++ examples/plugin/plugin_api/requirements-dev.txt | 3 --- examples/setuptools-rust-starter/noxfile.py | 2 +- examples/string-sum/.template/pyproject.toml | 3 +++ examples/string-sum/noxfile.py | 7 +++---- examples/string-sum/pyproject.toml | 5 ++++- examples/string-sum/requirements-dev.txt | 3 --- examples/word-count/.template/pre-script.rhai | 1 + examples/word-count/.template/pyproject.toml | 6 +++++- examples/word-count/.template/setup.cfg | 9 --------- examples/word-count/noxfile.py | 13 ++++++------- examples/word-count/pyproject.toml | 2 ++ examples/word-count/requirements-dev.txt | 2 -- 26 files changed, 56 insertions(+), 54 deletions(-) delete mode 100644 examples/decorator/requirements-dev.txt delete mode 100644 examples/getitem/requirements-dev.txt delete mode 100644 examples/maturin-starter/requirements-dev.txt delete mode 100644 examples/plugin/plugin_api/requirements-dev.txt delete mode 100644 examples/string-sum/requirements-dev.txt delete mode 100644 examples/word-count/.template/setup.cfg delete mode 100644 examples/word-count/requirements-dev.txt diff --git a/examples/decorator/.template/pyproject.toml b/examples/decorator/.template/pyproject.toml index 537fdacc666..d27a1be9832 100644 --- a/examples/decorator/.template/pyproject.toml +++ b/examples/decorator/.template/pyproject.toml @@ -5,3 +5,6 @@ build-backend = "maturin" [project] name = "{{project-name}}" version = "0.1.0" + +[project.optional-dependencies] +dev = ["pytest"] diff --git a/examples/decorator/noxfile.py b/examples/decorator/noxfile.py index 17a6b80f3f5..23d967cc695 100644 --- a/examples/decorator/noxfile.py +++ b/examples/decorator/noxfile.py @@ -2,8 +2,7 @@ @nox.session -def python(session): - session.install("-rrequirements-dev.txt") - session.install("maturin") - session.run_always("maturin", "develop") +def python(session: nox.Session): + session.env["MATURIN_PEP517_ARGS"] = "--profile=dev" + session.install(".[dev]") session.run("pytest") diff --git a/examples/decorator/pyproject.toml b/examples/decorator/pyproject.toml index 8575ca25fc2..88280a6b657 100644 --- a/examples/decorator/pyproject.toml +++ b/examples/decorator/pyproject.toml @@ -14,3 +14,6 @@ classifiers = [ "Operating System :: POSIX", "Operating System :: MacOS :: MacOS X", ] + +[project.optional-dependencies] +dev = ["pytest"] diff --git a/examples/decorator/requirements-dev.txt b/examples/decorator/requirements-dev.txt deleted file mode 100644 index 9ea7f14b6dd..00000000000 --- a/examples/decorator/requirements-dev.txt +++ /dev/null @@ -1,3 +0,0 @@ -pytest>=3.5.0 -pip>=21.3 -maturin>=0.12,<0.13 diff --git a/examples/getitem/.template/pyproject.toml b/examples/getitem/.template/pyproject.toml index 537fdacc666..d27a1be9832 100644 --- a/examples/getitem/.template/pyproject.toml +++ b/examples/getitem/.template/pyproject.toml @@ -5,3 +5,6 @@ build-backend = "maturin" [project] name = "{{project-name}}" version = "0.1.0" + +[project.optional-dependencies] +dev = ["pytest"] diff --git a/examples/getitem/noxfile.py b/examples/getitem/noxfile.py index 17a6b80f3f5..23d967cc695 100644 --- a/examples/getitem/noxfile.py +++ b/examples/getitem/noxfile.py @@ -2,8 +2,7 @@ @nox.session -def python(session): - session.install("-rrequirements-dev.txt") - session.install("maturin") - session.run_always("maturin", "develop") +def python(session: nox.Session): + session.env["MATURIN_PEP517_ARGS"] = "--profile=dev" + session.install(".[dev]") session.run("pytest") diff --git a/examples/getitem/pyproject.toml b/examples/getitem/pyproject.toml index 2cb6dc71680..6e05570eac6 100644 --- a/examples/getitem/pyproject.toml +++ b/examples/getitem/pyproject.toml @@ -14,3 +14,6 @@ classifiers = [ "Operating System :: POSIX", "Operating System :: MacOS :: MacOS X", ] + +[project.optional-dependencies] +dev = ["pytest"] diff --git a/examples/getitem/requirements-dev.txt b/examples/getitem/requirements-dev.txt deleted file mode 100644 index d9913970b49..00000000000 --- a/examples/getitem/requirements-dev.txt +++ /dev/null @@ -1,3 +0,0 @@ -pytest>=3.5.0 -pip>=21.3 -maturin>=1,<2 diff --git a/examples/maturin-starter/.template/pyproject.toml b/examples/maturin-starter/.template/pyproject.toml index 537fdacc666..d27a1be9832 100644 --- a/examples/maturin-starter/.template/pyproject.toml +++ b/examples/maturin-starter/.template/pyproject.toml @@ -5,3 +5,6 @@ build-backend = "maturin" [project] name = "{{project-name}}" version = "0.1.0" + +[project.optional-dependencies] +dev = ["pytest"] diff --git a/examples/maturin-starter/noxfile.py b/examples/maturin-starter/noxfile.py index 17a6b80f3f5..12ed90e5d1c 100644 --- a/examples/maturin-starter/noxfile.py +++ b/examples/maturin-starter/noxfile.py @@ -3,7 +3,6 @@ @nox.session def python(session): - session.install("-rrequirements-dev.txt") - session.install("maturin") - session.run_always("maturin", "develop") + session.env["MATURIN_PEP517_ARGS"] = "--profile=dev" + session.install(".[dev]") session.run("pytest") diff --git a/examples/maturin-starter/pyproject.toml b/examples/maturin-starter/pyproject.toml index fb9c808f283..52ecba15edc 100644 --- a/examples/maturin-starter/pyproject.toml +++ b/examples/maturin-starter/pyproject.toml @@ -14,3 +14,6 @@ classifiers = [ "Operating System :: POSIX", "Operating System :: MacOS :: MacOS X", ] + +[project.optional-dependencies] +dev = ["pytest"] diff --git a/examples/maturin-starter/requirements-dev.txt b/examples/maturin-starter/requirements-dev.txt deleted file mode 100644 index 9ea7f14b6dd..00000000000 --- a/examples/maturin-starter/requirements-dev.txt +++ /dev/null @@ -1,3 +0,0 @@ -pytest>=3.5.0 -pip>=21.3 -maturin>=0.12,<0.13 diff --git a/examples/plugin/plugin_api/noxfile.py b/examples/plugin/plugin_api/noxfile.py index 3b53c0c3e36..12ed90e5d1c 100644 --- a/examples/plugin/plugin_api/noxfile.py +++ b/examples/plugin/plugin_api/noxfile.py @@ -3,7 +3,6 @@ @nox.session def python(session): - session.install("-rrequirements-dev.txt") - session.install("maturin") - session.run_always("maturin", "develop", "--features", "extension-module") + session.env["MATURIN_PEP517_ARGS"] = "--profile=dev" + session.install(".[dev]") session.run("pytest") diff --git a/examples/plugin/plugin_api/pyproject.toml b/examples/plugin/plugin_api/pyproject.toml index 5a460385cb6..82f331d9573 100644 --- a/examples/plugin/plugin_api/pyproject.toml +++ b/examples/plugin/plugin_api/pyproject.toml @@ -10,3 +10,6 @@ classifiers = [ "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", ] + +[project.optional-dependencies] +dev = ["pytest"] diff --git a/examples/plugin/plugin_api/requirements-dev.txt b/examples/plugin/plugin_api/requirements-dev.txt deleted file mode 100644 index 20c7cdfbb1c..00000000000 --- a/examples/plugin/plugin_api/requirements-dev.txt +++ /dev/null @@ -1,3 +0,0 @@ -pytest>=3.5.0 -pip>=21.3 -maturin>=0.14 diff --git a/examples/setuptools-rust-starter/noxfile.py b/examples/setuptools-rust-starter/noxfile.py index f6c62af1de2..9edab96254b 100644 --- a/examples/setuptools-rust-starter/noxfile.py +++ b/examples/setuptools-rust-starter/noxfile.py @@ -2,7 +2,7 @@ @nox.session -def python(session): +def python(session: nox.Session): session.install("-rrequirements-dev.txt") session.run_always( "pip", "install", "-e", ".", "--no-build-isolation", env={"BUILD_DEBUG": "1"} diff --git a/examples/string-sum/.template/pyproject.toml b/examples/string-sum/.template/pyproject.toml index 537fdacc666..d27a1be9832 100644 --- a/examples/string-sum/.template/pyproject.toml +++ b/examples/string-sum/.template/pyproject.toml @@ -5,3 +5,6 @@ build-backend = "maturin" [project] name = "{{project-name}}" version = "0.1.0" + +[project.optional-dependencies] +dev = ["pytest"] diff --git a/examples/string-sum/noxfile.py b/examples/string-sum/noxfile.py index 17a6b80f3f5..23d967cc695 100644 --- a/examples/string-sum/noxfile.py +++ b/examples/string-sum/noxfile.py @@ -2,8 +2,7 @@ @nox.session -def python(session): - session.install("-rrequirements-dev.txt") - session.install("maturin") - session.run_always("maturin", "develop") +def python(session: nox.Session): + session.env["MATURIN_PEP517_ARGS"] = "--profile=dev" + session.install(".[dev]") session.run("pytest") diff --git a/examples/string-sum/pyproject.toml b/examples/string-sum/pyproject.toml index 088f2b47085..4433178e3be 100644 --- a/examples/string-sum/pyproject.toml +++ b/examples/string-sum/pyproject.toml @@ -3,7 +3,7 @@ requires = ["maturin>=1,<2"] build-backend = "maturin" [project] -name = "string sum" +name = "string_sum" version = "0.1.0" classifiers = [ "License :: OSI Approved :: MIT License", @@ -14,3 +14,6 @@ classifiers = [ "Operating System :: POSIX", "Operating System :: MacOS :: MacOS X", ] + +[project.optional-dependencies] +dev = ["pytest"] diff --git a/examples/string-sum/requirements-dev.txt b/examples/string-sum/requirements-dev.txt deleted file mode 100644 index 9ea7f14b6dd..00000000000 --- a/examples/string-sum/requirements-dev.txt +++ /dev/null @@ -1,3 +0,0 @@ -pytest>=3.5.0 -pip>=21.3 -maturin>=0.12,<0.13 diff --git a/examples/word-count/.template/pre-script.rhai b/examples/word-count/.template/pre-script.rhai index 0748517e4f0..5ba02b12ffd 100644 --- a/examples/word-count/.template/pre-script.rhai +++ b/examples/word-count/.template/pre-script.rhai @@ -1,3 +1,4 @@ variable::set("PYO3_VERSION", "0.20.0"); file::rename(".template/Cargo.toml", "Cargo.toml"); +file::rename(".template/pyproject.toml", "pyproject.toml"); file::delete(".template"); diff --git a/examples/word-count/.template/pyproject.toml b/examples/word-count/.template/pyproject.toml index b436b675322..8aa3c9553b8 100644 --- a/examples/word-count/.template/pyproject.toml +++ b/examples/word-count/.template/pyproject.toml @@ -1,9 +1,13 @@ [build-system] -requires = ["setuptools>=41.0.0", "wheel", "setuptools_rust>=1.0.0"] +requires = ["maturin>=1,<2"] +build-backend = "maturin" [project] name = "{{project-name}}" version = "0.1.0" +[project.optional-dependencies] +dev = ["pytest"] + [tool.pytest.ini_options] addopts = "--benchmark-disable" diff --git a/examples/word-count/.template/setup.cfg b/examples/word-count/.template/setup.cfg deleted file mode 100644 index 7b86182648b..00000000000 --- a/examples/word-count/.template/setup.cfg +++ /dev/null @@ -1,9 +0,0 @@ -[metadata] -name = {{project-name}} -version = 0.1.0 -packages = - word_count - -[options] -include_package_data = True -zip_safe = False diff --git a/examples/word-count/noxfile.py b/examples/word-count/noxfile.py index 39230d2609b..d64f210f3e5 100644 --- a/examples/word-count/noxfile.py +++ b/examples/word-count/noxfile.py @@ -4,15 +4,14 @@ @nox.session -def test(session): - session.install("-rrequirements-dev.txt") - session.install("maturin") - session.run_always("maturin", "develop") +def test(session: nox.Session): + session.env["MATURIN_PEP517_ARGS"] = "--profile=dev" + session.install(".[dev]") session.run("pytest") @nox.session -def bench(session): - session.install("-rrequirements-dev.txt") - session.install(".") +def bench(session: nox.Session): + session.env["MATURIN_PEP517_ARGS"] = "--profile=dev" + session.install(".[dev]") session.run("pytest", "--benchmark-enable") diff --git a/examples/word-count/pyproject.toml b/examples/word-count/pyproject.toml index 6f88a5170f1..e5703294af6 100644 --- a/examples/word-count/pyproject.toml +++ b/examples/word-count/pyproject.toml @@ -15,6 +15,8 @@ classifiers = [ "Operating System :: MacOS :: MacOS X", ] +[project.optional-dependencies] +dev = ["pytest", "pytest-benchmark"] [tool.pytest.ini_options] addopts = "--benchmark-disable" diff --git a/examples/word-count/requirements-dev.txt b/examples/word-count/requirements-dev.txt deleted file mode 100644 index fcd8fc54c18..00000000000 --- a/examples/word-count/requirements-dev.txt +++ /dev/null @@ -1,2 +0,0 @@ -pytest>=3.5.0 -pytest-benchmark>=3.1.1 From 04af02f15561b590676339bae28957a637547ce8 Mon Sep 17 00:00:00 2001 From: David Hewitt <1939362+davidhewitt@users.noreply.github.com> Date: Thu, 26 Oct 2023 08:52:07 +0100 Subject: [PATCH 19/52] ci: switch from black to ruff --- .github/workflows/ci.yml | 6 +++--- examples/decorator/tests/example.py | 3 +++ examples/decorator/tests/test_.py | 2 +- .../maturin_starter/__init__.py | 6 ++++-- .../plugin/plugin_api/tests/test_import.py | 2 +- .../setuptools_rust_starter/__init__.py | 6 ++++-- examples/string-sum/tests/test_.py | 8 +++---- noxfile.py | 21 +++++++------------ pyproject.toml | 21 ++----------------- pytests/tests/test_datetime.py | 9 +++----- pytests/tests/test_objstore.py | 2 -- pytests/tests/test_subclassing.py | 2 -- 12 files changed, 33 insertions(+), 55 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 55bacb77ca8..ba78ca84af8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,10 +27,10 @@ jobs: - uses: dtolnay/rust-toolchain@stable with: components: rustfmt - - name: Check python formatting (black) - run: nox -s fmt-py + - name: Check python formatting and lints (ruff) + run: nox -s ruff - name: Check rust formatting (rustfmt) - run: nox -s fmt-rust + run: nox -s rustfmt check-msrv: needs: [fmt] diff --git a/examples/decorator/tests/example.py b/examples/decorator/tests/example.py index 5725a2ed024..7be4b33e974 100644 --- a/examples/decorator/tests/example.py +++ b/examples/decorator/tests/example.py @@ -1,3 +1,6 @@ +from decorator import Counter + + @Counter def say_hello(): print("hello") diff --git a/examples/decorator/tests/test_.py b/examples/decorator/tests/test_.py index 26056713446..d97bf27ddab 100644 --- a/examples/decorator/tests/test_.py +++ b/examples/decorator/tests/test_.py @@ -45,7 +45,7 @@ def test_discussion_2598(): @Counter def say_hello(): if say_hello.count < 2: - print(f"hello from decorator") + print("hello from decorator") say_hello() say_hello() diff --git a/examples/maturin-starter/maturin_starter/__init__.py b/examples/maturin-starter/maturin_starter/__init__.py index 5fe00cb5454..d0525f19305 100644 --- a/examples/maturin-starter/maturin_starter/__init__.py +++ b/examples/maturin-starter/maturin_starter/__init__.py @@ -1,7 +1,9 @@ # import the contents of the Rust library into the Python extension -# optional: include the documentation from the Rust module from .maturin_starter import * -from .maturin_starter import __all__, __doc__ +from .maturin_starter import __all__ + +# optional: include the documentation from the Rust module +from .maturin_starter import __doc__ # noqa: F401 __all__ = __all__ + ["PythonClass"] diff --git a/examples/plugin/plugin_api/tests/test_import.py b/examples/plugin/plugin_api/tests/test_import.py index ae1d6f67f6e..2a728920565 100644 --- a/examples/plugin/plugin_api/tests/test_import.py +++ b/examples/plugin/plugin_api/tests/test_import.py @@ -1,2 +1,2 @@ def test_import(): - import plugin_api + import plugin_api # noqa: F401 diff --git a/examples/setuptools-rust-starter/setuptools_rust_starter/__init__.py b/examples/setuptools-rust-starter/setuptools_rust_starter/__init__.py index 1dcb91e00e2..ecca1d15e5c 100644 --- a/examples/setuptools-rust-starter/setuptools_rust_starter/__init__.py +++ b/examples/setuptools-rust-starter/setuptools_rust_starter/__init__.py @@ -1,7 +1,9 @@ # import the contents of the Rust library into the Python extension -# optional: include the documentation from the Rust module from ._setuptools_rust_starter import * -from ._setuptools_rust_starter import __all__, __doc__ +from ._setuptools_rust_starter import __all__ + +# optional: include the documentation from the Rust module +from ._setuptools_rust_starter import __doc__ # noqa: F401 __all__ = __all__ + ["PythonClass"] diff --git a/examples/string-sum/tests/test_.py b/examples/string-sum/tests/test_.py index d9b5ab5fd44..a3e88a6344f 100644 --- a/examples/string-sum/tests/test_.py +++ b/examples/string-sum/tests/test_.py @@ -14,7 +14,7 @@ def test_err1(): with pytest.raises( TypeError, match="sum_as_string expected an int for positional argument 1" - ) as e: + ): sum_as_string(a, b) @@ -23,19 +23,19 @@ def test_err2(): with pytest.raises( TypeError, match="sum_as_string expected an int for positional argument 2" - ) as e: + ): sum_as_string(a, b) def test_overflow1(): a, b = 0, 1 << 43 - with pytest.raises(OverflowError, match="cannot fit 8796093022208 in 32 bits") as e: + with pytest.raises(OverflowError, match="cannot fit 8796093022208 in 32 bits"): sum_as_string(a, b) def test_overflow2(): a, b = 1 << 30, 1 << 30 - with pytest.raises(OverflowError, match="arguments too large to add") as e: + with pytest.raises(OverflowError, match="arguments too large to add"): sum_as_string(a, b) diff --git a/noxfile.py b/noxfile.py index 583b6051292..27f8befbd1e 100644 --- a/noxfile.py +++ b/noxfile.py @@ -66,22 +66,17 @@ def coverage(session: nox.Session) -> None: ) -@nox.session -def fmt(session: nox.Session): - fmt_rust(session) - fmt_py(session) - - -@nox.session(name="fmt-rust", venv_backend="none") -def fmt_rust(session: nox.Session): +@nox.session(venv_backend="none") +def rustfmt(session: nox.Session): _run_cargo(session, "fmt", "--all", "--check") _run_cargo(session, "fmt", _FFI_CHECK, "--all", "--check") -@nox.session(name="fmt-py") -def fmt_py(session: nox.Session): - session.install("black==22.3.0") - _run(session, "black", ".", "--check") +@nox.session(name="ruff") +def ruff(session: nox.Session): + session.install("ruff") + _run(session, "ruff", "format", ".", "--check") + _run(session, "ruff", "check", ".") @nox.session(name="clippy", venv_backend="none") @@ -221,7 +216,7 @@ def contributors(session: nox.Session) -> None: for commit in body["commits"]: try: authors.add(commit["author"]["login"]) - except: + except Exception: continue if "next" in resp.links: diff --git a/pyproject.toml b/pyproject.toml index 70263bcf873..29626e970a4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,22 +1,5 @@ -[tool.black] -target_version = ['py35'] -include = '\.pyi?$' -exclude = ''' - -( - /( - \.eggs # exclude a few common directories in the - | \.git # root of the project - | \.mypy_cache - | \.tox - | \.nox - | \.venv - | venv - | target - | dist - )/ -) -''' +[tool.ruff.extend-per-file-ignores] +"__init__.py" = ["F403"] [tool.towncrier] filename = "CHANGELOG.md" diff --git a/pytests/tests/test_datetime.py b/pytests/tests/test_datetime.py index d4c1b60ec61..c81d13a929a 100644 --- a/pytests/tests/test_datetime.py +++ b/pytests/tests/test_datetime.py @@ -118,16 +118,15 @@ def test_time(args, kwargs): @given(t=st.times()) -def test_time(t): +def test_time_hypothesis(t): act = rdt.get_time_tuple(t) exp = (t.hour, t.minute, t.second, t.microsecond) assert act == exp -@pytest.mark.xfail(PYPY, reason="Feature not available on PyPy") @given(t=st.times()) -def test_time_fold(t): +def test_time_tuple_fold(t): t_nofold = t.replace(fold=0) t_fold = t.replace(fold=1) @@ -138,9 +137,8 @@ def test_time_fold(t): assert act == exp -@pytest.mark.xfail(PYPY, reason="Feature not available on PyPy") @pytest.mark.parametrize("fold", [False, True]) -def test_time_fold(fold): +def test_time_with_fold(fold): t = rdt.time_with_fold(0, 0, 0, 0, None, fold) assert t.fold == fold @@ -206,7 +204,6 @@ def test_datetime_tuple(dt): assert act == exp -@pytest.mark.xfail(PYPY, reason="Feature not available on PyPy") @given(dt=st.datetimes()) def test_datetime_tuple_fold(dt): dt_fold = dt.replace(fold=1) diff --git a/pytests/tests/test_objstore.py b/pytests/tests/test_objstore.py index 0b1f46f4933..bfd8bad84df 100644 --- a/pytests/tests/test_objstore.py +++ b/pytests/tests/test_objstore.py @@ -1,8 +1,6 @@ import gc -import platform import sys -import pytest from pyo3_pytests.objstore import ObjStore diff --git a/pytests/tests/test_subclassing.py b/pytests/tests/test_subclassing.py index 5851c03334d..2cee283dda7 100644 --- a/pytests/tests/test_subclassing.py +++ b/pytests/tests/test_subclassing.py @@ -1,5 +1,3 @@ -import platform - from pyo3_pytests.subclassing import Subclassable From 2aca7f53f043351fecaa7fcb0fca1fac86da28a0 Mon Sep 17 00:00:00 2001 From: Samuel Pastva Date: Thu, 26 Oct 2023 16:36:55 -0500 Subject: [PATCH 20/52] Add example of dynamic return type in the "Python classes" section of the guide. --- guide/src/class.md | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/guide/src/class.md b/guide/src/class.md index 2c8b1c4e1b0..bb6004cd02e 100644 --- a/guide/src/class.md +++ b/guide/src/class.md @@ -337,10 +337,27 @@ impl SubSubClass { let super_ = self_.into_super(); // Get PyRef<'_, SubClass> SubClass::method2(super_).map(|x| x * v) } + + #[staticmethod] + fn dynamic_type(py: Python<'_>, val: usize) -> PyResult { + let base = PyClassInitializer::from(BaseClass::new()); + let sub = base.add_subclass(SubClass { val2: val }); + if val % 2 == 0 { + Ok(Py::new(py, sub)?.to_object(py)) + } else { + let sub_sub = sub.add_subclass(SubSubClass { val3: val }); + Ok(Py::new(py, sub_sub)?.to_object(py)) + } + } } # Python::with_gil(|py| { # let subsub = pyo3::PyCell::new(py, SubSubClass::new()).unwrap(); -# pyo3::py_run!(py, subsub, "assert subsub.method3() == 3000") +# pyo3::py_run!(py, subsub, "assert subsub.method3() == 3000"); +# let subsub = SubSubClass::dynamic_type(py, 2).unwrap(); +# let subsubsub = SubSubClass::dynamic_type(py, 3).unwrap(); +# let cls = py.get_type::(); +# pyo3::py_run!(py, subsub cls, "assert not isinstance(subsub, cls)"); +# pyo3::py_run!(py, subsubsub cls, "assert isinstance(subsubsub, cls)"); # }); ``` From 15c280015dad0cd7502732b19fe739ee58cff484 Mon Sep 17 00:00:00 2001 From: Samuel Pastva Date: Thu, 26 Oct 2023 18:58:43 -0500 Subject: [PATCH 21/52] Update guide/src/class.md Co-authored-by: Bruno Kolenbrander <59372212+mejrs@users.noreply.github.com> --- guide/src/class.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guide/src/class.md b/guide/src/class.md index bb6004cd02e..7ce75d5ba0d 100644 --- a/guide/src/class.md +++ b/guide/src/class.md @@ -339,7 +339,7 @@ impl SubSubClass { } #[staticmethod] - fn dynamic_type(py: Python<'_>, val: usize) -> PyResult { + fn factory_method(py: Python<'_>, val: usize) -> PyResult { let base = PyClassInitializer::from(BaseClass::new()); let sub = base.add_subclass(SubClass { val2: val }); if val % 2 == 0 { From 10086d176a6f32819b3278fd36953794c620507e Mon Sep 17 00:00:00 2001 From: Samuel Pastva Date: Thu, 26 Oct 2023 19:02:06 -0500 Subject: [PATCH 22/52] Finish rename --- guide/src/class.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/guide/src/class.md b/guide/src/class.md index 7ce75d5ba0d..dbcbc444552 100644 --- a/guide/src/class.md +++ b/guide/src/class.md @@ -353,8 +353,8 @@ impl SubSubClass { # Python::with_gil(|py| { # let subsub = pyo3::PyCell::new(py, SubSubClass::new()).unwrap(); # pyo3::py_run!(py, subsub, "assert subsub.method3() == 3000"); -# let subsub = SubSubClass::dynamic_type(py, 2).unwrap(); -# let subsubsub = SubSubClass::dynamic_type(py, 3).unwrap(); +# let subsub = SubSubClass::factory_method(py, 2).unwrap(); +# let subsubsub = SubSubClass::factory_method(py, 3).unwrap(); # let cls = py.get_type::(); # pyo3::py_run!(py, subsub cls, "assert not isinstance(subsub, cls)"); # pyo3::py_run!(py, subsubsub cls, "assert isinstance(subsubsub, cls)"); From 04bb9f2110345a4474f2494365ceefc0665117b0 Mon Sep 17 00:00:00 2001 From: Joseph Perez Date: Sun, 29 Oct 2023 08:28:39 +0100 Subject: [PATCH 23/52] feat: add `take` and `into_inner` methods to `GILOnceCell` --- src/sync.rs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/sync.rs b/src/sync.rs index 50bb80da2a3..7f7e11c9ae7 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -167,6 +167,20 @@ impl GILOnceCell { *inner = Some(value); Ok(()) } + + /// Takes the value out of the cell, moving it back to an uninitialized state. + /// + /// Has no effect and returns None if the cell has not yet been written. + pub fn take(&mut self) -> Option { + self.0.get_mut().take() + } + + /// Consumes the cell, returning the wrapped value. + /// + /// Returns None if the cell has not yet been written. + pub fn into_inner(self) -> Option { + self.0.into_inner() + } } impl GILOnceCell> { @@ -278,7 +292,7 @@ mod tests { #[test] fn test_once_cell() { Python::with_gil(|py| { - let cell = GILOnceCell::new(); + let mut cell = GILOnceCell::new(); assert!(cell.get(py).is_none()); @@ -289,6 +303,9 @@ mod tests { assert_eq!(cell.get(py), Some(&2)); assert_eq!(cell.get_or_try_init(py, || Err(5)), Ok(&2)); + + assert_eq!(cell.take(), Some(2)); + assert_eq!(cell.into_inner(), None) }) } } From a4aba0a09a45dab95e1d258c20905340b23e3c78 Mon Sep 17 00:00:00 2001 From: Joseph Perez Date: Sun, 29 Oct 2023 13:50:36 +0100 Subject: [PATCH 24/52] refactor: remove useless `unsafe` in `get_mut` --- src/sync.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/sync.rs b/src/sync.rs index 7f7e11c9ae7..8500413ef97 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -149,8 +149,7 @@ impl GILOnceCell { /// Get the contents of the cell mutably. This is only possible if the reference to the cell is /// unique. pub fn get_mut(&mut self) -> Option<&mut T> { - // Safe because we have &mut self - unsafe { &mut *self.0.get() }.as_mut() + self.0.get_mut().as_mut() } /// Set the value in the cell. From b08c92b306295b1e2673c47ffec3944dd1221280 Mon Sep 17 00:00:00 2001 From: Joseph Perez Date: Sun, 29 Oct 2023 13:54:10 +0100 Subject: [PATCH 25/52] docs: add newsfragment --- newsfragments/3556.added.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 newsfragments/3556.added.md diff --git a/newsfragments/3556.added.md b/newsfragments/3556.added.md new file mode 100644 index 00000000000..014908a1bf5 --- /dev/null +++ b/newsfragments/3556.added.md @@ -0,0 +1 @@ +Add `take` and `into_inner` methods to `GILOnceCell` \ No newline at end of file From d649f6603f18a8936ac70ff74bb63ad2acfd1b3f Mon Sep 17 00:00:00 2001 From: David Hewitt <1939362+davidhewitt@users.noreply.github.com> Date: Mon, 30 Oct 2023 22:21:26 +0000 Subject: [PATCH 26/52] add link to user guide to cross compile error message --- pyo3-build-config/src/impl_.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index 8ef14d15b9d..ec05d7dc956 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -1361,9 +1361,13 @@ fn default_cross_compile(cross_compile_config: &CrossCompileConfig) -> Result Date: Fri, 10 Nov 2023 09:27:01 -0500 Subject: [PATCH 27/52] fix missing char conversion --- guide/src/conversions/tables.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guide/src/conversions/tables.md b/guide/src/conversions/tables.md index bd77450a30c..92dc813900a 100644 --- a/guide/src/conversions/tables.md +++ b/guide/src/conversions/tables.md @@ -13,7 +13,7 @@ The table below contains the Python type and the corresponding function argument | Python | Rust | Rust (Python-native) | | ------------- |:-------------------------------:|:--------------------:| | `object` | - | `&PyAny` | -| `str` | `String`, `Cow`, `&str`, `OsString`, `PathBuf`, `Path` | `&PyString`, `&PyUnicode` | +| `str` | `String`, `Cow`, `&str`, `char`, `OsString`, `PathBuf`, `Path` | `&PyString`, `&PyUnicode` | | `bytes` | `Vec`, `&[u8]`, `Cow<[u8]>` | `&PyBytes` | | `bool` | `bool` | `&PyBool` | | `int` | `i8`, `u8`, `i16`, `u16`, `i32`, `u32`, `i64`, `u64`, `i128`, `u128`, `isize`, `usize`, `num_bigint::BigInt`[^1], `num_bigint::BigUint`[^1] | `&PyLong` | From aa6f1466d2be58eaf668d3bf95bc93932da2ff1a Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Tue, 14 Nov 2023 19:09:45 +0000 Subject: [PATCH 28/52] ci: try to run without rust-toolchain.toml --- .github/workflows/ci.yml | 3 ++- rust-toolchain.toml | 2 -- 2 files changed, 2 insertions(+), 3 deletions(-) delete mode 100644 rust-toolchain.toml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ba78ca84af8..accfed4c4bf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -318,7 +318,7 @@ jobs: - uses: dtolnay/rust-toolchain@stable if: steps.should-skip.outputs.skip != 'true' with: - components: llvm-tools-preview + components: llvm-tools-preview,rust-src - name: Install cargo-llvm-cov if: steps.should-skip.outputs.skip != 'true' uses: taiki-e/install-action@cargo-llvm-cov @@ -347,6 +347,7 @@ jobs: uses: dtolnay/rust-toolchain@stable with: targets: wasm32-unknown-emscripten + components: rust-src - uses: actions/setup-node@v4 with: node-version: 14 diff --git a/rust-toolchain.toml b/rust-toolchain.toml deleted file mode 100644 index 133187e9cd7..00000000000 --- a/rust-toolchain.toml +++ /dev/null @@ -1,2 +0,0 @@ -[toolchain] -components = [ "rust-src" ] From 8d637b0b5bb7d047689e8a47b701d0ed60223455 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Thu, 16 Nov 2023 16:06:01 +0000 Subject: [PATCH 29/52] ci: updates for rust 1.74 --- pyo3-ffi/src/object.rs | 4 ++-- tests/ui/not_send.stderr | 2 +- tests/ui/not_send2.stderr | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pyo3-ffi/src/object.rs b/pyo3-ffi/src/object.rs index 1f3a1094973..0e0243cd764 100644 --- a/pyo3-ffi/src/object.rs +++ b/pyo3-ffi/src/object.rs @@ -487,7 +487,7 @@ pub unsafe fn Py_INCREF(op: *mut PyObject) { ) ))] { - return _Py_IncRef(op); + _Py_IncRef(op); } #[cfg(all(py_sys_config = "Py_REF_DEBUG", not(Py_3_10)))] @@ -552,7 +552,7 @@ pub unsafe fn Py_DECREF(op: *mut PyObject) { ) ))] { - return _Py_DecRef(op); + _Py_DecRef(op); } #[cfg(all(py_sys_config = "Py_REF_DEBUG", not(Py_3_10)))] diff --git a/tests/ui/not_send.stderr b/tests/ui/not_send.stderr index 395723cba1f..9ac51f36ec0 100644 --- a/tests/ui/not_send.stderr +++ b/tests/ui/not_send.stderr @@ -34,7 +34,7 @@ note: required because it's used within this closure | 4 | py.allow_threads(|| { drop(py); }); | ^^ - = note: required for `[closure@$DIR/tests/ui/not_send.rs:4:22: 4:24]` to implement `Ungil` + = note: required for `{closure@$DIR/tests/ui/not_send.rs:4:22: 4:24}` to implement `Ungil` note: required by a bound in `pyo3::Python::<'py>::allow_threads` --> src/marker.rs | diff --git a/tests/ui/not_send2.stderr b/tests/ui/not_send2.stderr index 2e6db009bad..d3a60a1f708 100644 --- a/tests/ui/not_send2.stderr +++ b/tests/ui/not_send2.stderr @@ -27,7 +27,7 @@ note: required because it's used within this closure | 8 | py.allow_threads(|| { | ^^ - = note: required for `[closure@$DIR/tests/ui/not_send2.rs:8:26: 8:28]` to implement `Ungil` + = note: required for `{closure@$DIR/tests/ui/not_send2.rs:8:26: 8:28}` to implement `Ungil` note: required by a bound in `pyo3::Python::<'py>::allow_threads` --> src/marker.rs | From bbc5404297eab6510fafba67d7e93693739e431a Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Fri, 17 Nov 2023 15:21:09 +0000 Subject: [PATCH 30/52] ci: move lints to new 1.74 cargo.toml tables --- .cargo/config | 20 -------------------- .github/workflows/build.yml | 2 +- Cargo.toml | 29 +++++++++++++++++++++++++++++ pyo3-ffi/Cargo.toml | 4 +++- pyo3-macros-backend/Cargo.toml | 3 +++ pyo3-macros/Cargo.toml | 3 +++ pyo3-macros/src/lib.rs | 2 -- pytests/Cargo.toml | 3 +++ src/lib.rs | 9 --------- 9 files changed, 42 insertions(+), 33 deletions(-) delete mode 100644 .cargo/config diff --git a/.cargo/config b/.cargo/config deleted file mode 100644 index 5b19f2fabde..00000000000 --- a/.cargo/config +++ /dev/null @@ -1,20 +0,0 @@ -[target.'cfg(feature = "cargo-clippy")'] -rustflags = [ - # Lints to enforce in CI - "-Dclippy::checked_conversions", - "-Dclippy::dbg_macro", - "-Dclippy::explicit_into_iter_loop", - "-Dclippy::explicit_iter_loop", - "-Dclippy::filter_map_next", - "-Dclippy::flat_map_option", - "-Dclippy::let_unit_value", - "-Dclippy::manual_assert", - "-Dclippy::manual_ok_or", - "-Dclippy::todo", - "-Dclippy::unnecessary_wraps", - "-Dclippy::useless_transmute", - "-Dclippy::used_underscore_binding", - "-Delided_lifetimes_in_paths", - "-Dunused_lifetimes", - "-Drust_2021_prelude_collisions" -] diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a2890001083..cec63ab10a0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -65,7 +65,7 @@ jobs: run: echo "TRYBUILD=overwrite" >> "$GITHUB_ENV" - name: Build docs - run: cargo doc --no-deps --no-default-features --features "full ${{ inputs.extra-features }}" + run: nox -s docs - name: Build (no features) run: cargo build --lib --tests --no-default-features diff --git a/Cargo.toml b/Cargo.toml index 8b4584a88ba..021ed963a60 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -127,3 +127,32 @@ members = [ no-default-features = true features = ["macros", "num-bigint", "num-complex", "hashbrown", "serde", "multiple-pymethods", "indexmap", "eyre", "chrono", "rust_decimal"] rustdoc-args = ["--cfg", "docsrs"] + +[workspace.lints.clippy] +checked_conversions = "warn" +dbg_macro = "warn" +explicit_into_iter_loop = "warn" +explicit_iter_loop = "warn" +filter_map_next = "warn" +flat_map_option = "warn" +let_unit_value = "warn" +manual_assert = "warn" +manual_ok_or = "warn" +todo = "warn" +unnecessary_wraps = "warn" +useless_transmute = "warn" +used_underscore_binding = "warn" + +[workspace.lints.rust] +elided_lifetimes_in_paths = "warn" +invalid_doc_attributes = "warn" +rust_2018_idioms = "warn" +rust_2021_prelude_collisions = "warn" +unused_lifetimes = "warn" + +[workspace.lints.rustdoc] +broken_intra_doc_links = "warn" +bare_urls = "warn" + +[lints] +workspace = true diff --git a/pyo3-ffi/Cargo.toml b/pyo3-ffi/Cargo.toml index 7397691e938..a380e1a36d9 100644 --- a/pyo3-ffi/Cargo.toml +++ b/pyo3-ffi/Cargo.toml @@ -36,6 +36,8 @@ abi3-py311 = ["abi3", "pyo3-build-config/abi3-py311"] # Automatically generates `python3.dll` import libraries for Windows targets. generate-import-lib = ["pyo3-build-config/python3-dll-a"] - [build-dependencies] pyo3-build-config = { path = "../pyo3-build-config", version = "0.20.0", features = ["resolve-config"] } + +[lints] +workspace = true diff --git a/pyo3-macros-backend/Cargo.toml b/pyo3-macros-backend/Cargo.toml index de782f25bfc..5e9520d3e9c 100644 --- a/pyo3-macros-backend/Cargo.toml +++ b/pyo3-macros-backend/Cargo.toml @@ -25,3 +25,6 @@ features = ["derive", "parsing", "printing", "clone-impls", "full", "extra-trait [features] abi3 = [] + +[lints] +workspace = true diff --git a/pyo3-macros/Cargo.toml b/pyo3-macros/Cargo.toml index 7e936f0b9d1..0e8f499fa0c 100644 --- a/pyo3-macros/Cargo.toml +++ b/pyo3-macros/Cargo.toml @@ -23,3 +23,6 @@ proc-macro2 = { version = "1", default-features = false } quote = "1" syn = { version = "2", features = ["full", "extra-traits"] } pyo3-macros-backend = { path = "../pyo3-macros-backend", version = "=0.20.0" } + +[lints] +workspace = true diff --git a/pyo3-macros/src/lib.rs b/pyo3-macros/src/lib.rs index 37c7e6e9b99..d00ede89143 100644 --- a/pyo3-macros/src/lib.rs +++ b/pyo3-macros/src/lib.rs @@ -2,8 +2,6 @@ //! must not contain any other public items. #![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] -extern crate proc_macro; - use proc_macro::TokenStream; use proc_macro2::TokenStream as TokenStream2; use pyo3_macros_backend::{ diff --git a/pytests/Cargo.toml b/pytests/Cargo.toml index 2b7e8ec8bdf..255094a6c40 100644 --- a/pytests/Cargo.toml +++ b/pytests/Cargo.toml @@ -15,3 +15,6 @@ pyo3-build-config = { path = "../pyo3-build-config" } [lib] name = "pyo3_pytests" crate-type = ["cdylib"] + +[lints] +workspace = true diff --git a/src/lib.rs b/src/lib.rs index f852f6e8daa..63403c176c8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,15 +1,6 @@ #![warn(missing_docs)] #![cfg_attr(feature = "nightly", feature(auto_traits, negative_impls))] #![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] -#![cfg_attr( - docsrs, // rustdoc:: is not supported on msrv - deny( - invalid_doc_attributes, - rustdoc::broken_intra_doc_links, - rustdoc::bare_urls - ) -)] -#![warn(rust_2018_idioms, unused_lifetimes)] // Deny some lints in doctests. // Use `#[allow(...)]` locally to override. #![doc(test(attr( From 9dbd81b47cb10a04654de6fa9eb97ad5cf235776 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Sat, 18 Nov 2023 13:25:05 +0000 Subject: [PATCH 31/52] enable cargo-semver-checks, try 2 --- .github/workflows/ci.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index accfed4c4bf..4c673701fda 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,6 +32,15 @@ jobs: - name: Check rust formatting (rustfmt) run: nox -s rustfmt + semver-checks: + if: github.ref != 'refs/heads/main' + needs: [fmt] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v4 + - uses: obi1kenobi/cargo-semver-checks-action@v2 + check-msrv: needs: [fmt] runs-on: ubuntu-latest From 466359a1c8b7cbc1476af448c54a8f20cc700a16 Mon Sep 17 00:00:00 2001 From: Joseph Perez Date: Mon, 20 Nov 2023 09:04:06 +0100 Subject: [PATCH 32/52] feat: allow `classmethod`/`pass_module` to receive owned types This is necessary for async functions --- newsfragments/3587.added.md | 2 ++ pyo3-macros-backend/src/method.rs | 6 ++-- pyo3-macros-backend/src/pyfunction.rs | 35 +++++++++++++------ tests/test_methods.rs | 13 +++++++ tests/test_module.rs | 13 +++++++ .../invalid_need_module_arg_position.stderr | 2 +- 6 files changed, 58 insertions(+), 13 deletions(-) create mode 100644 newsfragments/3587.added.md diff --git a/newsfragments/3587.added.md b/newsfragments/3587.added.md new file mode 100644 index 00000000000..f8ea280dd25 --- /dev/null +++ b/newsfragments/3587.added.md @@ -0,0 +1,2 @@ +- Classmethods can now receive `Py` as their first argument +- Function annotated with `pass_module` can now receive `Py` as their first argument \ No newline at end of file diff --git a/pyo3-macros-backend/src/method.rs b/pyo3-macros-backend/src/method.rs index 428efc950a1..2cff0a79381 100644 --- a/pyo3-macros-backend/src/method.rs +++ b/pyo3-macros-backend/src/method.rs @@ -113,12 +113,14 @@ impl FnType { } FnType::FnClass | FnType::FnNewClass => { quote! { - _pyo3::types::PyType::from_type_ptr(py, _slf as *mut _pyo3::ffi::PyTypeObject), + #[allow(clippy::useless_conversion)] + ::std::convert::Into::into(_pyo3::types::PyType::from_type_ptr(py, _slf as *mut _pyo3::ffi::PyTypeObject)), } } FnType::FnModule => { quote! { - py.from_borrowed_ptr::<_pyo3::types::PyModule>(_slf), + #[allow(clippy::useless_conversion)] + ::std::convert::Into::into(py.from_borrowed_ptr::<_pyo3::types::PyModule>(_slf)), } } } diff --git a/pyo3-macros-backend/src/pyfunction.rs b/pyo3-macros-backend/src/pyfunction.rs index b1a2bcc7a35..f1985047a5f 100644 --- a/pyo3-macros-backend/src/pyfunction.rs +++ b/pyo3-macros-backend/src/pyfunction.rs @@ -199,7 +199,8 @@ pub fn impl_wrap_pyfunction( .collect::>>()?; let tp = if pass_module.is_some() { - const PASS_MODULE_ERR: &str = "expected &PyModule as first argument with `pass_module`"; + const PASS_MODULE_ERR: &str = + "expected &PyModule or Py as first argument with `pass_module`"; ensure_spanned!( !arguments.is_empty(), func.span() => PASS_MODULE_ERR @@ -271,18 +272,32 @@ pub fn impl_wrap_pyfunction( } fn type_is_pymodule(ty: &syn::Type) -> bool { - if let syn::Type::Reference(tyref) = ty { - if let syn::Type::Path(typath) = tyref.elem.as_ref() { - if typath - .path - .segments - .last() - .map(|seg| seg.ident == "PyModule") - .unwrap_or(false) + let is_pymodule = |typath: &syn::TypePath| { + typath + .path + .segments + .last() + .map_or(false, |seg| seg.ident == "PyModule") + }; + match ty { + syn::Type::Reference(tyref) => { + if let syn::Type::Path(typath) = tyref.elem.as_ref() { + return is_pymodule(typath); + } + } + syn::Type::Path(typath) => { + if let Some(syn::PathSegment { + arguments: syn::PathArguments::AngleBracketed(args), + .. + }) = typath.path.segments.last() { - return true; + if args.args.len() != 1 { + return false; + } + return matches!(args.args.first().unwrap(), syn::GenericArgument::Type(syn::Type::Path(typath)) if is_pymodule(typath)); } } + _ => {} } false } diff --git a/tests/test_methods.rs b/tests/test_methods.rs index 8de5f5567e5..7919ac0c195 100644 --- a/tests/test_methods.rs +++ b/tests/test_methods.rs @@ -76,6 +76,14 @@ impl ClassMethod { fn method(cls: &PyType) -> PyResult { Ok(format!("{}.method()!", cls.name()?)) } + + #[classmethod] + fn method_owned(cls: Py) -> PyResult { + Ok(format!( + "{}.method_owned()!", + Python::with_gil(|gil| cls.as_ref(gil).name().map(ToString::to_string))? + )) + } } #[test] @@ -84,6 +92,11 @@ fn class_method() { let d = [("C", py.get_type::())].into_py_dict(py); py_assert!(py, *d, "C.method() == 'ClassMethod.method()!'"); py_assert!(py, *d, "C().method() == 'ClassMethod.method()!'"); + py_assert!( + py, + *d, + "C().method_owned() == 'ClassMethod.method_owned()!'" + ); py_assert!(py, *d, "C.method.__doc__ == 'Test class method.'"); py_assert!(py, *d, "C().method.__doc__ == 'Test class method.'"); }); diff --git a/tests/test_module.rs b/tests/test_module.rs index aef6995c64e..2de23b38324 100644 --- a/tests/test_module.rs +++ b/tests/test_module.rs @@ -348,6 +348,12 @@ fn pyfunction_with_module(module: &PyModule) -> PyResult<&str> { module.name() } +#[pyfunction] +#[pyo3(pass_module)] +fn pyfunction_with_module_owned(module: Py) -> PyResult { + Python::with_gil(|gil| module.as_ref(gil).name().map(Into::into)) +} + #[pyfunction] #[pyo3(pass_module)] fn pyfunction_with_module_and_py<'a>( @@ -393,6 +399,7 @@ fn pyfunction_with_pass_module_in_attribute(module: &PyModule) -> PyResult<&str> #[pymodule] fn module_with_functions_with_module(_py: Python<'_>, m: &PyModule) -> PyResult<()> { m.add_function(wrap_pyfunction!(pyfunction_with_module, m)?)?; + m.add_function(wrap_pyfunction!(pyfunction_with_module_owned, m)?)?; m.add_function(wrap_pyfunction!(pyfunction_with_module_and_py, m)?)?; m.add_function(wrap_pyfunction!(pyfunction_with_module_and_arg, m)?)?; m.add_function(wrap_pyfunction!(pyfunction_with_module_and_default_arg, m)?)?; @@ -401,6 +408,7 @@ fn module_with_functions_with_module(_py: Python<'_>, m: &PyModule) -> PyResult< pyfunction_with_pass_module_in_attribute, m )?)?; + m.add_function(wrap_pyfunction!(pyfunction_with_module, m)?)?; Ok(()) } @@ -413,6 +421,11 @@ fn test_module_functions_with_module() { m, "m.pyfunction_with_module() == 'module_with_functions_with_module'" ); + py_assert!( + py, + m, + "m.pyfunction_with_module_owned() == 'module_with_functions_with_module'" + ); py_assert!( py, m, diff --git a/tests/ui/invalid_need_module_arg_position.stderr b/tests/ui/invalid_need_module_arg_position.stderr index 8fce151f520..65ab4b16e16 100644 --- a/tests/ui/invalid_need_module_arg_position.stderr +++ b/tests/ui/invalid_need_module_arg_position.stderr @@ -1,4 +1,4 @@ -error: expected &PyModule as first argument with `pass_module` +error: expected &PyModule or Py as first argument with `pass_module` --> tests/ui/invalid_need_module_arg_position.rs:6:21 | 6 | fn fail(string: &str, module: &PyModule) -> PyResult<&str> { From 3d17f7442aeb96e85d5682a6dfd5e643c6bfa377 Mon Sep 17 00:00:00 2001 From: Joseph Perez Date: Wed, 22 Nov 2023 21:00:51 +0100 Subject: [PATCH 33/52] fix: replace removed `fmt` session by `rustfmt` and `ruff` --- noxfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/noxfile.py b/noxfile.py index 27f8befbd1e..f6aada6d9cc 100644 --- a/noxfile.py +++ b/noxfile.py @@ -11,7 +11,7 @@ import nox -nox.options.sessions = ["test", "clippy", "fmt", "docs"] +nox.options.sessions = ["test", "clippy", "rustfmt", "ruff", "docs"] PYO3_DIR = Path(__file__).parent From 25b8a375213d6ef2f08b46ea3c0335c05ddee295 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Wed, 22 Nov 2023 21:30:10 +0000 Subject: [PATCH 34/52] remove type_is_pymodule --- pyo3-macros-backend/src/method.rs | 10 +-- pyo3-macros-backend/src/pyfunction.rs | 67 +++++-------------- pyo3-macros-backend/src/pymethod.rs | 2 +- tests/ui/invalid_need_module_arg_position.rs | 4 +- .../invalid_need_module_arg_position.stderr | 17 +++-- 5 files changed, 39 insertions(+), 61 deletions(-) diff --git a/pyo3-macros-backend/src/method.rs b/pyo3-macros-backend/src/method.rs index 2cff0a79381..9ac87ef1bb1 100644 --- a/pyo3-macros-backend/src/method.rs +++ b/pyo3-macros-backend/src/method.rs @@ -81,7 +81,7 @@ pub enum FnType { FnNewClass, FnClass, FnStatic, - FnModule, + FnModule(Span), ClassAttribute, } @@ -93,7 +93,7 @@ impl FnType { | FnType::Fn(_) | FnType::FnClass | FnType::FnNewClass - | FnType::FnModule => true, + | FnType::FnModule(_) => true, FnType::FnNew | FnType::FnStatic | FnType::ClassAttribute => false, } } @@ -117,8 +117,8 @@ impl FnType { ::std::convert::Into::into(_pyo3::types::PyType::from_type_ptr(py, _slf as *mut _pyo3::ffi::PyTypeObject)), } } - FnType::FnModule => { - quote! { + FnType::FnModule(span) => { + quote_spanned! { *span => #[allow(clippy::useless_conversion)] ::std::convert::Into::into(py.from_borrowed_ptr::<_pyo3::types::PyModule>(_slf)), } @@ -627,7 +627,7 @@ impl<'a> FnSpec<'a> { // Getters / Setters / ClassAttribute are not callables on the Python side FnType::Getter(_) | FnType::Setter(_) | FnType::ClassAttribute => return None, FnType::Fn(_) => Some("self"), - FnType::FnModule => Some("module"), + FnType::FnModule(_) => Some("module"), FnType::FnClass | FnType::FnNewClass => Some("cls"), FnType::FnStatic | FnType::FnNew => None, }; diff --git a/pyo3-macros-backend/src/pyfunction.rs b/pyo3-macros-backend/src/pyfunction.rs index f1985047a5f..d721708f7ba 100644 --- a/pyo3-macros-backend/src/pyfunction.rs +++ b/pyo3-macros-backend/src/pyfunction.rs @@ -191,30 +191,30 @@ pub fn impl_wrap_pyfunction( let python_name = name.map_or_else(|| func.sig.ident.unraw(), |name| name.value.0); - let mut arguments = func + let tp = if pass_module.is_some() { + let span = match func.sig.inputs.first() { + Some(syn::FnArg::Typed(first_arg)) => first_arg.ty.span(), + Some(syn::FnArg::Receiver(_)) | None => bail_spanned!( + func.span() => "expected `&PyModule` or `Py` as first argument with `pass_module`" + ), + }; + method::FnType::FnModule(span) + } else { + method::FnType::FnStatic + }; + + let arguments = func .sig .inputs .iter_mut() + .skip(if tp.skip_first_rust_argument_in_python_signature() { + 1 + } else { + 0 + }) .map(FnArg::parse) .collect::>>()?; - let tp = if pass_module.is_some() { - const PASS_MODULE_ERR: &str = - "expected &PyModule or Py as first argument with `pass_module`"; - ensure_spanned!( - !arguments.is_empty(), - func.span() => PASS_MODULE_ERR - ); - let arg = arguments.remove(0); - ensure_spanned!( - type_is_pymodule(arg.ty), - arg.ty.span() => PASS_MODULE_ERR - ); - method::FnType::FnModule - } else { - method::FnType::FnStatic - }; - let signature = if let Some(signature) = signature { FunctionSignature::from_arguments_and_attribute(arguments, signature)? } else { @@ -270,34 +270,3 @@ pub fn impl_wrap_pyfunction( }; Ok(wrapped_pyfunction) } - -fn type_is_pymodule(ty: &syn::Type) -> bool { - let is_pymodule = |typath: &syn::TypePath| { - typath - .path - .segments - .last() - .map_or(false, |seg| seg.ident == "PyModule") - }; - match ty { - syn::Type::Reference(tyref) => { - if let syn::Type::Path(typath) = tyref.elem.as_ref() { - return is_pymodule(typath); - } - } - syn::Type::Path(typath) => { - if let Some(syn::PathSegment { - arguments: syn::PathArguments::AngleBracketed(args), - .. - }) = typath.path.segments.last() - { - if args.args.len() != 1 { - return false; - } - return matches!(args.args.first().unwrap(), syn::GenericArgument::Type(syn::Type::Path(typath)) if is_pymodule(typath)); - } - } - _ => {} - } - false -} diff --git a/pyo3-macros-backend/src/pymethod.rs b/pyo3-macros-backend/src/pymethod.rs index eaf050b8daa..7856621c8b8 100644 --- a/pyo3-macros-backend/src/pymethod.rs +++ b/pyo3-macros-backend/src/pymethod.rs @@ -258,7 +258,7 @@ pub fn gen_py_method( doc: spec.get_doc(meth_attrs), }, )?), - (_, FnType::FnModule) => { + (_, FnType::FnModule(_)) => { unreachable!("methods cannot be FnModule") } }) diff --git a/tests/ui/invalid_need_module_arg_position.rs b/tests/ui/invalid_need_module_arg_position.rs index b3722ae4ae7..2d45f35b866 100644 --- a/tests/ui/invalid_need_module_arg_position.rs +++ b/tests/ui/invalid_need_module_arg_position.rs @@ -3,10 +3,10 @@ use pyo3::prelude::*; #[pymodule] fn module(_py: Python<'_>, m: &PyModule) -> PyResult<()> { #[pyfn(m, pass_module)] - fn fail(string: &str, module: &PyModule) -> PyResult<&str> { + fn fail<'py>(string: &str, module: &'py PyModule) -> PyResult<&'py str> { module.name() } Ok(()) } -fn main(){} +fn main() {} diff --git a/tests/ui/invalid_need_module_arg_position.stderr b/tests/ui/invalid_need_module_arg_position.stderr index 65ab4b16e16..b9231c309a1 100644 --- a/tests/ui/invalid_need_module_arg_position.stderr +++ b/tests/ui/invalid_need_module_arg_position.stderr @@ -1,5 +1,14 @@ -error: expected &PyModule or Py as first argument with `pass_module` - --> tests/ui/invalid_need_module_arg_position.rs:6:21 +error[E0277]: the trait bound `&str: From<&pyo3::prelude::PyModule>` is not satisfied + --> tests/ui/invalid_need_module_arg_position.rs:6:26 | -6 | fn fail(string: &str, module: &PyModule) -> PyResult<&str> { - | ^ +6 | fn fail<'py>(string: &str, module: &'py PyModule) -> PyResult<&'py str> { + | ^ the trait `From<&pyo3::prelude::PyModule>` is not implemented for `&str` + | + = help: the following other types implement trait `From`: + > + >> + >> + > + > + > + = note: required for `&pyo3::prelude::PyModule` to implement `Into<&str>` From e0513d74f5d8327d48d945f7fdb0f6cff176bfc6 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Fri, 24 Nov 2023 03:11:05 +0000 Subject: [PATCH 35/52] improve error for invalid `#[classmethod]` receivers --- pyo3-macros-backend/src/method.rs | 71 ++++---- pyo3-macros-backend/src/pyfunction.rs | 2 +- pyo3-macros-backend/src/pymethod.rs | 6 +- tests/test_compile_error.rs | 2 +- tests/ui/invalid_need_module_arg_position.rs | 12 -- .../invalid_need_module_arg_position.stderr | 14 -- tests/ui/invalid_pyfunctions.rs | 8 + tests/ui/invalid_pyfunctions.stderr | 21 +++ tests/ui/invalid_pymethods.rs | 42 ++--- tests/ui/invalid_pymethods.stderr | 162 ++++++++---------- tests/ui/invalid_pymethods_duplicates.rs | 32 ++++ tests/ui/invalid_pymethods_duplicates.stderr | 32 ++++ 12 files changed, 225 insertions(+), 179 deletions(-) delete mode 100644 tests/ui/invalid_need_module_arg_position.rs delete mode 100644 tests/ui/invalid_need_module_arg_position.stderr create mode 100644 tests/ui/invalid_pymethods_duplicates.rs create mode 100644 tests/ui/invalid_pymethods_duplicates.stderr diff --git a/pyo3-macros-backend/src/method.rs b/pyo3-macros-backend/src/method.rs index 9ac87ef1bb1..c0aa898e0a7 100644 --- a/pyo3-macros-backend/src/method.rs +++ b/pyo3-macros-backend/src/method.rs @@ -78,8 +78,8 @@ pub enum FnType { Setter(SelfType), Fn(SelfType), FnNew, - FnNewClass, - FnClass, + FnNewClass(Span), + FnClass(Span), FnStatic, FnModule(Span), ClassAttribute, @@ -91,8 +91,8 @@ impl FnType { FnType::Getter(_) | FnType::Setter(_) | FnType::Fn(_) - | FnType::FnClass - | FnType::FnNewClass + | FnType::FnClass(_) + | FnType::FnNewClass(_) | FnType::FnModule(_) => true, FnType::FnNew | FnType::FnStatic | FnType::ClassAttribute => false, } @@ -111,10 +111,12 @@ impl FnType { FnType::FnNew | FnType::FnStatic | FnType::ClassAttribute => { quote!() } - FnType::FnClass | FnType::FnNewClass => { - quote! { + FnType::FnClass(span) | FnType::FnNewClass(span) => { + let py = syn::Ident::new("py", Span::call_site()); + let slf: Ident = syn::Ident::new("_slf", Span::call_site()); + quote_spanned! { *span => #[allow(clippy::useless_conversion)] - ::std::convert::Into::into(_pyo3::types::PyType::from_type_ptr(py, _slf as *mut _pyo3::ffi::PyTypeObject)), + ::std::convert::Into::into(_pyo3::types::PyType::from_type_ptr(#py, #slf.cast())), } } FnType::FnModule(span) => { @@ -305,7 +307,7 @@ impl<'a> FnSpec<'a> { FunctionSignature::from_arguments(arguments)? }; - let convention = if matches!(fn_type, FnType::FnNew | FnType::FnNewClass) { + let convention = if matches!(fn_type, FnType::FnNew | FnType::FnNewClass(_)) { CallingConvention::TpNew } else { CallingConvention::from_signature(&signature) @@ -353,36 +355,40 @@ impl<'a> FnSpec<'a> { .map(|stripped| syn::Ident::new(stripped, name.span())) }; + let mut set_name_to_new = || { + if let Some(name) = &python_name { + bail_spanned!(name.span() => "`name` not allowed with `#[new]`"); + } + *python_name = Some(syn::Ident::new("__new__", Span::call_site())); + Ok(()) + }; + let fn_type = match method_attributes.as_mut_slice() { [] => FnType::Fn(parse_receiver( "static method needs #[staticmethod] attribute", )?), [MethodTypeAttribute::StaticMethod(_)] => FnType::FnStatic, [MethodTypeAttribute::ClassAttribute(_)] => FnType::ClassAttribute, - [MethodTypeAttribute::New(_)] - | [MethodTypeAttribute::New(_), MethodTypeAttribute::ClassMethod(_)] - | [MethodTypeAttribute::ClassMethod(_), MethodTypeAttribute::New(_)] => { - if let Some(name) = &python_name { - bail_spanned!(name.span() => "`name` not allowed with `#[new]`"); - } - *python_name = Some(syn::Ident::new("__new__", Span::call_site())); - if matches!(method_attributes.as_slice(), [MethodTypeAttribute::New(_)]) { - FnType::FnNew - } else { - FnType::FnNewClass - } + [MethodTypeAttribute::New(_)] => { + set_name_to_new()?; + FnType::FnNew + } + [MethodTypeAttribute::New(_), MethodTypeAttribute::ClassMethod(span)] + | [MethodTypeAttribute::ClassMethod(span), MethodTypeAttribute::New(_)] => { + set_name_to_new()?; + FnType::FnNewClass(*span) } [MethodTypeAttribute::ClassMethod(_)] => { // Add a helpful hint if the classmethod doesn't look like a classmethod - match sig.inputs.first() { + let span = match sig.inputs.first() { // Don't actually bother checking the type of the first argument, the compiler // will error on incorrect type. - Some(syn::FnArg::Typed(_)) => {} + Some(syn::FnArg::Typed(first_arg)) => first_arg.ty.span(), Some(syn::FnArg::Receiver(_)) | None => bail_spanned!( - sig.inputs.span() => "Expected `cls: &PyType` as the first argument to `#[classmethod]`" + sig.paren_token.span.join() => "Expected `&PyType` or `Py` as the first argument to `#[classmethod]`" ), - } - FnType::FnClass + }; + FnType::FnClass(span) } [MethodTypeAttribute::Getter(_, name)] => { if let Some(name) = name.take() { @@ -510,17 +516,12 @@ impl<'a> FnSpec<'a> { } CallingConvention::TpNew => { let (arg_convert, args) = impl_arg_params(self, cls, false)?; - let call = match &self.tp { - FnType::FnNew => quote! { #rust_name(#(#args),*) }, - FnType::FnNewClass => { - quote! { #rust_name(_pyo3::types::PyType::from_type_ptr(py, subtype), #(#args),*) } - } - x => panic!("Only `FnNew` or `FnNewClass` may use the `TpNew` calling convention. Got: {:?}", x), - }; + let self_arg = self.tp.self_arg(cls, ExtractErrorMode::Raise); + let call = quote! { #rust_name(#self_arg #(#args),*) }; quote! { unsafe fn #ident( py: _pyo3::Python<'_>, - subtype: *mut _pyo3::ffi::PyTypeObject, + _slf: *mut _pyo3::ffi::PyTypeObject, _args: *mut _pyo3::ffi::PyObject, _kwargs: *mut _pyo3::ffi::PyObject ) -> _pyo3::PyResult<*mut _pyo3::ffi::PyObject> { @@ -529,7 +530,7 @@ impl<'a> FnSpec<'a> { #arg_convert let result = #call; let initializer: _pyo3::PyClassInitializer::<#cls> = result.convert(py)?; - let cell = initializer.create_cell_from_subtype(py, subtype)?; + let cell = initializer.create_cell_from_subtype(py, _slf)?; ::std::result::Result::Ok(cell as *mut _pyo3::ffi::PyObject) } } @@ -628,7 +629,7 @@ impl<'a> FnSpec<'a> { FnType::Getter(_) | FnType::Setter(_) | FnType::ClassAttribute => return None, FnType::Fn(_) => Some("self"), FnType::FnModule(_) => Some("module"), - FnType::FnClass | FnType::FnNewClass => Some("cls"), + FnType::FnClass(_) | FnType::FnNewClass(_) => Some("cls"), FnType::FnStatic | FnType::FnNew => None, }; diff --git a/pyo3-macros-backend/src/pyfunction.rs b/pyo3-macros-backend/src/pyfunction.rs index d721708f7ba..dc62d77f86a 100644 --- a/pyo3-macros-backend/src/pyfunction.rs +++ b/pyo3-macros-backend/src/pyfunction.rs @@ -195,7 +195,7 @@ pub fn impl_wrap_pyfunction( let span = match func.sig.inputs.first() { Some(syn::FnArg::Typed(first_arg)) => first_arg.ty.span(), Some(syn::FnArg::Receiver(_)) | None => bail_spanned!( - func.span() => "expected `&PyModule` or `Py` as first argument with `pass_module`" + func.sig.paren_token.span.join() => "expected `&PyModule` or `Py` as first argument with `pass_module`" ), }; method::FnType::FnModule(span) diff --git a/pyo3-macros-backend/src/pymethod.rs b/pyo3-macros-backend/src/pymethod.rs index 7856621c8b8..9b857dd8d9e 100644 --- a/pyo3-macros-backend/src/pymethod.rs +++ b/pyo3-macros-backend/src/pymethod.rs @@ -225,7 +225,7 @@ pub fn gen_py_method( &spec.get_doc(meth_attrs), None, )?), - (_, FnType::FnClass) => GeneratedPyMethod::Method(impl_py_method_def( + (_, FnType::FnClass(_)) => GeneratedPyMethod::Method(impl_py_method_def( cls, spec, &spec.get_doc(meth_attrs), @@ -238,7 +238,7 @@ pub fn gen_py_method( Some(quote!(_pyo3::ffi::METH_STATIC)), )?), // special prototypes - (_, FnType::FnNew) | (_, FnType::FnNewClass) => { + (_, FnType::FnNew) | (_, FnType::FnNewClass(_)) => { GeneratedPyMethod::Proto(impl_py_method_def_new(cls, spec)?) } @@ -312,7 +312,7 @@ pub fn impl_py_method_def( let add_flags = flags.map(|flags| quote!(.flags(#flags))); let methoddef_type = match spec.tp { FnType::FnStatic => quote!(Static), - FnType::FnClass => quote!(Class), + FnType::FnClass(_) => quote!(Class), _ => quote!(Method), }; let methoddef = spec.get_methoddef(quote! { #cls::#wrapper_ident }, doc); diff --git a/tests/test_compile_error.rs b/tests/test_compile_error.rs index 3919886f8a5..7c98886a58c 100644 --- a/tests/test_compile_error.rs +++ b/tests/test_compile_error.rs @@ -5,7 +5,6 @@ fn test_compile_errors() { let t = trybuild::TestCases::new(); - 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_proto_pymethods.rs"); t.compile_fail("tests/ui/invalid_pyclass_args.rs"); @@ -14,6 +13,7 @@ fn test_compile_errors() { t.compile_fail("tests/ui/invalid_pyfunction_signatures.rs"); #[cfg(any(not(Py_LIMITED_API), Py_3_11))] t.compile_fail("tests/ui/invalid_pymethods_buffer.rs"); + t.compile_fail("tests/ui/invalid_pymethods_duplicates.rs"); t.compile_fail("tests/ui/invalid_pymethod_names.rs"); t.compile_fail("tests/ui/invalid_pymodule_args.rs"); t.compile_fail("tests/ui/reject_generics.rs"); diff --git a/tests/ui/invalid_need_module_arg_position.rs b/tests/ui/invalid_need_module_arg_position.rs deleted file mode 100644 index 2d45f35b866..00000000000 --- a/tests/ui/invalid_need_module_arg_position.rs +++ /dev/null @@ -1,12 +0,0 @@ -use pyo3::prelude::*; - -#[pymodule] -fn module(_py: Python<'_>, m: &PyModule) -> PyResult<()> { - #[pyfn(m, pass_module)] - fn fail<'py>(string: &str, module: &'py PyModule) -> PyResult<&'py str> { - module.name() - } - Ok(()) -} - -fn main() {} diff --git a/tests/ui/invalid_need_module_arg_position.stderr b/tests/ui/invalid_need_module_arg_position.stderr deleted file mode 100644 index b9231c309a1..00000000000 --- a/tests/ui/invalid_need_module_arg_position.stderr +++ /dev/null @@ -1,14 +0,0 @@ -error[E0277]: the trait bound `&str: From<&pyo3::prelude::PyModule>` is not satisfied - --> tests/ui/invalid_need_module_arg_position.rs:6:26 - | -6 | fn fail<'py>(string: &str, module: &'py PyModule) -> PyResult<&'py str> { - | ^ the trait `From<&pyo3::prelude::PyModule>` is not implemented for `&str` - | - = help: the following other types implement trait `From`: - > - >> - >> - > - > - > - = note: required for `&pyo3::prelude::PyModule` to implement `Into<&str>` diff --git a/tests/ui/invalid_pyfunctions.rs b/tests/ui/invalid_pyfunctions.rs index 2a30a0d16fa..f67371ffba2 100644 --- a/tests/ui/invalid_pyfunctions.rs +++ b/tests/ui/invalid_pyfunctions.rs @@ -18,4 +18,12 @@ fn destructured_argument((a, b): (i32, i32)) {} #[pyfunction] fn function_with_required_after_option(_opt: Option, _x: i32) {} +#[pyfunction(pass_module)] +fn pass_module_but_no_arguments<'py>() {} + +#[pyfunction(pass_module)] +fn first_argument_not_module<'py>(string: &str, module: &'py PyModule) -> PyResult<&'py str> { + module.name() +} + fn main() {} diff --git a/tests/ui/invalid_pyfunctions.stderr b/tests/ui/invalid_pyfunctions.stderr index 9f1409260b6..407077d5830 100644 --- a/tests/ui/invalid_pyfunctions.stderr +++ b/tests/ui/invalid_pyfunctions.stderr @@ -36,3 +36,24 @@ error: required arguments after an `Option<_>` argument are ambiguous | 19 | fn function_with_required_after_option(_opt: Option, _x: i32) {} | ^^^ + +error: expected `&PyModule` or `Py` as first argument with `pass_module` + --> tests/ui/invalid_pyfunctions.rs:22:37 + | +22 | fn pass_module_but_no_arguments<'py>() {} + | ^^ + +error[E0277]: the trait bound `&str: From<&pyo3::prelude::PyModule>` is not satisfied + --> tests/ui/invalid_pyfunctions.rs:25:43 + | +25 | fn first_argument_not_module<'py>(string: &str, module: &'py PyModule) -> PyResult<&'py str> { + | ^ the trait `From<&pyo3::prelude::PyModule>` is not implemented for `&str` + | + = help: the following other types implement trait `From`: + > + >> + >> + > + > + > + = note: required for `&pyo3::prelude::PyModule` to implement `Into<&str>` diff --git a/tests/ui/invalid_pymethods.rs b/tests/ui/invalid_pymethods.rs index 8622d02bcab..d5779e85b47 100644 --- a/tests/ui/invalid_pymethods.rs +++ b/tests/ui/invalid_pymethods.rs @@ -32,6 +32,22 @@ impl MyClass { fn classmethod_with_receiver(&self) {} } +#[pymethods] +impl MyClass { + #[classmethod] + fn classmethod_missing_argument() -> Self { + Self {} + } +} + +#[pymethods] +impl MyClass { + #[classmethod] + fn classmethod_wrong_first_argument(_x: i32) -> Self { + Self {} + } +} + #[pymethods] impl MyClass { #[getter(x)] @@ -177,32 +193,6 @@ impl MyClass { fn method_self_by_value(self) {} } -struct TwoNew {} - -#[pymethods] -impl TwoNew { - #[new] - fn new_1() -> Self { - Self {} - } - - #[new] - fn new_2() -> Self { - Self {} - } -} - -struct DuplicateMethod {} - -#[pymethods] -impl DuplicateMethod { - #[pyo3(name = "func")] - fn func_a(&self) {} - - #[pyo3(name = "func")] - fn func_b(&self) {} -} - macro_rules! macro_invocation { () => {}; } diff --git a/tests/ui/invalid_pymethods.stderr b/tests/ui/invalid_pymethods.stderr index 24fb52428f2..81b16c7addb 100644 --- a/tests/ui/invalid_pymethods.stderr +++ b/tests/ui/invalid_pymethods.stderr @@ -22,194 +22,182 @@ error: unexpected receiver 26 | fn staticmethod_with_receiver(&self) {} | ^ -error: Expected `cls: &PyType` as the first argument to `#[classmethod]` - --> tests/ui/invalid_pymethods.rs:32:34 +error: Expected `&PyType` or `Py` as the first argument to `#[classmethod]` + --> tests/ui/invalid_pymethods.rs:32:33 | 32 | fn classmethod_with_receiver(&self) {} - | ^ + | ^^^^^^^ + +error: Expected `&PyType` or `Py` as the first argument to `#[classmethod]` + --> tests/ui/invalid_pymethods.rs:38:36 + | +38 | fn classmethod_missing_argument() -> Self { + | ^^ error: expected receiver for `#[getter]` - --> tests/ui/invalid_pymethods.rs:38:5 + --> tests/ui/invalid_pymethods.rs:54:5 | -38 | fn getter_without_receiver() {} +54 | fn getter_without_receiver() {} | ^^ error: expected receiver for `#[setter]` - --> tests/ui/invalid_pymethods.rs:44:5 + --> tests/ui/invalid_pymethods.rs:60:5 | -44 | fn setter_without_receiver() {} +60 | fn setter_without_receiver() {} | ^^ error: static method needs #[staticmethod] attribute - --> tests/ui/invalid_pymethods.rs:50:5 + --> tests/ui/invalid_pymethods.rs:66:5 | -50 | fn text_signature_on_call() {} +66 | fn text_signature_on_call() {} | ^^ error: `text_signature` not allowed with `getter` - --> tests/ui/invalid_pymethods.rs:56:12 + --> tests/ui/invalid_pymethods.rs:72:12 | -56 | #[pyo3(text_signature = "()")] +72 | #[pyo3(text_signature = "()")] | ^^^^^^^^^^^^^^ error: `text_signature` not allowed with `setter` - --> tests/ui/invalid_pymethods.rs:63:12 + --> tests/ui/invalid_pymethods.rs:79:12 | -63 | #[pyo3(text_signature = "()")] +79 | #[pyo3(text_signature = "()")] | ^^^^^^^^^^^^^^ error: `text_signature` not allowed with `classattr` - --> tests/ui/invalid_pymethods.rs:70:12 + --> tests/ui/invalid_pymethods.rs:86:12 | -70 | #[pyo3(text_signature = "()")] +86 | #[pyo3(text_signature = "()")] | ^^^^^^^^^^^^^^ error: expected a string literal or `None` - --> tests/ui/invalid_pymethods.rs:76:30 + --> tests/ui/invalid_pymethods.rs:92:30 | -76 | #[pyo3(text_signature = 1)] +92 | #[pyo3(text_signature = 1)] | ^ error: `text_signature` may only be specified once - --> tests/ui/invalid_pymethods.rs:83:12 + --> tests/ui/invalid_pymethods.rs:99:12 | -83 | #[pyo3(text_signature = None)] +99 | #[pyo3(text_signature = None)] | ^^^^^^^^^^^^^^ error: `signature` not allowed with `getter` - --> tests/ui/invalid_pymethods.rs:90:12 - | -90 | #[pyo3(signature = ())] - | ^^^^^^^^^ + --> tests/ui/invalid_pymethods.rs:106:12 + | +106 | #[pyo3(signature = ())] + | ^^^^^^^^^ error: `signature` not allowed with `setter` - --> tests/ui/invalid_pymethods.rs:97:12 - | -97 | #[pyo3(signature = ())] - | ^^^^^^^^^ + --> tests/ui/invalid_pymethods.rs:113:12 + | +113 | #[pyo3(signature = ())] + | ^^^^^^^^^ error: `signature` not allowed with `classattr` - --> tests/ui/invalid_pymethods.rs:104:12 + --> tests/ui/invalid_pymethods.rs:120:12 | -104 | #[pyo3(signature = ())] +120 | #[pyo3(signature = ())] | ^^^^^^^^^ error: `#[new]` may not be combined with `#[classmethod]` `#[staticmethod]`, `#[classattr]`, `#[getter]`, and `#[setter]` - --> tests/ui/invalid_pymethods.rs:110:7 + --> tests/ui/invalid_pymethods.rs:126:7 | -110 | #[new] +126 | #[new] | ^^^ error: `#[new]` does not take any arguments = help: did you mean `#[new] #[pyo3(signature = ())]`? - --> tests/ui/invalid_pymethods.rs:121:7 + --> tests/ui/invalid_pymethods.rs:137:7 | -121 | #[new(signature = ())] +137 | #[new(signature = ())] | ^^^ error: `#[new]` does not take any arguments = note: this was previously accepted and ignored - --> tests/ui/invalid_pymethods.rs:127:11 + --> tests/ui/invalid_pymethods.rs:143:11 | -127 | #[new = ()] // in this form there's no suggestion to move arguments to `#[pyo3()]` attribute +143 | #[new = ()] // in this form there's no suggestion to move arguments to `#[pyo3()]` attribute | ^ error: `#[classmethod]` does not take any arguments = help: did you mean `#[classmethod] #[pyo3(signature = ())]`? - --> tests/ui/invalid_pymethods.rs:133:7 + --> tests/ui/invalid_pymethods.rs:149:7 | -133 | #[classmethod(signature = ())] +149 | #[classmethod(signature = ())] | ^^^^^^^^^^^ error: `#[staticmethod]` does not take any arguments = help: did you mean `#[staticmethod] #[pyo3(signature = ())]`? - --> tests/ui/invalid_pymethods.rs:139:7 + --> tests/ui/invalid_pymethods.rs:155:7 | -139 | #[staticmethod(signature = ())] +155 | #[staticmethod(signature = ())] | ^^^^^^^^^^^^ error: `#[classattr]` does not take any arguments = help: did you mean `#[classattr] #[pyo3(signature = ())]`? - --> tests/ui/invalid_pymethods.rs:145:7 + --> tests/ui/invalid_pymethods.rs:161:7 | -145 | #[classattr(signature = ())] +161 | #[classattr(signature = ())] | ^^^^^^^^^ error: Python functions cannot have generic type parameters - --> tests/ui/invalid_pymethods.rs:151:23 + --> tests/ui/invalid_pymethods.rs:167:23 | -151 | fn generic_method(value: T) {} +167 | fn generic_method(value: T) {} | ^ error: Python functions cannot have `impl Trait` arguments - --> tests/ui/invalid_pymethods.rs:156:48 + --> tests/ui/invalid_pymethods.rs:172:48 | -156 | fn impl_trait_method_first_arg(impl_trait: impl AsRef) {} +172 | fn impl_trait_method_first_arg(impl_trait: impl AsRef) {} | ^^^^ error: Python functions cannot have `impl Trait` arguments - --> tests/ui/invalid_pymethods.rs:161:56 + --> tests/ui/invalid_pymethods.rs:177:56 | -161 | fn impl_trait_method_second_arg(&self, impl_trait: impl AsRef) {} +177 | fn impl_trait_method_second_arg(&self, impl_trait: impl AsRef) {} | ^^^^ error: `async fn` is not yet supported for Python functions. Additional crates such as `pyo3-asyncio` can be used to integrate async Rust and Python. For more information, see https://github.com/PyO3/pyo3/issues/1632 - --> tests/ui/invalid_pymethods.rs:166:5 + --> tests/ui/invalid_pymethods.rs:182:5 | -166 | async fn async_method(&self) {} +182 | async fn async_method(&self) {} | ^^^^^ error: `pass_module` cannot be used on Python methods - --> tests/ui/invalid_pymethods.rs:171:12 + --> tests/ui/invalid_pymethods.rs:187:12 | -171 | #[pyo3(pass_module)] +187 | #[pyo3(pass_module)] | ^^^^^^^^^^^ error: Python objects are shared, so 'self' cannot be moved out of the Python interpreter. Try `&self`, `&mut self, `slf: PyRef<'_, Self>` or `slf: PyRefMut<'_, Self>`. - --> tests/ui/invalid_pymethods.rs:177:29 + --> tests/ui/invalid_pymethods.rs:193:29 | -177 | fn method_self_by_value(self) {} +193 | fn method_self_by_value(self) {} | ^^^^ error: macros cannot be used as items in `#[pymethods]` impl blocks = note: this was previously accepted and ignored - --> tests/ui/invalid_pymethods.rs:212:5 + --> tests/ui/invalid_pymethods.rs:202:5 | -212 | macro_invocation!(); +202 | macro_invocation!(); | ^^^^^^^^^^^^^^^^ -error[E0119]: conflicting implementations of trait `pyo3::impl_::pyclass::PyClassNewTextSignature` for type `pyo3::impl_::pyclass::PyClassImplCollector` - --> tests/ui/invalid_pymethods.rs:182:1 - | -182 | #[pymethods] - | ^^^^^^^^^^^^ - | | - | first implementation here - | conflicting implementation for `pyo3::impl_::pyclass::PyClassImplCollector` - | - = note: this error originates in the attribute macro `pymethods` (in Nightly builds, run with -Z macro-backtrace for more info) - -error[E0592]: duplicate definitions with name `__pymethod___new____` - --> tests/ui/invalid_pymethods.rs:182:1 - | -182 | #[pymethods] - | ^^^^^^^^^^^^ - | | - | duplicate definitions for `__pymethod___new____` - | other definition for `__pymethod___new____` - | - = note: this error originates in the attribute macro `pymethods` (in Nightly builds, run with -Z macro-backtrace for more info) - -error[E0592]: duplicate definitions with name `__pymethod_func__` - --> tests/ui/invalid_pymethods.rs:197:1 - | -197 | #[pymethods] - | ^^^^^^^^^^^^ - | | - | duplicate definitions for `__pymethod_func__` - | other definition for `__pymethod_func__` - | - = note: this error originates in the attribute macro `pymethods` (in Nightly builds, run with -Z macro-backtrace for more info) +error[E0277]: the trait bound `i32: From<&PyType>` is not satisfied + --> tests/ui/invalid_pymethods.rs:46:45 + | +46 | fn classmethod_wrong_first_argument(_x: i32) -> Self { + | ^^^ the trait `From<&PyType>` is not implemented for `i32` + | + = help: the following other types implement trait `From`: + > + > + > + > + > + > + = note: required for `&PyType` to implement `Into` diff --git a/tests/ui/invalid_pymethods_duplicates.rs b/tests/ui/invalid_pymethods_duplicates.rs new file mode 100644 index 00000000000..d05d70959fc --- /dev/null +++ b/tests/ui/invalid_pymethods_duplicates.rs @@ -0,0 +1,32 @@ +//! These tests are located in a separate file because they cause conflicting implementation +//! errors, which means other errors such as typechecking errors are not reported. + +use pyo3::prelude::*; + +struct TwoNew {} + +#[pymethods] +impl TwoNew { + #[new] + fn new_1() -> Self { + Self {} + } + + #[new] + fn new_2() -> Self { + Self {} + } +} + +struct DuplicateMethod {} + +#[pymethods] +impl DuplicateMethod { + #[pyo3(name = "func")] + fn func_a(&self) {} + + #[pyo3(name = "func")] + fn func_b(&self) {} +} + +fn main() {} diff --git a/tests/ui/invalid_pymethods_duplicates.stderr b/tests/ui/invalid_pymethods_duplicates.stderr new file mode 100644 index 00000000000..38bb6f8655b --- /dev/null +++ b/tests/ui/invalid_pymethods_duplicates.stderr @@ -0,0 +1,32 @@ +error[E0119]: conflicting implementations of trait `pyo3::impl_::pyclass::PyClassNewTextSignature` for type `pyo3::impl_::pyclass::PyClassImplCollector` + --> tests/ui/invalid_pymethods_duplicates.rs:8:1 + | +8 | #[pymethods] + | ^^^^^^^^^^^^ + | | + | first implementation here + | conflicting implementation for `pyo3::impl_::pyclass::PyClassImplCollector` + | + = note: this error originates in the attribute macro `pymethods` (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0592]: duplicate definitions with name `__pymethod___new____` + --> tests/ui/invalid_pymethods_duplicates.rs:8:1 + | +8 | #[pymethods] + | ^^^^^^^^^^^^ + | | + | duplicate definitions for `__pymethod___new____` + | other definition for `__pymethod___new____` + | + = note: this error originates in the attribute macro `pymethods` (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0592]: duplicate definitions with name `__pymethod_func__` + --> tests/ui/invalid_pymethods_duplicates.rs:23:1 + | +23 | #[pymethods] + | ^^^^^^^^^^^^ + | | + | duplicate definitions for `__pymethod_func__` + | other definition for `__pymethod_func__` + | + = note: this error originates in the attribute macro `pymethods` (in Nightly builds, run with -Z macro-backtrace for more info) From 2312270ec1dc83c7853cd6b141a39bffe3ff3d37 Mon Sep 17 00:00:00 2001 From: Ivan Smirnov Date: Sat, 16 Sep 2023 12:07:33 +0100 Subject: [PATCH 36/52] add conversion support for `either::Either` --- Cargo.toml | 4 +- guide/src/features.md | 4 + newsfragments/3456.added.md | 1 + src/conversions/either.rs | 146 ++++++++++++++++++++++++++++++++++++ src/conversions/mod.rs | 1 + src/lib.rs | 4 + 6 files changed, 159 insertions(+), 1 deletion(-) create mode 100644 newsfragments/3456.added.md create mode 100644 src/conversions/either.rs diff --git a/Cargo.toml b/Cargo.toml index 021ed963a60..835a1f866f5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,6 +34,7 @@ inventory = { version = "0.3.0", optional = true } # crate integrations that can be added using the eponymous features anyhow = { version = "1.0", optional = true } chrono = { version = "0.4.25", default-features = false, optional = true } +either = { version = "1.9", optional = true } eyre = { version = ">= 0.4, < 0.7", optional = true } hashbrown = { version = ">= 0.9, < 0.15", optional = true } indexmap = { version = ">= 1.6, < 3", optional = true } @@ -107,6 +108,7 @@ full = [ "smallvec", "serde", "indexmap", + "either", "eyre", "anyhow", "experimental-inspect", @@ -125,7 +127,7 @@ members = [ [package.metadata.docs.rs] no-default-features = true -features = ["macros", "num-bigint", "num-complex", "hashbrown", "serde", "multiple-pymethods", "indexmap", "eyre", "chrono", "rust_decimal"] +features = ["macros", "num-bigint", "num-complex", "hashbrown", "serde", "multiple-pymethods", "indexmap", "eyre", "either", "chrono", "rust_decimal"] rustdoc-args = ["--cfg", "docsrs"] [workspace.lints.clippy] diff --git a/guide/src/features.md b/guide/src/features.md index fc3b8eebf8c..8771a748c04 100644 --- a/guide/src/features.md +++ b/guide/src/features.md @@ -109,6 +109,10 @@ Adds a dependency on [chrono](https://docs.rs/chrono). Enables a conversion from - [NaiveTime](https://docs.rs/chrono/latest/chrono/naive/struct.NaiveTime.html) -> [`PyTime`]({{#PYO3_DOCS_URL}}/pyo3/types/struct.PyTime.html) - [DateTime](https://docs.rs/chrono/latest/chrono/struct.DateTime.html) -> [`PyDateTime`]({{#PYO3_DOCS_URL}}/pyo3/types/struct.PyDateTime.html) +### `either` + +Adds a dependency on [either](https://docs.rs/either). Enables a conversions into [either](https://docs.rs/either)’s [`Either`](https://docs.rs/either/latest/either/struct.Report.html) type. + ### `eyre` Adds a dependency on [eyre](https://docs.rs/eyre). Enables a conversion from [eyre](https://docs.rs/eyre)’s [`Report`](https://docs.rs/eyre/latest/eyre/struct.Report.html) type to [`PyErr`]({{#PYO3_DOCS_URL}}/pyo3/struct.PyErr.html), for easy error handling. diff --git a/newsfragments/3456.added.md b/newsfragments/3456.added.md new file mode 100644 index 00000000000..6e9376ba65d --- /dev/null +++ b/newsfragments/3456.added.md @@ -0,0 +1 @@ +Add optional conversion support for `either::Either` sum type (under "either" feature). diff --git a/src/conversions/either.rs b/src/conversions/either.rs new file mode 100644 index 00000000000..4a41d2bd52f --- /dev/null +++ b/src/conversions/either.rs @@ -0,0 +1,146 @@ +#![cfg(feature = "either")] + +//! Conversion to/from +//! [either](https://docs.rs/either/ "A library for easy idiomatic error handling and reporting in Rust applications")’s +//! [`Either`] type to a union of two Python types. +//! +//! Use of a generic sum type like [either] is common when you want to either accept one of two possible +//! types as an argument or return one of two possible types from a function, without having to define +//! a helper type manually yourself. +//! +//! # Setup +//! +//! To use this feature, add this to your **`Cargo.toml`**: +//! +//! ```toml +//! [dependencies] +//! ## change * to the version you want to use, ideally the latest. +//! either = "*" +#![doc = concat!("pyo3 = { version = \"", env!("CARGO_PKG_VERSION"), "\", features = [\"either\"] }")] +//! ``` +//! +//! Note that you must use compatible versions of either and PyO3. +//! The required either version may vary based on the version of PyO3. +//! +//! # Example: Convert a `int | str` to `Either`. +//! +//! ```rust +//! use either::Either; +//! use pyo3::{Python, ToPyObject}; +//! +//! fn main() { +//! pyo3::prepare_freethreaded_python(); +//! Python::with_gil(|py| { +//! // Create a string and an int in Python. +//! let py_str = "crab".to_object(py); +//! let py_int = 42.to_object(py); +//! // Now convert it to an Either. +//! let either_str: Either = py_str.extract(py).unwrap(); +//! let either_int: Either = py_int.extract(py).unwrap(); +//! }); +//! } +//! ``` +//! +//! [either](https://docs.rs/either/ "A library for easy idiomatic error handling and reporting in Rust applications")’s + +use crate::{ + exceptions::PyTypeError, inspect::types::TypeInfo, FromPyObject, IntoPy, PyAny, PyObject, + PyResult, Python, ToPyObject, +}; +use either::Either; + +#[cfg_attr(docsrs, doc(cfg(feature = "either")))] +impl IntoPy for Either +where + L: IntoPy, + R: IntoPy, +{ + #[inline] + fn into_py(self, py: Python<'_>) -> PyObject { + match self { + Either::Left(l) => l.into_py(py), + Either::Right(r) => r.into_py(py), + } + } +} + +#[cfg_attr(docsrs, doc(cfg(feature = "either")))] +impl ToPyObject for Either +where + L: ToPyObject, + R: ToPyObject, +{ + #[inline] + fn to_object(&self, py: Python<'_>) -> PyObject { + match self { + Either::Left(l) => l.to_object(py), + Either::Right(r) => r.to_object(py), + } + } +} + +#[cfg_attr(docsrs, doc(cfg(feature = "either")))] +impl<'source, L, R> FromPyObject<'source> for Either +where + L: FromPyObject<'source>, + R: FromPyObject<'source>, +{ + #[inline] + fn extract(obj: &'source PyAny) -> PyResult { + if let Ok(l) = obj.extract::() { + Ok(Either::Left(l)) + } else if let Ok(r) = obj.extract::() { + Ok(Either::Right(r)) + } else { + let err_msg = format!("failed to convert the value to '{}'", Self::type_input()); + Err(PyTypeError::new_err(err_msg)) + } + } + + fn type_input() -> TypeInfo { + TypeInfo::union_of(&[L::type_input(), R::type_input()]) + } +} + +#[cfg(test)] +mod tests { + use crate::exceptions::PyTypeError; + use crate::{Python, ToPyObject}; + + use either::Either; + + #[test] + fn test_either_conversion() { + type E = Either; + type E1 = Either; + type E2 = Either; + + Python::with_gil(|py| { + let l = E::Left(42); + let obj_l = l.to_object(py); + assert_eq!(obj_l.extract::(py).unwrap(), 42); + assert_eq!(obj_l.extract::(py).unwrap(), l); + + let r = E::Right("foo".to_owned()); + let obj_r = r.to_object(py); + assert_eq!(obj_r.extract::<&str>(py).unwrap(), "foo"); + assert_eq!(obj_r.extract::(py).unwrap(), r); + + let obj_s = "foo".to_object(py); + let err = obj_s.extract::(py).unwrap_err(); + assert!(err.is_instance_of::(py)); + assert_eq!( + err.to_string(), + "TypeError: failed to convert the value to 'Union[int, float]'" + ); + + let obj_i = 42.to_object(py); + assert_eq!(obj_i.extract::(py).unwrap(), E1::Left(42)); + assert_eq!(obj_i.extract::(py).unwrap(), E2::Left(42.0)); + + let obj_f = 42.0.to_object(py); + assert_eq!(obj_f.extract::(py).unwrap(), E1::Right(42.0)); + assert_eq!(obj_f.extract::(py).unwrap(), E2::Left(42.0)); + }); + } +} diff --git a/src/conversions/mod.rs b/src/conversions/mod.rs index a9c2b0cd2a6..680ad9be6d5 100644 --- a/src/conversions/mod.rs +++ b/src/conversions/mod.rs @@ -2,6 +2,7 @@ pub mod anyhow; pub mod chrono; +pub mod either; pub mod eyre; pub mod hashbrown; pub mod indexmap; diff --git a/src/lib.rs b/src/lib.rs index 63403c176c8..1fbeeddfa98 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -82,6 +82,7 @@ //! The following features enable interactions with other crates in the Rust ecosystem: //! - [`anyhow`]: Enables a conversion from [anyhow]’s [`Error`][anyhow_error] type to [`PyErr`]. //! - [`chrono`]: Enables a conversion from [chrono]'s structures to the equivalent Python ones. +//! - [`either`]: Enables conversions between Python objects and [either]'s [`Either`] type. //! - [`eyre`]: Enables a conversion from [eyre]’s [`Report`] type to [`PyErr`]. //! - [`hashbrown`]: Enables conversions between Python objects and [hashbrown]'s [`HashMap`] and //! [`HashSet`] types. @@ -257,6 +258,9 @@ //! [`Serialize`]: https://docs.rs/serde/latest/serde/trait.Serialize.html //! [chrono]: https://docs.rs/chrono/ "Date and Time for Rust." //! [`chrono`]: ./chrono/index.html "Documentation about the `chrono` feature." +//! [either]: https://docs.rs/either/ "A type that represents one of two alternatives." +//! [`either`]: ./either/index.html "Documentation about the `either` feature." +//! [`Either`]: https://docs.rs/either/latest/either/enum.Either.html //! [eyre]: https://docs.rs/eyre/ "A library for easy idiomatic error handling and reporting in Rust applications." //! [`Report`]: https://docs.rs/eyre/latest/eyre/struct.Report.html //! [`eyre`]: ./eyre/index.html "Documentation about the `eyre` feature." From b1de927a3103683ac54dddc20af2b330b1f3b560 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Fri, 24 Nov 2023 22:11:40 +0000 Subject: [PATCH 37/52] docs: fixup docs for smallvec feature --- guide/src/features.md | 4 ++++ src/lib.rs | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/guide/src/features.md b/guide/src/features.md index 8771a748c04..8ed2a2ed0bc 100644 --- a/guide/src/features.md +++ b/guide/src/features.md @@ -163,3 +163,7 @@ struct User { } # } ``` + +### `smallvec` + +Adds a dependency on [smallvec](https://docs.rs/smallvec) and enables conversions into its [`SmallVec`](https://docs.rs/smallvec/latest/smallvec/struct.SmallVec.html) type. diff --git a/src/lib.rs b/src/lib.rs index 1fbeeddfa98..ecaaf648060 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -86,7 +86,6 @@ //! - [`eyre`]: Enables a conversion from [eyre]’s [`Report`] type to [`PyErr`]. //! - [`hashbrown`]: Enables conversions between Python objects and [hashbrown]'s [`HashMap`] and //! [`HashSet`] types. -//! - [`smallvec`][smallvec]: Enables conversions between Python list and [smallvec]'s [`SmallVec`]. //! - [`indexmap`][indexmap_feature]: Enables conversions between Python dictionary and [indexmap]'s [`IndexMap`]. //! - [`num-bigint`]: Enables conversions between Python objects and [num-bigint]'s [`BigInt`] and //! [`BigUint`] types. @@ -96,6 +95,7 @@ //! [`Decimal`] type. //! - [`serde`]: Allows implementing [serde]'s [`Serialize`] and [`Deserialize`] traits for //! [`Py`]`` for all `T` that implement [`Serialize`] and [`Deserialize`]. +//! - [`smallvec`][smallvec]: Enables conversions between Python list and [smallvec]'s [`SmallVec`]. //! //! ## Unstable features //! From 405d722a2d13fd691be6547437c46e4e25fd16a8 Mon Sep 17 00:00:00 2001 From: mejrs <59372212+mejrs@users.noreply.github.com> Date: Thu, 12 Oct 2023 16:44:05 +0200 Subject: [PATCH 38/52] Create subinterpreter example --- examples/README.md | 1 + examples/sequential/.template/Cargo.toml | 12 ++ examples/sequential/.template/pre-script.rhai | 4 + examples/sequential/.template/pyproject.toml | 7 + examples/sequential/Cargo.toml | 13 ++ examples/sequential/MANIFEST.in | 2 + examples/sequential/README.md | 36 +++++ examples/sequential/cargo-generate.toml | 5 + examples/sequential/noxfile.py | 11 ++ examples/sequential/pyproject.toml | 20 +++ examples/sequential/src/id.rs | 131 +++++++++++++++ examples/sequential/src/lib.rs | 14 ++ examples/sequential/src/module.rs | 82 ++++++++++ examples/sequential/tests/test.rs | 151 ++++++++++++++++++ examples/sequential/tests/test_.py | 21 +++ 15 files changed, 510 insertions(+) create mode 100644 examples/sequential/.template/Cargo.toml create mode 100644 examples/sequential/.template/pre-script.rhai create mode 100644 examples/sequential/.template/pyproject.toml create mode 100644 examples/sequential/Cargo.toml create mode 100644 examples/sequential/MANIFEST.in create mode 100644 examples/sequential/README.md create mode 100644 examples/sequential/cargo-generate.toml create mode 100644 examples/sequential/noxfile.py create mode 100644 examples/sequential/pyproject.toml create mode 100644 examples/sequential/src/id.rs create mode 100644 examples/sequential/src/lib.rs create mode 100644 examples/sequential/src/module.rs create mode 100644 examples/sequential/tests/test.rs create mode 100644 examples/sequential/tests/test_.py diff --git a/examples/README.md b/examples/README.md index 47ab5a9dc3a..baaa57b650d 100644 --- a/examples/README.md +++ b/examples/README.md @@ -11,6 +11,7 @@ Below is a brief description of each of these: | `setuptools-rust-starter` | A template project which is configured to use [`setuptools_rust`](https://github.com/PyO3/setuptools-rust/) for development. | | `word-count` | A quick performance comparison between word counter implementations written in each of Rust and Python. | | `plugin` | Illustrates how to use Python as a scripting language within a Rust application | +| `sequential` | Illustrates how to use pyo3-ffi to write subinterpreter-safe modules | ## Creating new projects from these examples diff --git a/examples/sequential/.template/Cargo.toml b/examples/sequential/.template/Cargo.toml new file mode 100644 index 00000000000..cd0ece4291f --- /dev/null +++ b/examples/sequential/.template/Cargo.toml @@ -0,0 +1,12 @@ +[package] +authors = ["{{authors}}"] +name = "{{project-name}}" +version = "0.1.0" +edition = "2021" + +[lib] +name = "sequential" +crate-type = ["cdylib", "lib"] + +[dependencies] +pyo3-ffi = { version = "{{PYO3_VERSION}}", features = ["extension-module"] } diff --git a/examples/sequential/.template/pre-script.rhai b/examples/sequential/.template/pre-script.rhai new file mode 100644 index 00000000000..d3341677b1f --- /dev/null +++ b/examples/sequential/.template/pre-script.rhai @@ -0,0 +1,4 @@ +variable::set("PYO3_VERSION", "0.19.2"); +file::rename(".template/Cargo.toml", "Cargo.toml"); +file::rename(".template/pyproject.toml", "pyproject.toml"); +file::delete(".template"); diff --git a/examples/sequential/.template/pyproject.toml b/examples/sequential/.template/pyproject.toml new file mode 100644 index 00000000000..537fdacc666 --- /dev/null +++ b/examples/sequential/.template/pyproject.toml @@ -0,0 +1,7 @@ +[build-system] +requires = ["maturin>=1,<2"] +build-backend = "maturin" + +[project] +name = "{{project-name}}" +version = "0.1.0" diff --git a/examples/sequential/Cargo.toml b/examples/sequential/Cargo.toml new file mode 100644 index 00000000000..4500c69b597 --- /dev/null +++ b/examples/sequential/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "sequential" +version = "0.1.0" +edition = "2021" + +[lib] +name = "sequential" +crate-type = ["cdylib", "lib"] + +[dependencies] +pyo3-ffi = { path = "../../pyo3-ffi", features = ["extension-module"] } + +[workspace] diff --git a/examples/sequential/MANIFEST.in b/examples/sequential/MANIFEST.in new file mode 100644 index 00000000000..becccf7bc9a --- /dev/null +++ b/examples/sequential/MANIFEST.in @@ -0,0 +1,2 @@ +include pyproject.toml Cargo.toml +recursive-include src * diff --git a/examples/sequential/README.md b/examples/sequential/README.md new file mode 100644 index 00000000000..e3c0078241d --- /dev/null +++ b/examples/sequential/README.md @@ -0,0 +1,36 @@ +# sequential + +A project built using only `pyo3_ffi`, without any of PyO3's safe api. It can be executed by subinterpreters that have their own GIL. + +## Building and Testing + +To build this package, first install `maturin`: + +```shell +pip install maturin +``` + +To build and test use `maturin develop`: + +```shell +pip install -r requirements-dev.txt +maturin develop +pytest +``` + +Alternatively, install nox and run the tests inside an isolated environment: + +```shell +nox +``` + +## Copying this example + +Use [`cargo-generate`](https://crates.io/crates/cargo-generate): + +```bash +$ cargo install cargo-generate +$ cargo generate --git https://github.com/PyO3/pyo3 examples/sequential +``` + +(`cargo generate` will take a little while to clone the PyO3 repo first; be patient when waiting for the command to run.) diff --git a/examples/sequential/cargo-generate.toml b/examples/sequential/cargo-generate.toml new file mode 100644 index 00000000000..d750c4de7a3 --- /dev/null +++ b/examples/sequential/cargo-generate.toml @@ -0,0 +1,5 @@ +[template] +ignore = [".nox"] + +[hooks] +pre = [".template/pre-script.rhai"] diff --git a/examples/sequential/noxfile.py b/examples/sequential/noxfile.py new file mode 100644 index 00000000000..b6e4bfe58bb --- /dev/null +++ b/examples/sequential/noxfile.py @@ -0,0 +1,11 @@ +import sys +import nox + + +@nox.session +def python(session): + if sys.version_info < (3, 12): + session.skip("Python 3.12+ is required") + session.env["MATURIN_PEP517_ARGS"] = "--profile=dev" + session.install(".[dev]") + session.run("pytest") diff --git a/examples/sequential/pyproject.toml b/examples/sequential/pyproject.toml new file mode 100644 index 00000000000..ee3bfa986de --- /dev/null +++ b/examples/sequential/pyproject.toml @@ -0,0 +1,20 @@ +[build-system] +requires = ["maturin>=1,<2"] +build-backend = "maturin" + +[project] +name = "sequential" +version = "0.1.0" +classifiers = [ + "License :: OSI Approved :: MIT License", + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "Programming Language :: Python", + "Programming Language :: Rust", + "Operating System :: POSIX", + "Operating System :: MacOS :: MacOS X", +] +requires-python = ">=3.12" + +[project.optional-dependencies] +dev = ["pytest"] diff --git a/examples/sequential/src/id.rs b/examples/sequential/src/id.rs new file mode 100644 index 00000000000..d80e84b4eab --- /dev/null +++ b/examples/sequential/src/id.rs @@ -0,0 +1,131 @@ +use core::sync::atomic::{AtomicU64, Ordering}; +use core::{mem, ptr}; +use std::os::raw::{c_char, c_int, c_uint, c_ulonglong, c_void}; + +use pyo3_ffi::*; + +#[repr(C)] +pub struct PyId { + _ob_base: PyObject, + id: Id, +} + +static COUNT: AtomicU64 = AtomicU64::new(0); + +#[derive(Clone, Copy, Eq, Ord, PartialEq, PartialOrd)] +pub struct Id(u64); + +impl Id { + fn new() -> Self { + Id(COUNT.fetch_add(1, Ordering::Relaxed)) + } +} + +unsafe extern "C" fn id_new( + subtype: *mut PyTypeObject, + args: *mut PyObject, + kwds: *mut PyObject, +) -> *mut PyObject { + if PyTuple_Size(args) != 0 || !kwds.is_null() { + PyErr_SetString( + PyExc_TypeError, + "Id() takes no arguments\0".as_ptr().cast::(), + ); + return ptr::null_mut(); + } + + let f: allocfunc = (*subtype).tp_alloc.unwrap_or(PyType_GenericAlloc); + let slf = f(subtype, 0); + + if slf.is_null() { + return ptr::null_mut(); + } else { + let id = Id::new(); + let slf = slf.cast::(); + ptr::addr_of_mut!((*slf).id).write(id); + } + + slf +} + +unsafe extern "C" fn id_repr(slf: *mut PyObject) -> *mut PyObject { + let slf = slf.cast::(); + let id = (*slf).id.0; + let string = format!("Id({})", id); + PyUnicode_FromStringAndSize(string.as_ptr().cast::(), string.len() as Py_ssize_t) +} + +unsafe extern "C" fn id_int(slf: *mut PyObject) -> *mut PyObject { + let slf = slf.cast::(); + let id = (*slf).id.0; + PyLong_FromUnsignedLongLong(id as c_ulonglong) +} + +unsafe extern "C" fn id_richcompare( + slf: *mut PyObject, + other: *mut PyObject, + op: c_int, +) -> *mut PyObject { + let pytype = Py_TYPE(slf); // guaranteed to be `sequential.Id` + if Py_TYPE(other) != pytype { + return Py_NewRef(Py_NotImplemented()); + } + let slf = (*slf.cast::()).id; + let other = (*other.cast::()).id; + + let cmp = match op { + pyo3_ffi::Py_LT => slf < other, + pyo3_ffi::Py_LE => slf <= other, + pyo3_ffi::Py_EQ => slf == other, + pyo3_ffi::Py_NE => slf != other, + pyo3_ffi::Py_GT => slf > other, + pyo3_ffi::Py_GE => slf >= other, + unrecognized => { + let msg = format!("unrecognized richcompare opcode {}\0", unrecognized); + PyErr_SetString(PyExc_SystemError, msg.as_ptr().cast::()); + return ptr::null_mut(); + } + }; + + if cmp { + Py_NewRef(Py_True()) + } else { + Py_NewRef(Py_False()) + } +} + +static mut SLOTS: &[PyType_Slot] = &[ + PyType_Slot { + slot: Py_tp_new, + pfunc: id_new as *mut c_void, + }, + PyType_Slot { + slot: Py_tp_doc, + pfunc: "An id that is increased every time an instance is created\0".as_ptr() + as *mut c_void, + }, + PyType_Slot { + slot: Py_tp_repr, + pfunc: id_repr as *mut c_void, + }, + PyType_Slot { + slot: Py_nb_int, + pfunc: id_int as *mut c_void, + }, + PyType_Slot { + slot: Py_tp_richcompare, + pfunc: id_richcompare as *mut c_void, + }, + PyType_Slot { + slot: 0, + pfunc: ptr::null_mut(), + }, +]; + +pub static mut ID_SPEC: PyType_Spec = PyType_Spec { + name: "sequential.Id\0".as_ptr().cast::(), + basicsize: mem::size_of::() as c_int, + itemsize: 0, + flags: (Py_TPFLAGS_DEFAULT | Py_TPFLAGS_IMMUTABLETYPE) as c_uint, + slots: unsafe { SLOTS as *const [PyType_Slot] as *mut PyType_Slot }, +}; diff --git a/examples/sequential/src/lib.rs b/examples/sequential/src/lib.rs new file mode 100644 index 00000000000..7e77064de4d --- /dev/null +++ b/examples/sequential/src/lib.rs @@ -0,0 +1,14 @@ +use std::ptr; + +use pyo3_ffi::*; + +mod id; +mod module; +use crate::module::MODULE_DEF; + +// The module initialization function, which must be named `PyInit_`. +#[allow(non_snake_case)] +#[no_mangle] +pub unsafe extern "C" fn PyInit_sequential() -> *mut PyObject { + PyModuleDef_Init(ptr::addr_of_mut!(MODULE_DEF)) +} diff --git a/examples/sequential/src/module.rs b/examples/sequential/src/module.rs new file mode 100644 index 00000000000..5552baf3368 --- /dev/null +++ b/examples/sequential/src/module.rs @@ -0,0 +1,82 @@ +use core::{mem, ptr}; +use pyo3_ffi::*; +use std::os::raw::{c_char, c_int, c_void}; + +pub static mut MODULE_DEF: PyModuleDef = PyModuleDef { + m_base: PyModuleDef_HEAD_INIT, + m_name: "sequential\0".as_ptr().cast::(), + m_doc: "A library for generating sequential ids, written in Rust.\0" + .as_ptr() + .cast::(), + m_size: mem::size_of::() as Py_ssize_t, + m_methods: std::ptr::null_mut(), + m_slots: unsafe { SEQUENTIAL_SLOTS as *const [PyModuleDef_Slot] as *mut PyModuleDef_Slot }, + m_traverse: Some(sequential_traverse), + m_clear: Some(sequential_clear), + m_free: Some(sequential_free), +}; + +static mut SEQUENTIAL_SLOTS: &[PyModuleDef_Slot] = &[ + PyModuleDef_Slot { + slot: Py_mod_exec, + value: sequential_exec as *mut c_void, + }, + PyModuleDef_Slot { + slot: Py_mod_multiple_interpreters, + value: Py_MOD_PER_INTERPRETER_GIL_SUPPORTED, + }, + PyModuleDef_Slot { + slot: 0, + value: ptr::null_mut(), + }, +]; + +unsafe extern "C" fn sequential_exec(module: *mut PyObject) -> c_int { + let state: *mut sequential_state = PyModule_GetState(module).cast(); + + let id_type = PyType_FromModuleAndSpec( + module, + ptr::addr_of_mut!(crate::id::ID_SPEC), + ptr::null_mut(), + ); + if id_type.is_null() { + PyErr_SetString( + PyExc_SystemError, + "cannot locate type object\0".as_ptr().cast::(), + ); + return -1; + } + (*state).id_type = id_type.cast::(); + + PyModule_AddObjectRef(module, "Id\0".as_ptr().cast::(), id_type) +} + +unsafe extern "C" fn sequential_traverse( + module: *mut PyObject, + visit: visitproc, + arg: *mut c_void, +) -> c_int { + let state: *mut sequential_state = PyModule_GetState(module.cast()).cast(); + let id_type: *mut PyObject = (*state).id_type.cast(); + + if id_type.is_null() { + 0 + } else { + (visit)(id_type, arg) + } +} + +unsafe extern "C" fn sequential_clear(module: *mut PyObject) -> c_int { + let state: *mut sequential_state = PyModule_GetState(module.cast()).cast(); + Py_CLEAR(ptr::addr_of_mut!((*state).id_type).cast()); + 0 +} + +unsafe extern "C" fn sequential_free(module: *mut c_void) { + sequential_clear(module.cast()); +} + +#[repr(C)] +struct sequential_state { + id_type: *mut PyTypeObject, +} diff --git a/examples/sequential/tests/test.rs b/examples/sequential/tests/test.rs new file mode 100644 index 00000000000..6076edd4974 --- /dev/null +++ b/examples/sequential/tests/test.rs @@ -0,0 +1,151 @@ +use core::ffi::{c_char, CStr}; +use core::ptr; +use std::thread; + +use pyo3_ffi::*; +use sequential::PyInit_sequential; + +static COMMAND: &'static str = " +from sequential import Id + +s = sum(int(Id()) for _ in range(12)) +\0"; + +// Newtype to be able to pass it to another thread. +struct State(*mut PyThreadState); +unsafe impl Sync for State {} +unsafe impl Send for State {} + +#[test] +fn lets_go_fast() -> Result<(), String> { + unsafe { + let ret = PyImport_AppendInittab( + "sequential\0".as_ptr().cast::(), + Some(PyInit_sequential), + ); + if ret == -1 { + return Err("could not add module to inittab".into()); + } + + Py_Initialize(); + + let main_state = PyThreadState_Swap(ptr::null_mut()); + + const NULL: State = State(ptr::null_mut()); + let mut subs = [NULL; 12]; + + let config = PyInterpreterConfig { + use_main_obmalloc: 0, + allow_fork: 0, + allow_exec: 0, + allow_threads: 1, + allow_daemon_threads: 0, + check_multi_interp_extensions: 1, + gil: PyInterpreterConfig_OWN_GIL, + }; + + for State(state) in &mut subs { + let status = Py_NewInterpreterFromConfig(state, &config); + if PyStatus_IsError(status) == 1 { + let msg = if status.err_msg.is_null() { + "no error message".into() + } else { + CStr::from_ptr(status.err_msg).to_string_lossy() + }; + PyThreadState_Swap(main_state); + Py_FinalizeEx(); + return Err(format!("could not create new subinterpreter: {msg}")); + } + } + + PyThreadState_Swap(main_state); + + let main_state = PyEval_SaveThread(); // a PyInterpreterConfig with shared gil would deadlock otherwise + + let ints: Vec<_> = thread::scope(move |s| { + let mut handles = vec![]; + + for state in subs { + let handle = s.spawn(move || { + let state = state; + PyEval_RestoreThread(state.0); + + let ret = run_code(); + + Py_EndInterpreter(state.0); + ret + }); + + handles.push(handle); + } + + handles.into_iter().map(|h| h.join().unwrap()).collect() + }); + + PyEval_RestoreThread(main_state); + + let ret = Py_FinalizeEx(); + if ret == -1 { + return Err("could not finalize interpreter".into()); + } + + let mut sum: u64 = 0; + for i in ints { + let i = i?; + sum += i; + } + + assert_eq!(sum, (0..).take(12 * 12).sum()); + } + + Ok(()) +} + +unsafe fn fetch() -> String { + let err = PyErr_GetRaisedException(); + let err_repr = PyObject_Str(err); + if !err_repr.is_null() { + let mut size = 0; + let p = PyUnicode_AsUTF8AndSize(err_repr, &mut size); + if !p.is_null() { + let s = std::str::from_utf8_unchecked(std::slice::from_raw_parts( + p.cast::(), + size as usize, + )); + let s = String::from(s); + Py_DECREF(err_repr); + return s; + } + } + String::from("could not get error") +} + +fn run_code() -> Result { + unsafe { + let code_obj = Py_CompileString( + COMMAND.as_ptr().cast::(), + "program\0".as_ptr().cast::(), + Py_file_input, + ); + if code_obj.is_null() { + return Err(fetch()); + } + let globals = PyDict_New(); + let res_ptr = PyEval_EvalCode(code_obj, globals, ptr::null_mut()); + Py_DECREF(code_obj); + if res_ptr.is_null() { + return Err(fetch()); + } else { + Py_DECREF(res_ptr); + } + let sum = PyDict_GetItemString(globals, "s\0".as_ptr().cast::()); /* borrowed reference */ + if sum.is_null() { + Py_DECREF(globals); + return Err("globals did not have `s`".into()); + } + let int = PyLong_AsUnsignedLongLong(sum) as u64; + + Py_DECREF(globals); + Ok(int) + } +} diff --git a/examples/sequential/tests/test_.py b/examples/sequential/tests/test_.py new file mode 100644 index 00000000000..a97522731cf --- /dev/null +++ b/examples/sequential/tests/test_.py @@ -0,0 +1,21 @@ +import pytest +from sequential import Id + + +def test_make_some(): + for x in range(12): + i = Id() + assert x == int(i) + + +def test_args(): + with pytest.raises(TypeError, match="Id\\(\\) takes no arguments"): + Id(3, 4) + + +def test_cmp(): + a = Id() + b = Id() + assert a <= b + assert a < b + assert a == a From 830b3bb81488155ba7691c12d6065e2b82cba2eb Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Tue, 7 Nov 2023 07:51:59 -0500 Subject: [PATCH 39/52] fixes #3561 -- silence new clippy warning --- newsfragments/3564.fixed.md | 1 + pyo3-macros-backend/src/method.rs | 1 + 2 files changed, 2 insertions(+) create mode 100644 newsfragments/3564.fixed.md diff --git a/newsfragments/3564.fixed.md b/newsfragments/3564.fixed.md new file mode 100644 index 00000000000..83e4dba05bb --- /dev/null +++ b/newsfragments/3564.fixed.md @@ -0,0 +1 @@ +Silenced new `clippy::unnecessary_fallible_conversions` warning when using a `Py` `self` receiver diff --git a/pyo3-macros-backend/src/method.rs b/pyo3-macros-backend/src/method.rs index c0aa898e0a7..194ce597963 100644 --- a/pyo3-macros-backend/src/method.rs +++ b/pyo3-macros-backend/src/method.rs @@ -182,6 +182,7 @@ impl SelfType { .map_err(::std::convert::Into::<_pyo3::PyErr>::into) .and_then( #[allow(clippy::useless_conversion)] // In case slf is PyCell + #[allow(unknown_lints, clippy::unnecessary_fallible_conversions)] // In case slf is Py (unknown_lints can be removed when MSRV is 1.75+) |cell| ::std::convert::TryFrom::try_from(cell).map_err(::std::convert::Into::into) ) From 485f5c00e2cc0b05e822227fbef2aad898c5b572 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Sun, 26 Nov 2023 10:00:08 +0000 Subject: [PATCH 40/52] ci: run beta clippy as an allowed-to-fail job --- .github/workflows/ci.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4c673701fda..2342e33d9f7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -113,7 +113,17 @@ jobs: rust-target: "i686-pc-windows-msvc", }, ] + include: + # Run beta clippy as a way to detect any incoming lints which may affect downstream users + - rust: beta + platform: + { + os: "ubuntu-latest", + python-architecture: "x64", + rust-target: "x86_64-unknown-linux-gnu", + } name: clippy/${{ matrix.platform.rust-target }}/${{ matrix.rust }} + continue-on-error: ${{ matrix.platform.rust != 'stable' }} steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@master From 413dda09f5f86e2896b00d592af86d436bec29e3 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Sun, 26 Nov 2023 10:23:39 +0000 Subject: [PATCH 41/52] fix pyo3-ffi beta clippy warnings --- pyo3-ffi/src/cpython/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyo3-ffi/src/cpython/mod.rs b/pyo3-ffi/src/cpython/mod.rs index efb93eff0b2..738ba37652e 100644 --- a/pyo3-ffi/src/cpython/mod.rs +++ b/pyo3-ffi/src/cpython/mod.rs @@ -31,7 +31,6 @@ pub(crate) mod pystate; pub(crate) mod pythonrun; // skipped sysmodule.h pub(crate) mod floatobject; -#[cfg(not(PyPy))] pub(crate) mod pyframe; pub(crate) mod tupleobject; pub(crate) mod unicodeobject; @@ -60,7 +59,7 @@ pub use self::object::*; pub use self::objimpl::*; pub use self::pydebug::*; pub use self::pyerrors::*; -#[cfg(not(PyPy))] +#[cfg(Py_3_11)] pub use self::pyframe::*; #[cfg(all(Py_3_8, not(PyPy)))] pub use self::pylifecycle::*; @@ -69,4 +68,5 @@ pub use self::pystate::*; pub use self::pythonrun::*; pub use self::tupleobject::*; pub use self::unicodeobject::*; +#[cfg(not(PyPy))] pub use self::weakrefobject::*; From 856b859efe1cd0428afc63a7cb943209d44c78f6 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Sun, 26 Nov 2023 10:26:21 +0000 Subject: [PATCH 42/52] fix test-serde beta clippy warning --- tests/test_serde.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_serde.rs b/tests/test_serde.rs index f9c965982d5..f1d5bee4bee 100644 --- a/tests/test_serde.rs +++ b/tests/test_serde.rs @@ -66,7 +66,7 @@ mod test_serde { assert_eq!(user.username, "danya"); assert!(user.group.is_none()); assert_eq!(user.friends.len(), 1usize); - let friend = user.friends.get(0).unwrap(); + let friend = user.friends.first().unwrap(); Python::with_gil(|py| { assert_eq!(friend.borrow(py).username, "friend"); From 1166a995a48e874275d8057478646d6a024f15b9 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Tue, 28 Nov 2023 20:01:10 -0500 Subject: [PATCH 43/52] Refactor create_type_object so that most of the code is monomorphic In pyca/cryptography this function is the #1 source of lines of generated LLVM IR, because it is duplicated 42x (and growing!). By rewriting it so most of the logic is monomorphic, we reduce the generated LLVM IR for this function by 4x. --- src/pyclass/create_type_object.rs | 58 +++++++++++++++++++++++++------ 1 file changed, 47 insertions(+), 11 deletions(-) diff --git a/src/pyclass/create_type_object.rs b/src/pyclass/create_type_object.rs index 33bcacf718b..a7196f30288 100644 --- a/src/pyclass/create_type_object.rs +++ b/src/pyclass/create_type_object.rs @@ -33,17 +33,34 @@ pub(crate) fn create_type_object(py: Python<'_>) -> PyResult, + base: *mut ffi::PyTypeObject, + dealloc: unsafe extern "C" fn(*mut ffi::PyObject), + dealloc_with_gc: unsafe extern "C" fn(*mut ffi::PyObject), + is_mapping: bool, + is_sequence: bool, + doc: &'static CStr, + dict_offset: Option, + weaklist_offset: Option, + is_basetype: bool, + items_iter: PyClassItemsIter, + name: &'static str, + module: Option<&'static str>, + size_of: usize, + ) -> PyResult { PyTypeBuilder { slots: Vec::new(), method_defs: Vec::new(), getset_builders: HashMap::new(), cleanup: Vec::new(), - tp_base: T::BaseType::type_object_raw(py), - tp_dealloc: tp_dealloc::, - tp_dealloc_with_gc: tp_dealloc_with_gc::, - is_mapping: T::IS_MAPPING, - is_sequence: T::IS_SEQUENCE, + tp_base: base, + tp_dealloc: dealloc, + tp_dealloc_with_gc: dealloc_with_gc, + is_mapping, + is_sequence, has_new: false, has_dealloc: false, has_getitem: false, @@ -55,11 +72,30 @@ where #[cfg(all(not(Py_3_9), not(Py_LIMITED_API)))] buffer_procs: Default::default(), } - .type_doc(T::doc(py)?) - .offsets(T::dict_offset(), T::weaklist_offset()) - .set_is_basetype(T::IS_BASETYPE) - .class_items(T::items_iter()) - .build(py, T::NAME, T::MODULE, std::mem::size_of::>()) + .type_doc(doc) + .offsets(dict_offset, weaklist_offset) + .set_is_basetype(is_basetype) + .class_items(items_iter) + .build(py, name, module, size_of) + } + + unsafe { + inner( + py, + T::BaseType::type_object_raw(py), + tp_dealloc::, + tp_dealloc_with_gc::, + T::IS_MAPPING, + T::IS_SEQUENCE, + T::doc(py)?, + T::dict_offset(), + T::weaklist_offset(), + T::IS_BASETYPE, + T::items_iter(), + T::NAME, + T::MODULE, + std::mem::size_of::>(), + ) } } From 7032789daf36f16c3c179eb66b90e2676a1d81f9 Mon Sep 17 00:00:00 2001 From: messense Date: Sun, 3 Dec 2023 21:47:00 +0800 Subject: [PATCH 44/52] Add additional definitions for `_PyImport_Frozen*` --- pyo3-ffi/src/cpython/import.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pyo3-ffi/src/cpython/import.rs b/pyo3-ffi/src/cpython/import.rs index cc7d9ead6db..aafd71a8355 100644 --- a/pyo3-ffi/src/cpython/import.rs +++ b/pyo3-ffi/src/cpython/import.rs @@ -65,4 +65,10 @@ pub struct _frozen { extern "C" { #[cfg(not(PyPy))] pub static mut PyImport_FrozenModules: *const _frozen; + #[cfg(all(not(PyPy), Py_3_11))] + pub static mut _PyImport_FrozenBootstrap: *const _frozen; + #[cfg(all(not(PyPy), Py_3_11))] + pub static mut _PyImport_FrozenStdlib: *const _frozen; + #[cfg(all(not(PyPy), Py_3_11))] + pub static mut _PyImport_FrozenTest: *const _frozen; } From 1896a320150acb13b8b87d41aea783ec3b068d6f Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Mon, 4 Dec 2023 22:03:56 +0300 Subject: [PATCH 45/52] ci: refactor pytests dev dependencies --- pytests/noxfile.py | 5 ++--- pytests/pyo3_pytests/__init__.py | 1 - pytests/pyproject.toml | 9 +++++++++ pytests/requirements-dev.txt | 5 ----- 4 files changed, 11 insertions(+), 9 deletions(-) delete mode 100644 pytests/pyo3_pytests/__init__.py delete mode 100644 pytests/requirements-dev.txt diff --git a/pytests/noxfile.py b/pytests/noxfile.py index bab55868011..7588e96603e 100644 --- a/pytests/noxfile.py +++ b/pytests/noxfile.py @@ -6,7 +6,7 @@ @nox.session def test(session: nox.Session): - session.install("-rrequirements-dev.txt") + session.install(".[dev]") try: session.install("--only-binary=numpy", "numpy>=1.16") except CommandFailed: @@ -19,6 +19,5 @@ def test(session: nox.Session): @nox.session def bench(session: nox.Session): - session.install("-rrequirements-dev.txt") - session.install(".") + session.install(".[dev]") session.run("pytest", "--benchmark-enable", "--benchmark-only", *session.posargs) diff --git a/pytests/pyo3_pytests/__init__.py b/pytests/pyo3_pytests/__init__.py deleted file mode 100644 index b9d3a92f82d..00000000000 --- a/pytests/pyo3_pytests/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .pyo3_pytests import * diff --git a/pytests/pyproject.toml b/pytests/pyproject.toml index dfebfe31173..fb1ac3dbdff 100644 --- a/pytests/pyproject.toml +++ b/pytests/pyproject.toml @@ -17,3 +17,12 @@ classifiers = [ "Operating System :: POSIX", "Operating System :: MacOS :: MacOS X", ] + +[project.optional-dependencies] +dev = [ + "hypothesis>=3.55", + "pytest-asyncio>=0.21", + "pytest-benchmark>=3.4", + "pytest>=6.0", + "typing_extensions>=4.0.0" +] diff --git a/pytests/requirements-dev.txt b/pytests/requirements-dev.txt deleted file mode 100644 index aa0c703a8cd..00000000000 --- a/pytests/requirements-dev.txt +++ /dev/null @@ -1,5 +0,0 @@ -hypothesis>=3.55 -pytest>=6.0 -pytest-asyncio>=0.21 -pytest-benchmark>=3.4 -typing_extensions>=4.0.0 From 5c1e4d10b3353359084831c57e38a309f6bfe08f Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Tue, 5 Dec 2023 08:01:02 +0300 Subject: [PATCH 46/52] ci: fixup pytests to compile in debug --- pytests/noxfile.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pytests/noxfile.py b/pytests/noxfile.py index 7588e96603e..57d9d63a044 100644 --- a/pytests/noxfile.py +++ b/pytests/noxfile.py @@ -6,14 +6,13 @@ @nox.session def test(session: nox.Session): - session.install(".[dev]") + session.env["MATURIN_PEP517_ARGS"] = "--profile=dev" + session.run_always("python", "-m", "pip", "install", "-v", ".[dev]") try: session.install("--only-binary=numpy", "numpy>=1.16") except CommandFailed: # No binary wheel for numpy available on this platform pass - session.install("maturin") - session.run_always("maturin", "develop") session.run("pytest", *session.posargs) From 8f6976d9a551354386f0550e1729b19375b691c1 Mon Sep 17 00:00:00 2001 From: Nathan Kent Date: Thu, 30 Nov 2023 13:27:57 -0800 Subject: [PATCH 47/52] Enable `GILProtected` access via `PyVisit` Closes #3615 This simply adds a new method which uses the existence of a `PyVisit` object as proof that the GIL is held instead of a `Python` object. This allows `GILProtected` to be used in instances where contained Python objects need to participate in garbage collection. Usage in this situation should be valid since no Python calls are made and this does not provide any additional mechanism for accessing a `Python` object. --- newsfragments/3616.added.md | 1 + src/sync.rs | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 newsfragments/3616.added.md diff --git a/newsfragments/3616.added.md b/newsfragments/3616.added.md new file mode 100644 index 00000000000..532dc6e56c1 --- /dev/null +++ b/newsfragments/3616.added.md @@ -0,0 +1 @@ +Add `traverse` method to `GILProtected` diff --git a/src/sync.rs b/src/sync.rs index 8500413ef97..91a83106798 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -1,5 +1,5 @@ //! Synchronization mechanisms based on the Python GIL. -use crate::{types::PyString, types::PyType, Py, PyErr, Python}; +use crate::{types::PyString, types::PyType, Py, PyErr, PyVisit, Python}; use std::cell::UnsafeCell; /// Value with concurrent access protected by the GIL. @@ -37,6 +37,11 @@ impl GILProtected { pub fn get<'py>(&'py self, _py: Python<'py>) -> &'py T { &self.value } + + /// Gain access to the inner value by giving proof that garbage collection is happening. + pub fn traverse<'py>(&'py self, _visit: PyVisit<'py>) -> &'py T { + &self.value + } } unsafe impl Sync for GILProtected where T: Send {} From d8974798315e639cc8d384bdd228534f576570be Mon Sep 17 00:00:00 2001 From: Adam Reichold Date: Thu, 14 Dec 2023 11:35:20 +0100 Subject: [PATCH 48/52] Fix the Crossbeam ecosystem to point releases before it required Rust 1.61. --- noxfile.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/noxfile.py b/noxfile.py index f6aada6d9cc..45951659dfa 100644 --- a/noxfile.py +++ b/noxfile.py @@ -490,6 +490,10 @@ def set_minimal_package_versions(session: nox.Session): "proptest": "1.0.0", "chrono": "0.4.25", "byteorder": "1.4.3", + "crossbeam-channel": "0.5.8", + "crossbeam-deque": "0.8.3", + "crossbeam-epoch": "0.9.15", + "crossbeam-utils": "0.8.16", } # run cargo update first to ensure that everything is at highest From b84271140ea04f4e0a21eba98fb3ad8a1ca1875f Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Thu, 21 Dec 2023 10:29:00 -0500 Subject: [PATCH 49/52] Fixes #3645 -- added `abi3-py312` feature --- Cargo.toml | 3 ++- newsfragments/3687.added.md | 1 + pyo3-build-config/Cargo.toml | 3 ++- pyo3-build-config/src/impl_.rs | 2 +- pyo3-ffi/Cargo.toml | 3 ++- 5 files changed, 8 insertions(+), 4 deletions(-) create mode 100644 newsfragments/3687.added.md diff --git a/Cargo.toml b/Cargo.toml index 835a1f866f5..dbbcbec640e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -85,7 +85,8 @@ abi3-py37 = ["abi3-py38", "pyo3-build-config/abi3-py37", "pyo3-ffi/abi3-py37"] abi3-py38 = ["abi3-py39", "pyo3-build-config/abi3-py38", "pyo3-ffi/abi3-py38"] abi3-py39 = ["abi3-py310", "pyo3-build-config/abi3-py39", "pyo3-ffi/abi3-py39"] abi3-py310 = ["abi3-py311", "pyo3-build-config/abi3-py310", "pyo3-ffi/abi3-py310"] -abi3-py311 = ["abi3", "pyo3-build-config/abi3-py311", "pyo3-ffi/abi3-py311"] +abi3-py311 = ["abi3-py312", "pyo3-build-config/abi3-py311", "pyo3-ffi/abi3-py311"] +abi3-py312 = ["abi3", "pyo3-build-config/abi3-py312", "pyo3-ffi/abi3-py312"] # Automatically generates `python3.dll` import libraries for Windows targets. generate-import-lib = ["pyo3-ffi/generate-import-lib"] diff --git a/newsfragments/3687.added.md b/newsfragments/3687.added.md new file mode 100644 index 00000000000..a6df28d939f --- /dev/null +++ b/newsfragments/3687.added.md @@ -0,0 +1 @@ +Added `abi3-py312` feature diff --git a/pyo3-build-config/Cargo.toml b/pyo3-build-config/Cargo.toml index de3189bf220..2a78583cd6a 100644 --- a/pyo3-build-config/Cargo.toml +++ b/pyo3-build-config/Cargo.toml @@ -35,7 +35,8 @@ abi3-py37 = ["abi3-py38"] abi3-py38 = ["abi3-py39"] abi3-py39 = ["abi3-py310"] abi3-py310 = ["abi3-py311"] -abi3-py311 = ["abi3"] +abi3-py311 = ["abi3-py312"] +abi3-py312 = ["abi3"] [package.metadata.docs.rs] features = ["resolve-config"] diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index ec05d7dc956..e188767fcd1 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -34,7 +34,7 @@ use crate::{ 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 = 11; +const ABI3_MAX_MINOR: u8 = 12; /// Gets an environment variable owned by cargo. /// diff --git a/pyo3-ffi/Cargo.toml b/pyo3-ffi/Cargo.toml index a380e1a36d9..c1ebbbf1a63 100644 --- a/pyo3-ffi/Cargo.toml +++ b/pyo3-ffi/Cargo.toml @@ -31,7 +31,8 @@ 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"] abi3-py310 = ["abi3-py311", "pyo3-build-config/abi3-py310"] -abi3-py311 = ["abi3", "pyo3-build-config/abi3-py311"] +abi3-py311 = ["abi3-py312", "pyo3-build-config/abi3-py311"] +abi3-py312 = ["abi3", "pyo3-build-config/abi3-py312"] # Automatically generates `python3.dll` import libraries for Windows targets. generate-import-lib = ["pyo3-build-config/python3-dll-a"] From ecb0e9cb6116057d2f19246db8729d1fe7b0ccd9 Mon Sep 17 00:00:00 2001 From: Adam Reichold Date: Fri, 22 Dec 2023 12:08:37 +0100 Subject: [PATCH 50/52] Copy note on using check_signals on non-main thread/interpreter from Python docs. --- src/marker.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/marker.rs b/src/marker.rs index dfe876c89fa..9048259d5a0 100644 --- a/src/marker.rs +++ b/src/marker.rs @@ -907,6 +907,9 @@ impl<'py> Python<'py> { /// As Python's [`signal`][2] API allows users to define custom signal handlers, calling this /// function allows arbitrary Python code inside signal handlers to run. /// + /// If the function is called from a non-main thread, or under a non-main Python interpreter, + /// it does nothing yet still returns `Ok(())`. + /// /// [1]: https://docs.python.org/3/c-api/exceptions.html?highlight=pyerr_checksignals#c.PyErr_CheckSignals /// [2]: https://docs.python.org/3/library/signal.html pub fn check_signals(self) -> PyResult<()> { From 985412fb8f84bb9def6841e3f94a1505a1a70bd2 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Fri, 29 Dec 2023 13:02:44 +0000 Subject: [PATCH 51/52] ci: updates for Rust 1.75 --- tests/ui/abi3_nativetype_inheritance.stderr | 14 ++++++++++++++ tests/ui/pyclass_send.stderr | 21 +++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/tests/ui/abi3_nativetype_inheritance.stderr b/tests/ui/abi3_nativetype_inheritance.stderr index eec14202fd5..647e1ad819e 100644 --- a/tests/ui/abi3_nativetype_inheritance.stderr +++ b/tests/ui/abi3_nativetype_inheritance.stderr @@ -7,3 +7,17 @@ error[E0277]: the trait bound `PyDict: PyClass` is not satisfied = help: the trait `PyClass` is implemented for `TestClass` = note: required for `PyDict` to implement `PyClassBaseType` = note: this error originates in the attribute macro `pyclass` (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0277]: the trait bound `PyDict: PyClass` is not satisfied + --> tests/ui/abi3_nativetype_inheritance.rs:5:19 + | +5 | #[pyclass(extends=PyDict)] + | ^^^^^^ the trait `PyClass` is not implemented for `PyDict` + | + = help: the trait `PyClass` is implemented for `TestClass` + = note: required for `PyDict` to implement `PyClassBaseType` +note: required by a bound in `PyClassImpl::BaseType` + --> src/impl_/pyclass.rs + | + | type BaseType: PyTypeInfo + PyClassBaseType; + | ^^^^^^^^^^^^^^^ required by this bound in `PyClassImpl::BaseType` diff --git a/tests/ui/pyclass_send.stderr b/tests/ui/pyclass_send.stderr index f279acc5f9f..7a80989b63f 100644 --- a/tests/ui/pyclass_send.stderr +++ b/tests/ui/pyclass_send.stderr @@ -16,3 +16,24 @@ note: required by a bound in `SendablePyClass` | pub struct SendablePyClass(PhantomData); | ^^^^ required by this bound in `SendablePyClass` = note: this error originates in the attribute macro `pyclass` (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0277]: `Rc` cannot be sent between threads safely + --> tests/ui/pyclass_send.rs:4:1 + | +4 | #[pyclass] + | ^^^^^^^^^^ `Rc` cannot be sent between threads safely + | + = help: within `NotThreadSafe`, the trait `Send` is not implemented for `Rc` + = help: the trait `pyo3::impl_::pyclass::PyClassThreadChecker` is implemented for `SendablePyClass` +note: required because it appears within the type `NotThreadSafe` + --> tests/ui/pyclass_send.rs:5:8 + | +5 | struct NotThreadSafe { + | ^^^^^^^^^^^^^ + = note: required for `SendablePyClass` to implement `pyo3::impl_::pyclass::PyClassThreadChecker` +note: required by a bound in `PyClassImpl::ThreadChecker` + --> src/impl_/pyclass.rs + | + | type ThreadChecker: PyClassThreadChecker; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `PyClassImpl::ThreadChecker` + = note: this error originates in the attribute macro `pyclass` (in Nightly builds, run with -Z macro-backtrace for more info) From d3f034a80f2973e4d21fc2fd49cd0af309f1ab4d Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Fri, 29 Dec 2023 22:58:16 +0000 Subject: [PATCH 52/52] release: 0.20.1 --- CHANGELOG.md | 21 ++++++++++++++++++- Cargo.toml | 8 +++---- README.md | 4 ++-- examples/Cargo.toml | 2 +- examples/decorator/.template/pre-script.rhai | 2 +- .../maturin-starter/.template/pre-script.rhai | 2 +- examples/plugin/.template/pre-script.rhai | 2 +- .../.template/pre-script.rhai | 2 +- examples/word-count/.template/pre-script.rhai | 2 +- newsfragments/3456.added.md | 1 - newsfragments/3507.added.md | 1 - newsfragments/3512.fixed.md | 1 - newsfragments/3556.added.md | 1 - newsfragments/3564.fixed.md | 1 - newsfragments/3587.added.md | 2 -- newsfragments/3616.added.md | 1 - newsfragments/3687.added.md | 1 - pyo3-build-config/Cargo.toml | 2 +- pyo3-ffi/Cargo.toml | 4 ++-- pyo3-macros-backend/Cargo.toml | 2 +- pyo3-macros/Cargo.toml | 4 ++-- pyproject.toml | 2 +- 22 files changed, 39 insertions(+), 29 deletions(-) delete mode 100644 newsfragments/3456.added.md delete mode 100644 newsfragments/3507.added.md delete mode 100644 newsfragments/3512.fixed.md delete mode 100644 newsfragments/3556.added.md delete mode 100644 newsfragments/3564.fixed.md delete mode 100644 newsfragments/3587.added.md delete mode 100644 newsfragments/3616.added.md delete mode 100644 newsfragments/3687.added.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 97ce9a00537..6f7708ce05b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,24 @@ To see unreleased changes, please see the [CHANGELOG on the main branch guide](h +## [0.20.1] - 2023-12-30 + +### Added + +- Add optional `either` feature to add conversions for `either::Either` sum type. [#3456](https://github.com/PyO3/pyo3/pull/3456) +- Add optional `smallvec` feature to add conversions for `smallvec::SmallVec`. [#3507](https://github.com/PyO3/pyo3/pull/3507) +- Add `take` and `into_inner` methods to `GILOnceCell` [#3556](https://github.com/PyO3/pyo3/pull/3556) +- `#[classmethod]` methods can now also receive `Py` as their first argument. [#3587](https://github.com/PyO3/pyo3/pull/3587) +- `#[pyfunction(pass_module)]` can now also receive `Py` as their first argument. [#3587](https://github.com/PyO3/pyo3/pull/3587) +- Add `traverse` method to `GILProtected`. [#3616](https://github.com/PyO3/pyo3/pull/3616) +- Added `abi3-py312` feature [#3687](https://github.com/PyO3/pyo3/pull/3687) + +### Fixed + +- Fix minimum version specification for optional `chrono` dependency. [#3512](https://github.com/PyO3/pyo3/pull/3512) +- Silenced new `clippy::unnecessary_fallible_conversions` warning when using a `Py` `self` receiver. [#3564](https://github.com/PyO3/pyo3/pull/3564) + + ## [0.20.0] - 2023-10-11 ### Packaging @@ -1599,7 +1617,8 @@ Yanked - Initial release -[Unreleased]: https://github.com/pyo3/pyo3/compare/v0.20.0...HEAD +[Unreleased]: https://github.com/pyo3/pyo3/compare/v0.20.1...HEAD +[0.20.1]: https://github.com/pyo3/pyo3/compare/v0.20.0...v0.20.1 [0.20.0]: https://github.com/pyo3/pyo3/compare/v0.19.2...v0.20.0 [0.19.2]: https://github.com/pyo3/pyo3/compare/v0.19.1...v0.19.2 [0.19.1]: https://github.com/pyo3/pyo3/compare/v0.19.0...v0.19.1 diff --git a/Cargo.toml b/Cargo.toml index dbbcbec640e..5bd9855f825 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3" -version = "0.20.0" +version = "0.20.1" description = "Bindings to Python interpreter" authors = ["PyO3 Project and Contributors "] readme = "README.md" @@ -21,10 +21,10 @@ parking_lot = ">= 0.11, < 0.13" memoffset = "0.9" # ffi bindings to the python interpreter, split into a separate crate so they can be used independently -pyo3-ffi = { path = "pyo3-ffi", version = "=0.20.0" } +pyo3-ffi = { path = "pyo3-ffi", version = "=0.20.1" } # support crates for macros feature -pyo3-macros = { path = "pyo3-macros", version = "=0.20.0", optional = true } +pyo3-macros = { path = "pyo3-macros", version = "=0.20.1", optional = true } indoc = { version = "2.0.1", optional = true } unindent = { version = "0.2.1", optional = true } @@ -57,7 +57,7 @@ rayon = "1.6.1" widestring = "0.5.1" [build-dependencies] -pyo3-build-config = { path = "pyo3-build-config", version = "0.20.0", features = ["resolve-config"] } +pyo3-build-config = { path = "pyo3-build-config", version = "0.20.1", features = ["resolve-config"] } [features] default = ["macros"] diff --git a/README.md b/README.md index 9f53285359e..ca2ac180e0f 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ name = "string_sum" crate-type = ["cdylib"] [dependencies] -pyo3 = { version = "0.20.0", features = ["extension-module"] } +pyo3 = { version = "0.20.1", features = ["extension-module"] } ``` **`src/lib.rs`** @@ -137,7 +137,7 @@ Start a new project with `cargo new` and add `pyo3` to the `Cargo.toml` like th ```toml [dependencies.pyo3] -version = "0.20.0" +version = "0.20.1" features = ["auto-initialize"] ``` diff --git a/examples/Cargo.toml b/examples/Cargo.toml index b210f0ee9a6..a7213e469dd 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -5,7 +5,7 @@ publish = false edition = "2021" [dev-dependencies] -pyo3 = { version = "0.20.0", path = "..", features = ["auto-initialize", "extension-module"] } +pyo3 = { version = "0.20.1", path = "..", features = ["auto-initialize", "extension-module"] } [[example]] name = "decorator" diff --git a/examples/decorator/.template/pre-script.rhai b/examples/decorator/.template/pre-script.rhai index 5ba02b12ffd..c7199a5303e 100644 --- a/examples/decorator/.template/pre-script.rhai +++ b/examples/decorator/.template/pre-script.rhai @@ -1,4 +1,4 @@ -variable::set("PYO3_VERSION", "0.20.0"); +variable::set("PYO3_VERSION", "0.20.1"); file::rename(".template/Cargo.toml", "Cargo.toml"); file::rename(".template/pyproject.toml", "pyproject.toml"); file::delete(".template"); diff --git a/examples/maturin-starter/.template/pre-script.rhai b/examples/maturin-starter/.template/pre-script.rhai index 5ba02b12ffd..c7199a5303e 100644 --- a/examples/maturin-starter/.template/pre-script.rhai +++ b/examples/maturin-starter/.template/pre-script.rhai @@ -1,4 +1,4 @@ -variable::set("PYO3_VERSION", "0.20.0"); +variable::set("PYO3_VERSION", "0.20.1"); file::rename(".template/Cargo.toml", "Cargo.toml"); file::rename(".template/pyproject.toml", "pyproject.toml"); file::delete(".template"); diff --git a/examples/plugin/.template/pre-script.rhai b/examples/plugin/.template/pre-script.rhai index 94a61826dc2..d325aca0eef 100644 --- a/examples/plugin/.template/pre-script.rhai +++ b/examples/plugin/.template/pre-script.rhai @@ -1,4 +1,4 @@ -variable::set("PYO3_VERSION", "0.20.0"); +variable::set("PYO3_VERSION", "0.20.1"); file::rename(".template/Cargo.toml", "Cargo.toml"); file::rename(".template/plugin_api/Cargo.toml", "plugin_api/Cargo.toml"); file::delete(".template"); diff --git a/examples/setuptools-rust-starter/.template/pre-script.rhai b/examples/setuptools-rust-starter/.template/pre-script.rhai index e4ede9b7aff..91eee121a7b 100644 --- a/examples/setuptools-rust-starter/.template/pre-script.rhai +++ b/examples/setuptools-rust-starter/.template/pre-script.rhai @@ -1,4 +1,4 @@ -variable::set("PYO3_VERSION", "0.20.0"); +variable::set("PYO3_VERSION", "0.20.1"); file::rename(".template/Cargo.toml", "Cargo.toml"); file::rename(".template/setup.cfg", "setup.cfg"); file::delete(".template"); diff --git a/examples/word-count/.template/pre-script.rhai b/examples/word-count/.template/pre-script.rhai index 5ba02b12ffd..c7199a5303e 100644 --- a/examples/word-count/.template/pre-script.rhai +++ b/examples/word-count/.template/pre-script.rhai @@ -1,4 +1,4 @@ -variable::set("PYO3_VERSION", "0.20.0"); +variable::set("PYO3_VERSION", "0.20.1"); file::rename(".template/Cargo.toml", "Cargo.toml"); file::rename(".template/pyproject.toml", "pyproject.toml"); file::delete(".template"); diff --git a/newsfragments/3456.added.md b/newsfragments/3456.added.md deleted file mode 100644 index 6e9376ba65d..00000000000 --- a/newsfragments/3456.added.md +++ /dev/null @@ -1 +0,0 @@ -Add optional conversion support for `either::Either` sum type (under "either" feature). diff --git a/newsfragments/3507.added.md b/newsfragments/3507.added.md deleted file mode 100644 index 2068ab4c3f7..00000000000 --- a/newsfragments/3507.added.md +++ /dev/null @@ -1 +0,0 @@ -Add `smallvec` feature to add `ToPyObject`, `IntoPy` and `FromPyObject` implementations for `smallvec::SmallVec`. diff --git a/newsfragments/3512.fixed.md b/newsfragments/3512.fixed.md deleted file mode 100644 index 39b8087669e..00000000000 --- a/newsfragments/3512.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Fix minimum version specification for optional `chrono` dependency diff --git a/newsfragments/3556.added.md b/newsfragments/3556.added.md deleted file mode 100644 index 014908a1bf5..00000000000 --- a/newsfragments/3556.added.md +++ /dev/null @@ -1 +0,0 @@ -Add `take` and `into_inner` methods to `GILOnceCell` \ No newline at end of file diff --git a/newsfragments/3564.fixed.md b/newsfragments/3564.fixed.md deleted file mode 100644 index 83e4dba05bb..00000000000 --- a/newsfragments/3564.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Silenced new `clippy::unnecessary_fallible_conversions` warning when using a `Py` `self` receiver diff --git a/newsfragments/3587.added.md b/newsfragments/3587.added.md deleted file mode 100644 index f8ea280dd25..00000000000 --- a/newsfragments/3587.added.md +++ /dev/null @@ -1,2 +0,0 @@ -- Classmethods can now receive `Py` as their first argument -- Function annotated with `pass_module` can now receive `Py` as their first argument \ No newline at end of file diff --git a/newsfragments/3616.added.md b/newsfragments/3616.added.md deleted file mode 100644 index 532dc6e56c1..00000000000 --- a/newsfragments/3616.added.md +++ /dev/null @@ -1 +0,0 @@ -Add `traverse` method to `GILProtected` diff --git a/newsfragments/3687.added.md b/newsfragments/3687.added.md deleted file mode 100644 index a6df28d939f..00000000000 --- a/newsfragments/3687.added.md +++ /dev/null @@ -1 +0,0 @@ -Added `abi3-py312` feature diff --git a/pyo3-build-config/Cargo.toml b/pyo3-build-config/Cargo.toml index 2a78583cd6a..37cd3e7e94c 100644 --- a/pyo3-build-config/Cargo.toml +++ b/pyo3-build-config/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3-build-config" -version = "0.20.0" +version = "0.20.1" description = "Build configuration for the PyO3 ecosystem" authors = ["PyO3 Project and Contributors "] keywords = ["pyo3", "python", "cpython", "ffi"] diff --git a/pyo3-ffi/Cargo.toml b/pyo3-ffi/Cargo.toml index c1ebbbf1a63..b2dbe4245ca 100644 --- a/pyo3-ffi/Cargo.toml +++ b/pyo3-ffi/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3-ffi" -version = "0.20.0" +version = "0.20.1" description = "Python-API bindings for the PyO3 ecosystem" authors = ["PyO3 Project and Contributors "] keywords = ["pyo3", "python", "cpython", "ffi"] @@ -38,7 +38,7 @@ abi3-py312 = ["abi3", "pyo3-build-config/abi3-py312"] generate-import-lib = ["pyo3-build-config/python3-dll-a"] [build-dependencies] -pyo3-build-config = { path = "../pyo3-build-config", version = "0.20.0", features = ["resolve-config"] } +pyo3-build-config = { path = "../pyo3-build-config", version = "0.20.1", features = ["resolve-config"] } [lints] workspace = true diff --git a/pyo3-macros-backend/Cargo.toml b/pyo3-macros-backend/Cargo.toml index 5e9520d3e9c..3bd9e7bdcc2 100644 --- a/pyo3-macros-backend/Cargo.toml +++ b/pyo3-macros-backend/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3-macros-backend" -version = "0.20.0" +version = "0.20.1" description = "Code generation for PyO3 package" authors = ["PyO3 Project and Contributors "] keywords = ["pyo3", "python", "cpython", "ffi"] diff --git a/pyo3-macros/Cargo.toml b/pyo3-macros/Cargo.toml index 0e8f499fa0c..7dd5debde97 100644 --- a/pyo3-macros/Cargo.toml +++ b/pyo3-macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3-macros" -version = "0.20.0" +version = "0.20.1" description = "Proc macros for PyO3 package" authors = ["PyO3 Project and Contributors "] keywords = ["pyo3", "python", "cpython", "ffi"] @@ -22,7 +22,7 @@ abi3 = ["pyo3-macros-backend/abi3"] proc-macro2 = { version = "1", default-features = false } quote = "1" syn = { version = "2", features = ["full", "extra-traits"] } -pyo3-macros-backend = { path = "../pyo3-macros-backend", version = "=0.20.0" } +pyo3-macros-backend = { path = "../pyo3-macros-backend", version = "=0.20.1" } [lints] workspace = true diff --git a/pyproject.toml b/pyproject.toml index 29626e970a4..f027db81d2b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ [tool.towncrier] filename = "CHANGELOG.md" -version = "0.20.0" +version = "0.20.1" start_string = "\n" template = ".towncrier.template.md" title_format = "## [{version}] - {project_date}"